~michaelforrest/use-case-mapper/trunk

« back to all changes in this revision

Viewing changes to vendor/rails/actionpack/lib/action_controller/session/cookie_store.rb

  • Committer: Richard Lee (Canonical)
  • Date: 2010-10-15 15:17:58 UTC
  • mfrom: (190.1.3 use-case-mapper)
  • Revision ID: richard.lee@canonical.com-20101015151758-wcvmfxrexsongf9d
Merge

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
module ActionController
2
 
  module Session
3
 
    # This cookie-based session store is the Rails default. Sessions typically
4
 
    # contain at most a user_id and flash message; both fit within the 4K cookie
5
 
    # size limit. Cookie-based sessions are dramatically faster than the
6
 
    # alternatives.
7
 
    #
8
 
    # If you have more than 4K of session data or don't want your data to be
9
 
    # visible to the user, pick another session store.
10
 
    #
11
 
    # CookieOverflow is raised if you attempt to store more than 4K of data.
12
 
    #
13
 
    # A message digest is included with the cookie to ensure data integrity:
14
 
    # a user cannot alter his +user_id+ without knowing the secret key
15
 
    # included in the hash. New apps are generated with a pregenerated secret
16
 
    # in config/environment.rb. Set your own for old apps you're upgrading.
17
 
    #
18
 
    # Session options:
19
 
    #
20
 
    # * <tt>:secret</tt>: An application-wide key string or block returning a
21
 
    #   string called per generated digest. The block is called with the
22
 
    #   CGI::Session instance as an argument. It's important that the secret
23
 
    #   is not vulnerable to a dictionary attack. Therefore, you should choose
24
 
    #   a secret consisting of random numbers and letters and more than 30
25
 
    #   characters. Examples:
26
 
    #
27
 
    #     :secret => '449fe2e7daee471bffae2fd8dc02313d'
28
 
    #     :secret => Proc.new { User.current_user.secret_key }
29
 
    #
30
 
    # * <tt>:digest</tt>: The message digest algorithm used to verify session
31
 
    #   integrity defaults to 'SHA1' but may be any digest provided by OpenSSL,
32
 
    #   such as 'MD5', 'RIPEMD160', 'SHA256', etc.
33
 
    #
34
 
    # To generate a secret key for an existing application, run
35
 
    # "rake secret" and set the key in config/environment.rb.
36
 
    #
37
 
    # Note that changing digest or secret invalidates all existing sessions!
38
 
    class CookieStore
39
 
      # Cookies can typically store 4096 bytes.
40
 
      MAX = 4096
41
 
      SECRET_MIN_LENGTH = 30 # characters
42
 
 
43
 
      DEFAULT_OPTIONS = {
44
 
        :key          => '_session_id',
45
 
        :domain       => nil,
46
 
        :path         => "/",
47
 
        :expire_after => nil,
48
 
        :httponly     => true
49
 
      }.freeze
50
 
 
51
 
      ENV_SESSION_KEY = "rack.session".freeze
52
 
      ENV_SESSION_OPTIONS_KEY = "rack.session.options".freeze
53
 
      HTTP_SET_COOKIE = "Set-Cookie".freeze
54
 
 
55
 
      # Raised when storing more than 4K of session data.
56
 
      class CookieOverflow < StandardError; end
57
 
 
58
 
      def initialize(app, options = {})
59
 
        # Process legacy CGI options
60
 
        options = options.symbolize_keys
61
 
        if options.has_key?(:session_path)
62
 
          options[:path] = options.delete(:session_path)
63
 
        end
64
 
        if options.has_key?(:session_key)
65
 
          options[:key] = options.delete(:session_key)
66
 
        end
67
 
        if options.has_key?(:session_http_only)
68
 
          options[:httponly] = options.delete(:session_http_only)
69
 
        end
70
 
 
71
 
        @app = app
72
 
 
73
 
        # The session_key option is required.
74
 
        ensure_session_key(options[:key])
75
 
        @key = options.delete(:key).freeze
76
 
 
77
 
        # The secret option is required.
78
 
        ensure_secret_secure(options[:secret])
79
 
        @secret = options.delete(:secret).freeze
80
 
 
81
 
        @digest = options.delete(:digest) || 'SHA1'
82
 
        @verifier = verifier_for(@secret, @digest)
83
 
 
84
 
        @default_options = DEFAULT_OPTIONS.merge(options).freeze
85
 
 
86
 
        freeze
87
 
      end
88
 
 
89
 
      def call(env)
90
 
        env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env)
91
 
        env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
92
 
 
93
 
        status, headers, body = @app.call(env)
94
 
 
95
 
        session_data = env[ENV_SESSION_KEY]
96
 
        options = env[ENV_SESSION_OPTIONS_KEY]
97
 
 
98
 
        if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?) || options[:expire_after]
99
 
          session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.send(:loaded?)
100
 
          session_data = marshal(session_data.to_hash)
101
 
 
102
 
          raise CookieOverflow if session_data.size > MAX
103
 
 
104
 
          cookie = Hash.new
