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|

2024-04-15(Mon) 遅ればせながら「DKIM」してみる

  「つぶやき」がメジャー化するにつれブログが衰退してきているように、各種チャットツールがメジャー化するにつれメールが衰退してきているように感じる。なんつうか「軽いノリ」が好まれるという傾向か。とはいえ、個人的には「つぶやき」では済まない記事を書くのが好きなので、このようにブログを書き続けているわけなのだが。

  個人的にメールの歴史はGmailの「前」か「後」に分かれると思う。無料にもかかわらずあまりに高機能なので個人的にメールサーバを運用する理由が消失してしまった。メーラの仕様には多少なりとも不満があるので個人的に皮を被せているが、Gmailの素晴らしさに疑う余地はない。

  そんな今日このごろだが「ドメインを勝手に使ってメールを送られないためのDNS設定」みたいな記事をふと目にして、ボチボチ「DKIM」とか何とかいうやつを勉強しておくべきかな、という気になった。つうのも、ちょっと前に神奈川県の高校入試関連システムが受験生にメールを出したが届かないトラブルが起きた、というニュースが心に引っかかっていたからだ。

  その文脈では、半ば「DKIM」が常識のように語られていたのだが、自分はほぼ聞いたことがなかった。OSサポートという仕事からは、ジャストではないものの、守備範囲としてはだいぶ近い。とはいえ、先のOAuth2.0の対応とか、Googleは攻めすぎている印象もある。

  というわけで、果たして自分は遅れているのか、それほどDKIM対応は難しいものなのか、そのヘンを明らかにするためにも、実際に設定をしてみたので以下にまとめるのである。

  まずは概念。なんか「3つくらいある」とボヤっと認識していたメール関連のセキュリティ機構だが、それは「SPF」「DKIM」「DMARC」の3つであった。「SPF」は簡単なので既に知っていたし「DMARC」は送る立場であれば必要性は薄く、重要なのは「DKIM」だけなのであった。

  「SPF」は、メール受信サーバがSMTP接続される都度、メールの「From」の送信元アドレスのドメインにDNS経由で照会し、SMTP接続してきた相手のIPが妥当か確認するものだ。DNSの汎用(TXT)レコードを流用する形の実装で、実に頭のいい実装である。

  「DKIM」は、それを先へ進めたもので、メール1通毎に「電子署名(ドメインの秘密鍵で署名)」を付けて送信し、メール受信サーバがメールを受信する都度、メールの「From」の送信元アドレスのドメインにDNS経由で「公開鍵」を要求し、メールの「電子署名」が妥当か確認するものらしい。こちらも、DNSの汎用(TXT)レコードを流用する形の実装になっている。

  「メール」「電子署名」というと、既に「S/MIME」があるじゃねーか、と思わなくもないが「S/MIME」が「個人の署名」「メーラへの実装」であるのに対して「DKIM」は「ドメインの署名」「メールサーバへの実装」という違いがある。誰もが「個人の公開鍵」を持ち、それを広く周知できる仕組みがあれば、それを使ってメールも認証すればいいわけなので「S/MIME」でもいいのだが、一般に「個人の公開鍵」を持つには金がかかる、という「セキュリティビジネス的な側面」の影響でそれができてないことに起因して、「DKIM」が生まれたという見方もできるだろう。

  別の見方をすると「組織に取り組ませる」は「誰もが取り組む必要がある」よりも格段に容易という見解も得られる。前者の例が法人税。後者の例がマイナンバ。前者と後者の中間の例がインボイス。「誰もが取り組む必要がある」ものは、仮にそれが正当でも実現は容易ではないということだ。

  やや脱線したが「DKIM」に戻る。送信時に「電子署名」の付与が必要になるが、それは「メールサーバへの実装」により行われる。具体的にメールサーバとはPostfixなのだが、そんな機能を持っているはずもない。調べていくと案の定だ。Milter機構を使って送信直前に署名処理を挟むらしい。Milter機構はsendmailの頃からあるが、個人的には気持ちのよくない実装である。

  それはそれとして、Milterの先に何を置くかというと、それは「opendkim」らしい。コマンドではなくデーモンだ。ちょっと大げさなような気がするが、処理するメールの数が多い場合は、都度プロセスを上げるより効率がいいのかしらん。

  不思議なのは「opendkim」がRHELには含まれないところ。場外の売場であるEPELでの扱いらしい。なんでも、何かRHEL要件に合わないということで、永久追放みたいな説明がある。永久追放て……ケンカでもしたのかしらん。デファクトスタンダードなGmailには必須技術なのに、デファクトスタンダードなRHELが対応しないのは矛盾だなぁ。サポートする立場としては責任放棄できる反面、扱わない説明に苦慮させられる予感がする。

  とはいえ、個人で使う分には勝手だ。まずは失敗から始めよう。何も考えず、VPS上にPostfixを起動し、自分のGmailアカウントに対してメールを送らせる。

