SVX日記

2004|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|
2021|01|02|03|04|05|06|07|08|09|10|11|12|
2022|01|02|03|04|05|06|07|08|09|10|11|12|
2023|01|02|03|04|05|06|07|08|09|10|11|12|
2024|01|02|03|04|05|06|07|08|09|10|11|

2024-03-02(Sat) Sinatraでhtpasswd認証したりldap認証したり

  回転のプログラミングの途中だが、ひょんなことから、Sinatraでウェブサービスを提供する各種のコンテナに認証機能を付ける必要が生じた。ハテ?基本的にはHAProxyを被せて運用しているのだが、認証ってどうすんだっけ?

  調べると、HAProxyには単純な認証機能はあるものの、どうもLDAP認証の機能はないらしい。まぁ、Sinatra側でやるべきだよな、と思ったら、Sinatraにも単純な認証機能しかないようだ。

use Rack::Auth::Basic do |username, password|
    username == 'admin' && password == 'secret'
end

  これを追加すると全体に認証がかかる。恐ろしいほどに見たまんまなコードだw。逆に言えば、ここに自分で仕組みを組み込んでやれば、好みの認証機能を実現できるということだ。ハッシュの知識はあるし、既にLDAPにアクセスするコードも持っている。ほんじゃ、ということで書いてみた。

require 'sinatra'
 
eval(File.read('pv/sinatra.config')) rescue true
@configs ||= {}
 
unless(@configs[:no_auth])
    use Rack::Auth::Basic, 'Authorization Required' do |username, password|
 
        authorized = false
 
        if(@configs[:auth_htpasswd])
            ht_password = false
            open(@configs[:htpasswd_file]) {|fh|
                fh.each {|l|
                    l =~ /^#{username}:(.+)/ and ht_password = $1 and break
                }
            }
            if((it = ht_password) and it =~ /^{SHA}(.+)/i)
                require 'digest/sha1'
                hash_base64 = $1
                hash = hash_base64.unpack('m*')[0]
                challenge = Digest::SHA1.digest(password)
                authorized |= (hash == challenge)
            end
        end
 
        if(@configs[:auth_ldap])
            require 'net/ldap'
            ldap_password = false
            ldap = Net::LDAP.new(
                :host   => @configs[:ldap_host],
                :port   => @configs[:ldap_port],
                :auth   => @configs[:ldap_auth],
            )
            results = ldap.search(
                :base       => @configs[:ldap_search_base],
                :filter     => '(cn=%s)' % username,
                :attributes => ['userpassword'],
            )
            (it = results) and (it = it[0]) and (it = it[:userpassword]) and (it = it[0]) and ldap_password = it
            if((it = ldap_password) and it =~ /^{SSHA}(.+)/i)
                require 'digest/sha1'
                hash_salt_base64 = $1
                hash_salt = hash_salt_base64.unpack('m*')[0]
                hash = hash_salt[0, 20]
                salt = hash_salt[20, 4]
                challenge = Digest::SHA1.digest(password + salt)
                authorized |= (hash == challenge)
            end
        end
 
        authorized
    end
end

  上記のコードをapp.rbの冒頭に追加すればいい。ApacheのhtpasswdとLDAPの両方に対応するが、今のところ前者は「{SHA}」後者は「{SSHA}」形式のみ対応。当初「$apr1$」形式に対応しようと思ったのだが、それは単なるMD5ではなく、どうもApacheの独自実装らしい。ソース読めばRubyで実装できなくはないが、そんなのに付き合っても今後とも得はなさそうなので「{SHA}」への対応にした。なので、htpasswdでハッシュを生成する際には-sを指定する必要がある。

  どうでもいいが、Rubyのpack/unpackって、ホントに覚えられないなぁ。いつもpack/unpackのどっちが配列化/バイナリ化だっけ? ってなる。で、考えるもの面倒になって、適当にコードを書いて済ましちゃう。pack/unpackって、機能の格納方法としては天才的な発想だと思うけど、記述方法としてはどうなのよ……Perlが元祖なのかな。うーん……うううーん。

  ま、それはそれとして、GitLabに置いてあるSinatraのスケルトンコンテナに上記のコードを組み込んで、とりあえずは作業完了である。

  画像の説明

  話は替わるが、だいぶ前から我が家では卓上カレンダを自作しているのだが、時期がきたので印刷したらグレートシングだった。え、もう2周もしたの? と思ったらそのとおりだった。うげぇ。エンジニアすぐ死ぬ。