105
 
          cookie[:value] = session_data
106
 
          unless options[:expire_after].nil?
107
 
            cookie[:expires] = Time.now + options[:expire_after]
108
 
          end
109
 
 
110
 
          cookie = build_cookie(@key, cookie.merge(options))
111
 
          unless headers[HTTP_SET_COOKIE].blank?
112
 
            headers[HTTP_SET_COOKIE] << "\n#{cookie}"
113
 
          else
114
 
            headers[HTTP_SET_COOKIE] = cookie
115
 
          end
116
 
        end
117
 
 
118
 
        [status, headers, body]
119
 
      end
120
 
 
121
 
      private
122
 
        # Should be in Rack::Utils soon
123
 
        def build_cookie(key, value)
124
 
          case value
125
 
          when Hash
126
 
            domain  = "; domain="  + value[:domain] if value[:domain]
127
 
            path    = "; path="    + value[:path]   if value[:path]
128
 
            # According to RFC 2109, we need dashes here.
129
 
            # N.B.: cgi.rb uses spaces...
130
 
            expires = "; expires=" + value[:expires].clone.gmtime.
131
 
              strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
132
 
            secure = "; secure" if value[:secure]
133
 
            httponly = "; HttpOnly" if value[:httponly]
134
 
            value = value[:value]
135
 
          end
136
 
          value = [value] unless Array === value
137
 
          cookie = Rack::Utils.escape(key) + "=" +
138
 
            value.map { |v| Rack::Utils.escape(v) }.join("&") +
139
 
            "#{domain}#{path}#{expires}#{secure}#{httponly}"
140
 
        end
141
 
 
142
 
        def load_session(env)
143
 
          request = Rack::Request.new(env)
144
 
          session_data = request.cookies[@key]
145
 
          data = unmarshal(session_data) || persistent_session_id!({})
146
 
          [data[:session_id], data]
147
 
        end
148
 
 
149
 
        # Marshal a session hash into safe cookie data. Include an integrity hash.
150
 
        def marshal(session)
151
 
          @verifier.generate(persistent_session_id!(session))
152
 
        end
153
 
 
154
 
        # Unmarshal cookie data to a hash and verify its integrity.
155
 
        def unmarshal(cookie)
156
 
          persistent_session_id!(@verifier.verify(cookie)) if cookie
157
 
        rescue ActiveSupport::MessageVerifier::InvalidSignature
158
 
          nil
159
 
        end
160
 
 
161
 
        def ensure_session_key(key)
162
 
          if key.blank?
163
 
            raise ArgumentError, 'A key is required to write a ' +
164
 
              'cookie containing the session data. Use ' +
165
 
              'config.action_controller.session = { :key => ' +
166
 
              '"_myapp_session", :secret => "some secret phrase" } in ' +
167
 
              'config/environment.rb'
168
 
          end
169
 
        end
170
 
 
171
 
        # To prevent users from using something insecure like "Password" we make sure that the
172
 
        # secret they've provided is at least 30 characters in length.
173
 
        def ensure_secret_secure(secret)
174
 
          # There's no way we can do this check if they've provided a proc for the
175
 
          # secret.
176
 
          return true if secret.is_a?(Proc)
177
 
 
178
 
          if secret.blank?
179
 
            raise ArgumentError, "A secret is required to generate an " +
180
 
              "integrity hash for cookie session data. Use " +
181
 
              "config.action_controller.session = { :key => " +
182
 
              "\"_myapp_session\", :secret => \"some secret phrase of at " +
183
 
              "least #{SECRET_MIN_LENGTH} characters\" } " +
184
 
              "in config/environment.rb"
185
 
          end
186
 
 
187
 
          if secret.length < SECRET_MIN_LENGTH
188
 
            raise ArgumentError, "Secret should be something secure, " +
189
 
              "like \"#{ActiveSupport::SecureRandom.hex(16)}\".  The value you " +
190
 
              "provided, \"#{secret}\", is shorter than the minimum length " +
191
 
              "of #{SECRET_MIN_LENGTH} characters"
192
 
          end
193
 
        end
194
 
 
195
 
        def verifier_for(secret, digest)
196
 
          key = secret.respond_to?(:call) ? secret.call : secret
197
 
          ActiveSupport::MessageVerifier.new(key, digest)
198
 
        end
199
 
 
200
 
        def generate_sid
201
 
          ActiveSupport::SecureRandom.hex(16)
202
 
        end
203
 
 
204
 
        def persistent_session_id!(data)
205
 
          (data ||= {}).merge!(inject_persistent_session_id(data))
206
 
        end
207
 
 
208
 
        def inject_persistent_session_id(data)
209
 
          requires_session_id?(data) ? { :session_id => generate_sid } : {}
210
 
        end
211
 
 
212
 
        def requires_session_id?(data)
213
 
          if data
214
 
            data.respond_to?(:key?) && !data.key?(:session_id)
215
 
          else
216
 
            true
217
 
          end
218
 
        end
219
 
    end
220
 
  end
221
 
end