SVX日記
2024-03-02(Sat) Sinatraでhtpasswd認証したりldap認証したり
回転のプログラミングの途中だが、ひょんなことから、Sinatraでウェブサービスを提供する各種のコンテナに認証機能を付ける必要が生じた。ハテ?基本的にはHAProxyを被せて運用しているのだが、認証ってどうすんだっけ?
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]
'%c%c%c' % [65, 66, 67]
=> ABC
「%」演算子を使っているのがミソだ。「文字列化する」「引数は配列」というイメージが自然に湧く。じゃ、逆に「配列化する」演算子はなんだ? 苦し紛れだが、こんなのはどうだ。こんな文法はないので動かないが。
'%c%c%c' << 'ABC' # 動きません
=> [65, 66, 67]
:c_ << 'ABC'
=> [65, 66, 67]
class Symbol
def <<(packed)
packed.unpack(self.to_s.sub(/_$/, '*'))
end
end
:m_ << 'QUJDREU='
=> ["ABCDE"]
'%m' % ['ABCDE'] # 動きません
=> "QUJDREU=\n"
class String
alias :perc :%
def %(arg)
self =~ /^:(.+)/ ? arg.pack($1.sub(/_$/, '*')) : perc(arg)
end
end
':m_' % ['ABCDE']
=> "QUJDREU=\n"
['ABCDE'].pack('m*')
=> "QUJDREU=\n"
'QUJDREU='.unpack('m*')[0]
=> "ABCDE"
2024-03-04(Mon) pack/unpackをよりどうにかする
たわむれは、おわり、のはずだったが、美作にいけてしまった(?)ので、もうちっとだけ続くんじゃ。以下は前回のコードだが。
:c_ << 'ABC'
=> [65, 66, 67]
[:c_] << 'ABC' # 動くけど……
=> [65, 66, 67]
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"]
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
エラく時間を浪費させられたので憤慨する気持ちもないわけではないが、まぁ、近年のフレームワークのラクチンさは、恐ろしく多数の物件を積み上げた成果なわけで、たまにはこういうのもしゃーない。というわけで、他の人の時間の浪費が防がれることを祈って、ここに記録しておく。
というような作業の合間に引き続きドルアーガ。ついに1人を残して59階に到達。いくのか? いってしまうのか? あ゛ぁー……連続のミスで打倒ならず。まぁ、だいぶ精度は上がってきているけどなぁ。もう少しだなぁ。
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, '赤道' ]
次は、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])
うぉーッ!! スターティンググリッドの位置が見事に揃いましたゼ、ダンナッ!! プログラミングって、こういう感じにわかりやすく美しい結果を得られた時が、たまらなく楽しい瞬間なんだよなぁ。これも今回、調べて初めて知ったことなのだが、スターティンググリッドの間隔は8mと決められているらしい。