2024-03-03(Sun) pack/unpackをどうにかする

  昨日、pack/unpackって記述方法としてはどうなのよ、と書いてから、なんだか考え始めてしまった。要するに、以下の書き方が全然ピンとこないのでちっとも覚えられない、って話である。

[65, 66, 67].pack('c*')
=> ABC
'ABC'.unpack('c*')
=> [65, 66, 67]

  じゃ、ピンとくる書き方ってなんだって考えたら、以下が思い浮かんだ。これは実際に動く。いわゆるprintfだよね。

'%c%c%c' % [65, 66, 67]
=> ABC

  「%」演算子を使っているのがミソだ。「文字列化する」「引数は配列」というイメージが自然に湧く。じゃ、逆に「配列化する」演算子はなんだ? 苦し紛れだが、こんなのはどうだ。こんな文法はないので動かないが。

'%c%c%c' << 'ABC' # 動きません
=> [65, 66, 67]

  これに近い記述方法で、実際に動かすことはできないか? って考えたら、思い浮かんでしまい、できてしまった。

:c_ << 'ABC'
=> [65, 66, 67]

  RubyのSymbolを悪用(?)して定義した。「<<」演算子を再定義している。「c*」とは書けないので「c_」で代用してみた。

class Symbol
    def <<(packed)
        packed.unpack(self.to_s.sub(/_$/, '*'))
    end
end

  これを使うと、BASE64のデコード処理を以下のように書ける。

:m_ << 'QUJDREU='
=> ["ABCDE"]

  そうなると、逆にエンコードする時はこう書きたい。そんな指示子はないので動かないが。

'%m' % ['ABCDE'] # 動きません
=> "QUJDREU=\n"

  んが、今度は逆に、Stringの「%」演算子を再定義してしまえばいい。指示子の指定が「%」だと既存の機能と衝突するので「:」を割り振ってみた。

class String
    alias :perc :%
    def %(arg)
        self =~ /^:(.+)/ ? arg.pack($1.sub(/_$/, '*')) : perc(arg)
    end
end

  これを使うと、BASE64のエンコード処理を以下のように書ける。

':m_' % ['ABCDE']
=> "QUJDREU=\n"

  そもそもBASE64の変換は「文字↔文字」だからピンときにくいだけのことかもしれない。無理にピンとこさせずとも、以下をメモっておけば十分か。

['ABCDE'].pack('m*')
=> "QUJDREU=\n"
'QUJDREU='.unpack('m*')[0]
=> "ABCDE"

  まぁ、単なるたわむれプログラミングだ。わはははははははは、たわむれは、おわりじゃ。


2024-03-04(Mon) pack/unpackをよりどうにかする

  たわむれは、おわり、のはずだったが、美作にいけてしまった(?)ので、もうちっとだけ続くんじゃ。以下は前回のコードだが。

:c_ << 'ABC'
=> [65, 66, 67]

  unpackの場合、結果は配列になるので、こう書いたほうが、より直感的な気がしてきた。

[:c_] << 'ABC' # 動くけど……
=> [65, 66, 67]

  しかし、これは文法として有効なので、正しい結果は「[:c, 'ABC']」だ。ほんじゃ、演算子「<<」じゃなく「<」を使うか。

class Array
    def <(packed)
        packed.unpack(self[0].to_s.sub(/_$/, '*'))
    end
end

  「<」の元来の意味は「より小さい」だが、「<<」の元来の意味だって「左シフト」なのに、Rubyオフィシャルに「左に追加」の意味で使っているのだから、「<」を「左に渡す」の意味で使ったって構わんだろう。

  以上をまとめると、以下のようになる。

class String
    alias :perc :%
    def %(arg)
        self =~ /^:(.+)/ ? arg.pack($1.sub(/_$/, '*')) : perc(arg)
    end
end
 
class Array
    def <(packed)
        packed.unpack(self[0].to_s.sub(/_$/, '*'))
    end
end
 
':c_' % [65, 66, 67]
=> "ABC"
 
[:c_] < 'ABC'
=> [65, 66, 67]
 
':m_' % ['ABCDE']
=> "QUJDREU=\n"
 
[:m_] < 'QUJDREU='
=> ["ABCDE"]

  この記述方法だと、オレ的には澱みなく気分よくコードを記述できた感がある。まぁ、pack/unpackなんてそう頻繁に使うわけじゃないので実用性は薄いが、想像以上にイイ感じになったのでもったいないなぁ。


