roda 3.71.0 → 3.73.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5460d6cecb4f9b9acfedd05f5b14793bb9aefa2df2a8c29c559da319df87ee4
4
- data.tar.gz: 75c3c8803abef4e27cac592a88597e8f9cd098be39bf1d0c1b1192175fd1f8e2
3
+ metadata.gz: 7a47f703af806a15f523b99b38d084520bada92f23196374d6d9781d5953faa8
4
+ data.tar.gz: 4612153820b89e6dbcfa2370c04269666c49277d8f11256869f6c1f0bb0b571a
5
5
  SHA512:
6
- metadata.gz: 6efc49bb205012a7ce50bc3c300d924a149de33416a35870f11efed492251c331804b970c967e47a481b0ddb0a83507c5926c1dc32069941b4edd2bd56df09e5
7
- data.tar.gz: 698b7e7daae4cd0712a20a1e3e274f7ba3cdb9068eefdeb3bb5f043f2d437cdd5dda961ce91d88794e22a8ca3ef5825e3f8bd3f92ff933eab772c352dd17c2ee
6
+ metadata.gz: bdc42cdc9540750195327dac290e3f0bb08c3ed0100b610441b644c85849e7e9f9f974013cea041cdf640f590e2027996d68b62eea9c31d41bd42a57df6530bb
7
+ data.tar.gz: 3ab05db704cf45803ad706e78b558ea7798d8d9782a06c7dec79814a4d862582249e5554946c59fff88e1f1d2d360e242268c593a8c9c95fe87b5489e4a4a44c
data/CHANGELOG CHANGED
@@ -1,3 +1,17 @@
1
+ = 3.73.0 (2023-10-13)
2
+
3
+ * Support :next_if_not_found option for middleware plugin (jeremyevans) (#334)
4
+
5
+ * Remove dependency on base64 library from sessions and route_csrf plugin, as it will not be part of the standard library in Ruby 3.4+ (jeremyevans)
6
+
7
+ = 3.72.0 (2023-09-12)
8
+
9
+ * Add invalid_request_body plugin for custom handling of invalid request bodies (jeremyevans)
10
+
11
+ * Warn when defining method that expects 1 argument when block requires multiple arguments when :check_arity option is set to :warn (jeremyevans)
12
+
13
+ * Implement the match_hooks plugin using the match_hook_args plugin (jeremyevans)
14
+
1
15
  = 3.71.0 (2023-08-14)
2
16
 
3
17
  * Add match_hook_args plugin, similar to match_hooks but support matchers and block args as hook arguments (jeremyevans)
@@ -0,0 +1,48 @@
1
+ = New Features
2
+
3
+ * An invalid_request_body plugin has been added for allowing custom
4
+ handling of invalid request bodies. Roda uses Rack's request body
5
+ parsing, and by default invalid request bodies can result in
6
+ different exceptions based on how the body is invalid and which
7
+ version of Rack is in use.
8
+
9
+ If you want to treat an invalid request body as the submission of
10
+ no parameters, you can use the :empty_hash argument when loading
11
+ the plugin:
12
+
13
+ plugin :invalid_request_body, :empty_hash
14
+
15
+ If you want to return a empty 400 (Bad Request) response if an
16
+ invalid request body is submitted, you can use the :empty_400
17
+ argument when loading the plugin:
18
+
19
+ plugin :invalid_request_body, :empty_400
20
+
21
+ If you want to raise a Roda::RodaPlugins::InvalidRequestBody::Error
22
+ exception if an invalid request body is submitted (which makes it
23
+ easier to handle these exceptions when using the error_handler
24
+ plugin), you can use the :raise argument when loading the plugin:
25
+
26
+ plugin :invalid_request_body, :raise
27
+
28
+ For custom behavior, you can pass a block when loading the plugin
29
+ The block is called with the exception Rack raised when parsing the
30
+ body. The block will be used to define a method in the application's
31
+ RodaRequest class. It can either return a hash of parameters, or
32
+ you can raise a different exception, or you can halt processing and
33
+ return a response:
34
+
35
+ plugin :invalid_request_body do |exception|
36
+ # To treat the exception raised as a submitted parameter
37
+ {body_error: exception}
38
+ end
39
+
40
+ = Other Improvements
41
+
42
+ * When using the check_arity: :warn Roda option, Roda now correctly
43
+ warns when defining a method that expects a single argument when
44
+ the provided block requires multiple arguments.
45
+
46
+ * The match_hooks plugin is now implemented using the match_hook_args
47
+ plugin, simplifying the implementation. This change should be
48
+ transparent unless you were reaching into the internals.
@@ -0,0 +1,33 @@
1
+ = New Features
2
+
3
+ * The middleware plugin now accepts a :next_if_not_found option.
4
+ This allows the middleware plugin to pass the request to the next
5
+ application if the current application handles the request but
6
+ ends up calling the not_found handler. With the following
7
+ middleware:
8
+
9
+ class Mid < Roda
10
+ plugin :middleware
11
+
12
+ route do |r|
13
+ r.on "foo" do
14
+ r.get "bar" do
15
+ 'bar'
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ Requests for /x would be forwarded to the next application, since
22
+ the application doesn't handle the request, but requests for /foo/x
23
+ would not be, because the middleware is partially handling the
24
+ request in the r.on "foo" block. With the :next_if_not_found
25
+ option, only requests for /foo/bar would be handled by the
26
+ middleware, and all other requests would be forwarded to the next
27
+ application.
28
+
29
+ = Other Improvements
30
+
31
+ * The sessions and route_csrf plugins no longer depend on the base64
32
+ library. base64 will be removed from Ruby's standard library
33
+ starting in Ruby 3.4.
@@ -0,0 +1,34 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ module Base64_
7
+ class << self
8
+ if RUBY_VERSION >= '2.4'
9
+ def decode64(str)
10
+ str.unpack1("m0")
11
+ end
12
+ # :nocov:
13
+ else
14
+ def decode64(str)
15
+ str.unpack("m0")[0]
16
+ end
17
+ # :nocov:
18
+ end
19
+
20
+ def urlsafe_encode64(bin)
21
+ str = [bin].pack("m0")
22
+ str.tr!("+/", "-_")
23
+ str
24
+ end
25
+
26
+ def urlsafe_decode64(str)
27
+ decode64(str.tr("-_", "+/"))
28
+ end
29
+ end
30
+ end
31
+
32
+ register_plugin(:_base64, Base64_)
33
+ end
34
+ end
@@ -411,7 +411,7 @@ END
411
411
 
412
412
  private
413
413
 
414
- if RUBY_VERSION >= '3.2'
414
+ if Exception.method_defined?(:detailed_message)
415
415
  def exception_page_exception_message(exception)
416
416
  exception.detailed_message(highlight: false).to_s
417
417
  end
@@ -0,0 +1,107 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The invalid_request_body plugin allows for custom handling of invalid request
7
+ # bodies. Roda uses Rack for parsing request bodies, so by default, any
8
+ # invalid request bodies would result in Rack raising an exception, and the
9
+ # exception could change for different reasons the request body is invalid.
10
+ # This plugin overrides RodaRequest#POST (which parses parameters from request
11
+ # bodies), and if parsing raises an exception, it allows for custom behavior.
12
+ #
13
+ # If you want to treat an invalid request body as the submission of no parameters,
14
+ # you can use the :empty_hash argument when loading the plugin:
15
+ #
16
+ # plugin :invalid_request_body, :empty_hash
17
+ #
18
+ # If you want to return a empty 400 (Bad Request) response if an invalid request
19
+ # body is submitted, you can use the :empty_400 argument when loading the plugin:
20
+ #
21
+ # plugin :invalid_request_body, :empty_400
22
+ #
23
+ # If you want to raise a Roda::RodaPlugins::InvalidRequestBody::Error exception
24
+ # if an invalid request body is submitted (which makes it easier to handle these
25
+ # exceptions when using the error_handler plugin), you can use the :raise argument
26
+ # when loading the plugin:
27
+ #
28
+ # plugin :invalid_request_body, :raise
29
+ #
30
+ # For custom behavior, you can pass a block when loading the plugin. The block
31
+ # is called with the exception Rack raised when parsing the body. The block will
32
+ # be used to define a method in the application's RodaRequest class. It can either
33
+ # return a hash of parameters, or you can raise a different exception, or you
34
+ # can halt processing and return a response:
35
+ #
36
+ # plugin :invalid_request_body do |exception|
37
+ # # To treat the exception raised as a submitted parameter
38
+ # {body_error: exception}
39
+ # end
40
+ module InvalidRequestBody
41
+ # Exception class raised for invalid request bodies.
42
+ Error = Class.new(RodaError)
43
+
44
+ # Set the action to use (:empty_400, :empty_hash, :raise) for invalid request bodies,
45
+ # or use a block for custom behavior.
46
+ def self.configure(app, action=nil, &block)
47
+ if action
48
+ if block
49
+ raise RodaError, "cannot provide both block and action when loading invalid_request_body plugin"
50
+ end
51
+
52
+ method = :"handle_invalid_request_body_#{action}"
53
+ unless RequestMethods.private_method_defined?(method)
54
+ raise RodaError, "invalid invalid_request_body action provided: #{action}"
55
+ end
56
+
57
+ app::RodaRequest.send(:alias_method, :handle_invalid_request_body, method)
58
+ elsif block
59
+ app::RodaRequest.class_eval do
60
+ define_method(:handle_invalid_request_body, &block)
61
+ alias handle_invalid_request_body handle_invalid_request_body
62
+ end
63
+ else
64
+ raise RodaError, "must provide block or action when loading invalid_request_body plugin"
65
+ end
66
+
67
+ app::RodaRequest.send(:private, :handle_invalid_request_body)
68
+ end
69
+
70
+ module RequestMethods
71
+ # Handle invalid request bodies as configured if the default behavior
72
+ # raises an exception.
73
+ def POST
74
+ super
75
+ rescue => e
76
+ handle_invalid_request_body(e)
77
+ end
78
+
79
+ private
80
+
81
+ # Return an empty 400 HTTP response for invalid request bodies.
82
+ def handle_invalid_request_body_empty_400(e)
83
+ response.status = 400
84
+ headers = response.headers
85
+ headers.clear
86
+ headers[RodaResponseHeaders::CONTENT_TYPE] = 'text/html'
87
+ headers[RodaResponseHeaders::CONTENT_LENGTH] ='0'
88
+ throw :halt, response.finish_with_body([])
89
+ end
90
+
91
+ # Treat invalid request bodies by using an empty hash as the
92
+ # POST params.
93
+ def handle_invalid_request_body_empty_hash(e)
94
+ {}
95
+ end
96
+
97
+ # Raise a specific error for all invalid request bodies,
98
+ # to allow for easy rescuing using the error_handler plugin.
99
+ def handle_invalid_request_body_raise(e)
100
+ raise Error, e.message
101
+ end
102
+ end
103
+ end
104
+
105
+ register_plugin(:invalid_request_body, InvalidRequestBody)
106
+ end
107
+ end
@@ -6,7 +6,8 @@ class Roda
6
6
  # The match_hook plugin adds hooks that are called upon a successful match
7
7
  # by any of the matchers. The hooks do not take any arguments. If you would
8
8
  # like hooks that pass the arguments/matchers and values yielded to the route block,
9
- # use the match_hook_args plugin.
9
+ # use the match_hook_args plugin. This uses the match_hook_args plugin internally,
10
+ # but doesn't pass the matchers and values yielded.
10
11
  #
11
12
  # plugin :match_hook
12
13
  #
@@ -14,56 +15,18 @@ class Roda
14
15
  # logger.debug("#{request.matched_path} matched. #{request.remaining_path} remaining.")
15
16
  # end
16
17
  module MatchHook
17
- def self.configure(app)
18
- app.opts[:match_hooks] ||= []
18
+ def self.load_dependencies(app)
19
+ app.plugin :match_hook_args
19
20
  end
20
21
 
21
22
  module ClassMethods
22
- # Freeze the array of hook methods when freezing the app
23
- def freeze
24
- opts[:match_hooks].freeze
25
- super
26
- end
27
-
28
23
  # Add a match hook.
29
24
  def match_hook(&block)
30
- opts[:match_hooks] << define_roda_method("match_hook", 0, &block)
31
-
32
- if opts[:match_hooks].length == 1
33
- class_eval("alias _match_hook #{opts[:match_hooks].first}", __FILE__, __LINE__)
34
- else
35
- class_eval("def _match_hook; #{opts[:match_hooks].join(';')} end", __FILE__, __LINE__)
36
- end
37
-
38
- public :_match_hook
39
-
25
+ meth = define_roda_method("match_hook", 0, &block)
26
+ add_match_hook{|_,_| send(meth)}
40
27
  nil
41
28
  end
42
29
  end
43
-
44
- module InstanceMethods
45
- # Default empty method if no match hooks are defined.
46
- def _match_hook
47
- end
48
- end
49
-
50
- module RequestMethods
51
- private
52
-
53
- # Call the match hook if yielding to the block before yielding to the block.
54
- def if_match(_)
55
- super do |*a|
56
- scope._match_hook
57
- yield(*a)
58
- end
59
- end
60
-
61
- # Call the match hook before yielding to the block
62
- def always
63
- scope._match_hook
64
- super
65
- end
66
- end
67
30
  end
68
31
 
69
32
  register_plugin :match_hook, MatchHook
@@ -33,6 +33,43 @@ class Roda
33
33
  #
34
34
  # run App
35
35
  #
36
+ # By default, when the app is used as middleware and handles the request at
37
+ # all, it does not forward the request to the next middleware. For the
38
+ # following setup:
39
+ #
40
+ # class Mid < Roda
41
+ # plugin :middleware
42
+ #
43
+ # route do |r|
44
+ # r.on "foo" do
45
+ # r.is "mid" do
46
+ # "Mid"
47
+ # end
48
+ # end
49
+ # end
50
+ # end
51
+ #
52
+ # class App < Roda
53
+ # use Mid
54
+ #
55
+ # route do |r|
56
+ # r.on "foo" do
57
+ # r.is "app" do
58
+ # "App"
59
+ # end
60
+ # end
61
+ # end
62
+ # end
63
+ #
64
+ # run App
65
+ #
66
+ # Requests for +/foo/mid will+ return +Mid+, but requests for +/foo/app+
67
+ # will return an empty 404 response, because the middleware handles the
68
+ # +/foo/app+ request in the <tt>r.on "foo" do</tt> block, but does not
69
+ # have the block return a result, which Roda treats as an empty 404 response.
70
+ # If you would like the middleware to forward +/foo/app+ request to the
71
+ # application, you should use the +:next_if_not_found+ plugin option.
72
+ #
36
73
  # It is possible to use the Roda app as a regular app even when using
37
74
  # the middleware plugin. Using an app as middleware automatically creates
38
75
  # a subclass of the app for the middleware. Because a subclass is automatically
@@ -64,6 +101,9 @@ class Roda
64
101
  # # Request to App for /mid returns
65
102
  # # "foo bar baz"
66
103
  module Middleware
104
+ NEXT_PROC = lambda{throw :next, true}
105
+ private_constant :NEXT_PROC
106
+
67
107
  # Configure the middleware plugin. Options:
68
108
  # :env_var :: Set the environment variable to use to indicate to the roda
69
109
  # application that the current request is a middleware request.
@@ -77,12 +117,15 @@ class Roda
77
117
  # the middleware's route block should be applied to the
78
118
  # final response when the request is forwarded to the app.
79
119
  # Defaults to false.
120
+ # :next_if_not_found :: If the middleware handles the request but returns a not found
121
+ # result (404 with no body), forward the result to the next middleware.
80
122
  def self.configure(app, opts={}, &block)
81
123
  app.opts[:middleware_env_var] = opts[:env_var] if opts.has_key?(:env_var)
82
124
  app.opts[:middleware_env_var] ||= 'roda.forward_next'
83
125
  app.opts[:middleware_configure] = block if block
84
126
  app.opts[:middleware_handle_result] = opts[:handle_result]
85
127
  app.opts[:middleware_forward_response_headers] = opts[:forward_response_headers]
128
+ app.opts[:middleware_next_if_not_found] = opts[:next_if_not_found]
86
129
  end
87
130
 
88
131
  # Forwarder instances are what is actually used as middleware.
@@ -91,6 +134,9 @@ class Roda
91
134
  # and store +app+ as the next middleware to call.
92
135
  def initialize(mid, app, *args, &block)
93
136
  @mid = Class.new(mid)
137
+ if @mid.opts[:middleware_next_if_not_found]
138
+ @mid.plugin(:not_found, &NEXT_PROC)
139
+ end
94
140
  if configure = @mid.opts[:middleware_configure]
95
141
  configure.call(@mid, *args, &block)
96
142
  elsif block || !args.empty?
@@ -1,6 +1,5 @@
1
1
  # frozen-string-literal: true
2
2
 
3
- require 'base64'
4
3
  require 'openssl'
5
4
  require 'securerandom'
6
5
  require 'uri'
@@ -163,6 +162,10 @@ class Roda
163
162
  # a valid CSRF token was not provided.
164
163
  class InvalidToken < RodaError; end
165
164
 
165
+ def self.load_dependencies(app, opts=OPTS)
166
+ app.plugin :_base64
167
+ end
168
+
166
169
  def self.configure(app, opts=OPTS, &block)
167
170
  options = app.opts[:route_csrf] = (app.opts[:route_csrf] || DEFAULTS).merge(opts)
168
171
  if block || opts[:csrf_failure].is_a?(Proc)
@@ -260,7 +263,7 @@ class Roda
260
263
  def csrf_token(path=nil, method=('POST' if path))
261
264
  token = SecureRandom.random_bytes(31)
262
265
  token << csrf_hmac(token, method, path)
263
- Base64.strict_encode64(token)
266
+ [token].pack("m0")
264
267
  end
265
268
 
266
269
  # Whether request-specific CSRF tokens should be used by default.
@@ -314,7 +317,7 @@ class Roda
314
317
  end
315
318
 
316
319
  begin
317
- submitted_hmac = Base64.strict_decode64(encoded_token)
320
+ submitted_hmac = Base64_.decode64(encoded_token)
318
321
  rescue ArgumentError
319
322
  return "encoded token is not valid base64"
320
323
  end
@@ -354,7 +357,7 @@ class Roda
354
357
  # JSON is used for session serialization).
355
358
  def csrf_secret
356
359
  key = session[csrf_options[:key]] ||= SecureRandom.base64(32)
357
- Base64.strict_decode64(key)
360
+ Base64_.decode64(key)
358
361
  end
359
362
  end
360
363
  end
@@ -10,7 +10,6 @@ rescue OpenSSL::Cipher::CipherError
10
10
  # :nocov:
11
11
  end
12
12
 
13
- require 'base64'
14
13
  require 'json'
15
14
  require 'securerandom'
16
15
  require 'zlib'
@@ -171,6 +170,10 @@ class Roda
171
170
  [cipher_secret.freeze, hmac_secret.freeze]
172
171
  end
173
172
 
173
+ def self.load_dependencies(app, opts=OPTS)
174
+ app.plugin :_base64
175
+ end
176
+
174
177
  # Configure the plugin, see Sessions for details on options.
175
178
  def self.configure(app, opts=OPTS)
176
179
  opts = (app.opts[:sessions] || DEFAULT_OPTIONS).merge(opts)
@@ -344,7 +347,7 @@ class Roda
344
347
  opts = roda_class.opts[:sessions]
345
348
 
346
349
  begin
347
- data = Base64.urlsafe_decode64(data)
350
+ data = Base64_.urlsafe_decode64(data)
348
351
  rescue ArgumentError
349
352
  return _session_serialization_error("Unable to decode session: invalid base64")
350
353
  end
@@ -493,7 +496,7 @@ class Roda
493
496
  data << encrypted_data
494
497
  data << OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, opts[:hmac_secret], data+opts[:key])
