roda 3.10.0 → 3.11.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 +8 -0
- data/doc/release_notes/3.11.0.txt +54 -0
- data/lib/roda.rb +11 -0
- data/lib/roda/plugins/_after_hook.rb +35 -0
- data/lib/roda/plugins/_before_hook.rb +51 -0
- data/lib/roda/plugins/class_level_routing.rb +16 -11
- data/lib/roda/plugins/error_handler.rb +23 -2
- data/lib/roda/plugins/flash.rb +7 -5
- data/lib/roda/plugins/head.rb +7 -4
- data/lib/roda/plugins/heartbeat.rb +8 -4
- data/lib/roda/plugins/hooks.rb +17 -11
- data/lib/roda/plugins/sessions.rb +25 -27
- data/lib/roda/plugins/static_routing.rb +10 -10
- data/lib/roda/plugins/status_handler.rb +9 -7
- data/lib/roda/version.rb +1 -1
- data/spec/plugin/error_handler_spec.rb +22 -0
- data/spec/plugin/flash_spec.rb +33 -31
- data/spec/plugin/hooks_spec.rb +28 -0
- data/spec/plugin/sessions_spec.rb +4 -4
- data/spec/plugin/static_routing_spec.rb +20 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b7fc4e03ddcb9b4e5fa37af6c0a151aef49965ef251d8ab177e7b530ec034529
|
4
|
+
data.tar.gz: 78ab54340363d9b963496e143809ca9f809c619d25f98faa08b08f9e3790ae19
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 56b9bb6e1e03cfa079457238d532d9223fdd6adef8438954dfd601afb68b384945b528c2606a3100881f3154e94024ae46f286364c41fce4c23af624a1ebcee3
|
7
|
+
data.tar.gz: 3856d03202576bd35465b29e975bb84fe23780695b87442c3ba8b09b7e36deb19cddf6a4ea5299249baf21c9e7b62b61e681221580b6d7a5af6b84858395d8a0
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
+
= 3.11.0 (2018-08-15)
|
2
|
+
|
3
|
+
* Disable default compression of sessions over 128 bytes in the sessions plugin (jeremyevans)
|
4
|
+
|
5
|
+
* Log but otherwise ignore exceptions raised by after processing of error handler response (jeremyevans)
|
6
|
+
|
7
|
+
* Modify internal before/after processing to avoid plugin load order issues (jeremyevans)
|
8
|
+
|
1
9
|
= 3.10.0 (2018-07-18)
|
2
10
|
|
3
11
|
* Remove flash key from session if new flash is empty when rotating flash (jeremyevans)
|
@@ -0,0 +1,54 @@
|
|
1
|
+
= Improvements
|
2
|
+
|
3
|
+
* The order in which internal plugin before and after hooks are run
|
4
|
+
when multiple plugins are loaded is now fixed and does not depend
|
5
|
+
on the order in which the plugins are loaded. This can prevent
|
6
|
+
some issues in cases the plugins were not loaded in the order
|
7
|
+
previously recommended in the documentation.
|
8
|
+
|
9
|
+
Internal plugin before hooks are now run in the following order:
|
10
|
+
|
11
|
+
* hooks
|
12
|
+
* heartbeat
|
13
|
+
* static_routing
|
14
|
+
|
15
|
+
and internal plugin after hooks are now run in the following order:
|
16
|
+
|
17
|
+
* class_level_routing
|
18
|
+
* status_handler
|
19
|
+
* head
|
20
|
+
* flash
|
21
|
+
* session
|
22
|
+
* hooks
|
23
|
+
|
24
|
+
* Default compression of sessions over 128 bytes in length has been
|
25
|
+
disabled in the sessions plugin. Compression of sessions must now
|
26
|
+
be manually enabled if it is desired by setting :gzip_over to an
|
27
|
+
integer.
|
28
|
+
|
29
|
+
This change is being made to avoid possible compression ratio
|
30
|
+
attacks if both sensitive data and user-submitted data are stored in
|
31
|
+
the session. Such attacks were mitigated by the sessions plugin's
|
32
|
+
default use of padding after compression, and the JSON serialization
|
33
|
+
format used, but disabling compression avoids the possibility.
|
34
|
+
|
35
|
+
This does not affect backwards compatibility, as compressed sessions
|
36
|
+
will still be decompressed correctly, unless the size of the session
|
37
|
+
cookie when not using compression is over 4096 bytes.
|
38
|
+
|
39
|
+
= Backwards Compatibility
|
40
|
+
|
41
|
+
* When using the error_handler plugin, if routing raises an exception that
|
42
|
+
is handled by the error handler, but an exception is raised by a plugin
|
43
|
+
internal after hook after the error handler has been run, the exception
|
44
|
+
will be logged to the rack.errors entry in the environment, but it will
|
45
|
+
be otherwise ignored.
|
46
|
+
|
47
|
+
Exceptions raised inside the error handler will continue to be be raised
|
48
|
+
to the application's caller.
|
49
|
+
|
50
|
+
Additionally, the error_handler plugin no longers call before hooks
|
51
|
+
during error handling.
|
52
|
+
|
53
|
+
* A private Roda#_call method has been added. This could potentially
|
54
|
+
cause issues for applications that add their own _call method.
|
data/lib/roda.rb
CHANGED
@@ -239,6 +239,7 @@ class Roda
|
|
239
239
|
# Build the rack app to use
|
240
240
|
def build_rack_app
|
241
241
|
if block = @route_block
|
242
|
+
block = rack_app_route_block(block)
|
242
243
|
app = lambda{|env| new(env).call(&block)}
|
243
244
|
@middleware.reverse_each do |args, bl|
|
244
245
|
mid, *args = args
|
@@ -248,6 +249,12 @@ class Roda
|
|
248
249
|
@app = app
|
249
250
|
end
|
250
251
|
end
|
252
|
+
|
253
|
+
# The route block to use when building the rack app.
|
254
|
+
# Can be modified by plugins.
|
255
|
+
def rack_app_route_block(block)
|
256
|
+
block
|
257
|
+
end
|
251
258
|
end
|
252
259
|
|
253
260
|
# Instance methods for the Roda class.
|
@@ -277,6 +284,10 @@ class Roda
|
|
277
284
|
end
|
278
285
|
end
|
279
286
|
|
287
|
+
# Private alias for internal use
|
288
|
+
alias _call call
|
289
|
+
private :_call
|
290
|
+
|
280
291
|
# The environment hash for the current request. Example:
|
281
292
|
#
|
282
293
|
# env['REQUEST_METHOD'] # => 'GET'
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
class Roda
|
5
|
+
module RodaPlugins
|
6
|
+
# Internal after hook module, not for external use.
|
7
|
+
# Allows for plugins to configure the order in which
|
8
|
+
# after processing is done by using _roda_after_*
|
9
|
+
# private instance methods that are called in sorted order.
|
10
|
+
module AfterHook # :nodoc:
|
11
|
+
module ClassMethods
|
12
|
+
# Rebuild the _roda_after method whenever a plugin might
|
13
|
+
# have added a _roda_after_* method.
|
14
|
+
def include(*)
|
15
|
+
res = super
|
16
|
+
meths = private_instance_methods.grep(/\A_roda_after_\d\d\z/).sort.map{|s| "#{s}(res)"}.join(';')
|
17
|
+
class_eval("def _roda_after(res); #{meths} end", __FILE__, __LINE__)
|
18
|
+
private :_roda_after
|
19
|
+
res
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module InstanceMethods
|
24
|
+
# Run internal after hooks with the response
|
25
|
+
def call
|
26
|
+
res = super
|
27
|
+
ensure
|
28
|
+
_roda_after(res)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
register_plugin(:_after_hook, AfterHook)
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
class Roda
|
5
|
+
module RodaPlugins
|
6
|
+
# Internal before hook module, not for external use.
|
7
|
+
# Allows for plugins to configure the order in which
|
8
|
+
# before processing is done by using _roda_before_*
|
9
|
+
# private instance methods that are called in sorted order.
|
10
|
+
module BeforeHook # :nodoc:
|
11
|
+
# Rebuild the rack app if the rack app already exists,
|
12
|
+
# so the before hooks are setup inside the rack app
|
13
|
+
# route block.
|
14
|
+
def self.configure(app)
|
15
|
+
app.instance_exec do
|
16
|
+
build_rack_app if @app
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
# Rebuild the _roda_before method whenever a plugin might
|
22
|
+
# have added a _roda_before_* method.
|
23
|
+
def include(*a)
|
24
|
+
res = super
|
25
|
+
def_roda_before
|
26
|
+
res
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# Build a _roda_before method that calls each _roda_before_* method
|
32
|
+
# in order.
|
33
|
+
def def_roda_before
|
34
|
+
meths = private_instance_methods.grep(/\A_roda_before_\d\d\z/).sort.join(';')
|
35
|
+
class_eval("def _roda_before; #{meths} end", __FILE__, __LINE__)
|
36
|
+
private :_roda_before
|
37
|
+
end
|
38
|
+
|
39
|
+
# Modify rack app route block to use before hook.
|
40
|
+
def rack_app_route_block(block)
|
41
|
+
lambda do |r|
|
42
|
+
_roda_before
|
43
|
+
instance_exec(r, &block)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
register_plugin(:_before_hook, BeforeHook)
|
50
|
+
end
|
51
|
+
end
|
@@ -52,6 +52,10 @@ class Roda
|
|
52
52
|
# the normal +route+ class method to define your routing tree. This plugin does make it simpler to
|
53
53
|
# add additional routes after the routing tree has already been defined, though.
|
54
54
|
module ClassLevelRouting
|
55
|
+
def self.load_dependencies(app)
|
56
|
+
app.plugin :_after_hook
|
57
|
+
end
|
58
|
+
|
55
59
|
# Initialize the class_routes array when the plugin is loaded. Also, if the application doesn't
|
56
60
|
# currently have a routing block, setup an empty routing block so that things will still work if
|
57
61
|
# a routing block isn't added.
|
@@ -76,28 +80,29 @@ class Roda
|
|
76
80
|
end
|
77
81
|
|
78
82
|
module InstanceMethods
|
83
|
+
def initialize(_)
|
84
|
+
super
|
85
|
+
@_original_remaining_path = @_request.remaining_path
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
79
90
|
# If the normal routing tree doesn't handle an action, try each class level route
|
80
91
|
# to see if it matches.
|
81
|
-
def
|
82
|
-
|
83
|
-
rp = req.remaining_path
|
84
|
-
result = super
|
85
|
-
|
86
|
-
if result[0] == 404 && (v = result[2]).is_a?(Array) && v.empty?
|
92
|
+
def _roda_after_10(result)
|
93
|
+
if result && result[0] == 404 && (v = result[2]).is_a?(Array) && v.empty?
|
87
94
|
# Reset the response so it doesn't inherit the status or any headers from
|
88
95
|
# the original response.
|
89
96
|
@_response.send(:initialize)
|
90
|
-
|
97
|
+
result.replace(_call do |r|
|
91
98
|
opts[:class_level_routes].each do |meth, args, blk|
|
92
|
-
|
99
|
+
r.instance_variable_set(:@remaining_path, @_original_remaining_path)
|
93
100
|
r.public_send(meth, *args) do |*a|
|
94
101
|
instance_exec(*a, &blk)
|
95
102
|
end
|
96
103
|
end
|
97
104
|
nil
|
98
|
-
end
|
99
|
-
else
|
100
|
-
result
|
105
|
+
end)
|
101
106
|
end
|
102
107
|
end
|
103
108
|
end
|
@@ -28,7 +28,15 @@ class Roda
|
|
28
28
|
# default status set to 500, before executing the error handler.
|
29
29
|
# The error handler can change the response status if necessary,
|
30
30
|
# as well set headers and/or write to the body, just like a regular
|
31
|
-
# request.
|
31
|
+
# request. After the error handler returns a response, normal after
|
32
|
+
# processing of that response occurs, except that an exception during
|
33
|
+
# after processing is logged to <tt>env['rack.errors']</tt> but
|
34
|
+
# otherwise ignored. This avoids recursive calls into the
|
35
|
+
# error_handler. Note that if the error_handler itself raises
|
36
|
+
# an exception, the exception will be raised without normal after
|
37
|
+
# processing. This can cause some after processing to run twice
|
38
|
+
# (once before the error_handler is called and once after) if
|
39
|
+
# later after processing raises an exception.
|
32
40
|
#
|
33
41
|
# By default, this plugin handles StandardError and ScriptError.
|
34
42
|
# To override the exception classes it will handle, pass a :classes
|
@@ -36,6 +44,10 @@ class Roda
|
|
36
44
|
#
|
37
45
|
# plugin :error_handler, classes: [StandardError, ScriptError, NoMemoryError]
|
38
46
|
module ErrorHandler
|
47
|
+
def self.load_dependencies(app, *)
|
48
|
+
app.plugin :_after_hook
|
49
|
+
end
|
50
|
+
|
39
51
|
DEFAULT_ERROR_HANDLER_CLASSES = [StandardError, ScriptError].freeze
|
40
52
|
|
41
53
|
# If a block is given, automatically call the +error+ method on
|
@@ -66,7 +78,16 @@ class Roda
|
|
66
78
|
rescue *opts[:error_handler_classes] => e
|
67
79
|
@_response.send(:initialize)
|
68
80
|
@_response.status = 500
|
69
|
-
|
81
|
+
res = _call{handle_error(e)}
|
82
|
+
begin
|
83
|
+
_roda_after(res)
|
84
|
+
rescue => e2
|
85
|
+
if errors = env['rack.errors']
|
86
|
+
errors.puts "Error in after hook processing of error handler: #{e2.class}: #{e2.message}"
|
87
|
+
e2.backtrace.each{|line| errors.puts(line)}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
res
|
70
91
|
end
|
71
92
|
|
72
93
|
private
|
data/lib/roda/plugins/flash.rb
CHANGED
@@ -36,6 +36,10 @@ class Roda
|
|
36
36
|
# flash['a'] # = >'b'
|
37
37
|
# end
|
38
38
|
module Flash
|
39
|
+
def self.load_dependencies(app)
|
40
|
+
app.plugin :_after_hook
|
41
|
+
end
|
42
|
+
|
39
43
|
# Simple flash hash, where assiging to the hash updates the flash
|
40
44
|
# used in the following request.
|
41
45
|
class FlashHash < DelegateClass(Hash)
|
@@ -95,11 +99,11 @@ class Roda
|
|
95
99
|
@_flash ||= FlashHash.new(session['_flash'] || (session['_flash'] = session.delete(:_flash)))
|
96
100
|
end
|
97
101
|
|
102
|
+
private
|
103
|
+
|
98
104
|
# If the routing doesn't raise an error, rotate the flash
|
99
105
|
# hash in the session so the next request has access to it.
|
100
|
-
def
|
101
|
-
res = super
|
102
|
-
|
106
|
+
def _roda_after_40(_)
|
103
107
|
if f = @_flash
|
104
108
|
f = f.next
|
105
109
|
if f.empty?
|
@@ -108,8 +112,6 @@ class Roda
|
|
108
112
|
session['_flash'] = f
|
109
113
|
end
|
110
114
|
end
|
111
|
-
|
112
|
-
res
|
113
115
|
end
|
114
116
|
end
|
115
117
|
end
|
data/lib/roda/plugins/head.rb
CHANGED
@@ -37,6 +37,9 @@ class Roda
|
|
37
37
|
# this plugin those HEAD requests will return a 404 status, which
|
38
38
|
# may prevent search engines from crawling your website.
|
39
39
|
module Head
|
40
|
+
def self.load_dependencies(app)
|
41
|
+
app.plugin :_after_hook
|
42
|
+
end
|
40
43
|
|
41
44
|
# used to ensure proper resource release on HEAD requests
|
42
45
|
# we do not respond to a to_path method, here.
|
@@ -56,11 +59,12 @@ class Roda
|
|
56
59
|
end
|
57
60
|
|
58
61
|
module InstanceMethods
|
62
|
+
private
|
63
|
+
|
59
64
|
# Always use an empty response body for head requests, with a
|
60
65
|
# content length of 0.
|
61
|
-
def
|
62
|
-
res
|
63
|
-
if @_request.head?
|
66
|
+
def _roda_after_30(res)
|
67
|
+
if res && @_request.head?
|
64
68
|
body = res[2]
|
65
69
|
if body.respond_to?(:close)
|
66
70
|
res[2] = CloseLater.new(body)
|
@@ -68,7 +72,6 @@ class Roda
|
|
68
72
|
res[2] = EMPTY_ARRAY
|
69
73
|
end
|
70
74
|
end
|
71
|
-
res
|
72
75
|
end
|
73
76
|
end
|
74
77
|
|
@@ -16,20 +16,24 @@ class Roda
|
|
16
16
|
module Heartbeat
|
17
17
|
HEARTBEAT_RESPONSE = [200, {'Content-Type'=>'text/plain'}.freeze, ['OK'.freeze].freeze].freeze
|
18
18
|
|
19
|
+
def self.load_dependencies(app, opts=OPTS)
|
20
|
+
app.plugin :_before_hook
|
21
|
+
end
|
22
|
+
|
19
23
|
# Set the heartbeat path to the given path.
|
20
24
|
def self.configure(app, opts=OPTS)
|
21
25
|
app.opts[:heartbeat_path] = (opts[:path] || app.opts[:heartbeat_path] || "/heartbeat").dup.freeze
|
22
26
|
end
|
23
27
|
|
24
28
|
module InstanceMethods
|
29
|
+
private
|
30
|
+
|
25
31
|
# If the request is for a heartbeat path, return the heartbeat response.
|
26
|
-
def
|
32
|
+
def _roda_before_20
|
27
33
|
if env['PATH_INFO'] == opts[:heartbeat_path]
|
28
34
|
response = HEARTBEAT_RESPONSE.dup
|
29
35
|
response[1] = Hash[response[1]]
|
30
|
-
response
|
31
|
-
else
|
32
|
-
super
|
36
|
+
throw :halt, response
|
33
37
|
end
|
34
38
|
end
|
35
39
|
end
|
data/lib/roda/plugins/hooks.rb
CHANGED
@@ -30,8 +30,14 @@ class Roda
|
|
30
30
|
# Note that the after hook is called with the rack response array
|
31
31
|
# of status, headers, and body. If it wants to change the response,
|
32
32
|
# it must mutate this argument, calling <tt>response.status=</tt> inside
|
33
|
-
# an after block will not affect the returned status.
|
33
|
+
# an after block will not affect the returned status. Note that after
|
34
|
+
# hooks can be called with nil if an exception is raised during routing.
|
34
35
|
module Hooks
|
36
|
+
def self.load_dependencies(app)
|
37
|
+
app.plugin :_before_hook
|
38
|
+
app.plugin :_after_hook
|
39
|
+
end
|
40
|
+
|
35
41
|
def self.configure(app)
|
36
42
|
app.opts[:before_hook] ||= nil
|
37
43
|
app.opts[:after_hook] ||= nil
|
@@ -70,21 +76,21 @@ class Roda
|
|
70
76
|
end
|
71
77
|
|
72
78
|
module InstanceMethods
|
73
|
-
|
74
|
-
# execute the after hooks before returning.
|
75
|
-
def call(&block)
|
76
|
-
res = super do |r|
|
77
|
-
if b = opts[:before_hook]
|
78
|
-
instance_exec(&b)
|
79
|
-
end
|
79
|
+
private
|
80
80
|
|
81
|
-
|
82
|
-
|
83
|
-
ensure
|
81
|
+
# Run after hooks.
|
82
|
+
def _roda_after_90(res)
|
84
83
|
if b = opts[:after_hook]
|
85
84
|
instance_exec(res, &b)
|
86
85
|
end
|
87
86
|
end
|
87
|
+
|
88
|
+
# Run before hooks.
|
89
|
+
def _roda_before_10
|
90
|
+
if b = opts[:before_hook]
|
91
|
+
instance_exec(&b)
|
92
|
+
end
|
93
|
+
end
|
88
94
|
end
|
89
95
|
end
|
90
96
|
|
@@ -21,8 +21,7 @@ class Roda
|
|
21
21
|
# way to support sessions in Roda applications.
|
22
22
|
#
|
23
23
|
# The session cookies are encrypted with AES-256-CTR and then signed with HMAC-SHA-256.
|
24
|
-
# By default, session data
|
25
|
-
# is padded to reduce information leaked based on the session size.
|
24
|
+
# By default, session data is padded to reduce information leaked based on the session size.
|
26
25
|
#
|
27
26
|
# Sessions are serialized via JSON, so session information should only store data that
|
28
27
|
# allows roundtrips via JSON (String, Integer, Float, Array, Hash, true, false, and nil).
|
@@ -34,26 +33,20 @@ class Roda
|
|
34
33
|
# and does not convert all keys to strings.
|
35
34
|
#
|
36
35
|
# All sessions are timestamped and session expiration is enabled by default, with sessions
|
37
|
-
# being valid for 30 days maximum and 7 days since last use. Session creation time is
|
36
|
+
# being valid for 30 days maximum and 7 days since last use by default. Session creation time is
|
38
37
|
# reset whenever the session is empty when serialized and also whenever +clear_session+
|
39
38
|
# is called while processing the request.
|
40
39
|
#
|
41
|
-
# Session secrets can be rotated
|
42
|
-
# rotated at the same time. See options below.
|
40
|
+
# Session secrets can be rotated. See options below.
|
43
41
|
#
|
44
42
|
# The sessions plugin can transparently upgrade sessions from Rack::Session::Cookie
|
45
43
|
# if the default Rack::Session::Cookie coder and HMAC are used, see options below.
|
46
44
|
# It is recommended to only enable transparent upgrades for a brief transition period,
|
47
45
|
# and remove support for them once old sessions have converted or timed out.
|
48
46
|
#
|
49
|
-
#
|
50
|
-
# if the final cookie is too large (>=4096 bytes), a Roda::RodaPlugins::Sessions::CookieTooLarge
|
47
|
+
# If the final cookie is too large (>=4096 bytes), a Roda::RodaPlugins::Sessions::CookieTooLarge
|
51
48
|
# exception will be raised.
|
52
49
|
#
|
53
|
-
# If the flash plugin is used, the sessions plugin should be loaded after the flash
|
54
|
-
# plugin, so that the flash plugin rotates the flash in the session before the sessions
|
55
|
-
# plugin serializes the session.
|
56
|
-
#
|
57
50
|
# = Required Options
|
58
51
|
#
|
59
52
|
# The session cookies this plugin uses are both encrypted and signed, so two separate
|
@@ -70,7 +63,9 @@ class Roda
|
|
70
63
|
# that. If the +:secure+ option is not present in the hash, then
|
71
64
|
# <tt>secure: true</tt> is also set if the request is made over HTTPS. If this option is
|
72
65
|
# given, it will be merged into the default cookie options.
|
73
|
-
# :gzip_over :: For session data over this many bytes, compress it with the deflate algorithm (default:
|
66
|
+
# :gzip_over :: For session data over this many bytes, compress it with the deflate algorithm (default: nil,
|
67
|
+
# so never compress). Note that compression should not be enabled if you are storing data in
|
68
|
+
# the session derived from user input and also storing sensitive data in the session.
|
74
69
|
# :key :: The cookie name to use (default: <tt>'roda.session'</tt>)
|
75
70
|
# :max_seconds :: The maximum number of seconds to allow for total session lifetime, starting with when
|
76
71
|
# the session was originally created. Default is <tt>86400*30</tt> (30 days). Can be set to
|
@@ -140,7 +135,7 @@ class Roda
|
|
140
135
|
# deflate compression, this contains the deflate compressed data.
|
141
136
|
module Sessions
|
142
137
|
DEFAULT_COOKIE_OPTIONS = {:httponly=>true, :path=>'/'.freeze, :same_site=>:lax}.freeze
|
143
|
-
DEFAULT_OPTIONS = {:key => 'roda.session'.freeze, :max_seconds=>86400*30, :max_idle_seconds=>86400*7, :pad_size=>32, :gzip_over=>
|
138
|
+
DEFAULT_OPTIONS = {:key => 'roda.session'.freeze, :max_seconds=>86400*30, :max_idle_seconds=>86400*7, :pad_size=>32, :gzip_over=>nil, :skip_within=>3600}.freeze
|
144
139
|
DEFLATE_BIT = 0x1000
|
145
140
|
PADDING_MASK = 0x0fff
|
146
141
|
SESSION_CREATED_AT = 'roda.session.created_at'.freeze
|
@@ -153,6 +148,10 @@ class Roda
|
|
153
148
|
class CookieTooLarge < RodaError
|
154
149
|
end
|
155
150
|
|
151
|
+
def self.load_dependencies(app, opts=OPTS)
|
152
|
+
app.plugin :_after_hook
|
153
|
+
end
|
154
|
+
|
156
155
|
# Split given secret into a cipher secret and an hmac secret.
|
157
156
|
def self.split_secret(name, secret)
|
158
157
|
raise RodaError, "sessions plugin :#{name} option must be a String" unless secret.is_a?(String)
|
@@ -194,19 +193,6 @@ class Roda
|
|
194
193
|
end
|
195
194
|
|
196
195
|
module InstanceMethods
|
197
|
-
# If session information has been set in the request environment,
|
198
|
-
# update the rack response headers to set the session cookie in
|
199
|
-
# the response.
|
200
|
-
def call
|
201
|
-
res = super
|
202
|
-
|
203
|
-
if session = env['rack.session']
|
204
|
-
@_request.persist_session(res[1], session)
|
205
|
-
end
|
206
|
-
|
207
|
-
res
|
208
|
-
end
|
209
|
-
|
210
196
|
# Clear data from the session, and update the request environment
|
211
197
|
# so that the session cookie will use a new creation timestamp
|
212
198
|
# instead of the previous creation timestamp.
|
@@ -216,6 +202,17 @@ class Roda
|
|
216
202
|
env.delete(SESSION_UPDATED_AT)
|
217
203
|
nil
|
218
204
|
end
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
# If session information has been set in the request environment,
|
209
|
+
# update the rack response headers to set the session cookie in
|
210
|
+
# the response.
|
211
|
+
def _roda_after_50(res)
|
212
|
+
if res && (session = env['rack.session'])
|
213
|
+
@_request.persist_session(res[1], session)
|
214
|
+
end
|
215
|
+
end
|
219
216
|
end
|
220
217
|
|
221
218
|
module RequestMethods
|
@@ -401,8 +398,9 @@ class Roda
|
|
401
398
|
|
402
399
|
bitmap = 0
|
403
400
|
json_length = json_data.bytesize
|
401
|
+
gzip_over = opts[:gzip_over]
|
404
402
|
|
405
|
-
if json_length >
|
403
|
+
if gzip_over && json_length > gzip_over
|
406
404
|
json_data = Zlib.deflate(json_data)
|
407
405
|
json_length = json_data.bytesize
|
408
406
|
bitmap |= DEFLATE_BIT
|
@@ -48,10 +48,11 @@ class Roda
|
|
48
48
|
# As shown above, you can use Roda's routing tree methods inside the
|
49
49
|
# static_route block to have shared behavior for different request methods,
|
50
50
|
# while still handling the request methods differently.
|
51
|
-
#
|
52
|
-
# Note that if you want to use the static_routing plugin and the hooks
|
53
|
-
# plugin at the same time, you should load the hooks plugin first.
|
54
51
|
module StaticRouting
|
52
|
+
def self.load_dependencies(app)
|
53
|
+
app.plugin :_before_hook
|
54
|
+
end
|
55
|
+
|
55
56
|
def self.configure(app)
|
56
57
|
app.opts[:static_routes] = {}
|
57
58
|
end
|
@@ -104,15 +105,14 @@ class Roda
|
|
104
105
|
end
|
105
106
|
|
106
107
|
module InstanceMethods
|
108
|
+
private
|
109
|
+
|
107
110
|
# If there is a static routing method for the given path, call it
|
108
111
|
# instead having the routing tree handle the request.
|
109
|
-
def
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
else
|
114
|
-
instance_exec(r, &block)
|
115
|
-
end
|
112
|
+
def _roda_before_30
|
113
|
+
r = @_request
|
114
|
+
if route = self.class.static_route_for(r.request_method, r.path_info)
|
115
|
+
r.static_route(&route)
|
116
116
|
end
|
117
117
|
end
|
118
118
|
end
|
@@ -23,6 +23,10 @@ class Roda
|
|
23
23
|
# cleared. So if you want to be sure the headers are set even in your block,
|
24
24
|
# you need to reset them in the block.
|
25
25
|
module StatusHandler
|
26
|
+
def self.load_dependencies(app)
|
27
|
+
app.plugin :_after_hook
|
28
|
+
end
|
29
|
+
|
26
30
|
def self.configure(app)
|
27
31
|
app.opts[:status_handler] ||= {}
|
28
32
|
end
|
@@ -41,15 +45,13 @@ class Roda
|
|
41
45
|
end
|
42
46
|
|
43
47
|
module InstanceMethods
|
44
|
-
|
45
|
-
def call
|
46
|
-
result = super
|
48
|
+
private
|
47
49
|
|
48
|
-
|
50
|
+
# If routing returns a response we have a handler for, call that handler.
|
51
|
+
def _roda_after_20(result)
|
52
|
+
if result && (block = opts[:status_handler][result[0]]) && (v = result[2]).is_a?(Array) && v.empty?
|
49
53
|
@_response.headers.clear
|
50
|
-
|
51
|
-
else
|
52
|
-
result
|
54
|
+
result.replace(_call(&block))
|
53
55
|
end
|
54
56
|
end
|
55
57
|
end
|
data/lib/roda/version.rb
CHANGED
@@ -144,6 +144,28 @@ describe "error_handler plugin" do
|
|
144
144
|
proc{req}.must_raise(ArgumentError)
|
145
145
|
end
|
146
146
|
|
147
|
+
it "logs exceptions during after processing of error handler" do
|
148
|
+
app(:bare) do
|
149
|
+
plugin :error_handler do |e|
|
150
|
+
e.message * 2
|
151
|
+
end
|
152
|
+
plugin :hooks
|
153
|
+
|
154
|
+
after do
|
155
|
+
raise "foo"
|
156
|
+
end
|
157
|
+
|
158
|
+
route do |r|
|
159
|
+
''
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
errors = StringIO.new
|
164
|
+
body('rack.errors'=>errors).must_equal 'foofoo'
|
165
|
+
errors.rewind
|
166
|
+
errors.read.split("\n").first.must_equal "Error in after hook processing of error handler: RuntimeError: foo"
|
167
|
+
end
|
168
|
+
|
147
169
|
it "has access to current remaining_path" do
|
148
170
|
app(:bare) do
|
149
171
|
plugin :error_handler do |e|
|
data/spec/plugin/flash_spec.rb
CHANGED
@@ -3,47 +3,49 @@ require_relative "../spec_helper"
|
|
3
3
|
describe "flash plugin" do
|
4
4
|
include CookieJar
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
6
|
+
[lambda{send(*DEFAULT_SESSION_ARGS); plugin :flash},
|
7
|
+
lambda{plugin :flash; send(*DEFAULT_SESSION_ARGS)}].each do |config|
|
8
|
+
|
9
|
+
it "flash.now[] sets flash for current page" do
|
10
|
+
app(:bare) do
|
11
|
+
instance_exec(&config)
|
12
|
+
|
13
|
+
route do |r|
|
14
|
+
r.on do
|
15
|
+
flash.now['a'] = 'b'
|
16
|
+
flash['a']
|
17
|
+
end
|
15
18
|
end
|
16
19
|
end
|
17
|
-
end
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
+
body.must_equal 'b'
|
22
|
+
end
|
21
23
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
send(*DEFAULT_SESSION_ARGS)
|
24
|
+
it "flash[] sets flash for next page" do
|
25
|
+
app(:bare) do
|
26
|
+
instance_exec(&config)
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
28
|
+
route do |r|
|
29
|
+
r.get('a'){"c#{flash['a']}"}
|
30
|
+
r.get('f'){flash; session['_flash'].inspect}
|
30
31
|
|
31
|
-
|
32
|
-
|
32
|
+
flash['a'] = "b#{flash['a']}"
|
33
|
+
flash['a'] || ''
|
34
|
+
end
|
33
35
|
end
|
34
|
-
end
|
35
36
|
|
36
|
-
|
37
|
-
|
38
|
-
|
37
|
+
body.must_equal ''
|
38
|
+
body.must_equal 'b'
|
39
|
+
body.must_equal 'bb'
|
39
40
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
41
|
+
body('/a').must_equal 'cbbb'
|
42
|
+
body.must_equal ''
|
43
|
+
body.must_equal 'b'
|
44
|
+
body.must_equal 'bb'
|
44
45
|
|
45
|
-
|
46
|
-
|
46
|
+
body('/f').must_equal '{"a"=>"bbb"}'
|
47
|
+
body('/f').must_equal 'nil'
|
48
|
+
end
|
47
49
|
end
|
48
50
|
end
|
49
51
|
|
data/spec/plugin/hooks_spec.rb
CHANGED
@@ -100,4 +100,32 @@ describe "hooks plugin" do
|
|
100
100
|
end
|
101
101
|
status.must_equal 201
|
102
102
|
end
|
103
|
+
|
104
|
+
it "works with error plugin when loaded first" do
|
105
|
+
app.plugin(:error_handler){|e| "error"}
|
106
|
+
app.before do
|
107
|
+
raise "before" if @_request.path == '/b'
|
108
|
+
end
|
109
|
+
app.after do
|
110
|
+
raise "after" if @_request.path == '/a'
|
111
|
+
end
|
112
|
+
body('/a').must_equal "error"
|
113
|
+
body('/b').must_equal "error"
|
114
|
+
end
|
115
|
+
|
116
|
+
it "works with error plugin when loaded after" do
|
117
|
+
app(:bare) do
|
118
|
+
plugin(:error_handler){|e| "error"}
|
119
|
+
plugin :hooks
|
120
|
+
before do
|
121
|
+
raise "before" if @_request.path == '/b'
|
122
|
+
end
|
123
|
+
after do
|
124
|
+
raise "after" if @_request.path == '/a'
|
125
|
+
end
|
126
|
+
route{}
|
127
|
+
end
|
128
|
+
body('/a').must_equal "error"
|
129
|
+
body('/b').must_equal "error"
|
130
|
+
end
|
103
131
|
end
|
@@ -205,15 +205,15 @@ describe "sessions plugin" do
|
|
205
205
|
|
206
206
|
it "compresses data over a certain size by default" do
|
207
207
|
long = 'b'*8192
|
208
|
+
proc{body("/s/foo/#{long}")}.must_raise Roda::RodaPlugins::Sessions::CookieTooLarge
|
209
|
+
|
210
|
+
@app.plugin(:sessions, :gzip_over=>8000)
|
208
211
|
body("/s/foo/#{long}").must_equal long
|
209
|
-
body("/g/foo").must_equal long
|
212
|
+
body("/g/foo", 'QUERY_STRING'=>'sut=3700').must_equal long
|
210
213
|
|
211
214
|
@app.plugin(:sessions, :gzip_over=>15000)
|
212
215
|
proc{body("/g/foo", 'QUERY_STRING'=>'sut=3700')}.must_raise Roda::RodaPlugins::Sessions::CookieTooLarge
|
213
216
|
|
214
|
-
@app.plugin(:sessions, :gzip_over=>8000)
|
215
|
-
body("/g/foo", 'QUERY_STRING'=>'sut=3700').must_equal long
|
216
|
-
|
217
217
|
errors.must_equal []
|
218
218
|
end
|
219
219
|
|
@@ -66,6 +66,26 @@ describe "static_routing plugin" do
|
|
66
66
|
a.must_equal [1,3,2]
|
67
67
|
end
|
68
68
|
|
69
|
+
it "works with hooks plugin if loaded before" do
|
70
|
+
a = []
|
71
|
+
app(:bare) do
|
72
|
+
plugin :static_routing
|
73
|
+
plugin :hooks
|
74
|
+
|
75
|
+
before{a << 1}
|
76
|
+
after{a << 2}
|
77
|
+
|
78
|
+
static_route "/foo" do |r|
|
79
|
+
a << 3
|
80
|
+
"bar"
|
81
|
+
end
|
82
|
+
|
83
|
+
route{}
|
84
|
+
end
|
85
|
+
body('/foo').must_equal 'bar'
|
86
|
+
a.must_equal [1,3,2]
|
87
|
+
end
|
88
|
+
|
69
89
|
it "does not allow placeholders in static routes" do
|
70
90
|
app(:bare) do
|
71
91
|
plugin :static_routing
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: roda
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.11.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Evans
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-08-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -206,6 +206,7 @@ extra_rdoc_files:
|
|
206
206
|
- doc/release_notes/3.8.0.txt
|
207
207
|
- doc/release_notes/3.9.0.txt
|
208
208
|
- doc/release_notes/3.10.0.txt
|
209
|
+
- doc/release_notes/3.11.0.txt
|
209
210
|
files:
|
210
211
|
- CHANGELOG
|
211
212
|
- MIT-LICENSE
|
@@ -250,6 +251,7 @@ files:
|
|
250
251
|
- doc/release_notes/3.0.0.txt
|
251
252
|
- doc/release_notes/3.1.0.txt
|
252
253
|
- doc/release_notes/3.10.0.txt
|
254
|
+
- doc/release_notes/3.11.0.txt
|
253
255
|
- doc/release_notes/3.2.0.txt
|
254
256
|
- doc/release_notes/3.3.0.txt
|
255
257
|
- doc/release_notes/3.4.0.txt
|
@@ -259,6 +261,8 @@ files:
|
|
259
261
|
- doc/release_notes/3.8.0.txt
|
260
262
|
- doc/release_notes/3.9.0.txt
|
261
263
|
- lib/roda.rb
|
264
|
+
- lib/roda/plugins/_after_hook.rb
|
265
|
+
- lib/roda/plugins/_before_hook.rb
|
262
266
|
- lib/roda/plugins/_symbol_regexp_matchers.rb
|
263
267
|
- lib/roda/plugins/all_verbs.rb
|
264
268
|
- lib/roda/plugins/assets.rb
|