rack 1.1.6 → 1.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +1 -1
  3. data/HISTORY.md +375 -0
  4. data/KNOWN-ISSUES +23 -0
  5. data/README.rdoc +312 -0
  6. data/Rakefile +124 -0
  7. data/SPEC +125 -32
  8. data/contrib/rack.png +0 -0
  9. data/contrib/rack.svg +150 -0
  10. data/contrib/rack_logo.svg +1 -1
  11. data/contrib/rdoc.css +412 -0
  12. data/example/protectedlobster.rb +1 -1
  13. data/lib/rack/auth/abstract/handler.rb +4 -4
  14. data/lib/rack/auth/abstract/request.rb +7 -5
  15. data/lib/rack/auth/basic.rb +1 -1
  16. data/lib/rack/auth/digest/md5.rb +7 -3
  17. data/lib/rack/auth/digest/nonce.rb +1 -1
  18. data/lib/rack/auth/digest/params.rb +7 -9
  19. data/lib/rack/auth/digest/request.rb +10 -9
  20. data/lib/rack/backports/uri/common_18.rb +56 -0
  21. data/lib/rack/backports/uri/common_192.rb +52 -0
  22. data/lib/rack/backports/uri/common_193.rb +29 -0
  23. data/lib/rack/body_proxy.rb +39 -0
  24. data/lib/rack/builder.rb +106 -22
  25. data/lib/rack/cascade.rb +17 -6
  26. data/lib/rack/chunked.rb +44 -24
  27. data/lib/rack/commonlogger.rb +36 -13
  28. data/lib/rack/conditionalget.rb +49 -17
  29. data/lib/rack/config.rb +5 -0
  30. data/lib/rack/content_length.rb +14 -6
  31. data/lib/rack/content_type.rb +7 -1
  32. data/lib/rack/deflater.rb +73 -15
  33. data/lib/rack/directory.rb +18 -8
  34. data/lib/rack/etag.rb +59 -9
  35. data/lib/rack/file.rb +106 -44
  36. data/lib/rack/handler/cgi.rb +11 -11
  37. data/lib/rack/handler/fastcgi.rb +18 -6
  38. data/lib/rack/handler/lsws.rb +2 -4
  39. data/lib/rack/handler/mongrel.rb +22 -6
  40. data/lib/rack/handler/scgi.rb +16 -8
  41. data/lib/rack/handler/thin.rb +19 -4
  42. data/lib/rack/handler/webrick.rb +72 -19
  43. data/lib/rack/handler.rb +47 -14
  44. data/lib/rack/head.rb +10 -2
  45. data/lib/rack/lint.rb +260 -75
  46. data/lib/rack/lobster.rb +13 -8
  47. data/lib/rack/lock.rb +13 -3
  48. data/lib/rack/logger.rb +0 -2
  49. data/lib/rack/methodoverride.rb +27 -8
  50. data/lib/rack/mime.rb +625 -167
  51. data/lib/rack/mock.rb +78 -53
  52. data/lib/rack/multipart/generator.rb +93 -0
  53. data/lib/rack/multipart/parser.rb +253 -0
  54. data/lib/rack/multipart/uploaded_file.rb +34 -0
  55. data/lib/rack/multipart.rb +34 -0
  56. data/lib/rack/nulllogger.rb +21 -2
  57. data/lib/rack/recursive.rb +10 -5
  58. data/lib/rack/reloader.rb +3 -2
  59. data/lib/rack/request.rb +201 -74
  60. data/lib/rack/response.rb +41 -28
  61. data/lib/rack/rewindable_input.rb +15 -11
  62. data/lib/rack/runtime.rb +16 -3
  63. data/lib/rack/sendfile.rb +47 -29
  64. data/lib/rack/server.rb +223 -47
  65. data/lib/rack/session/abstract/id.rb +289 -30
  66. data/lib/rack/session/cookie.rb +133 -44
  67. data/lib/rack/session/memcache.rb +30 -56
  68. data/lib/rack/session/pool.rb +19 -43
  69. data/lib/rack/showexceptions.rb +53 -15
  70. data/lib/rack/showstatus.rb +14 -7
  71. data/lib/rack/static.rb +124 -12
  72. data/lib/rack/tempfile_reaper.rb +22 -0
  73. data/lib/rack/urlmap.rb +49 -15
  74. data/lib/rack/utils/okjson.rb +600 -0
  75. data/lib/rack/utils.rb +363 -361
  76. data/lib/rack.rb +17 -23
  77. data/rack.gemspec +11 -20
  78. data/test/builder/anything.rb +5 -0
  79. data/test/builder/comment.ru +4 -0
  80. data/test/builder/end.ru +5 -0
  81. data/test/builder/line.ru +1 -0
  82. data/test/builder/options.ru +2 -0
  83. data/test/cgi/assets/folder/test.js +1 -0
  84. data/test/cgi/assets/fonts/font.eot +1 -0
  85. data/test/cgi/assets/images/image.png +1 -0
  86. data/test/cgi/assets/index.html +1 -0
  87. data/test/cgi/assets/javascripts/app.js +1 -0
  88. data/test/cgi/assets/stylesheets/app.css +1 -0
  89. data/test/cgi/lighttpd.conf +26 -0
  90. data/test/cgi/rackup_stub.rb +6 -0
  91. data/test/cgi/sample_rackup.ru +5 -0
  92. data/test/cgi/test +9 -0
  93. data/test/cgi/test+directory/test+file +1 -0
  94. data/test/cgi/test.fcgi +8 -0
  95. data/test/cgi/test.ru +5 -0
  96. data/test/gemloader.rb +10 -0
  97. data/test/multipart/bad_robots +259 -0
  98. data/test/multipart/binary +0 -0
  99. data/test/multipart/content_type_and_no_filename +6 -0
  100. data/test/multipart/empty +10 -0
  101. data/test/multipart/fail_16384_nofile +814 -0
  102. data/test/multipart/file1.txt +1 -0
  103. data/test/multipart/filename_and_modification_param +7 -0
  104. data/test/multipart/filename_and_no_name +6 -0
  105. data/test/multipart/filename_with_escaped_quotes +6 -0
  106. data/test/multipart/filename_with_escaped_quotes_and_modification_param +7 -0
  107. data/test/multipart/filename_with_null_byte +7 -0
  108. data/test/multipart/filename_with_percent_escaped_quotes +6 -0
  109. data/test/multipart/filename_with_unescaped_percentages +6 -0
  110. data/test/multipart/filename_with_unescaped_percentages2 +6 -0
  111. data/test/multipart/filename_with_unescaped_percentages3 +6 -0
  112. data/test/multipart/filename_with_unescaped_quotes +6 -0
  113. data/test/multipart/ie +6 -0
  114. data/test/multipart/invalid_character +6 -0
  115. data/test/multipart/mixed_files +21 -0
  116. data/test/multipart/nested +10 -0
  117. data/test/multipart/none +9 -0
  118. data/test/multipart/semicolon +6 -0
  119. data/test/multipart/text +15 -0
  120. data/test/multipart/three_files_three_fields +31 -0
  121. data/test/multipart/webkit +32 -0
  122. data/test/rackup/config.ru +31 -0
  123. data/test/registering_handler/rack/handler/registering_myself.rb +8 -0
  124. data/test/{spec_rack_auth_basic.rb → spec_auth_basic.rb} +23 -15
  125. data/test/{spec_rack_auth_digest.rb → spec_auth_digest.rb} +56 -29
  126. data/test/spec_body_proxy.rb +85 -0
  127. data/test/spec_builder.rb +223 -0
  128. data/test/{spec_rack_cascade.rb → spec_cascade.rb} +28 -15
  129. data/test/{spec_rack_cgi.rb → spec_cgi.rb} +44 -31
  130. data/test/spec_chunked.rb +101 -0
  131. data/test/spec_commonlogger.rb +93 -0
  132. data/test/spec_conditionalget.rb +102 -0
  133. data/test/{spec_rack_config.rb → spec_config.rb} +6 -8
  134. data/test/spec_content_length.rb +85 -0
  135. data/test/spec_content_type.rb +45 -0
  136. data/test/spec_deflater.rb +339 -0
  137. data/test/{spec_rack_directory.rb → spec_directory.rb} +37 -10
  138. data/test/spec_etag.rb +107 -0
  139. data/test/{spec_rack_fastcgi.rb → spec_fastcgi.rb} +47 -29
  140. data/test/spec_file.rb +221 -0
  141. data/test/spec_handler.rb +72 -0
  142. data/test/spec_head.rb +45 -0
  143. data/test/{spec_rack_lint.rb → spec_lint.rb} +82 -60
  144. data/test/spec_lobster.rb +58 -0
  145. data/test/spec_lock.rb +164 -0
  146. data/test/spec_logger.rb +23 -0
  147. data/test/spec_methodoverride.rb +95 -0
  148. data/test/spec_mime.rb +51 -0
  149. data/test/{spec_rack_mock.rb → spec_mock.rb} +92 -38
  150. data/test/{spec_rack_mongrel.rb → spec_mongrel.rb} +46 -53
  151. data/test/spec_multipart.rb +600 -0
  152. data/test/spec_nulllogger.rb +20 -0
  153. data/test/spec_recursive.rb +72 -0
  154. data/test/spec_request.rb +1227 -0
  155. data/test/spec_response.rb +407 -0
  156. data/test/spec_rewindable_input.rb +118 -0
  157. data/test/spec_runtime.rb +49 -0
  158. data/test/spec_sendfile.rb +130 -0
  159. data/test/spec_server.rb +167 -0
  160. data/test/spec_session_abstract_id.rb +53 -0
  161. data/test/spec_session_cookie.rb +410 -0
  162. data/test/{spec_rack_session_memcache.rb → spec_session_memcache.rb} +119 -71
  163. data/test/{spec_rack_session_pool.rb → spec_session_pool.rb} +106 -69
  164. data/test/spec_showexceptions.rb +85 -0
  165. data/test/spec_showstatus.rb +103 -0
  166. data/test/spec_static.rb +145 -0
  167. data/test/spec_tempfile_reaper.rb +63 -0
  168. data/test/{spec_rack_thin.rb → spec_thin.rb} +35 -35
  169. data/test/{spec_rack_urlmap.rb → spec_urlmap.rb} +40 -19
  170. data/test/spec_utils.rb +647 -0
  171. data/test/spec_version.rb +17 -0
  172. data/test/spec_webrick.rb +184 -0
  173. data/test/static/another/index.html +1 -0
  174. data/test/static/index.html +1 -0
  175. data/test/testrequest.rb +78 -0
  176. data/test/unregistered_handler/rack/handler/unregistered.rb +7 -0
  177. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +7 -0
  178. metadata +220 -239
  179. data/RDOX +0 -0
  180. data/README +0 -592
  181. data/lib/rack/adapter/camping.rb +0 -22
  182. data/test/spec_auth.rb +0 -57
  183. data/test/spec_rack_builder.rb +0 -84
  184. data/test/spec_rack_camping.rb +0 -55
  185. data/test/spec_rack_chunked.rb +0 -62
  186. data/test/spec_rack_commonlogger.rb +0 -61
  187. data/test/spec_rack_conditionalget.rb +0 -41
  188. data/test/spec_rack_content_length.rb +0 -43
  189. data/test/spec_rack_content_type.rb +0 -30
  190. data/test/spec_rack_deflater.rb +0 -127
  191. data/test/spec_rack_etag.rb +0 -17
  192. data/test/spec_rack_file.rb +0 -75
  193. data/test/spec_rack_handler.rb +0 -43
  194. data/test/spec_rack_head.rb +0 -30
  195. data/test/spec_rack_lobster.rb +0 -45
  196. data/test/spec_rack_lock.rb +0 -38
  197. data/test/spec_rack_logger.rb +0 -21
  198. data/test/spec_rack_methodoverride.rb +0 -60
  199. data/test/spec_rack_nulllogger.rb +0 -13
  200. data/test/spec_rack_recursive.rb +0 -77
  201. data/test/spec_rack_request.rb +0 -594
  202. data/test/spec_rack_response.rb +0 -221
  203. data/test/spec_rack_rewindable_input.rb +0 -118
  204. data/test/spec_rack_runtime.rb +0 -35
  205. data/test/spec_rack_sendfile.rb +0 -86
  206. data/test/spec_rack_session_cookie.rb +0 -92
  207. data/test/spec_rack_showexceptions.rb +0 -21
  208. data/test/spec_rack_showstatus.rb +0 -72
  209. data/test/spec_rack_static.rb +0 -37
  210. data/test/spec_rack_utils.rb +0 -557
  211. data/test/spec_rack_webrick.rb +0 -130
  212. data/test/spec_rackup.rb +0 -164
