roda 3.9.0 → 3.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,175 @@
1
+ # frozen-string-literal: true
2
+
3
+ require_relative '../roda'
4
+ require_relative 'plugins/sessions'
5
+
6
+ # Session middleware that can be used in any Rack application
7
+ # that uses Roda's sessions plugin for encrypted and signed cookies.
8
+ # See Roda::RodaPlugins::Sessions for details on options.
9
+ class RodaSessionMiddleware
10
+ # Class to hold session data. This is designed to mimic the API
11
+ # of Rack::Session::Abstract::SessionHash, but is simpler and faster.
12
+ # Undocumented methods operate the same as hash methods, but load the
13
+ # session from the cookie if it hasn't been loaded yet, and convert
14
+ # keys to strings.
15
+ #
16
+ # One difference between SessionHash and Rack::Session::Abstract::SessionHash
17
+ # is that SessionHash does not attempt to setup a session id, since
18
+ # one is not needed for cookie-based sessions, only for sessions
19
+ # that are loaded out of a database. If you need to have a session id
20
+ # for other reasons, manually create a session id using a randomly generated
21
+ # string.
22
+ class SessionHash
23
+ # The Roda::RodaRequest subclass instance related to the session.
24
+ attr_reader :req
25
+
26
+ # The underlying data hash, or nil if the session has not yet been
27
+ # loaded.
28
+ attr_reader :data
29
+
30
+ def initialize(req)
31
+ @req = req
32
+ end
33
+
34
+ # The Roda sessions plugin options used by the middleware for this
35
+ # session hash.
36
+ def options
37
+ @req.roda_class.opts[:sessions]
38
+ end
39
+
40
+ def each(&block)
41
+ load!
42
+ @data.each(&block)
43
+ end
44
+
45
+ def [](key)
46
+ load!
47
+ @data[key.to_s]
48
+ end
49
+
50
+ def fetch(key, default = (no_default = true), &block)
51
+ load!
52
+ if no_default
53
+ @data.fetch(key.to_s, &block)
54
+ else
55
+ @data.fetch(key.to_s, default, &block)
56
+ end
57
+ end
58
+
59
+ def has_key?(key)
60
+ load!
61
+ @data.has_key?(key.to_s)
62
+ end
63
+ alias :key? :has_key?
64
+ alias :include? :has_key?
65
+
66
+ def []=(key, value)
67
+ load!
68
+ @data[key.to_s] = value
69
+ end
70
+ alias :store :[]=
71
+
72
+ # Clear the session, also removing a couple of roda session
73
+ # keys from the environment so that the related cookie will
74
+ # either be set or cleared in the rack response.
75
+ def clear
76
+ load!
77
+ env = @req.env
78
+ env.delete('roda.session.created_at')
79
+ env.delete('roda.session.updated_at')
80
+ @data.clear
81
+ end
82
+ alias :destroy :clear
83
+
84
+ def to_hash
85
+ load!
86
+ @data.dup
87
+ end
88
+
89
+ def update(hash)
90
+ load!
91
+ hash.each do |key, value|
92
+ @data[key.to_s] = value
93
+ end
94
+ @data
95
+ end
96
+ alias :merge! :update
97
+
98
+ def replace(hash)
99
+ load!
100
+ @data.clear
101
+ update(hash)
102
+ end
103
+
104
+ def delete(key)
105
+ load!
106
+ @data.delete(key.to_s)
107
+ end
108
+
109
+ # If the session hasn't been loaded, display that.
110
+ def inspect
111
+ if loaded?
112
+ @data.inspect
113
+ else
114
+ "#<#{self.class}:0x#{self.object_id.to_s(16)} not yet loaded>"
115
+ end
116
+ end
117
+
118
+ # Return whether the session cookie already exists.
119
+ # If this is false, then the session was set to an empty hash.
120
+ def exists?
121
+ load!
122
+ req.env.has_key?('roda.session.serialized')
123
+ end
124
+
125
+ # Whether the session has already been loaded from the cookie yet.
126
+ def loaded?
127
+ !!defined?(@data)
128
+ end
129
+
130
+ def empty?
131
+ load!
132
+ @data.empty?
133
+ end
134
+
135
+ def keys
136
+ load!
137
+ @data.keys
138
+ end
139
+
140
+ def values
141
+ load!
142
+ @data.values
143
+ end
144
+
145
+ private
146
+
147
+ # Load the session from the cookie.
148
+ def load!
149
+ @data ||= @req.send(:_load_session)
150
+ end
151
+ end
152
+
153
+ # Setup the middleware, passing +opts+ as the Roda sessions plugin options.
154
+ def initialize(app, opts)
155
+ mid = Class.new(Roda)
156
+ mid.plugin :sessions, opts
157
+ @req_class = mid::RodaRequest
158
+ @app = app
159
+ end
160
+
161
+ # Initialize the session hash in the environment before calling the next
162
+ # application, and if the session has been loaded after the result has been
163
+ # returned, then persist the session in the cookie.
164
+ def call(env)
165
+ session = env['rack.session'] = SessionHash.new(@req_class.new(nil, env))
166
+
167
+ res = @app.call(env)
168
+
169
+ if session.loaded?
170
+ session.req.persist_session(res[1], session.data)
171
+ end
172
+
173
+ res
174
+ end
175
+ end
@@ -4,7 +4,7 @@ class Roda
4
4
  RodaMajorVersion = 3