2024-03-06(Wed) SinatraのPOSTでrequest.body.rewindが動かない

  先日、Sinatraに認証機能を付けたのだが、その際にbundle addをやり直してGem関係を最新にしたらPOSTの内容がreadできなくなってしまった。

NoMethodError at / undefined method `rewind' for #<Rack::Lint::Wrapper::InputWrapper:0xXXXX @input=#<StringIO:0xXXXX>>

  なんて出る。何かやらかしたかと思ったが、特段イジっていない。該当のコードは「request.body.rewind」らしいのだが、だいぶ前なので、なんでそんなコードを書いたのか覚えていない。が、どうもSinatraのオフィシャルサイトのサンプルコードから持ってきていたようだ。

post "/api" do
  request.body.rewind  # in case someone already read it
  data = JSON.parse request.body.read
  "Hello #{data['name']}!"
end

  それなのに動かないってどういうことよ。と思って追跡していくと、どうもRackのバージョンアップの影響らしい。Gemfileの記述を戻すと動く。「request.body」のメソッドを表示させてみると、実際に変わってしまっていて「rewind」メソッドがなくなってる。そら動かん。bundleの仕組みは、今回のようにバージョンアップの影響で動かなくなることを防ぐ意図もあるのだが、実際にこんなことあるんやな。

# 動くバージョン
request.body: #<Rack::Lint::InputWrapper:0xXXXX @input=#<StringIO:0xXXXX>>
request.body.methods: [:!, :!=, :!~, :<=>, :==, :===, :__id__,
:__send__, :assert, :class, :clone, :close, :define_singleton_method,
:display, :dup, :each, :enum_for, :eql?, :equal?, :extend, :freeze,
:frozen?, :gem, :gets, :hash, :inspect, :instance_eval,
:instance_exec, :instance_of?, :instance_variable_defined?,
:instance_variable_get, :instance_variable_set, :instance_variables,
:is_a?, :itself, :kind_of?, :method, :methods, :nil?, :object_id,
:private_methods, :protected_methods, :public_method, :public_methods,
:public_send, :read, :remove_instance_variable, :respond_to?, :rewind,
:send, :singleton_class, :singleton_method, :singleton_methods, :tap,
:then, :to_enum, :to_json, :to_s, :yield_self]
request.body.read: "key=value1&key=value2&commit=do+POST+test"
# 動かないバージョン
request.body: #<Rack::Lint::Wrapper::InputWrapper:0xXXXX @input=#<StringIO:0xXXXX>>
request.body.methods: [:!, :!=, :!~, :<=>, :==, :===, :__id__,
:__send__, ※, :class, :clone, :close, :define_singleton_method, 
:display, :dup, :each, :enum_for, :eql?, :equal?, :extend, :freeze,
:frozen?, :gem, :gets, :hash, :inspect, :instance_eval,
:instance_exec, :instance_of?, :instance_variable_defined?,
:instance_variable_get, :instance_variable_set, :instance_variables,
:is_a?, :itself, :kind_of?, :method, :methods, :nil?, :object_id,
:private_methods, :protected_methods, :public_method, :public_methods,
:public_send, :read, :remove_instance_variable, :respond_to?, ※,
:send, :singleton_class, :singleton_method, :singleton_methods, :tap,
:then, :to_enum, :to_json, :to_s, :yield_self]
request.body.read: ""

  そんなら、バージョンを指定する仕組みもあるんかいな、と思ったらシッカリある。

$ gem search '^rack$' --all
rack (3.0.9.1, 3.0.9, 3.0.8, 3.0.7, 3.0.6.1, 3.0.6, 3.0.5, 3.0.4.2,
3.0.4.1, 3.0.4, 3.0.3, 3.0.2, 3.0.1, 3.0.0, 2.2.8.1, 2.2.8, 2.2.7,
2.2.6.4, 2.2.6.3, 2.2.6.2, 2.2.6.1, 2.2.6, 2.2.5, 2.2.4, 2.2.3.1,
2.2.3, 2.2.2, 2.2.1, 2.2.0, 2.1.4.4, 2.1.4.3, 2.1.4.2, 2.1.4.1, 2.1.4,
2.1.3, 2.1.2, 2.1.1, 2.1.0, 2.0.9.4, 2.0.9.3, 2.0.9.2, 2.0.9.1, 2.0.9,
2.0.8, 2.0.7, 2.0.6, 2.0.5, 2.0.4, 2.0.3, 2.0.2, 2.0.1, 1.6.13,
1.6.12, 1.6.11, 1.6.10, 1.6.9, 1.6.8, 1.6.7, 1.6.6, 1.6.5, 1.6.4,
1.6.3, 1.6.2, 1.6.1, 1.6.0, 1.5.5, 1.5.4, 1.5.3, 1.5.2, 1.5.1, 1.5.0,
1.4.7, 1.4.6, 1.4.5, 1.4.4, 1.4.3, 1.4.2, 1.4.1, 1.4.0, 1.3.10, 1.3.9,
1.3.8, 1.3.7, 1.3.6, 1.3.5, 1.3.4, 1.3.3, 1.3.2, 1.3.1, 1.3.0, 1.2.8,
1.2.7, 1.2.6, 1.2.5, 1.2.4, 1.2.3, 1.2.2, 1.2.1, 1.2.0, 1.1.6, 1.1.5,
1.1.4, 1.1.3, 1.1.2, 1.1.1, 1.1.0, 1.0.1, 1.0.0, 0.9.1, 0.9.0, 0.4.0,
0.3.0, 0.2.0, 0.1.0)
$ bundle add rack --version=2.2.8.1 --skip-install
$ bundle add puma sinatra net-ldap --skip-install

  バージョンを検索する仕組みもあるし、指定する仕組みもある。これはよくできているな。

  問題は「3.0.0」から起こるようだ。根も深そうなので、今回はバージョンを戻す形で対処することにする。

  エラく時間を浪費させられたので憤慨する気持ちもないわけではないが、まぁ、近年のフレームワークのラクチンさは、恐ろしく多数の物件を積み上げた成果なわけで、たまにはこういうのもしゃーない。というわけで、他の人の時間の浪費が防がれることを祈って、ここに記録しておく。

  というような作業の合間に引き続きドルアーガ。ついに1人を残して59階に到達。いくのか? いってしまうのか? あ゛ぁー……連続のミスで打倒ならず。まぁ、だいぶ精度は上がってきているけどなぁ。もう少しだなぁ。

  画像の説明


2024-03-10(Sun) 気づいたら走り出してたのさ

  というほどに簡単ではなかったが、まずは任意の場所(シルバーストン!)から走り出せるようになった。

 

  現状、ほぼ60FPSを確保できているが、もう少し高速化(というよりは負荷分散)する予定(動きがぎこちないのは録画した都合)。また、地図の切り替わりが遅いのはややワザとではあるが、さすがにレーシングスピードだとキャッシュは不可避かなぁ。


2024-03-11(Mon) そんなメルカトル、補正してやるッ!!

  ついに走り出したところで、次はなにを実装すべぇかなぁ、と考えなしに考えていたら、我ながら意外なところに向かってしまった。緯度補正だ。Google Map(ほかオンライン)の地図データはメルカトル図法なので、北に行くほど「より大きく」表示されるのだ。

  当初、そんなもん大した違いではないから無視しよう、と思っていたのだが、実は無視していいレベルの違いではなかった。だいぶ違う。一応は、実際に遊んだときに実際のF1のラップに近いタイムが出るようにしたいと思っているのだが、そうしたければとても無視できないレベルだ。

  とりあえず「鈴鹿」「シルバーストン」をテストの対象に開発を進めていたのだが、こうなれば赤道に至近なサーキットもテストの対象に加えるべきだ。最近はF1観てないんだよなぁ……なので、ググる。すると「マリーナベイ・ストリート・サーキット」がそれらしい。シンガポールGPが開催されている市街地サーキットで、北緯1度17分にある。ほぼ、赤道直下といっていい。

  画像の説明 画像の説明 画像の説明

  で、なにげなく座標データだけ入力してやると……「ちっさ」。左から「シルバーストン」「鈴鹿」「マリーナベイ」である。「シルバーストン」では気づかなかったが、こりゃ補正不可避である。幸い、開発したばかりの「BG版の回転拡大縮小機能」は、特段の負荷なしに自在に拡大縮小が可能だ。で、ここからは数学の時間である。まずは、一番直感的に書けるRubyで補正値を求めるプログラムを書いてみた。

include Math
 
# equ_px = (2 **  0) * 256                      # 赤道のピクセル数(ズームレベル0)
equ_px = (2 ** 20) * 256                        # 赤道のピクセル数(ズームレベル20)
equ_m = 40075 * 1000.0                          # 赤道の周長(m)
 
p equ_1px = equ_m / equ_px                      # 赤道下の1ピクセルの長さ(m)
#=> 0.1492910087108612
 
car_px = 24                                     # 車幅のピクセル数
car_m = 2.0                                     # 車幅(m)
 
p car_1px = car_m / car_px                      # 車の1ピクセルの長さ(m)
#=> 0.08333333333333333
 
p equ_times = car_1px / equ_1px                 # 赤道下の補正値
 
p 256 * equ_times                               # 回転拡大縮小機能への補正値
#=> 142.8976434518611

  まずは、赤道直下を対象にした補正値だ。使用する定数は「赤道の周長(40075km)」と「F1の車幅(2.0m)」だ。どちらもWikipediaで調べた。ゲームとしての基本的な仕様はスーパーフォーミュラをパク……オマージュるつもりなので、車のサイズは24x43ピクセルだ。2.0mを24ピクセルで表現したい、ということになる。回転拡大縮小機能に与える補正値は、サンプリングベクトルで256が標準値であり、それより小さい値を与えると拡大される。計算の結果、赤道直下の場合は143を与えればいいと出た。

  次は、緯度補正だ。緯度が高くなるにつれ地球の周長は減少していくが、それはコサイン一発で求められる。

p   [lat = 50, 'イギリス' ]
# p [lat = 35, '日本' ]
# p [lat =  0, '赤道' ]
 
p cos = Math.cos(lat * PI / 180)
p lat_times = car_1px / equ_1px / cos           # 任意の緯度下の補正値
 
p 256 * lat_times                               # 回転拡大縮小機能への補正値
#=> 222.30926872026404                          # [lat = 50, 'イギリス' ]
#=> 174.44581191992688                          # [lat = 35, '日本' ]
#=> 142.8976434518611                           # [lat =  0, '赤道' ]

  日本の場合は174、イギリスの場合は222を与えればいいと出た。概ね45度が7割なんだから合っていそうだ。

  次は、CoffeeScriptへの組み込み。ほぼ、上記のRubyのコードがそのまま動いたが、ひとつ考慮することがある。車を南北に移動した場合、補正値も変化させるべきか? ということだ。さすがに1フレームの移動毎に計算するのは過剰で、タイルをまたがったタイミング毎で十分だろうから処理は軽い。ゲーム内で使用する座標情報はWposというクラスで管理しているから、それに関数を追加するか……と実装しかけて気づいた。Wposクラスは経緯度で座標を与えられる仕様ではあるが、直後にメルカトル座標に変換して保持し、緯度情報は破棄してしまうのであった。ゲーム中の車の座標管理ならメルカトル座標のが扱いやすく、緯度を継続的に保持する必要性はないからだ。

  つうわけで、今回の目的は主にサーキットを走るのがメインであって、大陸縦断をするわけではないので、サーキットを選択した時点で補正値を計算し、それ以後の補正値の更新はなしとした。結果、コードは以下のようになった。

equ_times = car_1px / equ_1px                   # 赤道下の補正値
@lat_index = equ_times * 65536                  # 緯度の補正指数
@t = Math.round(@lat_index / Vec.v2vxy(128 - Math.round((@car.lat0 * 64) / 90.0), 1)[0])

  コサイン値は既存のVecクラスのテーブルの参照で済ませる。テーブルは90度を64分割、1.0を256として保持しているので上記のようになる。そして、表示させたスターティンググリッドが以下だ。

  画像の説明 画像の説明 画像の説明 画像の説明

  うぉーッ!! スターティンググリッドの位置が見事に揃いましたゼ、ダンナッ!! プログラミングって、こういう感じにわかりやすく美しい結果を得られた時が、たまらなく楽しい瞬間なんだよなぁ。これも今回、調べて初めて知ったことなのだが、スターティンググリッドの間隔は8mと決められているらしい。

  それではと、画面上のスターティンググリッドの間隔を測ってみると、ポールポジションと3番手のグリッドの間隔は192ピクセル。今回は2.0mを24ピクセルで表現しているのだから……16mッ!! パーフェクトだウォルター。

  ……と、ふと気になって、パク……オマージュり元のスーパーフォーミュラはどうなのかとおもったら……あれ? 狭い……つうか、ちょうど半分の間隔になってる!? なんだこれッ!? 偶然ッ!?