@@ -4,12 +4,163 @@
4
4
  require 'time'
5
5
  require 'rack/request'
6
6
  require 'rack/response'
7
+ begin
8
+ require 'securerandom'
9
+ rescue LoadError
10
+ # We just won't get securerandom
11
+ end
7
12
 
8
13
  module Rack
9
14
 
10
15
  module Session
11
16
 
12
17
  module Abstract
18
+ ENV_SESSION_KEY = 'rack.session'.freeze
19
+ ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze
20
+
21
+ # SessionHash is responsible to lazily load the session from store.
22
+
23
+ class SessionHash
24
+ include Enumerable
25
+ attr_writer :id
26
+
27
+ def self.find(env)
28
+ env[ENV_SESSION_KEY]
29
+ end
30
+
31
+ def self.set(env, session)
32
+ env[ENV_SESSION_KEY] = session
33
+ end
34
+
35
+ def self.set_options(env, options)
36
+ env[ENV_SESSION_OPTIONS_KEY] = options.dup
37
+ end
38
+
39
+ def initialize(store, env)
40
+ @store = store
41
+ @env = env
42
+ @loaded = false
43
+ end
44
+
45
+ def id
46
+ return @id if @loaded or instance_variable_defined?(:@id)
47
+ @id = @store.send(:extract_session_id, @env)
48
+ end
49
+
50
+ def options
51
+ @env[ENV_SESSION_OPTIONS_KEY]
52
+ end
53
+
54
+ def each(&block)
55
+ load_for_read!
56
+ @data.each(&block)
57
+ end
58
+
59
+ def [](key)
60
+ load_for_read!
61
+ @data[key.to_s]
62
+ end
63
+ alias :fetch :[]
64
+
65
+ def has_key?(key)
66
+ load_for_read!
67
+ @data.has_key?(key.to_s)
68
+ end
69
+ alias :key? :has_key?
70
+ alias :include? :has_key?
71
+
72
+ def []=(key, value)
73
+ load_for_write!
74
+ @data[key.to_s] = value
75
+ end
76
+ alias :store :[]=
77
+
78
+ def clear
79
+ load_for_write!
80
+ @data.clear
81
+ end
82
+
83
+ def destroy
84
+ clear
85
+ @id = @store.send(:destroy_session, @env, id, options)
86
+ end
87
+
88
+ def to_hash
89
+ load_for_read!
90
+ @data.dup
91
+ end
92
+
93
+ def update(hash)
94
+ load_for_write!
95
+ @data.update(stringify_keys(hash))
96
+ end
97
+ alias :merge! :update
98
+
99
+ def replace(hash)
100
+ load_for_write!
101
+ @data.replace(stringify_keys(hash))
102
+ end
103
+
104
+ def delete(key)
105
+ load_for_write!
106
+ @data.delete(key.to_s)
107
+ end
108
+
109
+ def inspect
110
+ if loaded?
111
+ @data.inspect
112
+ else
113
+ "#<#{self.class}:0x#{self.object_id.to_s(16)} not yet loaded>"
114
+ end
115
+ end
116
+
117
+ def exists?
118
+ return @exists if instance_variable_defined?(:@exists)
119
+ @data = {}
120
+ @exists = @store.send(:session_exists?, @env)
121
+ end
122
+
123
+ def loaded?
124
+ @loaded
125
+ end
126
+
127
+ def empty?
128
+ load_for_read!
129
+ @data.empty?
130
+ end
131
+
132
+ def keys
133
+ @data.keys
134
+ end
135
+
136
+ def values
137
+ @data.values
138
+ end
139
+
140
+ private
141
+
142
+ def load_for_read!
143
+ load! if !loaded? && exists?
144
+ end
145
+
146
+ def load_for_write!
147
+ load! unless loaded?
148
+ end
149
+
150
+ def load!
151
+ @id, session = @store.send(:load_session, @env)
152
+ @data = stringify_keys(session)
153
+ @loaded = true
154
+ end
155
+
156
+ def stringify_keys(other)
157
+ hash = {}
158
+ other.each do |key, value|
159
+ hash[key.to_s] = value
160
+ end
161
+ hash
162
+ end
163
+ end
13
164
 