5
5
 
6
6
  # The minor version of Roda, updated for new feature releases of Roda.
7
- RodaMinorVersion = 9
7
+ RodaMinorVersion = 10
8
8
 
9
9
  # The patch version of Roda, updated only for bug fixes from the last
10
10
  # feature release.
@@ -10,7 +10,7 @@ describe "csrf plugin" do
10
10
 
11
11
  it "adds csrf protection and csrf helper methods" do
12
12
  app(:bare) do
13
- use Rack::Session::Cookie, :secret=>'1'
13
+ use(*DEFAULT_SESSION_MIDDLEWARE_ARGS)
14
14
  plugin :csrf, :skip=>['POST:/foo']
15
15
 
16
16
  route do |r|
@@ -74,7 +74,7 @@ describe "csrf plugin" do
74
74
  end
75
75
 
76
76
  app(:bare) do
77
- use Rack::Session::Cookie, :secret=>'1'
77
+ use(*DEFAULT_SESSION_MIDDLEWARE_ARGS)
78
78
  plugin :csrf, :skip=>['POST:/foo/bar']
79
79
 
80
80
  route do |r|
@@ -5,7 +5,7 @@ describe "flash plugin" do
5
5
 
6
6
  it "flash.now[] sets flash for current page" do
7
7
  app(:bare) do
8
- use Rack::Session::Cookie, :secret => "1"
8
+ send(*DEFAULT_SESSION_ARGS)
9
9
  plugin :flash
10
10
 
11
11
  route do |r|
@@ -21,35 +21,29 @@ describe "flash plugin" do
21
21
 
22
22
  it "flash[] sets flash for next page" do
23
23
  app(:bare) do
24
- use Rack::Session::Cookie, :secret => "1"
25
24
  plugin :flash
25
+ send(*DEFAULT_SESSION_ARGS)
26
26
 
27
27
  route do |r|
28
- r.on 'a' do
29
- "c#{flash['a']}"
30
- end
28
+ r.get('a'){"c#{flash['a']}"}
29
+ r.get('f'){flash; session['_flash'].inspect}
31
30
 
32
- r.on do
33
- flash['a'] = "b#{flash['a']}"
34
- flash['a'] || ''
35
- end
31
+ flash['a'] = "b#{flash['a']}"
32
+ flash['a'] || ''
36
33
  end
37
34
  end
38
35
 
39
- _, h, b = req
40
- b.join.must_equal ''
41
- _, h, b = req
42
- b.join.must_equal 'b'
43
- _, h, b = req
44
- b.join.must_equal 'bb'
45
- _, h, b = req('/a')
46
- b.join.must_equal 'cbbb'
47
- _, h, b = req
48
- b.join.must_equal ''
49
- _, h, b = req
50
- b.join.must_equal 'b'
51
- _, h, b = req
52
- b.join.must_equal 'bb'
36
+ body.must_equal ''
37
+ body.must_equal 'b'
38
+ body.must_equal 'bb'
39
+
40
+ body('/a').must_equal 'cbbb'
41
+ body.must_equal ''
42
+ body.must_equal 'b'
43
+ body.must_equal 'bb'
44
+
45
+ body('/f').must_equal '{"a"=>"bbb"}'
46
+ body('/f').must_equal 'nil'
53
47
  end
54
48
  end
55
49
 
@@ -36,7 +36,7 @@ describe "heartbeat plugin" do
36
36
 
