読者です 読者をやめる 読者になる 読者になる

人間とウェブの未来

「ウェブの歴史は人類の歴史の繰り返し」という観点から色々勉強しています。

Trusterd HTTP/2 Webサーバのmrubyによる設定と機能拡張について

trusterd HTTP/2 Web Serverは、設定や機能の拡張をRuby(mruby)で容易に実装できるという特徴を持つミドルウェアです。

また、別のアプリケーションにtrusterdのサーバ機能やクライアント機能を組み込めるという特徴も持っていますが、これについては以前記事で紹介しました。

C言語のアプリにmruby経由でtrusterdのHTTP/2サーバ機能を5分で組み込む方法 - 人間とウェブの未来

trusterdのHTTP/2クライアント機能をCアプリに組み込んでみよう - 人間とウェブの未来

今日は、trusterdの設定や機能拡張のためのコールバックの書き方を簡単に紹介します。

まずは基本設定を書く

trusterdの基本設定は以下のようになります。

SERVER_NAME = "Trusterd"
SERVER_VERSION = "0.0.1"
SERVER_DESCRIPTION = "#{SERVER_NAME}/#{SERVER_VERSION}"

root_dir = "/usr/local/trusterd"

s = HTTP2::Server.new({

  :port           => 8080,
  :document_root  => "#{root_dir}/htdocs",
  :server_name    => SERVER_DESCRIPTION,
  :run_user       => "daemon",
  :tls            => false,
  :callback       => true,
  :worker         => "auto",

})

各種パラメータの意味はREADMEや設定サンプルを御覧ください。基本的にはこの設定で、s.runすればサーバは起動するのですが、今回は引き続きコールバックの設定を記述してみます。そのために、:callback => trueが必要になります。デフォルトはfalseです。

set_map_to_strage_cb:URLとファイルのマッピングでコールバック

Webサーバにおいて、アクセスのあったURLとサーバ内のファイルを紐付ける処理があります。その処理のフェイズで、任意のマッピング処理を書いてみます。

s.set_map_to_strage_cb do
  # .phpにアクセスがあったら全てindex.htmlにマッピング
  s.location ".*\.php$" do
    s.filename = s.document_root + "/index.html"
  end

  # /helloにアクセスがあったらcontentコールバックを設定(後述)
  s.location "\/hello$" do
    # set content handler phase
    s.set_content_cb do
      s.echo "hello #{s.request_headers["user-agent"]} from #{s.conn.client_ip}, welcome to trusterd"
    end
  end
end

上記のように処理を書くことができます。内容はコメントの通りです。ここで、2つめのマッピング処理で利用しているset_content_cbは、レスポンス生成時にコールバックすることで、レスポンスを独自で生成することができます。set_content_cbset_map_to_strage_cbコールバックの外でも定義できますが、使い方としてはマッピング時に利用することが多いと思います。

set_content_cb:レスポンス生成時にコールバック

上述のset_map_to_strageで説明しましたが、レスポンス生成時に拡張でレスポンスボディを生成したい場合等に使用します。echorputsメソッドによってレスポンス出力できます。

set_access_checker_cb:URLマッピング後のアクセスチェックでコールバック

続いて、URLのマッピング後に実際にレスポンスを返していよいかどうかを判定し、アクセスを制御するためのコールバック処理を書いてみます。その場合はset_access_checker_cbを使います。

s.set_access_checker_cb do
  s.file "#{s.document_root}/index.cgi" do
    s.set_status 403
  end
end

上記の処理では、index.cgiというファイルが要求されたら、問答無用でFORBIDDENを返すという処理です。簡単ですね。ここでベーシック認証やRedisを使った様々な認証を書くことができるでしょう。

set_fixups_cb:レスポンス送信の直前でコールバック

レスポンヘッダーやレスポンスボディがほぼ確定し、後はレスポンスを送信するだけというタイミングでset_fixups_cbを使うと処理をコールバックできます。例えば、生成済みのレスポンスヘッダーをさらに上書きしたり追加したりすることもこのコールバックで処理できます。

s.set_fixups_cb do
  # lastヘッダを追加
  s.response_headers["last"] = "OK"

  # serverヘッダをアップデート
  s.response_headers["server"] = "change_server"

  # リクエストボディをヘッダに追加
  if ! s.body.nil?
    s.response_headers["post-data"] = s.body
  end
end