14
165
  # ID sets up a basic framework for implementing an id based sessioning
15
166
  # service. Cookies sent to the client for maintaining sessions will only
@@ -21,7 +172,9 @@ module Rack
21
172
  # 'rack.session'
22
173
  # * :path, :domain, :expire_after, :secure, and :httponly set the related
23
174
  # cookie options as by Rack::Response#add_cookie
24
- # * :defer will not set a cookie in the response.
175
+ # * :skip will not a set a cookie in the response nor update the session state
176
+ # * :defer will not set a cookie in the response but still update the session
177
+ # state if it is used with a backend
25
178
  # * :renew (implementation dependent) will prompt the generation of a new
26
179
  # session id, and migration of data to be referenced at the new id. If
27
180
  # :defer is set, it will be overridden and the cookie will be set.
@@ -34,9 +187,13 @@ module Rack
34
187
  # recommended to change its value.
35
188
  #
36
189
  # Is Rack::Utils::Context compatible.
190
+ #
191
+ # Not included by default; you must require 'rack/session/abstract/id'
192
+ # to use.
37
193
 
38
194
  class ID
39
195
  DEFAULT_OPTIONS = {
196
+ :key => 'rack.session',
40
197
  :path => '/',
41
198
  :domain => nil,
42
199
  :expire_after => nil,
@@ -44,14 +201,19 @@ module Rack
44
201
  :httponly => true,
45
202
  :defer => false,
46
203
  :renew => false,
47
- :sidbits => 128
204
+ :sidbits => 128,
205
+ :cookie_only => true,
206
+ :secure_random => (::SecureRandom rescue false)
48
207
  }