37
37
  it "should work when using sessions" do
38
38
  app(:bare) do
39
- use Rack::Session::Cookie, :secret=>'foo'
39
+ send(*DEFAULT_SESSION_ARGS)
40
40
  plugin :heartbeat
41
41
 
42
42
  route do |r|
@@ -146,4 +146,19 @@ describe "middleware plugin" do
146
146
  end
147
147
  body.must_equal 'bar'
148
148
  end
149
+
150
+ it "calls :handle_result option with env and response" do
151
+ app(:bare) do
152
+ plugin :middleware, :handle_result=>(proc do |env, res|
153
+ res[2] << env['foo']
154
+ end)
155
+ route{}
156
+ end
157
+ mid2 = app
158
+ app(:bare) do
159
+ use mid2
160
+ route{env['foo'] = 'bar'; 'baz'}
161
+ end
162
+ body.must_equal 'bazbar'
163
+ end
149
164
  end
@@ -5,7 +5,7 @@ describe "route_csrf plugin" do
5
5
 
6
6
  def route_csrf_app(opts={}, &block)
7
7
  app(:bare) do
8
- use Rack::Session::Cookie, :secret=>'1'
8
+ send(*DEFAULT_SESSION_ARGS) unless opts[:no_sessions_plugin]
9
9
  plugin(:route_csrf, opts, &opts[:block])
10
10
  route do |r|
11
11
  check_csrf! unless env['SKIP']
@@ -257,13 +257,14 @@ rescue LoadError
257
257
  warn "rack_csrf not installed, skipping route_csrf plugin test for rack_csrf upgrade"
258
258
  else
259
259
  it "supports upgrades from existing rack_csrf token" do
260
- route_csrf_app(:upgrade_from_rack_csrf_key=>'csrf.token') do |r|
260
+ route_csrf_app(:upgrade_from_rack_csrf_key=>'csrf.token', :no_sessions_plugin=>true) do |r|
261
261
  r.get 'clear' do
262
262
  session.clear
263
263
  ''
264
264
  end
265
265
  Rack::Csrf.token(env)
266
266
  end
267
+ app.use(*DEFAULT_SESSION_MIDDLEWARE_ARGS)
267
268
  app.use Rack::Csrf, :skip=>['POST:/foo', 'POST:/bar'], :raise=>true
268
269
  token = body
269
270
  token.length.wont_equal 84
