roda 3.9.0 → 3.10.0

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.
@@ -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