49
208
 
50
209
  attr_reader :key, :default_options
210
+
51
211
  def initialize(app, options={})
52
212
  @app = app
53
- @key = options[:key] || "rack.session"
54
213
  @default_options = self.class::DEFAULT_OPTIONS.merge(options)
214
+ @key = @default_options.delete(:key)
215
+ @cookie_only = @default_options.delete(:cookie_only)
216
+ initialize_sid
55
217
  end
56
218
 
57
219
  def call(env)
@@ -59,40 +221,102 @@ module Rack
59
221
  end
60
222
 
61
223
  def context(env, app=@app)
62
- load_session(env)
224
+ prepare_session(env)
63
225
  status, headers, body = app.call(env)
64
226
  commit_session(env, status, headers, body)
65
227
  end
66
228
 
67
229
  private
68
230
 
231
+ def initialize_sid
232
+ @sidbits = @default_options[:sidbits]
233
+ @sid_secure = @default_options[:secure_random]
234
+ @sid_length = @sidbits / 4
235
+ end
236
+
69
237
  # Generate a new session id using Ruby #rand. The size of the
70
238
  # session id is controlled by the :sidbits option.
71
239
  # Monkey patch this to use custom methods for session id generation.
72
240
 
73
- def generate_sid
74
- "%0#{@default_options[:sidbits] / 4}x" %
75
- rand(2**@default_options[:sidbits] - 1)
241
+ def generate_sid(secure = @sid_secure)
242
+ if secure
243
+ secure.hex(@sid_length)
244
+ else
245
+ "%0#{@sid_length}x" % Kernel.rand(2**@sidbits - 1)
246
+ end
247
+ rescue NotImplementedError
248
+ generate_sid(false)
249
+ end
250
+
251
+ # Sets the lazy session at 'rack.session' and places options and session
252
+ # metadata into 'rack.session.options'.
253
+
254
+ def prepare_session(env)
255
+ session_was = env[ENV_SESSION_KEY]
256
+ env[ENV_SESSION_KEY] = session_class.new(self, env)
257
+ env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
258
+ env[ENV_SESSION_KEY].merge! session_was if session_was
76
259
  end