# telnet localhost 25
mail from: xxxxxxxxxx@itline.jp
rcpt to: xxxxxxxxxx@gmail.com

  メールはPostfixには受け取ってもらえたが、PostfixとGmailサーバとのSMTPセッション内でエラーが発生して送信に失敗した。maillogには以下が記録されていた。

xxx xx xx:xx:xx xxxxxx postfix/smtp[xxxxxxx]: xxxxxxxxxx:
to=<xxxxxxxxxx@gmail.com>, relay=gmail-smtp-in.l.google.com[74.125.23.27]:25, delay=21, delays=20/0.03/0.81/0.81, dsn=5.7.26, 
status=bounced (host gmail-smtp-in.l.google.com[74.125.23.27] said: 
550-5.7.26 This mail has been blocked because the sender is unauthenticated. 
550-5.7.26 Gmail requires all senders to authenticate with either SPF or DKIM. 
550-5.7.26  
550-5.7.26  Authentication results: 
550-5.7.26  DKIM = did not pass 
550-5.7.26  SPF [itline.jp] with ip: [xxx.xxx.xxx.xxx] = did not pass 
550-5.7.26  
550-5.7.26  For instructions on setting up authentication, go to 
550 5.7.26  https://support.google.com/mail/answer/81126#authentication xxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.xxx  - gsmtp (in reply to end of DATA command))

  ちゃんと「SPFもDKIMも通っていない」と返された。改めて失敗させよう。DNSにSPFレコードを足してやる。

# cat /var/named/external.itline.jp
@               IN  TXT     "v=spf1 +ip4:xxx.xxx.xxx.xxx -all"

  すると意外なことにGmailアカウントにメールが届くようになった。ただし「迷惑メール」の中にではあるが。

  と、ここで満を持してDKIM様がリングに上がるのである。

# dnf install opendkim opendkim-tools
 
# opendkim-genkey -D /etc/opendkim/keys -d itline.jp
# ls /etc/opendkim/keys
default.private  default.txt
# chown opendkim:opendkim /etc/opendkim/keys/*
 
# vi opendkim.conf
# diff opendkim.conf.org opendkim.conf
< Mode	v
> Mode	s
< Umask	002
> Umask	000
< # KeyTable	/etc/opendkim/KeyTable
> KeyTable	/etc/opendkim/KeyTable
< # SigningTable	refile:/etc/opendkim/SigningTable
> SigningTable	refile:/etc/opendkim/SigningTable
 
# vi KeyTable
# diff KeyTable.org KeyTable
> default._domainkey.itline.jp itline.jp:default:/etc/opendkim/keys/default.private
 
# vi SigningTable
# diff SigningTable.org SigningTable
> *@itline.jp default._domainkey.itline.jp
 
# systemctl start opendkim
# systemctl status opendkim
# netstat -anp | grep dkim
unix  2      [ ACC ]     STREAM     LISTENING     181622904 3273917/opendkim     /run/opendkim/opendkim.sock
 
# ls -l /run/opendkim/opendkim.sock
srwxrwxrwx 1 opendkim opendkim 0  4月 13 20:54 /run/opendkim/opendkim.sock=
 
# chown opendkim:mail /run/opendkim
# ls -l /run | grep dkim
drwxr-x---  2 opendkim       mail             60  4月 13 20:54 opendkim/
 
# diff main.cf.org main.cf
> smtpd_milters = unix:/run/opendkim/opendkim.sock
> non_smtpd_milters = $smtpd_milters
> milter_default_action = accept
 
# cat /etc/opendkim/keys/default.txt >> external.itline.jp
# systemctl reload named-chroot

  ほぼ書いてあるどおりの内容だが、権限関係で少し引っかかった。「Umask 000」と「chown opendkim:mail /run/opendkim」だ。これをしないとソケットの権限の都合で、Postfixがopendkimへの接続に失敗し、署名が付かない。opendkimのパッケージングバグという印象も感じるが……EPEL扱いと関連があるのかしらん。なんにせよ設定を済ませたら、改めて自分のGmailアカウントに対してメールを送らせる。

# telnet localhost 25
mail from: xxxxxxxxxx@itline.jp
rcpt to: xxxxxxxxxx@gmail.com
 
# tail /var/log/maillog
xxx xx xx:xx:xx xxxxxx opendkim[xxxxxxx]: xxxxxxxxxx: DKIM-Signature field added (s=default, d=itline.jp)

  ちゃんとログに「署名を足したゼ」という報告が出ている。よっしゃよっしゃ……と、Gmailを開くが、メールがちっとも来ない……と、思ったら「迷惑メール」じゃなくて「受信トレイ」に届いていた。そらそうか。

  Gmailでは、メールを開いて、左上の「︙」アイコンから「メッセージのソースを表示」を実行することでDKIMの認証結果を確認することができる。

SPF:	PASS(IP: xxx.xxx.xxx.xxx)。詳細
DKIM:	'PASS'(ドメイン: itline.jp)詳細

  ならばよし! これにて「DKIM」対応は完了である。ま、別に使わないけどさ。

  ちなみに上で必要性は薄いと書いた「DMARC」だが、「DKIM」と同列に語られるものの、別にセキュリティレベルが向上するわけでもなんでもなく、DKIM関連の情報をやりとりするというだけの機構だ。送る立場であればDNSの設定に以下を加えるくらい。

_dmarc          IN TXT      "v=DMARC1; p=quarantine; rua=mailto:xxxxxxxxxx@gmail.com"

  この追加により、Gmailの認証結果に「DMARC」が加わる。「PASS」ってのもヘンだな。一応、メールアドレスを書いておいたら、結果がメールで届いたが。

SPF:	PASS(IP: xxx.xxx.xxx.xxx)。詳細
DKIM:	'PASS'(ドメイン: itline.jp)詳細
DMARC:	'PASS' 詳細

  つうわけで「DKIM」について十分に理解が進んだわけだが、それほど難しいわけでもないものの、RHELでは真っ当に使えない「opendkim」が必要というのは微妙。しかしそれとしても、メールサーバを立てるのにGmailに考えが及ばなかったシステム屋はちょっと十分にアレだと言える気がするな。拒否された際のエラーメッセージもこれ以上ないくらいにわかりやすいし。

  それはそれとしても、考えるほどに「SPF」だけで十分な気がするんだよな……と、ふとRFC化はされているのかいな、と思って調べてみると、3つともされているものの、なんと「SPF」は「DKIM」の後にRFC化されているようだ。え?それって、どういうこと? 「DKIM」は「SPF」を先へ進めたもの、じゃない?そういうこと?


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ッ!! パーフェクトだウォルター。

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


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

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

 

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


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-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-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-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-02-26(Mon) トラディショナルなバックグラウンドの回転拡大縮小技術を再現完了

  特定の位置を中心に回転拡大縮小ができるようになり、非常に使い勝手がよくなった。十分な速度であり、描画の結果も非常に正確だ。我ながら傑作コードである。

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

  自分で言うのも何だが、今回のコードには無駄な部分が一切存在しない。極めてシンプル。書きながら、煮詰めていくうちに、自然にそうなった。そこで、ハッと気づかされたのだ、当時の回転拡大縮小機能は「こう実装されていた」のかと。

  今回のコードは、ループの終了をゼロフラグで判断する(値比較で判断するより命令が減らせる)都合で、右下から左上にラスタースキャンするように回転パターンを生成しているが「ラスタースキャンするように生成している」のがミソだ。原理は同じなのだ。当時の回転拡大縮小機能も、ブラウン管モニタに「ラスタースキャンしながら回転パターンを生成していた」に違いないのである。PCG、スプライトなどと同様、CRTCに搭載されていた機能だったのだ。左上から順に、VRAMアドレス空間を「斜めに」参照しながら塗りつぶしていくという原理だったのだ。

  思えば、なんとなーく手を染め始めたWebAssemblyであったが、往年の回転拡大縮小機能の実装を再発明してしまうとは、気づけばエラいトコロに着地したもんだ。さて、レーシングゲームの実装を進めよう。


2024-02-23(Fri) アチラもコチラも微妙に進捗

  先日の思いつきを得て、BGの回転処理を作り始めたのだが、wasmのロード処理が非同期なのがどうにも扱いづらい。以前には悩んだ末にどうにかできたと思っていたのだが、ワザとヒドく遅延するプロキシを作って試しに通してみたら、以前に回っていたものも実はダメだった。JavaScriptの非同期処理はややこしく、検索すると多数ヒットするので悩んでいる人は多いようだ。

  結局、それだけで4日くらい地獄を這い回ってしまった。いや、Promiseとかasyncとかawaitとか、必要なときには便利なんだろうけど、必要じゃないときには邪魔すぎるんですが。UNIXのブロッキングみたいな扱いにできないもんすかね。ホント、本質じゃないところに時間かかり過ぎて殺意が湧きましたわ。まぁ、そのへんのコードは一段落したところで公開予定。

  その問題がクリアできたら、以前に自分が書いた回転処理を参考に、BGの回転処理を書くのだが、何しろアセンブラは可読性が悪く、自分の書いたコードとはいえ、再度、脳に染み込むまでには時間がかかる。んが、結局は大きくない修正でBGが回転するようになった。回転のサンプル画像には、知っている人なら知っている、回転するにふさわしい画像を使ってみたw。

  画像の説明

  BGの回転処理ではパターンのキャッシュは行わないので、都度の生成だが、手元の環境だと、240x320、60FPSでのCPU使用率は、Firefoxで20%、Chromeで6%程度。ちなみに240x320というサイズは、スーパーフォーミュラへのオマージュであるが、その前提であれば処理速度にはまだ十分に余裕があるといえよう。SIMD命令は、まぁ気が向いたら、で。次は、地球上の任意の場所を走り回れるようにするのがマイルストーンかな。

  そして、引き続きドルアーガ。上達するにつれて、難しい印象のフロアが変わってくるんだな。最近は、フロア10のレッドスライム、フロア45のナイトの重なり、ダークグリーンスライム、ブルーウィルオーウィスプが出てくるフロアがツラい。要するに「運が悪かった」で済まそうとしないと、テキが変わってくるというワケなのだ。

  画像の説明

  高次面で終わってそのままだと、その先が上達しないので、コンティニューしてクリアする。フロア59もだいぶ運が絡むよなぁ。もう少し練習したら、天野に出陣しようかしらん。


2024-02-19(Mon) オマエラが観たかったのはコ、レ!

  公開されてからしばらくして、突然に観たくなってきて「SEED FREEDOM」を観てきた。結論からいうと面白かった。大満足。以下、失礼な言い方を多数しているが、ほぼポジティブな意味で書いている。まさに「こういうのでいいんだよ」という映画だった。ホント、面白いって、なんだろう?

  振り返ればSEEDはDESTINYの途中くらいから、サカノボる形でハマったのだが、十分に楽しんだ反面、DESTINYでは、終盤で主人公を「戻す」という展開があり、どうもそれがネットの評判に影響を受けた結果のように感じて、なんだか安っぽいなぁ、というのが最後に残った印象であった。なので、しばらくして劇場化を計画中という話を聞いた時は、あんなグダグダの続きをどうしたいってんだ、と醒めていた。

  しかし、だ。時間のもたらす効果なのだろうか。無性に観たくなってしまった。どうせ、いつものペラい内容なのだろうし、ご都合主義なのだろうが、たぶん絶対に面白い予感しかない。

  画像の説明

  で、観た。せっかくなので4DXで観た。これまで4DXで観て損したと感じたことはないし、今回のモーションもバッチリであった。いや、肝心の映画の内容は安っぽい。20年前から、まったく進歩していない。登場人物は、パイロットであり政治家でありガキンチョだ。主にスキだのキライだので行動する。不殺というテーマらしいが、そんなのまったく感じさせないし、どうでもいい。でもそれでいい。もう、調味料をコレでもかコレでもかとブッかけすぎたトリプルチーズバーガーである。足せば足すほどウマいに違いないと思ってる。全部入りが正義だと思ってる。「面白ければなんでもいーんだよー」と思ってる。

  変に作家性を出さずに、同窓会に徹したのは、なかなかの選択ではないかと思う。目新しいテーマを立てたかっただろうし、新型モビルスーツを活躍させたかっただろうし、新曲を流したかっただろう、と思う。でもそこは、特段に妙なテーマを押し付けるわけでもなく、新型、新曲は序盤に片付けwて、クライマックスでは「いつもの」ですよ。「ストライクフリーダム」と「Meteor-ミーティア」ですよ。「オマエラが観たかったのはコレ」だろ、と。そうなんだよ「オレタチが観たかったのはソレ」なんだ、と。

  そういう意味では「トップガン マーヴェリック」にも似ているが、アッチはそれなりに時間の経過を感じさせているよね。コッチは、相変わらず「僕たちは何も守れてない」とか言って主人公がスネちゃうからね。成長していないにもホドがある。まぁ、劇中の時間では2年だからそれで妥当なのかもしれんが。

  こういう同窓会的な作り方をすると、次作が作りにくい気もするが、また10年後くらいにヒョコっと作る分にはいいかもしれない。DESTINYで「グフ」「ドム」まで、FREEDOMで「ギャン」「ズゴック」「ゲルググ」まで使ったから、まだ「ビグザム」「ジオング」あたりは残っているしなw。こういう過去ネタ頼りも、DESTINYの頃にはアザトさを感じたが、もう芸風として許せちゃう。ナイスバカ。あー、もう一回みたい。小ネタを全部回収したい。

  観終わって、深夜、雨の止んだ季節外れの暖かさの中を、オープンにしたロードスターでチンタラと帰りつつ振り返る。ロードスターも二番煎じな企画から始まって、代を重ねても「楽しければなんでもいーんだよー」だけで、ある意味、進歩させずに来たクルマなんだよな、と。なんだかシアワセを感じるな。

  自分は、以前エヴァのラストは新鮮味がないのがイマイチな原因だと書いたが、逆だったのかもしれないな。観たかったのが「いつもの」じゃなかったのが原因だったのかも。きっと「こういうのでいいんだよ」っていう感じのが観たかったんだ。

  マクロスもやりません? こういうの。なぜか、ミンメイが輝に迫ってきて、グラっときて、新型バルキリーで逃避行して、行き先で壊されて、なぜだかそこにあったVF-1で脱出するけど、ピンチになったところに、マクロスが来てダイダロスアタックして、美沙とモトサヤって感じでいいのでw。

  ホント、面白いって、なんなんだろう……?w