0

Rackミドルウェアの作り方を勉強した

今スイスにいる。
行きの飛行機の中での勉強用にこのページを保存しておいて、Rack middlewareの作り方を学んだ。

第25回 Rackとは何か(3)ミドルウェアのすすめ:Ruby Freaks Lounge|gihyo.jp … 技術評論社

というのもSinatra::RocketIOをRack::RocketIOにしたいからなんだけど、Rack Hijack APIがよくわからない。(hijackについてはそのうち書く)


ソースコードはgithubに全部置いてある。
github.com/shokai/rack-plugin-study


Rack

Sinatra/Rails/Padrino等のRuby製webアプリケーションフレームワークと、
webrick/thin/mongrel/unicorn等などのRuby製webサーバーを接続するしくみがRackです。

Webアプリフレームワークが、どのwebサーバーででも動くように接続部分を抽象化してくれている。

以前rackの上にsinatra風のフレームワークを実装してみた時に、くわしく解説した。
SinatraっぽいWAFを作る、46行で


rack middleware

rack middlewareを作ると、WebサーバーがWebアプリケーションフレームワーク(WAF)とやりとりするデータを書き換えたりできる。
rack::auth系の認証プラグインとか、rack::cache系のキャッシュ系が有名。
どちらもリクエストとレスポンスの間をいじる処理で、プラグインをsinatraやrailsで実装するよりもrackでやったほうが楽だと思った。

rackの実装はほぼ安定していてWAFほどコロコロ変わったりしないし、rack middlewareとして実装しておけばrailsでもsinatraでもpadrinoでも使える。



rack middlewareを作って、sinatraアプリから使えるかどうか試した。

まずSinatraアプリを準備する

まず、単なるHTMLを返すSinatraアプリを作る。文章は感嘆符や句読点が色々欲しかったので、艦これwikiからお借りしました。


main.rb

get '/' do
'<p><img src="/shokai.jpg"></p>
<p><遠征の成功条件(暫定)></p>
<p>編成隻数</p>
<p>特定の艦種を一定数以上?(条件が無い遠征もある?要検証)</p>
<p>旗艦のLv</p>
<p>編成合計Lv?(下記参照、要検証) 敵地偵察作戦の場合、軽巡洋艦を1隻含む6隻構成、旗艦Lv20以上、編成合計Lvは駆逐艦のみで構成した場合に100Lv以上。</p>
<p>旗艦に軽巡を据える必要性は無く、旗艦:駆逐改でも成功することが確認されている。</p>
<p>また、編成艦船の最低Lvは関係無いことも判明している。</p>'
end

このmain.rbをconfig.ruからrequireして、rackupすればsinatraアプリが起動する。

config.ru

require 'rack'
require 'sinatra'

require File.expand_path 'main', File.dirname(__FILE__)

run Sinatra::Application

rackup config.ru -p 5000



アプリのレスポンスを書き換えるrack middleware


語尾がクマ語になるrack middlewareを作った。
https://github.com/shokai/rack-plugin-study/tree/master/kuma

こうなる


kuma_response_filter.rb

クラスを作って、initializeとcallだけメソッドを宣言する。
# -*- coding: utf-8 -*-
class KumaResponseFilter
def initialize(app)
@app = app
end

def call(env)
res = @app.call env # sinatraに処理させる
if res[1]["Content-Type"] =~ /^text\/.+/
res[2] = res[2].map{|body_part|
body_part.gsub(/([。!?])/){|s| "クマ#{s}"}
}
res[1]["Content-Length"] = res[2].map{|body_part| body_part.bytesize }.inject{|a,b| a+b }.to_s
end
return res
end
end
sinatraに処理させてから、そのレスポンスがtextの時のみ中身をいじってから返している。
感嘆符や句読点の前にクマを挿入するだけ。
Content-Lengthが変わるので最後に計算し直す。


config.ru

use クラス名 するだけでプラグインが有効になって、response bodyが書き換えられる。
require 'rack'
require 'sinatra'
require File.expand_path 'kuma_response_filter', File.dirname(__FILE__)
use KumaResponseFilter

require File.expand_path 'main', File.dirname(__FILE__)

run Sinatra::Application


JPEG画像をglitchしてから配信するrack middleware


https://github.com/shokai/rack-plugin-study/tree/master/image_glitch


JPEG画像がこうなる。glitchはmakimotoさんのスライドを参考にした

image_glitch.rb

Sinatraのレスポンスをみて、image/jpegだったらa/b置換glitchする。
module Rack
class ImageGlitch
def initialize(app)
@app = app
end

def call(env)
res = @app.call env # sinatraに処理させる
if res[1]["Content-Type"] == "image/jpeg"
body = []
res[2].each do |i|
body.push i.gsub('a', 'b')
end
res[2] = body
end
res
end
end
end


これも、config.ruで
use Rack::ImageGlitch
するだけで有効になる。


リクエストがアプリに届く前に修正するrack middleware


https://github.com/shokai/rack-plugin-study/tree/master/jpeg2jpg

試しに、 .jpg を .jpegや.JPEG と打ち間違えてもレスポンスが返ってくるプラグインを作った。

.JpEGとかでリクエストされても、Sinatraのlogには.jpgとしてリクエストされた、と表示される。
127.0.0.1 - - [09/Sep/2013 15:31:26] "GET /shokai.jpg HTTP/1.1" 200 - 0.0024


jpeg_request_filter.rb

PATH_INFO, REQUEST_PATH, REQUEST_URI全部書き換える。
# -*- coding: utf-8 -*-
class JpegRequestFilter
def initialize(app)
@app = app
end