77
260
 
78
261
  # Extracts the session id from provided cookies and passes it and the
79
- # environment to #get_session. It then sets the resulting session into
80
- # 'rack.session', and places options and session metadata into
81
- # 'rack.session.options'.
262
+ # environment to #get_session.
82
263
 
83
264
  def load_session(env)
265
+ sid = current_session_id(env)
266
+ sid, session = get_session(env, sid)
267
+ [sid, session || {}]
268
+ end
269
+
270
+ # Extract session id from request object.
271
+
272
+ def extract_session_id(env)
84
273
  request = Rack::Request.new(env)
85
- session_id = request.cookies[@key]
274
+ sid = request.cookies[@key]
275
+ sid ||= request.params[@key] unless @cookie_only
276
+ sid
277
+ end
278
+
279
+ # Returns the current session id from the SessionHash.
280
+
281
+ def current_session_id(env)
282
+ env[ENV_SESSION_KEY].id
283
+ end
284
+
285
+ # Check if the session exists or not.
286
+
287
+ def session_exists?(env)
288
+ value = current_session_id(env)
289
+ value && !value.empty?
290
+ end
86
291
 
87
- begin
88
- session_id, session = get_session(env, session_id)
89
- env['rack.session'] = session
90
- rescue
91
- env['rack.session'] = Hash.new
292
+ # Session should be committed if it was loaded, any of specific options like :renew, :drop
293
+ # or :expire_after was given and the security permissions match. Skips if skip is given.
294
+
295
+ def commit_session?(env, session, options)
296
+ if options[:skip]
297
+ false
298
+ else
299
+ has_session = loaded_session?(session) || forced_session_update?(session, options)
300
+ has_session && security_matches?(env, options)
92
301
  end
