rack middlewareをざっくり触ってみた

## 概要

webアプリでとあるパスへのリクエストをフックしてゴニョゴニョする。
そんな方法ないかなぁと思っていたらタイムリーな記事を見つけた。

[開発環境でのみ、リクエスト毎になんか処理をフックしたい in Ruby](http://nekogata.hatenablog.com/entry/2014/08/22/162243)

しかしrack middlewareってどんなもの？状態なので勉強からスタート。

## rack middlewareの簡単なお勉強

[Rackミドルウェアの作り方を勉強した](http://shokai.org/blog/archives/8233)
ここみたら何となく分かった。

## 簡単なmiddlewareを書いてみる

/hogeにアクセスが来たら、"hogehoge"を返すmiddleware。
元のアプリに/hogeの処理が定義してあっても、その処理を通さずにクライアントへ結果を返す。
そんなmiddlewareを書いてみた。

```rb:my_rack_middleware.rb
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に変換されるか](http://qiita.com/udzura/items/b558baffcd1421773d97)

ざっくり言えば、

* 特定のヘッダはRackが埋めてくれる
* Rackで定義してないヘッダは、upcaseして、頭に"HTTP_"をつけたものがセットされる

実際にはこんな感じになってる。

```rb:env
{"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が見つからず、???となった。
よく見れば...だったけど、同じハマり方してた人もいたので、一応記載。

http://stackoverflow.com/questions/15458334/how-to-read-post-data-in-rack-request

env['rack.input']に入ってる。
StringIOオブジェクトで入っているので、getsで取り出す。

```rb
# 先ほどのenv
env["rack.input"]       #=> #<StringIO:0x00000003a66be0>
env["rack.input"].gets  #=> "email=test%40example.org%26pass=password"

```

## 使い方

* sinatraの場合 => config.ru に書く

```rb:config.ru
# ...
require File.expand_path 'lib/my_rack_middleware', File.dirname(__FILE__)
use MyRackMiddleware
```

* railsの場合 => config/environments/xxx.rbやconfig/application.rb に書く

```rb:config/environments/development.rb
MyApp::Application.configure do
  # ...
  config.middleware.use MyRackMiddelware
end
```

## テスト

別記事をupしたのでそちらを参照。

* [rack middlewareのテストをrspecで書いてみた](http://qiita.com/ogawatti/items/4db6dbdfb2405342e0da)