summary refs log tree commit
diff options
context:
space:
mode:
authorAaron Patterson <aaron.patterson@gmail.com>2010-12-18 06:01:13 +0800
committerJoshua Peek <josh@joshpeek.com>2010-12-20 04:00:54 +0800
commit260fca3dd54d071bd0e69cd6a6381c979acc9f36 (patch)
treed06e183941f53d2bc28ac0a911bd189d4515d387
parenta5dae21c4f9044a7422aebd6f1137ef94a20f684 (diff)
downloadrack-260fca3dd54d071bd0e69cd6a6381c979acc9f36.tar.gz
Cookies may be configured with an object that will serialize and deserialize the session cookie data. fixes #70, fixes #4
-rw-r--r--lib/rack/session/cookie.rb65
-rw-r--r--test/spec_session_cookie.rb51
2 files changed, 111 insertions, 5 deletions
diff --git a/lib/rack/session/cookie.rb b/lib/rack/session/cookie.rb
index d398a0a0..b9bbea91 100644
--- a/lib/rack/session/cookie.rb
+++ b/lib/rack/session/cookie.rb
@@ -8,8 +8,11 @@ module Rack
   module Session
 
     # Rack::Session::Cookie provides simple cookie based session management.
-    # The session is a Ruby Hash stored as base64 encoded marshalled data
-    # set to :key (default: rack.session).
+    # By default, the session is a Ruby Hash stored as base64 encoded marshalled
+    # data set to :key (default: rack.session).  The object that encodes the
+    # session data is configurable and must respond to +encode+ and +decode+.
+    # Both methods must take a string and return a string.
+    #
     # When the secret key is set, cookie data is checked for data integrity.
     #
     # Example:
@@ -21,10 +24,63 @@ module Rack
     #                                :secret => 'change_me'
     #
     #     All parameters are optional.
+    #
+    # Example of a cookie with no encoding:
+    #
+    #   Rack::Session::Cookie.new(application, {
+    #     :coder => Racke::Session::Cookie::Identity.new
+    #   })
+    #
+    # Example of a cookie with custom encoding:
+    #
+    #   Rack::Session::Cookie.new(application, {
+    #     :coder => Class.new {
+    #       def encode(str); str.reverse; end
+    #       def decode(str); str.reverse; end
+    #     }.new
+    #   })
+    #
 
     class Cookie < Abstract::ID
+      # Encode session cookies as Base64
+      class Base64
+        def encode(str)
+          [str].pack('m')
+        end
+
+        def decode(str)
+          str.unpack('m').first
+        end
+
+        # Encode session cookies as Marshaled Base64 data
+        class Marshal < Base64
+          def encode(str)
+            super(::Marshal.dump(str))
+          end
+
+          def decode(str)
+            ::Marshal.load(super(str)) rescue nil
+          end
+        end
+      end
+
+      # Use no encoding for session cookies
+      class Identity
+        def encode(str); str; end
+        def decode(str); str; end
+      end
+
+      # Reverse string encoding. (trollface)
+      class Reverse
+        def encode(str); str.reverse; end
+        def decode(str); str.reverse; end
+      end
+
+      attr_reader :coder
+
       def initialize(app, options={})
         @secret = options.delete(:secret)
+        @coder  = options.delete(:coder) || Base64::Marshal.new
         super(app, options.merge!(:cookie_only => true))
       end
 
@@ -50,8 +106,7 @@ module Rack
             session_data = nil  unless digest == generate_hmac(session_data)
           end
 
-          data = Marshal.load(session_data.unpack("m*").first) rescue nil
-          data || {}
+          coder.decode(session_data) || {}
         end
       end
 
@@ -69,7 +124,7 @@ module Rack
 
       def set_session(env, session_id, session, options)
         session = persistent_session_id!(session, session_id)
-        session_data = [Marshal.dump(session)].pack("m*")
+        session_data = coder.encode(session)
 
         if @secret
           session_data = "#{session_data}--#{generate_hmac(session_data)}"
diff --git a/test/spec_session_cookie.rb b/test/spec_session_cookie.rb
index e37141fd..7e6bfb15 100644
--- a/test/spec_session_cookie.rb
+++ b/test/spec_session_cookie.rb
@@ -18,6 +18,57 @@ describe Rack::Session::Cookie do
     Rack::Response.new("Nothing").to_a
   end
 
+  describe 'Base64' do
+    it 'uses base64 to encode' do
+      coder = Rack::Session::Cookie::Base64.new
+      str   = 'fuuuuu'
+      coder.encode(str).should.equal [str].pack('m')
+    end
+
+    it 'uses base64 to decode' do
+      coder = Rack::Session::Cookie::Base64.new
+      str   = ['fuuuuu'].pack('m')
+      coder.decode(str).should.equal str.unpack('m').first
+    end
+
+    describe 'Marshal' do
+      it 'marshals and base64 encodes' do
+        coder = Rack::Session::Cookie::Base64::Marshal.new
+        str   = 'fuuuuu'
+        coder.encode(str).should.equal [::Marshal.dump(str)].pack('m')
+      end
+
+      it 'marshals and base64 decodes' do
+        coder = Rack::Session::Cookie::Base64::Marshal.new
+        str   = [::Marshal.dump('fuuuuu')].pack('m')
+        coder.decode(str).should.equal ::Marshal.load(str.unpack('m').first)
+      end
+
+      it 'rescues failures on decode' do
+        coder = Rack::Session::Cookie::Base64::Marshal.new
+        coder.decode('lulz').should.equal nil
+      end
+    end
+  end
+
+  it 'uses a coder' do
+    identity = Class.new {
+      attr_reader :calls
+
+      def initialize
+        @calls = []
+      end
+
+      def encode(str); @calls << :encode; str; end
+      def decode(str); @calls << :decode; str; end
+    }.new
+    cookie = Rack::Session::Cookie.new(incrementor, :coder => identity)
+    res = Rack::MockRequest.new(cookie).get("/")
+    res["Set-Cookie"].should.include("rack.session=")
+    res.body.should.equal '{"counter"=>1}'
+    identity.calls.should.equal [:decode, :encode]
+  end
+
   it "creates a new cookie" do
     res = Rack::MockRequest.new(Rack::Session::Cookie.new(incrementor)).get("/")
     res["Set-Cookie"].should.include("rack.session=")