302
+ end
303
+
304
+ def loaded_session?(session)
305
+ !session.is_a?(session_class) || session.loaded?
306
+ end
307
+
308
+ def forced_session_update?(session, options)
309
+ force_options?(options) && session && !session.empty?
310
+ end
93
311
 
94
- env['rack.session.options'] = @default_options.
95
- merge(:id => session_id)
312
+ def force_options?(options)
313
+ options.values_at(:max_age, :renew, :drop, :defer, :expire_after).any?
314
+ end
315
+
316
+ def security_matches?(env, options)
317
+ return true unless options[:secure]
318
+ request = Rack::Request.new(env)
319
+ request.ssl?
96
320
  end
97
321
 
98
322
  # Acquires the session from the environment and the session id from
@@ -101,25 +325,52 @@ module Rack
101
325
  # response with the session's id.
102
326
 
103
327
  def commit_session(env, status, headers, body)
104
- session = env['rack.session']
105
- options = env['rack.session.options']
106
- session_id = options[:id]
328
+ session = env[ENV_SESSION_KEY]
329
+ options = session.options
330
+
331
+ if options[:drop] || options[:renew]
332
+ session_id = destroy_session(env, session.id || generate_sid, options)
333
+ return [status, headers, body] unless session_id
334
+ end
335
+
336
+ return [status, headers, body] unless commit_session?(env, session, options)
107
337
 
108
- if not session_id = set_session(env, session_id, session, options)
338
+ session.send(:load!) unless loaded_session?(session)
339
+ session_id ||= session.id
340
+ session_data = session.to_hash.delete_if { |k,v| v.nil? }
341
+
342
+ if not data = set_session(env, session_id, session_data, options)
109
343
  env["rack.errors"].puts("Warning! #{self.class.name} failed to save session. Content dropped.")
110
344
  elsif options[:defer] and not options[:renew]
111
- env["rack.errors"].puts("Defering cookie for #{session_id}") if $VERBOSE
345
+ env["rack.errors"].puts("Deferring cookie for #{session_id}") if $VERBOSE
112
346
  else
113
347
  cookie = Hash.new
114
- cookie[:value] = session_id
115
- cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil?
116
- Utils.set_cookie_header!(headers, @key, cookie.merge(options))
348
+ cookie[:value] = data
349
+ cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after]
350
+ cookie[:expires] = Time.now + options[:max_age] if options[:max_age]
351
+ set_cookie(env, headers, cookie.merge!(options))
117
352
  end
118
353
 
119
354
  [status, headers, body]
120
355
  end
121
356
 
122
- # All thread safety and session retrival proceedures should occur here.
357
+ # Sets the cookie back to the client with session id. We skip the cookie
358
+ # setting if the value didn't change (sid is the same) or expires was given.
359
+
360
+ def set_cookie(env, headers, cookie)
361
+ request = Rack::Request.new(env)
362
+ if request.cookies[@key] != cookie[:value] || cookie[:expires]
363
+ Utils.set_cookie_header!(headers, @key, cookie)
364
+ end
365
+ end
366
+
367
+ # Allow subclasses to prepare_session for different Session classes
368
+
369
+ def session_class
370
+ SessionHash
371
+ end
372
+
373
+ # All thread safety and session retrieval procedures should occur here.
123
374
  # Should return [session_id, session].
124
375
  # If nil is provided as the session id, generation of a new valid id
125
376
  # should occur within.
@@ -128,12 +379,20 @@ module Rack
128
379
  raise '#get_session not implemented.'
129
380
  end
130
381
 
131
- # All thread safety and session storage proceedures should occur here.
132
- # Should return true or false dependant on whether or not the session
133
- # was saved or not.
382
+ # All thread safety and session storage procedures should occur here.
383
+ # Must return the session id if the session was saved successfully, or
384
+ # false if the session could not be saved.
385
+
134
386
  def set_session(env, sid, session, options)