def call(env)
# sinatraに処理させる前にrequestを書き換える
if env["PATH_INFO"] =~ /.+\.jpeg$/i
["PATH_INFO", "REQUEST_PATH", "REQUEST_URI"].each do |k|
env[k].gsub!(/\.jpeg$/i, ".jpg")
end
end
res = @app.call env # sinatraに処理させる
end
end


まとめ

リクエスト・レスポンスを加工するならrackでやったほうが楽な場合もある。

2

SinatraっぽいWAFを作る、46行で

“Tamago”というWeb Application Frameworkを作った。
https://github.com/shokai/tamagoに置いてある。


昨日学校に行く前にメシを食いながらHerokuやSqaleやらPaaSについて調べていたら、SinatraやRailsではなくRackを直接使ってPaaSで動かしている人たちが何人かいた。
sqale使ってみた – komagataとか。


で、電車の中でふとWAFを作ってみたらどうかと思って作ってみた。
最終的に学校に着く頃にこんな風に書けるのができてた。
GETやらPOSTで指定したパスへのアクセスを受け取って、Hamlのテンプレートが使えたりするDSLが使える。

get '/' do
haml :index
end

post '/' do
Time.now.to_s
end

get '/env' do
ENV.keys.sort.map{|k|
"#{k}=#{ENV[k]}"
}.join("\n")
end
まんまSinatraの書き方で、何の面白みもない・・・



■Rackとは?
RubyにはWeb Application Framework(以下WAF)にRails、Sinatra、Merb、Padrinoやら色々なのがある。サーバーもThin、Mongorel、Webrick、Passenger+Apacheとか色々ある。これらのWAFとサーバーのうち、どれとどれを組み合わせても動くようにするために、RackというWAFとサーバーの接続方法が決められている。

すごくおおざっぱに言うと、callというクラスメソッドを持ったクラスを宣言して、call内でRack::Requestを受信してRack::Responseを返すようにするだけでいいらしい。



■Rackアプリ最小構成
これがRackを直接使う最小構成になる。
この例ではTamago.callの中でGETやPOSTの判別とかPathとか判別してHTMLを作ってしまっているが、適当にDSLとか決めたりして書けるように拡張したらWAFと呼んでいいだろう。

tamago.rb
require 'rubygems'
require 'rack'
require 'uri'

class Tamago
def self.call(env)
req = Rack::Request.new(env)
body = case req.request_method
when 'GET'
"<html><body><h1>#{URI.decode req.path_info}</h1></body></html>"
when 'POST'
req.params.keys.map{|k| "#{k}=#{req.params[k]}"}.join("\n")
end
Rack::Response.new { |r|
r.status = 200
r['Content-Type'] = 'text/html;charset=utf-8'
r.write body
}.finish
end
end


rackupの設定ファイルを書く
config.ru
require 'tamago'
run Tamago


起動する
rackup config.ru -p 8080

GETアクセスしたらURLのパスの部分がh1タグで囲まれたページがでてくる。



■Sinatraっぽいのを作る
HTTPリクエストをどう処理するかをDSLで書けて、Hamlのテンプレートが使えるようにする。

まずconfig.ru
require 'rubygems'
require File.dirname(__FILE__)+'/tamago'
require File.dirname(__FILE__)+'/app'

run Tamago::Application

WAF本体はこうなった。
本体をTamago::Applicationクラスにして、Viewを作る部分とかはまた別のクラスに分けた。
tamago.rb
require 'rubygems'
require 'rack'
require 'haml'

class Object
def method_missing(name, *args, &block)
case name
when :get, :post, :head, :delete
path = args[0]
Tamago.procs["[#{name.to_s.upcase}] #{path}"] = block
when :haml
Tamago::View.render args[0]
end
end
end

class Tamago
def self.procs
@@procs ||= Hash.new
end

class View
def self.render(template)
template = case true
when template.kind_of?(File)
template.read
when template.kind_of?(Symbol)
File.open("#{ENV['PWD']}/views/#{template}.haml").read
end
raise ArgumentError, 'Argument must be instance of File, String or Symbol.' unless template.kind_of? String
Haml::Engine.new(template).render
end
end

class Application
def self.call(env)
@request = Rack::Request.new(env)
body = Tamago.procs["[#{@request.request_method}] #{@request.path_info}"].call(self)
Rack::Response.new { |r|
r.status = 200
r['Content-Type'] = 'text/html;charset=utf-8'
r.write body
}.finish
end
end
end
app.rbのgetやpostは、config.ruからrequireされた時点で関数として呼ばれる。Object.method_missingを定義しておいてgetやpostを横取りし、blockの中身をHashに保存しておく。
Tamago.callでRack::Requestが来たら、Hashに保存しておいたblockを実行する。


最後にapp.rb
get '/' do
haml :index
end

post '/' do
Time.now.to_s
end

get '/env' do
ENV.keys.sort.map{|k|
"#{k}=#{ENV[k]}"
}.join("
")
end


起動
rackup config.ru -port 8080


というわけでRack上で簡単なWAFを作ってみた。
この分なら、たぶんRackミドルウェアとかを作るのもそんなに難しくないと思う。今度やってみよう。
Sinatraとかでも、Sinatraで書くよりRackの方をいじったほうがスマートに書ける事もあるだろうし。

0

Apache+Passengerでenvironmentの設定

いままでsinatra使う時にconfig.ruに

set :environemt, :production
とか
set :environemt, :development
って書いてたんだけど、httpd.confに
RackBaseURI /app_path
RackEnv production
って書けばいいのだった。