h2oでmrubyを使ってリダイレクト

概要

h2oを使っているときにリダイレクト処理を入れたい場合、PATHベースであれば下記のようにシンプルに記載が出来ます。

https://h2o.examp1e.net/configure/redirect_directives.html

    "/hoge":
      redirect:
        url:  https://inamuu.com/
        status: 301

しかし、クエリストリングを使っている場合に下記のように記述してもリダイレクトされません。

    "/?p=100":
      redirect:
        url:  https://inamuu.com/?p=100
        status: 301

そこでmrubyを使うことでクエリストリングが含まれている場合の書き方になります。

https://h2o.examp1e.net/configure/mruby.html

前提

h2oをmrubyで使うのにあたり、幾つか仕様として最低限覚えておかないといけないことがあります。

1つは、戻り値は配列にする必要があること。
下記のようなコードを書いたとします。

mruby.handler: |
  Proc.new do |env|
    if /\.(html|css)\z/.match(env["PATH_INFO"])
      p 'test'
    end
  end

そうすると、エラーログにarrayじゃなかったよって出力されます。

[h2o_mruby] in request:/hoge:mruby raised: RuntimeError: rack app did not return an array

配列は下記のように ステータスコード, ヘッダー, body にしてあげる必要があります。

Proc.new do |env|
  if /\.(html|css)\z/.match(env["PATH_INFO"])
    [200, { 'content-type' => 'text/plain' }, ['hoge']]
  end
end

次に、条件にマッチしなかったら、yamlの処理、つまり次に処理を進めたい場合はステータスコードを399で返す必要がある点です。
上記のコードは、/hoge/hoge.html は問題ありませんが、 /hogeInternal Server Error となります。

そこでマッチしなかったら、ステータス399を返すようにしてあげればその後の /var/www/html が参照されます。

Proc.new do |env|
  if /\.(html|css)\z/.match(env["PATH_INFO"])
    [200, { 'content-type' => 'text/plain'}, ['hoge']]
  else
    [399, {}, []]
  end
end
file.dir: /var/www/html

各クライアントからのリクエスト時に得られた値は、envハッシュに格納されています。
これらを参照することで、リクエスト時に色々な処理をしてあげることが出来るようになります。

{"REQUEST_METHOD"=>"GET", "SCRIPT_NAME"=>"/hoge", "PATH_INFO"=>"/", "QUERY_STRING"=>"p=100", "SERVER_NAME"=>"wiki.kazuma.tokyo", "SERVER_PROTOCOL"=>"HTTP/2", "SERVER_ADDR"=>"XXX.XXX.XXX.XXX", "SERVER_PORT"=>"443", "HTTP_HOST"=>"wiki.kazuma.tokyo", "REMOTE_ADDR"=>"XXX.XXX.XXX.XXX", "REMOTE_PORT"=>"56716", "HTTP_ACCEPT"=>"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "HTTP_COOKIE"=>"_ga=GAXXXXX; _gid=GAXXXX; _ga=GAXXXX; _gid=GAXXXX; XSRF-TOKEN=XXXX; laravel_session=XXXX", "HTTP_PRAGMA"=>"no-cache", "HTTP_USER_AGENT"=>"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36", "HTTP_AUTHORIZATION"=>"Basic XXXXX", "HTTP_CACHE_CONTROL"=>"no-cache", "HTTP_SEC_FETCH_DEST"=>"document", "HTTP_SEC_FETCH_MODE"=>"navigate", "HTTP_SEC_FETCH_SITE"=>"none", "HTTP_SEC_FETCH_USER"=>"?1", "HTTP_ACCEPT_ENCODING"=>"gzip, deflate, br", "HTTP_ACCEPT_LANGUAGE"=>"ja,en-US;q=0.9,en;q=0.8", "HTTP_UPGRADE_INSECURE_REQUESTS"=>"1", "rack.url_scheme"=>"https", "rack.multithread"=>false, "rack.multiprocess"=>true, "rack.run_once"=>false, "rack.hijack?"=>false, "rack.errors"=>#<IO:0x1c51f08>, "SERVER_SOFTWARE"=>"h2o/2.2.6"}

クエリストリングを評価してリダイレクト

env["QUERY_STRING"] を正規表現で評価して、マッチしたらLocationヘッダーを返すことで、リダイレクトされるようになります。

Proc.new do | env |
  if /(p\=14|p\=32|p\=37|p\=39)/.match(env["QUERY_STRING"])
     [301, { 'Location' => "https://#{env['HTTP_HOST']}/test/?#{env['QUERY_STRING']}"}, []]
  else
     [399, {}, []]
  end
end

自作のファイルを指定する

上記のように少なければ直接confに書いても良いとは思いますが、mrubyのファイルだけ別にしたい場合は下記のようにします。

"/hoge":
  mruby.handler-file: /etc/h2o/mruby/redirect.rb
  file.dir: /var/www/html  

ちなみに、過去には handler_path: というディレクティブが使われていたようで、それにしたところ下記のようにエラーが出てきました。

[/etc/h2o/conf.d/wiki.kazuma.tokyo.conf:55] in command mruby.handler_path, the command has been removed; see https://github.com/h2o/h2o/pull/467

そして、PRのリンクを参照すると、mruby.handler-fileに変わったことがわかります。

まとめ

前提条件をスッとばして書き始めると動きませんが、理解できると大変便利です。
個人的にはmruby.handler_pathのエラーにGitHubのリンクがあったことが大変感動的で(たまに特定のソフトウェアのエラーログにリンクがあったりすることがありますが)、今まで特にh2oは使ってこなかったのですがmrubyを使うことで色々処理していけることがわかったので、これからは積極的に採用してみようと思います。