135
387
  raise '#set_session not implemented.'
136
388
  end
389
+
390
+ # All thread safety and session destroy procedures should occur here.
391
+ # Should return a new session id or nil if options[:drop]
392
+
393
+ def destroy_session(env, sid, options)
394
+ raise '#destroy_session not implemented'
395
+ end
137
396
  end
138
397
  end
139
398
  end
@@ -1,15 +1,21 @@
1
1
  require 'openssl'
2
+ require 'zlib'
2
3
  require 'rack/request'
3
4
  require 'rack/response'
5
+ require 'rack/session/abstract/id'
4
6
 
5
7
  module Rack
6
8
 
7
9
  module Session
8
10
 
9
11
  # Rack::Session::Cookie provides simple cookie based session management.
10
- # The session is a Ruby Hash stored as base64 encoded marshalled data
11
- # set to :key (default: rack.session).
12
+ # By default, the session is a Ruby Hash stored as base64 encoded marshalled
13
+ # data set to :key (default: rack.session). The object that encodes the
14
+ # session data is configurable and must respond to +encode+ and +decode+.
15
+ # Both methods must take a string and return a string.
16
+ #
12
17
  # When the secret key is set, cookie data is checked for data integrity.
18
+ # The old secret key is also accepted and allows graceful secret rotation.
13
19
  #
14
20
  # Example:
15
21
  #
@@ -17,17 +23,88 @@ module Rack
17
23
  # :domain => 'foo.com',
18
24
  # :path => '/',
19
25
  # :expire_after => 2592000,
20
- # :secret => 'change_me'
26
+ # :secret => 'change_me',
27
+ # :old_secret => 'also_change_me'
21
28
  #
22
29
  # All parameters are optional.
30
+ #
31
+ # Example of a cookie with no encoding:
32
+ #
33
+ # Rack::Session::Cookie.new(application, {
34
+ # :coder => Rack::Session::Cookie::Identity.new
35
+ # })
36
+ #
37
+ # Example of a cookie with custom encoding:
38
+ #
39
+ # Rack::Session::Cookie.new(application, {
40
+ # :coder => Class.new {
41
+ # def encode(str); str.reverse; end
42
+ # def decode(str); str.reverse; end
43
+ # }.new
44
+ # })
45
+ #
46
+
47
+ class Cookie < Abstract::ID
48
+ # Encode session cookies as Base64
49
+ class Base64
50
+ def encode(str)
51
+ [str].pack('m')
52
+ end
53
+
54
+ def decode(str)
55
+ str.unpack('m').first
56
+ end
57
+
58
+ # Encode session cookies as Marshaled Base64 data
59
+ class Marshal < Base64
60
+ def encode(str)
61
+ super(::Marshal.dump(str))
62
+ end
63
+
64
+ def decode(str)
65
+ return unless str
66
+ ::Marshal.load(super(str)) rescue nil
67
+ end
68
+ end
69
+
70
+ # N.B. Unlike other encoding methods, the contained objects must be a
71
+ # valid JSON composite type, either a Hash or an Array.
72
+ class JSON < Base64
73
+ def encode(obj)
74
+ super(::Rack::Utils::OkJson.encode(obj))
75
+ end
76
+
77
+ def decode(str)
78
+ return unless str
79
+ ::Rack::Utils::OkJson.decode(super(str)) rescue nil
80
+ end
81
+ end
23
82
 
24
- class Cookie
83
+ class ZipJSON < Base64
84
+ def encode(obj)
85
+ super(Zlib::Deflate.deflate(::Rack::Utils::OkJson.encode(obj)))
86
+ end
87
+
88
+ def decode(str)
89
+ return unless str
90
+ ::Rack::Utils::OkJson.decode(Zlib::Inflate.inflate(super(str)))
91
+ rescue
92
+ nil
93
+ end
94
+ end
95
+ end
96
+
97
+ # Use no encoding for session cookies
98
+ class Identity
99
+ def encode(str); str; end
100
+ def decode(str); str; end
101
+ end
102
+
103
+ attr_reader :coder
25
104
 
26
105
  def initialize(app, options={})
