Hatena::ブログ(Diary)

Mi manca qualche giovedi`? このページをアンテナに追加 RSSフィード Twitter

2007-09-14 Rack でセッションを使う

Rack でセッションを使う

Rack を使って Web サーバで統一されたインターフェイスの利用する
http://d.hatena.ne.jp/secondlife/20070307/1173253661
Rack:a Ruby Webserver Interface
http://rack.rubyforge.org/

Rack を使えば生 RubyWEBrickMongrelFastCGI やいろいろあっても統一したインターフェースで書けるよ!

デバッグの時は WEBrick の方が挙動が落ち着いてるしスタックトレースも見やすいし。

でも本番はやっぱりせめて Mongrel か、ちょっと面倒だけど FastCGI がええな〜。


と啓蒙されたので最近 Rack を使い始めたんだが、ドキュメントがまるっきりない……

とりあえずセッションを使いたくて、Rack::Session::Cookie というのがそれっぽいのだが、ドキュメントがかけらもない。

しかたなくソースを読んで、しばし瞑想。うーん、こうかな?

require 'rubygems'
require 'rack'

test_app = Proc.new do |env|
  session_data = env["rack.session"]
  session_data["counter"] ||= 0
  session_data["counter"] += 1
  Rack::Response.new("counter : #{session_data["counter"]}").finish
end

app = Rack::URLMap.new([['/test', Rack::Session::Cookie.new(test_app)]])
Rack::Handler::Mongrel.run app, :Port => 11080

お、いけたいけた。リロードするたびにカウントアップ。

でも、Rack::Session::Cookie はセッションの内容を Marshal して cookie に積んでるだけなので、ちょっと苦しい。

やっぱりちゃんとセッションIDを発行してもきょもきょやりたいところ。


というわけで、Rack::Session::CookieCGI::Session を参考に、セッションIDに対応した Proc を作ってくれるクラスを書いてみた。

まだプチ試作なので、最適化が足りなかったり、セッションデータの expire が考慮されていなかったりするあたりは勘弁。あーあとちゃんとスレッドセーフかどうかも検証しきれてないかも( Hash#[] がスレッドセーフならおおむね大丈夫だと思うんだが)。

[3/18] Proc を継承する必要ないんじゃない、とのご指摘をいただいたので修正。

require 'rubygems'
require 'rack'

module Rack
  class SessionProc
    @@sessiondata = {}
    def initialize(opt={}, &proc)
      @proc = proc
      @key = opt[:key] || "rack.session.id"
      @default_options = {:domain => nil,
        :path => "/",
        :expire_after => nil}.merge(opt)
    end
    def call(env)
      load_session(env)
      status, headers, body = @proc.call(env)
      commit_session(env, status, headers, body)
    end

    private

    def load_session(env)
      request = Rack::Request.new(env)
      session_key = request.cookies[@key]
      env["rack.session.options"] = @default_options.dup
      session_data = env["rack.session"] = (@@sessiondata[session_key] || {}).dup

      def session_data.create_new_id(renew=false)
        return @old_session_id if !renew && @old_session_id
        require 'digest/md5'
        md5 = Digest::MD5::new
        now = Time::now
        md5.update(now.to_s)
        md5.update(String(now.usec))
        md5.update('foobar')
        md5.update($$.to_s)
        md5.update(String(rand(0)))
        @new_session_id = md5.hexdigest
      end
      def session_data.new_session_id
        @new_session_id
      end
      def session_data._init(id)
        @new_session_id = nil
        @old_session_id = id
        puts "@old_session_id: #{@old_session_id}"
        self
      end
      session_data._init session_key
    end

    def commit_session(env, status, headers, body)
      session_data = env["rack.session"]
      request = Rack::Request.new(env)
      session_key = request.cookies[@key]

      if session_data.new_session_id 
        @@sessiondata.delete(session_key) if session_key
        session_key = session_data.new_session_id
        puts "session_key: #{session_key}"
        @@sessiondata[session_key] = session_data

        options = env["rack.session.options"]
        cookie = {:value => session_key}
        cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil?
        response = Rack::Response.new(body, status, headers)
        response.set_cookie(@key, cookie.merge(options))
        return response.to_a

      elsif session_key && @@sessiondata[session_key] != session_data
        @@sessiondata[session_key] = session_data
      end
      [status, headers, body]
    end
  end
end

こんな風に使う。

普通に Proc.new するところを Rack::SessionProc.new して、session_data に対して create_new_id してあげるだけ。

require 'rubygems'
require 'rack'
require "sessionproc.rb"  # 追加

test_app = Rack::SessionProc.new do |env|
  session_data = env["rack.session"]
  session_data.create_new_id  # セッションID生成。すでに生成されている場合は何もしない(引数に true を指定で強制生成)
  
  session_data["counter"] ||= 0
  session_data["counter"] += 1
  Rack::Response.new("counter : #{session_data["counter"]}").finish
end

app = Rack::URLMap.new([['/test', test_app]])
Rack::Handler::Mongrel.run app, :Port => 10080

WEBrickMongrel で動作確認。FastCGI はいけるんじゃないかと思うんだけど、未確認。

セッションデータはオンメモリで保持しているので、CGI はアウトな気がする。

なかだなかだ 2008/02/21 14:23 SessionProcはProcを継承する必要ないですね。

n_shuyon_shuyo 2008/02/27 19:33 そうですね、そう言われてみればそんな気がします〜
実際に確認してから返事しようと思ってたんですがなかなか時間が取れなくて試せないので、とりあえずお返事。

n_shuyon_shuyo 2008/03/18 16:14 動作確認できたので記事を修正しました。
ありがとうございます〜

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/n_shuyo/20070914/rack