roda 3.71.0 → 3.73.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 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