概要
webアプリでとあるパスへのリクエストをフックしてゴニョゴニョする。
そんな方法ないかなぁと思っていたらタイムリーな記事を見つけた。
開発環境でのみ、リクエスト毎になんか処理をフックしたい in Ruby
しかしrack middlewareってどんなもの?状態なので勉強からスタート。
rack middlewareの簡単なお勉強
Rackミドルウェアの作り方を勉強した
ここみたら何となく分かった。
簡単なmiddlewareを書いてみる
/hogeにアクセスが来たら、"hogehoge"を返すmiddleware。
元のアプリに/hogeの処理が定義してあっても、その処理を通さずにクライアントへ結果を返す。
そんなmiddlewareを書いてみた。
class MyRackMiddelware
def initialize(app)
@app = app
end
def call(env)
if env["PATH_INFO"] == '/hoge'
res = []
res[0] = 200
res[2] = [ "hogehoge" ]
content_length = res[2].inject(0) do |sum, content|
sum + content.bytesize
end
res[1] = { "Content-Type" => "text/html;charset=utf-8",
"Content-Length" => content_length.to_s,
"X-XSS-Protection" => "1; mode=block",
"X-Content-Type-Options" => "nosniff",
"X-Frame-Options" => "SAMEORIGIN"}
else
res = @app.call(env)
end
res
end
end
何のことはないinitializeとcallメソッドがあるだけの普通のクラス。
rack middlewareの形式は、
- リクエストの情報はenvに格納されている (詳細は後述)
- レスポンスの形式は配列で、[ステータスコード、ヘッダ、ボディ]の順に格納される
ここで使っているenv["PATH_INFO"]にはリクエストパスが格納されているので、これを使って特定のパスへのリクエストをフックしている。
@app.call(env)とすることで、本来呼び出されるWebアプリ(RailsやSinatra)の処理が呼ばれる。
レスポンスの中身を直接指定することで、全く違うページを返したり、リダイレクトさせたり、何かを作成したように見せかけたりできる。
つまり、何でもできてしまう。
(ちなみに、適当なボディだけでレスポンスを返したらContent-Lengthの不一致で怒られた。何でもできるけど、値はきちんと設定すべし。)
envの中身
既に記事を書かれている方がいたのでそちらのリンクを。
HTTP Request Headerがどういう感じでRackのenvに変換されるか
ざっくり言えば、
- 特定のヘッダはRackが埋めてくれる
- Rackで定義してないヘッダは、upcaseして、頭に"HTTP_"をつけたものがセットされる
実際にはこんな感じになってる。
{"rack.version"=>[1, 2],
"rack.input"=>#<StringIO:0x00000003a66be0>,
"rack.errors"=>#<StringIO:0x00000003a66e88>,
"rack.multithread"=>true,
"rack.multiprocess"=>true,
"rack.run_once"=>false,
"REQUEST_METHOD"=>"POST",
"SERVER_NAME"=>"example.org",
"SERVER_PORT"=>"80",
"QUERY_STRING"=>"",
"PATH_INFO"=>"/",
"rack.url_scheme"=>"http",
"HTTPS"=>"off",
"SCRIPT_NAME"=>"",
"CONTENT_LENGTH"=>"40",
"rack.test"=>true,
"REMOTE_ADDR"=>"127.0.0.1",
"CONTENT_TYPE"=>"application/x-www-form-urlencoded",
"HTTP_HOST"=>"example.org",
"HTTP_COOKIE"=>""}
postのbody
試している際にpostで送ったbodyが見つからず、???となった。
よく見れば...だったけど、同じハマり方してた人もいたので、一応記載。
env['rack.input']に入ってる。
StringIOオブジェクトで入っているので、getsで取り出す。
# 先ほどのenv
env["rack.input"] #=> #<StringIO:0x00000003a66be0>
env["rack.input"].gets #=> "email=test%40example.org%26pass=password"
使い方
- sinatraの場合 => config.ru に書く
# ...
require File.expand_path 'lib/my_rack_middleware', File.dirname(__FILE__)
use MyRackMiddleware
- railsの場合 => config/environments/xxx.rbやconfig/application.rb に書く
MyApp::Application.configure do
# ...
config.middleware.use MyRackMiddelware
end
テスト
別記事をupしたのでそちらを参照。