495
498
 
496
- data = Base64.urlsafe_encode64(data)
499
+ data = Base64_.urlsafe_encode64(data)
497
500
 
498
501
  if data.bytesize >= 4096
499
502
  raise CookieTooLarge, "attempted to create cookie larger than 4096 bytes"
data/lib/roda/version.rb CHANGED
@@ -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 = 71
7
+ RodaMinorVersion = 73
8
8
 
9
9
  # The patch version of Roda, updated only for bug fixes from the last
10
10
  # feature release.
data/lib/roda.rb CHANGED
@@ -88,6 +88,7 @@ class Roda
88
88
  end
89
89
  call_meth = meth
90
90
 
91
+ # RODA4: Switch to false # :warn in last Roda 3 version
91
92
  if (check_arity = opts.fetch(:check_arity, true)) && !block.lambda?
92
93
  required_args, optional_args, rest, keyword = _define_roda_method_arg_numbers(block)
93
94
 
@@ -117,6 +118,9 @@ class Roda
117
118
  alias_method meth, meth
118
119
  meth = :"#{meth}_arity"
119
120
  elsif required_args > 1
121
+ if check_arity == :warn
122
+ RodaPlugins.warn "Arity mismatch in block passed to define_roda_method. Expected Arity 1, but multiple arguments required for #{block.inspect}"
123
+ end
120
124
  b = block
