roda 3.18.0 → 3.19.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 +24 -0
- data/README.rdoc +7 -9
- data/doc/conventions.rdoc +10 -10
- data/doc/release_notes/3.19.0.txt +229 -0
- data/lib/roda.rb +88 -45
- data/lib/roda/plugins/assets.rb +11 -4
- data/lib/roda/plugins/delay_build.rb +3 -30
- data/lib/roda/plugins/empty_root.rb +1 -1
- data/lib/roda/plugins/hash_routes.rb +455 -0
- data/lib/roda/plugins/match_hook.rb +69 -0
- data/lib/roda/plugins/multi_route.rb +4 -0
- data/lib/roda/plugins/multi_view.rb +4 -0
- data/lib/roda/plugins/optimized_string_matchers.rb +1 -1
- data/lib/roda/plugins/sessions.rb +63 -16
- data/lib/roda/plugins/static_routing.rb +7 -40
- data/lib/roda/version.rb +1 -1
- data/spec/define_roda_method_spec.rb +3 -0
- data/spec/freeze_spec.rb +10 -1
- data/spec/integration_spec.rb +1 -1
- data/spec/plugin/assets_spec.rb +16 -0
- data/spec/plugin/delay_build_spec.rb +2 -3
- data/spec/plugin/hash_routes_spec.rb +535 -0
- data/spec/plugin/match_hook_spec.rb +79 -0
- data/spec/plugin/middleware_spec.rb +1 -0
- data/spec/plugin/sessions_spec.rb +363 -320
- metadata +8 -2
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
require_relative "../spec_helper"
|
|
2
|
+
|
|
3
|
+
describe "match hook plugin" do
|
|
4
|
+
it "matches verbs" do
|
|
5
|
+
matches = []
|
|
6
|
+
app(:bare) do
|
|
7
|
+
plugin :match_hook
|
|
8
|
+
match_hook do
|
|
9
|
+
matches << [request.matched_path, request.remaining_path]
|
|
10
|
+
end
|
|
11
|
+
route do |r|
|
|
12
|
+
r.on "foo" do
|
|
13
|
+
r.on "bar" do
|
|
14
|
+
r.get "baz" do
|
|
15
|
+
"fbb"
|
|
16
|
+
end
|
|
17
|
+
"fb"
|
|
18
|
+
end
|
|
19
|
+
"f"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
r.get "bar" do
|
|
23
|
+
"b"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
r.root do
|
|
27
|
+
"r"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
"n"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
body("/foo").must_equal 'f'
|
|
35
|
+
matches.must_equal [["/foo", ""]]
|
|
36
|
+
|
|
37
|
+
matches.clear
|
|
38
|
+
body("/foo/bar").must_equal 'fb'
|
|
39
|
+
matches.must_equal [["/foo", "/bar"], ["/foo/bar", ""]]
|
|
40
|
+
|
|
41
|
+
matches.clear
|
|
42
|
+
body("/foo/bar/baz").must_equal 'fbb'
|
|
43
|
+
matches.must_equal [["/foo", "/bar/baz"], ["/foo/bar", "/baz"], ["/foo/bar/baz", ""]]
|
|
44
|
+
|
|
45
|
+
matches.clear
|
|
46
|
+
body("/bar").must_equal 'b'
|
|
47
|
+
matches.must_equal [["/bar", ""]]
|
|
48
|
+
|
|
49
|
+
matches.clear
|
|
50
|
+
body.must_equal 'r'
|
|
51
|
+
matches.must_equal [["", "/"]]
|
|
52
|
+
|
|
53
|
+
matches.clear
|
|
54
|
+
body('/x').must_equal 'n'
|
|
55
|
+
matches.must_be_empty
|
|
56
|
+
|
|
57
|
+
matches.clear
|
|
58
|
+
body("/foo/baz").must_equal 'f'
|
|
59
|
+
matches.must_equal [["/foo", "/baz"]]
|
|
60
|
+
|
|
61
|
+
matches.clear
|
|
62
|
+
body("/foo/bar/bar").must_equal 'fb'
|
|
63
|
+
matches.must_equal [["/foo", "/bar/bar"], ["/foo/bar", "/bar"]]
|
|
64
|
+
|
|
65
|
+
app.match_hook{matches << :x }
|
|
66
|
+
|
|
67
|
+
matches.clear
|
|
68
|
+
body("/foo/bar/baz").must_equal 'fbb'
|
|
69
|
+
matches.must_equal [["/foo", "/bar/baz"], :x, ["/foo/bar", "/baz"], :x, ["/foo/bar/baz", ""], :x]
|
|
70
|
+
|
|
71
|
+
app.freeze
|
|
72
|
+
|
|
73
|
+
matches.clear
|
|
74
|
+
body("/foo/bar/baz").must_equal 'fbb'
|
|
75
|
+
matches.must_equal [["/foo", "/bar/baz"], :x, ["/foo/bar", "/baz"], :x, ["/foo/bar/baz", ""], :x]
|
|
76
|
+
|
|
77
|
+
app.opts[:match_hooks].must_be :frozen?
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -1,396 +1,439 @@
|
|
|
1
1
|
require_relative "../spec_helper"
|
|
2
2
|
|
|
3
3
|
if RUBY_VERSION >= '2'
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
[true, false].each do |per_cookie_cipher_secret|
|
|
5
|
+
describe "sessions plugin with per_cookie_cipher_secret: #{per_cookie_cipher_secret}" do
|
|
6
|
+
include CookieJar
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
def req(path, opts={})
|
|
9
|
+
@errors ||= (errors = []; def errors.puts(s) self << s; end; errors)
|
|
10
|
+
super(path, opts.merge('rack.errors'=>@errors))
|
|
11
|
+
end
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
def errors
|
|
14
|
+
e = @errors.dup
|
|
15
|
+
@errors.clear
|
|
16
|
+
e
|
|
17
|
+
end
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
before do
|
|
20
|
+
app(:bare) do
|
|
21
|
+
plugin :sessions, :secret=>'1'*64, :per_cookie_cipher_secret=>per_cookie_cipher_secret
|
|
22
|
+
route do |r|
|
|
23
|
+
if r.GET['sut']
|
|
24
|
+
session
|
|
25
|
+
env['roda.session.updated_at'] -= r.GET['sut'].to_i if r.GET['sut']
|
|
26
|
+
end
|
|
27
|
+
r.get('s', String, String){|k, v| session[k] = v}
|
|
28
|
+
r.get('g', String){|k| session[k].to_s}
|
|
29
|
+
r.get('sct'){|i| session; env['roda.session.created_at'].to_s}
|
|
30
|
+
r.get('ssct', Integer){|i| session; (env['roda.session.created_at'] -= i).to_s}
|
|
31
|
+
r.get('sc'){session.clear; 'c'}
|
|
32
|
+
r.get('cs', String, String){|k, v| clear_session; session[k] = v}
|
|
33
|
+
''
|
|
25
34
|
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
35
|
end
|
|
34
36
|
end
|
|
35
|
-
end
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
it "requires appropriate :secret option" do
|
|
39
|
+
proc{app(:bare){plugin :sessions}}.must_raise Roda::RodaError
|
|
40
|
+
proc{app(:bare){plugin :sessions, :secret=>Object.new}}.must_raise Roda::RodaError
|
|
41
|
+
proc{app(:bare){plugin :sessions, :secret=>'1'*63}}.must_raise Roda::RodaError
|
|
42
|
+
end
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
it "has session store data between requests" do
|
|
45
|
+
req('/').must_equal [200, {"Content-Type"=>"text/html", "Content-Length"=>"0"}, [""]]
|
|
46
|
+
body('/s/foo/bar').must_equal 'bar'
|
|
47
|
+
body('/g/foo').must_equal 'bar'
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
body('/s/foo/baz').must_equal 'baz'
|
|
50
|
+
body('/g/foo').must_equal 'baz'
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
body("/s/foo/\u1234").must_equal "\u1234"
|
|
53
|
+
body("/g/foo").must_equal "\u1234"
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
errors.must_equal []
|
|
56
|
+
end
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
58
|
+
it "supports loading sessions created when per_cookie_cipher_secret: #{!per_cookie_cipher_secret} " do
|
|
59
|
+
req('/').must_equal [200, {"Content-Type"=>"text/html", "Content-Length"=>"0"}, [""]]
|
|
60
|
+
body('/s/foo/bar').must_equal 'bar'
|
|
61
|
+
body('/g/foo').must_equal 'bar'
|
|
83
62
|
|
|
84
|
-
|
|
85
|
-
body('/s/foo/bar').must_equal 'bar'
|
|
86
|
-
sct = body('/sct').to_i
|
|
87
|
-
body('/g/foo').must_equal 'bar'
|
|
63
|
+
app.plugin :sessions, :per_cookie_cipher_secret=>!per_cookie_cipher_secret
|
|
88
64
|
|
|
89
|
-
|
|
65
|
+
body('/s/foo/baz').must_equal 'baz'
|
|
66
|
+
body('/g/foo').must_equal 'baz'
|
|
90
67
|
|
|
91
|
-
|
|
92
|
-
['roda.session=', 'max-age=0', 'path=/'].each do |param|
|
|
93
|
-
h['Set-Cookie'].must_match /#{Regexp.escape(param)}(;|\z)/
|
|
68
|
+
errors.must_equal []
|
|
94
69
|
end
|
|
95
|
-
h['Set-Cookie'].must_match /expires=Thu, 01 Jan 1970 00:00:00 (-0000|GMT)(;|\z)/
|
|
96
70
|
|
|
97
|
-
|
|
71
|
+
it "does not add Set-Cookie header if session does not change, unless outside :skip_within seconds" do
|
|
72
|
+
req('/').must_equal [200, {"Content-Type"=>"text/html", "Content-Length"=>"0"}, [""]]
|
|
73
|
+
_, h, b = req('/s/foo/bar')
|
|
74
|
+
h['Set-Cookie'].must_match(/\Aroda.session/)
|
|
75
|
+
b.must_equal ["bar"]
|
|
76
|
+
req('/g/foo').must_equal [200, {"Content-Type"=>"text/html", "Content-Length"=>"3"}, ["bar"]]
|
|
77
|
+
req('/s/foo/bar').must_equal [200, {"Content-Type"=>"text/html", "Content-Length"=>"3"}, ["bar"]]
|
|
78
|
+
|
|
79
|
+
_, h, b = req('/s/foo/baz')
|
|
80
|
+
h['Set-Cookie'].must_match(/\Aroda.session/)
|
|
81
|
+
b.must_equal ["baz"]
|
|
82
|
+
req('/g/foo').must_equal [200, {"Content-Type"=>"text/html", "Content-Length"=>"3"}, ["baz"]]
|
|
83
|
+
|
|
84
|
+
req('/g/foo', 'QUERY_STRING'=>'sut=3500').must_equal [200, {"Content-Type"=>"text/html", "Content-Length"=>"3"}, ["baz"]]
|
|
85
|
+
_, h, b = req('/g/foo', 'QUERY_STRING'=>'sut=3700')
|
|
86
|
+
h['Set-Cookie'].must_match(/\Aroda.session/)
|
|
87
|
+
b.must_equal ["baz"]
|
|
88
|
+
|
|
89
|
+
@app.plugin(:sessions, :skip_within=>3800)
|
|
90
|
+
req('/g/foo', 'QUERY_STRING'=>'sut=3700').must_equal [200, {"Content-Type"=>"text/html", "Content-Length"=>"3"}, ["baz"]]
|
|
91
|
+
_, h, b = req('/g/foo', 'QUERY_STRING'=>'sut=3900')
|
|
92
|
+
h['Set-Cookie'].must_match(/\Aroda.session/)
|
|
93
|
+
b.must_equal ["baz"]
|
|
94
|
+
|
|
95
|
+
errors.must_equal []
|
|
96
|
+
end
|
|
98
97
|
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
it "removes session cookie when session is submitted but empty after request" do
|
|
99
|
+
body('/s/foo/bar').must_equal 'bar'
|
|
100
|
+
sct = body('/sct').to_i
|
|
101
|
+
body('/g/foo').must_equal 'bar'
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
app.plugin :sessions, :cookie_options=>{:max_age=>'1000', :expires=>Time.now+1000}
|
|
104
|
-
body('/s/foo/bar').must_equal 'bar'
|
|
105
|
-
sct = body('/sct').to_i
|
|
106
|
-
body('/g/foo').must_equal 'bar'
|
|
103
|
+
_, h, b = req('/sc')
|
|
107
104
|
|
|
108
|
-
|
|
105
|
+
# Parameters can come in any order, and only the final parameter may omit the ;
|
|
106
|
+
['roda.session=', 'max-age=0', 'path=/'].each do |param|
|
|
107
|
+
h['Set-Cookie'].must_match /#{Regexp.escape(param)}(;|\z)/
|
|
108
|
+
end
|
|
109
|
+
h['Set-Cookie'].must_match /expires=Thu, 01 Jan 1970 00:00:00 (-0000|GMT)(;|\z)/
|
|
110
|
+
|
|
111
|
+
b.must_equal ['c']
|
|
109
112
|
|
|
110
|
-
|
|
111
|
-
['roda.session=', 'max-age=0', 'path=/'].each do |param|
|
|
112
|
-
h['Set-Cookie'].must_match /#{Regexp.escape(param)}(;|\z)/
|
|
113
|
+
errors.must_equal []
|
|
113
114
|
end
|
|
114
|
-
h['Set-Cookie'].must_match /expires=Thu, 01 Jan 1970 00:00:00 (-0000|GMT)(;|\z)/
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
it "removes session cookie even when max-age and expires are in cookie options" do
|
|
117
|
+
app.plugin :sessions, :cookie_options=>{:max_age=>'1000', :expires=>Time.now+1000}
|
|
118
|
+
body('/s/foo/bar').must_equal 'bar'
|
|
119
|
+
sct = body('/sct').to_i
|
|
120
|
+
body('/g/foo').must_equal 'bar'
|
|
117
121
|
|
|
118
|
-
|
|
119
|
-
end
|
|
122
|
+
_, h, b = req('/sc')
|
|
120
123
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
body('/ssct/10').to_i.must_equal(sct - 10)
|
|
124
|
+
# Parameters can come in any order, and only the final parameter may omit the ;
|
|
125
|
+
['roda.session=', 'max-age=0', 'path=/'].each do |param|
|
|
126
|
+
h['Set-Cookie'].must_match /#{Regexp.escape(param)}(;|\z)/
|
|
127
|
+
end
|
|
128
|
+
h['Set-Cookie'].must_match /expires=Thu, 01 Jan 1970 00:00:00 (-0000|GMT)(;|\z)/
|
|
127
129
|
|
|
128
|
-
|
|
129
|
-
body('/sct').to_i.must_be :>=, sct
|
|
130
|
+
b.must_equal ['c']
|
|
130
131
|
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
errors.must_equal []
|
|
133
|
+
end
|
|
133
134
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
it "sets new session create time when clear_session is called even when session is not empty when serializing" do
|
|
136
|
+
body('/s/foo/bar').must_equal 'bar'
|
|
137
|
+
sct = body('/sct').to_i
|
|
138
|
+
body('/g/foo').must_equal 'bar'
|
|
139
|
+
body('/sct').to_i.must_equal sct
|
|
140
|
+
body('/ssct/10').to_i.must_equal(sct - 10)
|
|
138
141
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
h.must_include('; secure')
|
|
142
|
+
body('/cs/foo/baz').must_equal 'baz'
|
|
143
|
+
body('/sct').to_i.must_be :>=, sct
|
|
142
144
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
h.must_include('; HttpOnly')
|
|
146
|
-
h.wont_include('; secure')
|
|
147
|
-
end
|
|
145
|
+
errors.must_equal []
|
|
146
|
+
end
|
|
148
147
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
148
|
+
it "should include HttpOnly and secure cookie options appropriately" do
|
|
149
|
+
h = header('Set-Cookie', '/s/foo/bar')
|
|
150
|
+
h.must_include('; HttpOnly')
|
|
151
|
+
h.wont_include('; secure')
|
|
152
|
+
|
|
153
|
+
h = header('Set-Cookie', '/s/foo/baz', 'HTTPS'=>'on')
|
|
154
|
+
h.must_include('; HttpOnly')
|
|
155
|
+
h.must_include('; secure')
|
|
156
156
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
@app.plugin(:sessions, :cookie_options=>{})
|
|
158
|
+
h = header('Set-Cookie', '/s/foo/bar')
|
|
159
|
+
h.must_include('; HttpOnly')
|
|
160
|
+
h.wont_include('; secure')
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it "should merge :cookie_options options into the default cookie options" do
|
|
164
|
+
@app.plugin(:sessions, :cookie_options=>{:secure=>true})
|
|
165
|
+
h = header('Set-Cookie', '/s/foo/bar')
|
|
166
|
+
h.must_include('; HttpOnly')
|
|
167
|
+
h.must_include('; path=/')
|
|
168
|
+
h.must_include('; secure')
|
|
169
|
+
end
|
|
160
170
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
171
|
+
it "handles secret rotation using :old_secret option" do
|
|
172
|
+
body('/s/foo/bar').must_equal 'bar'
|
|
173
|
+
body('/g/foo').must_equal 'bar'
|
|
164
174
|
|
|
165
|
-
|
|
166
|
-
|
|
175
|
+
old_cookie = @cookie
|
|
176
|
+
@app.plugin(:sessions, :secret=>'2'*64, :old_secret=>'1'*64)
|
|
177
|
+
body('/g/foo', 'QUERY_STRING'=>'sut=3700').must_equal 'bar'
|
|
167
178
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
errors.must_equal ["Not decoding session: HMAC invalid"]
|
|
179
|
+
@app.plugin(:sessions, :secret=>'2'*64, :old_secret=>nil)
|
|
180
|
+
body('/g/foo', 'QUERY_STRING'=>'sut=3700').must_equal 'bar'
|
|
171
181
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
182
|
+
@cookie = old_cookie
|
|
183
|
+
body('/g/foo').must_equal ''
|
|
184
|
+
errors.must_equal ["Not decoding session: HMAC invalid"]
|
|
175
185
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
_, h1, b = req('/s/foo/bar')
|
|
180
|
-
b.must_equal ['bar']
|
|
181
|
-
_, h2, b = req('/s/foo/bar', 'QUERY_STRING'=>'sut=3700')
|
|
182
|
-
b.must_equal ['bar']
|
|
183
|
-
_, h3, b = req('/s/foo/bar2')
|
|
184
|
-
b.must_equal ['bar2']
|
|
185
|
-
_, h4, b = req("/s/foo/#{long}")
|
|
186
|
-
b.must_equal [long]
|
|
187
|
-
h1['Set-Cookie'].length.must_equal h2['Set-Cookie'].length
|
|
188
|
-
h1['Set-Cookie'].wont_equal h2['Set-Cookie']
|
|
189
|
-
h1['Set-Cookie'].length.must_equal h3['Set-Cookie'].length
|
|
190
|
-
h1['Set-Cookie'].wont_equal h3['Set-Cookie']
|
|
191
|
-
h1['Set-Cookie'].length.wont_equal h4['Set-Cookie'].length
|
|
192
|
-
|
|
193
|
-
@app.plugin(:sessions, :pad_size=>256)
|
|
194
|
-
|
|
195
|
-
_, h1, b = req('/s/foo/bar')
|
|
196
|
-
b.must_equal ['bar']
|
|
197
|
-
_, h2, b = req('/s/foo/bar', 'QUERY_STRING'=>'sut=3700')
|
|
198
|
-
b.must_equal ['bar']
|
|
199
|
-
_, h3, b = req('/s/foo/bar2')
|
|
200
|
-
b.must_equal ['bar2']
|
|
201
|
-
_, h4, b = req("/s/foo/#{long}")
|
|
202
|
-
b.must_equal [long]
|
|
203
|
-
h1['Set-Cookie'].length.must_equal h2['Set-Cookie'].length
|
|
204
|
-
h1['Set-Cookie'].wont_equal h2['Set-Cookie']
|
|
205
|
-
h1['Set-Cookie'].length.must_equal h3['Set-Cookie'].length
|
|
206
|
-
h1['Set-Cookie'].wont_equal h3['Set-Cookie']
|
|
207
|
-
h1['Set-Cookie'].length.must_equal h4['Set-Cookie'].length
|
|
208
|
-
h1['Set-Cookie'].wont_equal h3['Set-Cookie']
|
|
209
|
-
|
|
210
|
-
@app.plugin(:sessions, :pad_size=>nil)
|
|
211
|
-
|
|
212
|
-
_, h1, b = req('/s/foo/bar')
|
|
213
|
-
b.must_equal ['bar']
|
|
214
|
-
_, h2, b = req('/s/foo/bar', 'QUERY_STRING'=>'sut=3700')
|
|
215
|
-
b.must_equal ['bar']
|
|
216
|
-
_, h3, b = req('/s/foo/bar2')
|
|
217
|
-
b.must_equal ['bar2']
|
|
218
|
-
h1['Set-Cookie'].length.must_equal h2['Set-Cookie'].length
|
|
219
|
-
h1['Set-Cookie'].wont_equal h2['Set-Cookie']
|
|
220
|
-
if !defined?(JRUBY_VERSION) || JRUBY_VERSION >= '9.2'
|
|
221
|
-
h1['Set-Cookie'].length.wont_equal h3['Set-Cookie'].length
|
|
186
|
+
proc{app(:bare){plugin :sessions, :old_secret=>'1'*63}}.must_raise Roda::RodaError
|
|
187
|
+
proc{app(:bare){plugin :sessions, :old_secret=>Object.new}}.must_raise Roda::RodaError
|
|
222
188
|
end
|
|
223
189
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
190
|
+
it "handles secret rotation using :old_secret option when also changing :per_cookie_cipher_secret option" do
|
|
191
|
+
body('/s/foo/bar').must_equal 'bar'
|
|
192
|
+
body('/g/foo').must_equal 'bar'
|
|
227
193
|
|
|
228
|
-
|
|
229
|
-
|
|
194
|
+
old_cookie = @cookie
|
|
195
|
+
@app.plugin(:sessions, :secret=>'2'*64, :old_secret=>'1'*64, :per_cookie_cipher_secret=>!per_cookie_cipher_secret)
|
|
230
196
|
|
|
231
|
-
|
|
232
|
-
long = 'b'*8192
|
|
233
|
-
proc{body("/s/foo/#{long}")}.must_raise Roda::RodaPlugins::Sessions::CookieTooLarge
|
|
197
|
+
body('/g/foo', 'QUERY_STRING'=>'sut=3700').must_equal 'bar'
|
|
234
198
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
body("/g/foo", 'QUERY_STRING'=>'sut=3700').must_equal long
|
|
199
|
+
@app.plugin(:sessions, :secret=>'2'*64, :old_secret=>nil)
|
|
200
|
+
body('/g/foo', 'QUERY_STRING'=>'sut=3700').must_equal 'bar'
|
|
238
201
|
|
|
239
|
-
|
|
240
|
-
|
|
202
|
+
@cookie = old_cookie
|
|
203
|
+
body('/g/foo').must_equal ''
|
|
204
|
+
errors.must_equal ["Not decoding session: HMAC invalid"]
|
|
241
205
|
|
|
242
|
-
|
|
243
|
-
|
|
206
|
+
proc{app(:bare){plugin :sessions, :old_secret=>'1'*63}}.must_raise Roda::RodaError
|
|
207
|
+
proc{app(:bare){plugin :sessions, :old_secret=>Object.new}}.must_raise Roda::RodaError
|
|
208
|
+
end
|
|
244
209
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
210
|
+
it "pads data by default to make it more difficult to guess session contents based on size" do
|
|
211
|
+
long = "bar"*35
|
|
212
|
+
|
|
213
|
+
_, h1, b = req('/s/foo/bar')
|
|
214
|
+
b.must_equal ['bar']
|
|
215
|
+
_, h2, b = req('/s/foo/bar', 'QUERY_STRING'=>'sut=3700')
|
|
216
|
+
b.must_equal ['bar']
|
|
217
|
+
_, h3, b = req('/s/foo/bar2')
|
|
218
|
+
b.must_equal ['bar2']
|
|
219
|
+
_, h4, b = req("/s/foo/#{long}")
|
|
220
|
+
b.must_equal [long]
|
|
221
|
+
h1['Set-Cookie'].length.must_equal h2['Set-Cookie'].length
|
|
222
|
+
h1['Set-Cookie'].wont_equal h2['Set-Cookie']
|
|
223
|
+
h1['Set-Cookie'].length.must_equal h3['Set-Cookie'].length
|
|
224
|
+
h1['Set-Cookie'].wont_equal h3['Set-Cookie']
|
|
225
|
+
h1['Set-Cookie'].length.wont_equal h4['Set-Cookie'].length
|
|
226
|
+
|
|
227
|
+
@app.plugin(:sessions, :pad_size=>256)
|
|
228
|
+
|
|
229
|
+
_, h1, b = req('/s/foo/bar')
|
|
230
|
+
b.must_equal ['bar']
|
|
231
|
+
_, h2, b = req('/s/foo/bar', 'QUERY_STRING'=>'sut=3700')
|
|
232
|
+
b.must_equal ['bar']
|
|
233
|
+
_, h3, b = req('/s/foo/bar2')
|
|
234
|
+
b.must_equal ['bar2']
|
|
235
|
+
_, h4, b = req("/s/foo/#{long}")
|
|
236
|
+
b.must_equal [long]
|
|
237
|
+
h1['Set-Cookie'].length.must_equal h2['Set-Cookie'].length
|
|
238
|
+
h1['Set-Cookie'].wont_equal h2['Set-Cookie']
|
|
239
|
+
h1['Set-Cookie'].length.must_equal h3['Set-Cookie'].length
|
|
240
|
+
h1['Set-Cookie'].wont_equal h3['Set-Cookie']
|
|
241
|
+
h1['Set-Cookie'].length.must_equal h4['Set-Cookie'].length
|
|
242
|
+
h1['Set-Cookie'].wont_equal h3['Set-Cookie']
|
|
243
|
+
|
|
244
|
+
@app.plugin(:sessions, :pad_size=>nil)
|
|
245
|
+
|
|
246
|
+
_, h1, b = req('/s/foo/bar')
|
|
247
|
+
b.must_equal ['bar']
|
|
248
|
+
_, h2, b = req('/s/foo/bar', 'QUERY_STRING'=>'sut=3700')
|
|
249
|
+
b.must_equal ['bar']
|
|
250
|
+
_, h3, b = req('/s/foo/bar2')
|
|
251
|
+
b.must_equal ['bar2']
|
|
252
|
+
h1['Set-Cookie'].length.must_equal h2['Set-Cookie'].length
|
|
253
|
+
h1['Set-Cookie'].wont_equal h2['Set-Cookie']
|
|
254
|
+
if !defined?(JRUBY_VERSION) || JRUBY_VERSION >= '9.2'
|
|
255
|
+
h1['Set-Cookie'].length.wont_equal h3['Set-Cookie'].length
|
|
256
|
+
end
|
|
248
257
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
258
|
+
proc{@app.plugin(:sessions, :pad_size=>0)}.must_raise Roda::RodaError
|
|
259
|
+
proc{@app.plugin(:sessions, :pad_size=>1)}.must_raise Roda::RodaError
|
|
260
|
+
proc{@app.plugin(:sessions, :pad_size=>Object.new)}.must_raise Roda::RodaError
|
|
252
261
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
errors.must_equal ["Not returning session: maximum session time expired"]
|
|
262
|
+
errors.must_equal []
|
|
263
|
+
end
|
|
256
264
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
265
|
+
it "compresses data over a certain size by default" do
|
|
266
|
+
long = 'b'*8192
|
|
267
|
+
proc{body("/s/foo/#{long}")}.must_raise Roda::RodaPlugins::Sessions::CookieTooLarge
|
|
260
268
|
|
|
261
|
-
|
|
262
|
-
|
|
269
|
+
@app.plugin(:sessions, :gzip_over=>8000)
|
|
270
|
+
body("/s/foo/#{long}").must_equal long
|
|
271
|
+
body("/g/foo", 'QUERY_STRING'=>'sut=3700').must_equal long
|
|
263
272
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
body("/g/foo").must_equal 'bar'
|
|
273
|
+
@app.plugin(:sessions, :gzip_over=>15000)
|
|
274
|
+
proc{body("/g/foo", 'QUERY_STRING'=>'sut=3700')}.must_raise Roda::RodaPlugins::Sessions::CookieTooLarge
|
|
267
275
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
errors.must_equal ["Not returning session: maximum session idle time expired"]
|
|
276
|
+
errors.must_equal []
|
|
277
|
+
end
|
|
271
278
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
279
|
+
it "raises CookieTooLarge if cookie is too large" do
|
|
280
|
+
proc{req('/s/foo/'+Base64.urlsafe_encode64(SecureRandom.random_bytes(8192)))}.must_raise Roda::RodaPlugins::Sessions::CookieTooLarge
|
|
281
|
+
end
|
|
275
282
|
|
|
276
|
-
|
|
277
|
-
|
|
283
|
+
it "ignores session cookies if session exceeds max time since create" do
|
|
284
|
+
body("/s/foo/bar").must_equal 'bar'
|
|
285
|
+
body("/g/foo").must_equal 'bar'
|
|
278
286
|
|
|
279
|
-
|
|
280
|
-
|
|
287
|
+
@app.plugin(:sessions, :max_seconds=>-1)
|
|
288
|
+
body("/g/foo").must_equal ''
|
|
289
|
+
errors.must_equal ["Not returning session: maximum session time expired"]
|
|
281
290
|
|
|
282
|
-
|
|
283
|
-
|
|
291
|
+
@app.plugin(:sessions, :max_seconds=>10)
|
|
292
|
+
body("/s/foo/bar").must_equal 'bar'
|
|
293
|
+
body("/g/foo").must_equal 'bar'
|
|
284
294
|
|
|
285
|
-
|
|
295
|
+
errors.must_equal []
|
|
296
|
+
end
|
|
286
297
|
|
|
287
|
-
|
|
288
|
-
|
|
298
|
+
it "ignores session cookies if session exceeds max idle time since update" do
|
|
299
|
+
body("/s/foo/bar").must_equal 'bar'
|
|
300
|
+
body("/g/foo").must_equal 'bar'
|
|
289
301
|
|
|
290
|
-
|
|
291
|
-
|
|
302
|
+
@app.plugin(:sessions, :max_idle_seconds=>-1)
|
|
303
|
+
body("/g/foo").must_equal ''
|
|
304
|
+
errors.must_equal ["Not returning session: maximum session idle time expired"]
|
|
292
305
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
k = c.split('=', 2)[0] + '='
|
|
306
|
+
@app.plugin(:sessions, :max_idle_seconds=>10)
|
|
307
|
+
body("/s/foo/bar").must_equal 'bar'
|
|
308
|
+
body("/g/foo").must_equal 'bar'
|
|
297
309
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
errors.must_equal ["Unable to decode session: invalid base64"]
|
|
310
|
+
errors.must_equal []
|
|
311
|
+
end
|
|
301
312
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
errors.must_equal ["Unable to decode session: data too short"]
|
|
313
|
+
it "supports :serializer and :parser options to override serializer/deserializer" do
|
|
314
|
+
body('/s/foo/bar').must_equal 'bar'
|
|
305
315
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
errors.must_equal ["Unable to decode session: version marker unsupported"]
|
|
316
|
+
@app.plugin(:sessions, :parser=>proc{|s| JSON.parse("{#{s[1...-1].reverse}}")})
|
|
317
|
+
body('/g/rab').must_equal 'oof'
|
|
309
318
|
|
|
310
|
-
|
|
311
|
-
body('/g/foo').must_equal ''
|
|
312
|
-
errors.must_equal ["Not decoding session: HMAC invalid"]
|
|
313
|
-
end
|
|
314
|
-
end
|
|
319
|
+
@app.plugin(:sessions, :serializer=>proc{|s| s.to_json.upcase})
|
|
315
320
|
|
|
316
|
-
|
|
317
|
-
|
|
321
|
+
body('/s/foo/baz').must_equal 'baz'
|
|
322
|
+
body('/g/ZAB').must_equal 'OOF'
|
|
318
323
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
324
|
+
errors.must_equal []
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
it "logs session decoding errors to rack.errors" do
|
|
328
|
+
body('/s/foo/bar').must_equal 'bar'
|
|
329
|
+
c = @cookie.dup
|
|
330
|
+
k = c.split('=', 2)[0] + '='
|
|
323
331
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
332
|
+
@cookie[20] = '!'
|
|
333
|
+
body('/g/foo').must_equal ''
|
|
334
|
+
errors.must_equal ["Unable to decode session: invalid base64"]
|
|
335
|
+
|
|
336
|
+
@cookie = k+Base64.urlsafe_encode64('')
|
|
337
|
+
body('/g/foo').must_equal ''
|
|
338
|
+
errors.must_equal ["Unable to decode session: no data"]
|
|
339
|
+
|
|
340
|
+
@cookie = k+Base64.urlsafe_encode64("\0" * 60)
|
|
341
|
+
body('/g/foo').must_equal ''
|
|
342
|
+
errors.must_equal ["Unable to decode session: data too short"]
|
|
343
|
+
|
|
344
|
+
@cookie = k+Base64.urlsafe_encode64("\1" * 92)
|
|
345
|
+
body('/g/foo').must_equal ''
|
|
346
|
+
errors.must_equal ["Unable to decode session: data too short"]
|
|
347
|
+
|
|
348
|
+
@cookie = k+Base64.urlsafe_encode64('1'*75)
|
|
349
|
+
body('/g/foo').must_equal ''
|
|
350
|
+
errors.must_equal ["Unable to decode session: version marker unsupported"]
|
|
351
|
+
|
|
352
|
+
@cookie = k+Base64.urlsafe_encode64("\0"*75)
|
|
353
|
+
body('/g/foo').must_equal ''
|
|
354
|
+
errors.must_equal ["Not decoding session: HMAC invalid"]
|
|
355
|
+
end
|
|
328
356
|
end
|
|
329
357
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
358
|
+
describe "sessions plugin" do
|
|
359
|
+
include CookieJar
|
|
360
|
+
|
|
361
|
+
def req(path, opts={})
|
|
362
|
+
@errors ||= (errors = []; def errors.puts(s) self << s; end; errors)
|
|
363
|
+
super(path, opts.merge('rack.errors'=>@errors))
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def errors
|
|
367
|
+
e = @errors.dup
|
|
368
|
+
@errors.clear
|
|
369
|
+
e
|
|
339
370
|
end
|
|
340
371
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
372
|
+
it "supports transparent upgrade from Rack::Session::Cookie with default HMAC and coder" do
|
|
373
|
+
app(:bare) do
|
|
374
|
+
use Rack::Session::Cookie, :secret=>'1'
|
|
375
|
+
plugin :middleware_stack
|
|
376
|
+
route do |r|
|
|
377
|
+
r.get('s', String, String){|k, v| session[k] = {:a=>v}; v}
|
|
378
|
+
r.get('g', String){|k| session[k].inspect}
|
|
379
|
+
''
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
_, h, b = req('/s/foo/bar')
|
|
384
|
+
(h['Set-Cookie'] =~ /\A(rack\.session=.*); path=\/; HttpOnly\z/).must_equal 0
|
|
385
|
+
c = $1
|
|
386
|
+
b.must_equal ['bar']
|
|
387
|
+
_, h, b = req('/g/foo')
|
|
388
|
+
h['Set-Cookie'].must_be_nil
|
|
389
|
+
b.must_equal ['{:a=>"bar"}']
|
|
390
|
+
|
|
391
|
+
@app.plugin :sessions, :secret=>'1'*64,
|
|
392
|
+
:upgrade_from_rack_session_cookie_secret=>'1'
|
|
393
|
+
@app.middleware_stack.remove{|m, *| m == Rack::Session::Cookie}
|
|
394
|
+
|
|
395
|
+
@cookie = c.dup
|
|
396
|
+
@cookie.slice!(15)
|
|
397
|
+
body('/g/foo').must_equal 'nil'
|
|
398
|
+
errors.must_equal ["Not decoding Rack::Session::Cookie session: HMAC invalid"]
|
|
399
|
+
|
|
400
|
+
@cookie = c.split('--', 2)[0]
|
|
401
|
+
body('/g/foo').must_equal 'nil'
|
|
402
|
+
errors.must_equal ["Not decoding Rack::Session::Cookie session: invalid format"]
|
|
403
|
+
|
|
404
|
+
@cookie = c.split('--', 2)[0][13..-1]
|
|
405
|
+
@cookie = Rack::Utils.unescape(@cookie).unpack('m')[0]
|
|
406
|
+
@cookie[2] = "^"
|
|
407
|
+
@cookie = [@cookie].pack('m')
|
|
408
|
+
cookie = String.new
|
|
409
|
+
cookie << 'rack.session=' << @cookie << '--' << OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, '1', @cookie)
|
|
410
|
+
@cookie = cookie
|
|
411
|
+
body('/g/foo').must_equal 'nil'
|
|
412
|
+
errors.must_equal ["Error decoding Rack::Session::Cookie session: not base64 encoded marshal dump"]
|
|
413
|
+
|
|
414
|
+
@cookie = c
|
|
415
|
+
_, h, b = req('/g/foo')
|
|
416
|
+
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)
|
|
417
|
+
b.must_equal ['{"a"=>"bar"}']
|
|
418
|
+
|
|
419
|
+
@app.plugin :sessions, :cookie_options=>{:path=>'/foo'}, :upgrade_from_rack_session_cookie_options=>{}
|
|
420
|
+
@cookie = c
|
|
421
|
+
_, h, b = req('/g/foo')
|
|
422
|
+
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)
|
|
423
|
+
b.must_equal ['{"a"=>"bar"}']
|
|
424
|
+
|
|
425
|
+
@app.plugin :sessions, :upgrade_from_rack_session_cookie_options=>{:path=>'/baz'}
|
|
426
|
+
@cookie = c
|
|
427
|
+
_, h, b = req('/g/foo')
|
|
428
|
+
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)
|
|
429
|
+
b.must_equal ['{"a"=>"bar"}']
|
|
430
|
+
|
|
431
|
+
@app.plugin :sessions, :upgrade_from_rack_session_cookie_key=>'quux.session'
|
|
432
|
+
@cookie = c.sub(/\Arack/, 'quux')
|
|
433
|
+
_, h, b = req('/g/foo')
|
|
434
|
+
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)
|
|
435
|
+
b.must_equal ['{"a"=>"bar"}']
|
|
436
|
+
end
|
|
394
437
|
end
|
|
395
438
|
end
|
|
396
439
|
end
|