@@ -0,0 +1,371 @@
1
+ require_relative "../spec_helper"
2
+
3
+ if RUBY_VERSION >= '2'
4
+ describe "sessions plugin" do
5
+ include CookieJar
6
+
7
+ def req(path, opts={})
8
+ @errors ||= (errors = []; def errors.puts(s) self << s; end; errors)
9
+ super(path, opts.merge('rack.errors'=>@errors))
10
+ end
11
+
12
+ def errors
13
+ e = @errors.dup
14
+ @errors.clear
15
+ e
16
+ end
17
+
18
+ before do
19
+ app(:bare) do
20
+ plugin :sessions, :secret=>'1'*64
21
+ route do |r|
22
+ if r.GET['sut']
23
+ session
24
+ env['roda.session.updated_at'] -= r.GET['sut'].to_i if r.GET['sut']
25
+ end
26
+ r.get('s', String, String){|k, v| session[k] = v}
27
+ r.get('g', String){|k| session[k].to_s}
28
+ r.get('sct'){|i| session; env['roda.session.created_at'].to_s}
29
+ r.get('ssct', Integer){|i| session; (env['roda.session.created_at'] -= i).to_s}
30
+ r.get('sc'){session.clear; 'c'}
31
+ r.get('cs', String, String){|k, v| clear_session; session[k] = v}
32
+ ''
33
+ end
34
+ end
35
+ end
36
+
37
+ it "requires appropriate :secret option" do
38
+ proc{app(:bare){plugin :sessions}}.must_raise Roda::RodaError
39
+ proc{app(:bare){plugin :sessions, :secret=>Object.new}}.must_raise Roda::RodaError
40
+ proc{app(:bare){plugin :sessions, :secret=>'1'*63}}.must_raise Roda::RodaError
41
+ end
42
+
43
+ it "has session store data between requests" do
44
+ req('/').must_equal [200, {"Content-Type"=>"text/html", "Content-Length"=>"0"}, [""]]
45
+ body('/s/foo/bar').must_equal 'bar'
46
+ body('/g/foo').must_equal 'bar'
47
+
48
+ body('/s/foo/baz').must_equal 'baz'
49
+ body('/g/foo').must_equal 'baz'
50
+
51
+ body("/s/foo/\u1234").must_equal "\u1234"
52
+ body("/g/foo").must_equal "\u1234"
53
+
54
+ errors.must_equal []
55
+ end
56
+
57
+ it "does not add Set-Cookie header if session does not change, unless outside :skip_within seconds" do
58
+ req('/').must_equal [200, {"Content-Type"=>"text/html", "Content-Length"=>"0"}, [""]]
59
+ _, h, b = req('/s/foo/bar')
60
+ h['Set-Cookie'].must_match(/\Aroda.session/)
61
+ b.must_equal ["bar"]
62
+ req('/g/foo').must_equal [200, {"Content-Type"=>"text/html", "Content-Length"=>"3"}, ["bar"]]
63
+ req('/s/foo/bar').must_equal [200, {"Content-Type"=>"text/html", "Content-Length"=>"3"}, ["bar"]]
64
+
65
+ _, h, b = req('/s/foo/baz')
66
+ h['Set-Cookie'].must_match(/\Aroda.session/)
67
+ b.must_equal ["baz"]
68
+ req('/g/foo').must_equal [200, {"Content-Type"=>"text/html", "Content-Length"=>"3"}, ["baz"]]
69
+
70
+ req('/g/foo', 'QUERY_STRING'=>'sut=3500').must_equal [200, {"Content-Type"=>"text/html", "Content-Length"=>"3"}, ["baz"]]
71
+ _, h, b = req('/g/foo', 'QUERY_STRING'=>'sut=3700')
72
+ h['Set-Cookie'].must_match(/\Aroda.session/)
73
+ b.must_equal ["baz"]
74
+
75
+ @app.plugin(:sessions, :skip_within=>3800)
76
+ req('/g/foo', 'QUERY_STRING'=>'sut=3700').must_equal [200, {"Content-Type"=>"text/html", "Content-Length"=>"3"}, ["baz"]]
77
+ _, h, b = req('/g/foo', 'QUERY_STRING'=>'sut=3900')
78
+ h['Set-Cookie'].must_match(/\Aroda.session/)
79
+ b.must_equal ["baz"]
80
+
81
+ errors.must_equal []
82
+ end
83
+
84
+ it "removes session cookie when session is submitted but empty after request" do
85
+ body('/s/foo/bar').must_equal 'bar'
86
+ sct = body('/sct').to_i
87
+ body('/g/foo').must_equal 'bar'
88
+
89
+ _, h, b = req('/sc')
90
+ h['Set-Cookie'].must_include "roda.session=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00"
91
+ b.must_equal ['c']
92
+
93
+ errors.must_equal []
94
+ end
95
+
96
+ it "sets new session create time when clear_session is called even when session is not empty when serializing" do
97
+ body('/s/foo/bar').must_equal 'bar'
98
+ sct = body('/sct').to_i
99
+ body('/g/foo').must_equal 'bar'
100
+ body('/sct').to_i.must_equal sct
101
+ body('/ssct/10').to_i.must_equal(sct - 10)
102
+
103
+ body('/cs/foo/baz').must_equal 'baz'
104
+ body('/sct').to_i.must_be :>=, sct
105
+
106
+ errors.must_equal []
107
+ end
108
+
109
+ it "should include HttpOnly and secure cookie options appropriately" do
110
+ h = header('Set-Cookie', '/s/foo/bar')
111
+ h.must_include('; HttpOnly')
112
+ h.wont_include('; secure')
113
+
114
+ h = header('Set-Cookie', '/s/foo/baz', 'HTTPS'=>'on')
115
+ h.must_include('; HttpOnly')
116
+ h.must_include('; secure')
117
+
118
+ @app.plugin(:sessions, :cookie_options=>{})
119
+ h = header('Set-Cookie', '/s/foo/bar')
120
+ h.must_include('; HttpOnly')
121
+ h.wont_include('; secure')
122
+ end
123
+
124
+ it "should merge :cookie_options options into the default cookie options" do
125
+ @app.plugin(:sessions, :cookie_options=>{:secure=>true})
126
+ h = header('Set-Cookie', '/s/foo/bar')
127
+ h.must_include('; HttpOnly')
128
+ h.must_include('; path=/')
129
+ h.must_include('; secure')
130
+ end
131
+
132
+ it "handles secret rotation using :old_secret option" do
133
+ body('/s/foo/bar').must_equal 'bar'
134
+ body('/g/foo').must_equal 'bar'
135
+
136
+ old_cookie = @cookie
137
+ @app.plugin(:sessions, :secret=>'2'*64, :old_secret=>'1'*64)
138
+ body('/g/foo', 'QUERY_STRING'=>'sut=3700').must_equal 'bar'
139
+
140
+ @app.plugin(:sessions, :secret=>'2'*64, :old_secret=>nil)
141
+ body('/g/foo', 'QUERY_STRING'=>'sut=3700').must_equal 'bar'
142
+
143
+ @cookie = old_cookie
144
+ body('/g/foo').must_equal ''
145
+ errors.must_equal ["Not decoding session: HMAC invalid"]
146
+
147
+ proc{app(:bare){plugin :sessions, :old_secret=>'1'*63}}.must_raise Roda::RodaError
148
+ proc{app(:bare){plugin :sessions, :old_secret=>Object.new}}.must_raise Roda::RodaError
149
+ end
150
+
151
+ it "pads data by default to make it more difficult to guess session contents based on size" do
152
+ long = "bar"*35
153
+
154
+ _, h1, b = req('/s/foo/bar')
155
+ b.must_equal ['bar']
156
+ _, h2, b = req('/s/foo/bar', 'QUERY_STRING'=>'sut=3700')
157
+ b.must_equal ['bar']
158
+ _, h3, b = req('/s/foo/bar2')
159
+ b.must_equal ['bar2']
160
+ _, h4, b = req("/s/foo/#{long}")
161
+ b.must_equal [long]
162
+ h1['Set-Cookie'].length.must_equal h2['Set-Cookie'].length
163
+ h1['Set-Cookie'].wont_equal h2['Set-Cookie']
164
+ h1['Set-Cookie'].length.must_equal h3['Set-Cookie'].length
165
+ h1['Set-Cookie'].wont_equal h3['Set-Cookie']
166
+ h1['Set-Cookie'].length.wont_equal h4['Set-Cookie'].length
167
+
168
+ @app.plugin(:sessions, :pad_size=>256)
169
+
170
+ _, h1, b = req('/s/foo/bar')
171
+ b.must_equal ['bar']
172
+ _, h2, b = req('/s/foo/bar', 'QUERY_STRING'=>'sut=3700')
173
+ b.must_equal ['bar']
174
+ _, h3, b = req('/s/foo/bar2')
175
+ b.must_equal ['bar2']
176
+ _, h4, b = req("/s/foo/#{long}")
177
+ b.must_equal [long]
178
+ h1['Set-Cookie'].length.must_equal h2['Set-Cookie'].length
179
+ h1['Set-Cookie'].wont_equal h2['Set-Cookie']
180
+ h1['Set-Cookie'].length.must_equal h3['Set-Cookie'].length
181
+ h1['Set-Cookie'].wont_equal h3['Set-Cookie']
182
+ h1['Set-Cookie'].length.must_equal h4['Set-Cookie'].length
183
+ h1['Set-Cookie'].wont_equal h3['Set-Cookie']
184
+
185
+ @app.plugin(:sessions, :pad_size=>nil)
186
+
187
+ _, h1, b = req('/s/foo/bar')
188
+ b.must_equal ['bar']
189
+ _, h2, b = req('/s/foo/bar', 'QUERY_STRING'=>'sut=3700')
190
+ b.must_equal ['bar']
191
+ _, h3, b = req('/s/foo/bar2')
192
+ b.must_equal ['bar2']
193
+ h1['Set-Cookie'].length.must_equal h2['Set-Cookie'].length
194
+ h1['Set-Cookie'].wont_equal h2['Set-Cookie']
195
+ if !defined?(JRUBY_VERSION) || JRUBY_VERSION >= '9.2'
196
+ h1['Set-Cookie'].length.wont_equal h3['Set-Cookie'].length
197
+ end
198
+
199
+ proc{@app.plugin(:sessions, :pad_size=>0)}.must_raise Roda::RodaError
200
+ proc{@app.plugin(:sessions, :pad_size=>1)}.must_raise Roda::RodaError
201
+ proc{@app.plugin(:sessions, :pad_size=>Object.new)}.must_raise Roda::RodaError
202
+
203
+ errors.must_equal []
204
+ end
205
+
206
+ it "compresses data over a certain size by default" do
207
+ long = 'b'*8192
208
+ body("/s/foo/#{long}").must_equal long
209
+ body("/g/foo").must_equal long
210
+
211
+ @app.plugin(:sessions, :gzip_over=>15000)
212
+ proc{body("/g/foo", 'QUERY_STRING'=>'sut=3700')}.must_raise Roda::RodaPlugins::Sessions::CookieTooLarge
213
+
214
+ @app.plugin(:sessions, :gzip_over=>8000)
215
+ body("/g/foo", 'QUERY_STRING'=>'sut=3700').must_equal long
216
+
217
+ errors.must_equal []
218
+ end
219
+
220
+ it "raises CookieTooLarge if cookie is too large" do
221
+ proc{req('/s/foo/'+Base64.urlsafe_encode64(SecureRandom.random_bytes(8192)))}.must_raise Roda::RodaPlugins::Sessions::CookieTooLarge
222
+ end
223
+
224
+ it "ignores session cookies if session exceeds max time since create" do
225
+ body("/s/foo/bar").must_equal 'bar'
226
+ body("/g/foo").must_equal 'bar'
227
+
228
+ @app.plugin(:sessions, :max_seconds=>-1)
229
+ body("/g/foo").must_equal ''
230
+ errors.must_equal ["Not returning session: maximum session time expired"]
231
+
232
+ @app.plugin(:sessions, :max_seconds=>10)
233
+ body("/s/foo/bar").must_equal 'bar'
234
+ body("/g/foo").must_equal 'bar'
235
+
236
+ errors.must_equal []
237
+ end
238
+
239
+ it "ignores session cookies if session exceeds max idle time since update" do
240
+ body("/s/foo/bar").must_equal 'bar'
241
+ body("/g/foo").must_equal 'bar'
242
+
243
+ @app.plugin(:sessions, :max_idle_seconds=>-1)
244
+ body("/g/foo").must_equal ''
245
+ errors.must_equal ["Not returning session: maximum session idle time expired"]
246
+
247
+ @app.plugin(:sessions, :max_idle_seconds=>10)
248
+ body("/s/foo/bar").must_equal 'bar'
249
+ body("/g/foo").must_equal 'bar'
250
+
251
+ errors.must_equal []
252
+ end
253
+
254
+ it "supports :serializer and :parser options to override serializer/deserializer" do
255
+ body('/s/foo/bar').must_equal 'bar'
256
+
257
+ @app.plugin(:sessions, :parser=>proc{|s| JSON.parse("{#{s[1...-1].reverse}}")})
258
+ body('/g/rab').must_equal 'oof'
259
+
260
+ @app.plugin(:sessions, :serializer=>proc{|s| s.to_json.upcase})
261
+
262
+ body('/s/foo/baz').must_equal 'baz'
263
+ body('/g/ZAB').must_equal 'OOF'
264
+
265
+ errors.must_equal []
266
+ end
267
+
268
+ it "logs session decoding errors to rack.errors" do
269
+ body('/s/foo/bar').must_equal 'bar'
270
+ c = @cookie.dup
271
+ k = c.split('=', 2)[0] + '='
272
+
273
+ @cookie[20] = '!'
274
+ body('/g/foo').must_equal ''
275
+ errors.must_equal ["Unable to decode session: invalid base64"]
276
+
277
+ @cookie = k+Base64.urlsafe_encode64('1'*60)
278
+ body('/g/foo').must_equal ''
279
+ errors.must_equal ["Unable to decode session: data too short"]
280
+
281
+ @cookie = k+Base64.urlsafe_encode64('1'*75)
282
+ body('/g/foo').must_equal ''
283
+ errors.must_equal ["Unable to decode session: version marker unsupported"]
284
+
285
+ @cookie = k+Base64.urlsafe_encode64("\0"*75)
286
+ body('/g/foo').must_equal ''
287
+ errors.must_equal ["Not decoding session: HMAC invalid"]
288
+ end
289
+ end
290
+
291
+ describe "sessions plugin" do
292
+ include CookieJar
293
+
294
+ def req(path, opts={})
295
+ @errors ||= (errors = []; def errors.puts(s) self << s; end; errors)
296
+ super(path, opts.merge('rack.errors'=>@errors))
297
+ end
298
+
299
+ def errors
300
+ e = @errors.dup
301
+ @errors.clear
302
+ e
303
+ end
304
+
305
+ it "supports transparent upgrade from Rack::Session::Cookie with default HMAC and coder" do
306
+ app(:bare) do
307
+ use Rack::Session::Cookie, :secret=>'1'
308
+ plugin :middleware_stack
309
+ route do |r|
310
+ r.get('s', String, String){|k, v| session[k] = {:a=>v}; v}
311
+ r.get('g', String){|k| session[k].inspect}
312
+ ''
313
+ end
314
+ end
315
+
316
+ _, h, b = req('/s/foo/bar')
317
+ (h['Set-Cookie'] =~ /\A(rack\.session=.*); path=\/; HttpOnly\z/).must_equal 0
318
+ c = $1
319
+ b.must_equal ['bar']
320
+ _, h, b = req('/g/foo')
321
+ h['Set-Cookie'].must_be_nil
322
+ b.must_equal ['{:a=>"bar"}']
323
+
324
+ @app.plugin :sessions, :secret=>'1'*64,
325
+ :upgrade_from_rack_session_cookie_secret=>'1'
326
+ @app.middleware_stack.remove{|m, *| m == Rack::Session::Cookie}
327
+
328
+ @cookie = c.dup
329
+ @cookie.slice!(15)
330
+ body('/g/foo').must_equal 'nil'
331
+ errors.must_equal ["Not decoding Rack::Session::Cookie session: HMAC invalid"]
332
+
333
+ @cookie = c.split('--', 2)[0]
334
+ body('/g/foo').must_equal 'nil'
335
+ errors.must_equal ["Not decoding Rack::Session::Cookie session: invalid format"]
336
+
337
+ @cookie = c.split('--', 2)[0][13..-1]
338
+ @cookie = Rack::Utils.unescape(@cookie).unpack('m')[0]
339
+ @cookie[2] = "^"
340
+ @cookie = [@cookie].pack('m')
341
+ cookie = String.new
342
+ cookie << 'rack.session=' << @cookie << '--' << OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, '1', @cookie)
343
+ @cookie = cookie
344
+ body('/g/foo').must_equal 'nil'
345
+ errors.must_equal ["Error decoding Rack::Session::Cookie session: not base64 encoded marshal dump"]
346
+
347
+ @cookie = c
348
+ _, h, b = req('/g/foo')
349
+ h['Set-Cookie'].must_match(/\Aroda\.session=(.*); path=\/; HttpOnly(; SameSite=Lax)?\nrack\.session=; path=\/; max-age=0; expires=Thu, 01 Jan 1970 00:00:00/m)
350
+ b.must_equal ['{"a"=>"bar"}']
351
+
352
+ @app.plugin :sessions, :cookie_options=>{:path=>'/foo'}, :upgrade_from_rack_session_cookie_options=>{}
353
+ @cookie = c
354
+ _, h, b = req('/g/foo')
355
+ h['Set-Cookie'].must_match(/\Aroda\.session=(.*); path=\/foo; HttpOnly(; SameSite=Lax)?\nrack\.session=; path=\/foo; max-age=0; expires=Thu, 01 Jan 1970 00:00:00/m)
356
+ b.must_equal ['{"a"=>"bar"}']
357
+
358
+ @app.plugin :sessions, :upgrade_from_rack_session_cookie_options=>{:path=>'/baz'}
359
+ @cookie = c
360
+ _, h, b = req('/g/foo')
361
+ h['Set-Cookie'].must_match(/\Aroda\.session=(.*); path=\/foo; HttpOnly(; SameSite=Lax)?\nrack\.session=; path=\/baz; max-age=0; expires=Thu, 01 Jan 1970 00:00:00/m)
362
+ b.must_equal ['{"a"=>"bar"}']
363
+
364
+ @app.plugin :sessions, :upgrade_from_rack_session_cookie_key=>'quux.session'
365
+ @cookie = c.sub(/\Arack/, 'quux')
366
+ _, h, b = req('/g/foo')
367
+ h['Set-Cookie'].must_match(/\Aroda\.session=(.*); path=\/foo; HttpOnly(; SameSite=Lax)?\nquux\.session=; path=\/baz; max-age=0; expires=Thu, 01 Jan 1970 00:00:00/m)
368
+ b.must_equal ['{"a"=>"bar"}']
369
+ end
370
+ end
371
+ end