27
- @app = app
28
- @key = options[:key] || "rack.session"
29
- @secret = options[:secret]
30
- warn <<-MSG unless @secret
106
+ @secrets = options.values_at(:secret, :old_secret).compact
107
+ warn <<-MSG unless @secrets.size >= 1
31
108
  SECURITY WARNING: No secret option provided to Rack::Session::Cookie.
32
109
  This poses a security threat. It is strongly recommended that you
33
110
  provide a secret to prevent exploits that may be possible from crafted
@@ -36,62 +113,74 @@ module Rack
36
113
 
37
114
  Called from: #{caller[0]}.
38
115
  MSG
39
- @default_options = {:domain => nil,
40
- :path => "/",
41
- :expire_after => nil}.merge(options)
116
+ @coder = options[:coder] ||= Base64::Marshal.new
117
+ super(app, options.merge!(:cookie_only => true))
42
118
  end
43
119
 
44
- def call(env)
45
- load_session(env)
46
- status, headers, body = @app.call(env)
47
- commit_session(env, status, headers, body)
120
+ private
121
+
122
+ def get_session(env, sid)
123
+ data = unpacked_cookie_data(env)
124
+ data = persistent_session_id!(data)
125
+ [data["session_id"], data]
48
126
  end
49
127
 
50
- private
128
+ def extract_session_id(env)
129
+ unpacked_cookie_data(env)["session_id"]
130
+ end
51
131
 
52
- def load_session(env)
53
- request = Rack::Request.new(env)
54
- session_data = request.cookies[@key]
132
+ def unpacked_cookie_data(env)
133
+ env["rack.session.unpacked_cookie_data"] ||= begin
134
+ request = Rack::Request.new(env)
135
+ session_data = request.cookies[@key]
55
136
 
56
- if @secret && session_data
57
- session_data, digest = session_data.split("--")
58
- session_data = nil unless Utils.secure_compare(digest, generate_hmac(session_data))
59
- end
137
+ if @secrets.size > 0 && session_data
138
+ digest, session_data = session_data.reverse.split("--", 2)
139
+ digest.reverse! if digest
140
+ session_data.reverse! if session_data
141
+ session_data = nil unless digest_match?(session_data, digest)
142
+ end
60
143
 
61
- begin
62
- session_data = session_data.unpack("m*").first
63
- session_data = Marshal.load(session_data)
64
- env["rack.session"] = session_data
65
- rescue
66
- env["rack.session"] = Hash.new
144
+ coder.decode(session_data) || {}
67
145
  end
146
+ end
68
147
 
69
- env["rack.session.options"] = @default_options.dup
148
+ def persistent_session_id!(data, sid=nil)
149
+ data ||= {}
150
+ data["session_id"] ||= sid || generate_sid
151
+ data
70
152
  end
71
153
 
72
- def commit_session(env, status, headers, body)
73
- session_data = Marshal.dump(env["rack.session"])
74
- session_data = [session_data].pack("m*")
154
+ def set_session(env, session_id, session, options)
155
+ session = session.merge("session_id" => session_id)
156
+ session_data = coder.encode(session)
75
157
 
76
- if @secret
77
- session_data = "#{session_data}--#{generate_hmac(session_data)}"
158
+ if @secrets.first
159
+ session_data << "--#{generate_hmac(session_data, @secrets.first)}"
78
160
  end
79
161
 
80
162
  if session_data.size > (4096 - @key.size)
81
- env["rack.errors"].puts("Warning! Rack::Session::Cookie data size exceeds 4K. Content dropped.")
163
+ env["rack.errors"].puts("Warning! Rack::Session::Cookie data size exceeds 4K.")
164
+ nil
82
165
  else
83
- options = env["rack.session.options"]
84
- cookie = Hash.new
85
- cookie[:value] = session_data
86
- cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil?
87
- Utils.set_cookie_header!(headers, @key, cookie.merge(options))
166
+ session_data
88
167
  end
168
+ end
89
169
 
90
- [status, headers, body]
170
+ def destroy_session(env, session_id, options)
171
+ # Nothing to do here, data is in the client
172
+ generate_sid unless options[:drop]
173
+ end
174
+
175
+ def digest_match?(data, digest)
176
+ return unless data && digest
177
+ @secrets.any? do |secret|
178
+ Rack::Utils.secure_compare(digest, generate_hmac(data, secret))
179
+ end
91
180
  end
92
181
 
93
- def generate_hmac(data)
94
- OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, @secret, data)
182
+ def generate_hmac(data, secret)
183
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret, data)
95
184
  end
96
185
 
97
186
  end