上記のコメントのように、ヘッダの追加や更新ができます。また、唐突にリクエストボディが出てきましたが、これもs.bodyによって取得することができるので、ここで使うのではなくもっと早い段階でリクエストボディを利用することもできます。

set_logging_cb:ログ生成時にコールバック

レスポンスを送った後のログ生成時にset_logging_cbを使うと処理をコールバックできます。今回は予め容易してるアクセスログのためのメソッドを使ってみます。まずはコールバックする前のsetup処理をコールバックの外に書きます。

s.setup_access_log({
  :file   => "#{root_dir}/logs/access.log",
  :format => :default,
  :type   => :plain,
})

このメソッドにより、アクセスログ出力先ファイルやフォーマット、出力タイプ等を指定します。:formatには:default:custom:typeには:plain:jsonが指定できます。

これを書いて上で、ログ生成時のコールバックを記述します。

s.set_logging_cb do

  s.write_access_log

end

今回は簡単のためにログ出力するだけの処理をコールバックします。この状態でログを出力すると、

127.0.0.1 - - [Wed, 18 Feb 2015 03:47:01 GMT] "GET /index.html HTTP/2" 200 22 "-" ""

と出力されます。また、

s.setup_access_log({
  :file   => "#{root_dir}/logs/access.log",
  :format => :default,
  :type   => :json,
})

として、json出力の場合は、

{"ip":"127.0.0.1","date":"Wed, 18 Feb 2015 05:09:54 GMT","scheme":"http","mehtod":"GET","status":200,"content_length":22,"uri":"/index.html","filename":"/usr/local/trusterd/htdocs/index.html","user_agent":"nghttp2/0.7.5-DEV"}

のように出力されます。

:customを指定した場合は、コールバック内のwrite_access_logにログ出力を引数で渡します。例えば、

s.set_logging_cb do

  log = {
    :ip => s.conn.client_ip,
    :date => s.date,
    :scheme => s.request_headers[":scheme"],
    :mehtod => s.request_headers[":method"],
    :status => s.status,
    :content_length => s.content_length,
    :uri => s.uri,
    :filename => s.filename,
    :user_agent => s.user_agent,
  }

  s.write_access_log JSON.stringify(log) + "\n"

end

等と書くことができます。これは、:default:jsonフォーマットと同様です。

まとめ

今回はtrusterdの基本的な設定とコールバックを使った簡単な機能拡張の例を紹介しました。息子が泣くので今は抱っこしながら片手で書いているので、まとめはこの辺りで...

今日紹介した設定はここにもありますので参考まで。

trusterd/trusterd.conf.callbacks.example.rb at master · trusterd/trusterd · GitHub

その他、サーバを起動しつつクライアントから情報を取るというような良くわからない設定も以下のように書くことができますので、色々遊べると思います。

SERVER_NAME = "Trusterd"
SERVER_VERSION = "0.0.1"
SERVER_DESCRIPTION = "#{SERVER_NAME}/#{SERVER_VERSION}"

root_dir = "/usr/local/trusterd"
run_user = "matsumotory"
new_config = JSON::stringify({:port => 8003, :worker => 3})

tls = HTTP2::Server.new({

  :port                            => 8080,
  :document_root                   => "#{root_dir}/htdocs",
  :server_name                     => SERVER_DESCRIPTION,

  :worker                          => "auto",
  :run_user                        => run_user,

  :rlimit_nofile                   => 65535,
  :write_packet_buffer_expand_size => 4096,
  :write_packet_buffer_limit_size  => 4096,

  :key                             => "#{root_dir}/ssl/server.key",
  :crt                             => "#{root_dir}/ssl/server.crt",
  :callback                        => true,

})

tls.set_content_cb {
  if tls.r.uri =~ /\/config\//
    tls.r.rputs new_config
  end
}

pid1 = Process.fork() { tls.run }
sleep 2

config = JSON::parse(HTTP2::Client.get("https://127.0.0.1:8080/config/").body)

puts "get new_port=#{config['port']} and n_worker=#{config['worker']}"

no_tls = HTTP2::Server.new({

  :port                            => config['port'],
  :document_root                   => "#{root_dir}/htdocs",
  :server_name                     => SERVER_DESCRIPTION + "no_tls",

  :worker                          => config['worker'],
  :run_user                        => run_user,

  :tls                             => false,

})

pid2 = Process.fork() { no_tls.run }

Process.waitpid pid1
Process.waitpid pid2