Sinatra+PumaでSSL (TLS)クライアント証明書認証サイトを立てたり止めたりする

Playwright 1.46ではクライアント証明書認証が実装されたので、
これをクライアント証明書認証に対応する必要がある。
従来は Sinatra::Base
の run!
と quit!
を使用していた。

Pumaでクライアント証明書認証
PumaでSSLクライアント証明書認証するコードとして参考になりそうなのは、Puma自身の単体試験コード。
Puma::MiniSSL::Context
で証明書の設定をして、
Puma::Server#add_ssl_listener
で登録して、あとは Puma::Server
の run, stop を呼び出している。

Sinatra::Base#run! と Puma::Server#run の関係
Sinatra::Base#run!
Rack::Handler::Pumaのインスタンスを作成して、server_settingをハッシュにして、start_serverしている。なんかブロック引数もそのまま渡されている。 If given a block, will call with the constructed handler once we have taken the stage.
うーん、なんだろう。
Sinatra::Base#start_server (private)
Rack::Handler::Puma#run(app, server_setting)
のように呼び出している。呼び出し時にブロックが指定されて、 on_start_callback
が引数無しで呼ばれ、 Sinatra::Base#run!
から渡されたブロックが呼ばれる。
Rack::Handler::Puma#run
Puma::Launcher
を作って、 runを呼ぶ直前にlauncherを引数に渡してブロックを実行して Puma::Launcher#run
を実行している。
Puma::Launcher
argsが渡されて、
runはただ Puma::Runner#run
を呼ぶだけ。
Puma::Runner
基本的には Puma::Server#run
を呼ぶだけ。

一見すると、外からadd_ssl_listenerを仕掛ける口はなさそうに見える。
ただ、ソースをよく見てみると...
そもそも Puma::Server#add_ssl_listener
は Puma::Binder#add_ssl_listener
にスルーパスしているだけで、
Puma::Binder
のなかには .parse
というメソッドで ssl://
のようなURLが指定されると、クライアント証明書を指定してContextを作って、 Puma::Binder#add_ssl_listener
している実装がある。
Puma::Runnerの options[:bind]
が ssl://
ならいい。
おおもとは、 Rack::Handler::Pumaのほうで
このあたりか。
Rack::Handler::Puma#run
の最初に実行されている configっていうメソッドで、Hostが ssl://
なURLだったら自動的にSSLモードになる、と。

app.run!(port: port, bind: 'localhost')
だと
Puma starting in single mode...
* Puma version: 6.4.2 (ruby 3.3.0-p0) ("The Eagle of Durango")
* Min threads: 0
* Max threads: 5
* Environment: development
* PID: 66068
* Listening on http://127.0.0.1:8000
* Listening on http://[::1]:8000
app.run!(port: port, bind: '127.0.0.1')
だと
Puma starting in single mode...
* Puma version: 6.4.2 (ruby 3.3.0-p0) ("The Eagle of Durango")
* Min threads: 0
* Max threads: 5
* Environment: development
* PID: 66704
* Listening on http://127.0.0.1:8000
のように、 bind
パラメータを指定すると挙動は変わってそうだ。

ssl://localhost/?ca=xxx&cert=yyy
のように、クエリパラメータで指定したものが
ここでparamsに入り、そのままMiniSSL::ContextBuilderに渡されるっぽい。
verify_mode=force_peer を指定すると、ちょうどPumaのテストコードにあったクライアント証明書必須、になるようだ。