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.
- checksums.yaml +4 -4
- data/CHANGELOG +18 -0
- data/README.rdoc +43 -23
- data/doc/release_notes/3.10.0.txt +132 -0
- data/lib/roda.rb +3 -3
- data/lib/roda/plugins/assets.rb +2 -2
- data/lib/roda/plugins/flash.rb +8 -2
- data/lib/roda/plugins/json.rb +1 -3
- data/lib/roda/plugins/json_parser.rb +1 -2
- data/lib/roda/plugins/middleware.rb +12 -3
- data/lib/roda/plugins/route_csrf.rb +34 -32
- data/lib/roda/plugins/sessions.rb +451 -0
- data/lib/roda/plugins/typecast_params.rb +15 -2
- data/lib/roda/session_middleware.rb +175 -0
- data/lib/roda/version.rb +1 -1
- data/spec/plugin/csrf_spec.rb +2 -2
- data/spec/plugin/flash_spec.rb +17 -23
- data/spec/plugin/heartbeat_spec.rb +1 -1
- data/spec/plugin/middleware_spec.rb +15 -0
- data/spec/plugin/route_csrf_spec.rb +3 -2
- data/spec/plugin/sessions_spec.rb +371 -0
- data/spec/plugin/typecast_params_spec.rb +11 -0
- data/spec/session_middleware_spec.rb +129 -0
- data/spec/session_spec.rb +2 -2
- data/spec/spec_helper.rb +10 -1
- metadata +9 -3
@@ -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
|
data/lib/roda/version.rb
CHANGED
data/spec/plugin/csrf_spec.rb
CHANGED
@@ -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
|
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
|
77
|
+
use(*DEFAULT_SESSION_MIDDLEWARE_ARGS)
|
78
78
|
plugin :csrf, :skip=>['POST:/foo/bar']
|
79
79
|
|
80
80
|
route do |r|
|
data/spec/plugin/flash_spec.rb
CHANGED
@@ -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
|
-
|
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.
|
29
|
-
|
30
|
-
end
|
28
|
+
r.get('a'){"c#{flash['a']}"}
|
29
|
+
r.get('f'){flash; session['_flash'].inspect}
|
31
30
|
|
32
|
-
|
33
|
-
|
34
|
-
flash['a'] || ''
|
35
|
-
end
|
31
|
+
flash['a'] = "b#{flash['a']}"
|
32
|
+
flash['a'] || ''
|
36
33
|
end
|
37
34
|
end
|
38
35
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
|
@@ -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
|
-
|
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
|