121
125
  block = lambda{|r| instance_exec(r, &b)} # Fallback
122
126
  end
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.71.0
4
+ version: 3.73.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: 2023-08-14 00:00:00.000000000 Z
11
+ date: 2023-10-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -245,6 +245,8 @@ extra_rdoc_files:
245
245
  - doc/release_notes/3.7.0.txt
246
246
  - doc/release_notes/3.70.0.txt
247
247
  - doc/release_notes/3.71.0.txt
248
+ - doc/release_notes/3.72.0.txt
249
+ - doc/release_notes/3.73.0.txt
248
250
  - doc/release_notes/3.8.0.txt
249
251
  - doc/release_notes/3.9.0.txt
250
252
  files:
@@ -323,6 +325,8 @@ files:
323
325
  - doc/release_notes/3.7.0.txt
324
326
  - doc/release_notes/3.70.0.txt
325
327
  - doc/release_notes/3.71.0.txt
328
+ - doc/release_notes/3.72.0.txt
329
+ - doc/release_notes/3.73.0.txt
326
330
  - doc/release_notes/3.8.0.txt
327
331
  - doc/release_notes/3.9.0.txt
328
332
  - lib/roda.rb
@@ -330,6 +334,7 @@ files:
330
334
  - lib/roda/plugins.rb
331
335
  - lib/roda/plugins/Integer_matcher_max.rb
332
336
  - lib/roda/plugins/_after_hook.rb
337
+ - lib/roda/plugins/_base64.rb
333
338
  - lib/roda/plugins/_before_hook.rb
334
339
  - lib/roda/plugins/_optimized_matching.rb
335
340
  - lib/roda/plugins/_symbol_regexp_matchers.rb
@@ -386,6 +391,7 @@ files:
386
391
  - lib/roda/plugins/host_authorization.rb
387
392
  - lib/roda/plugins/indifferent_params.rb
388
393
  - lib/roda/plugins/inject_erb.rb
394
+ - lib/roda/plugins/invalid_request_body.rb
389
395
  - lib/roda/plugins/json.rb
390
396
  - lib/roda/plugins/json_parser.rb
391
397
  - lib/roda/plugins/link_to.rb