roda 3.41.0 → 3.45.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: f09e850b5f4ee0406c5686317145571fa0bc5e8158b0b39c5161bb9a3cbb3878
4
- data.tar.gz: d839bbfa3ff4e7ef4a37501a46ce65c12425dc620ccbdf9aae174a059898aa84
3
+ metadata.gz: 5dbdf55425a681a0cb32fa0b4a44cd50bf5be6f85c78c1ea1adbdd19e28c2d61
4
+ data.tar.gz: e91ee695e5f8648f4269a2c229b794ef2ee921f4291102519d4ca2cadd9bf13a
5
5
  SHA512:
6
- metadata.gz: dee1ec11e6ca9ca18f74fedf260f10e25e9c49efa3297ca2df02aab02efa6282464dd88f0f1f79e3529c8c56239749c72779d8bfb7fbf8508b40047470e4f6f6
7
- data.tar.gz: c7b4d4e1d4cdf7f60707621a57cfdd6a622ef91f6d6724abd1b344fc18b59ab4271ee7536513417023c20c3f470cac73c94ff50675661162d6ec1e62df02cd70
6
+ metadata.gz: aa46429e09091c3cf0112c251a9863845c69d4245674dcaddaec030244213d8b9ac1aae96cfa6093680c33f87c732ee4464e4cbe2572d192eb0a9f7e09acd43b
7
+ data.tar.gz: 57cf84f7db6e6334918f0c01f7308c4c48db7e0ba9f2a14fe87420c3b2bd17471a229ec02a2ad9242ec6fd4aa67848f6dff04b319fea361a985dd264b52252ab
data/CHANGELOG CHANGED
@@ -1,3 +1,33 @@
1
+ = 3.45.0 (2021-06-14)
2
+
3
+ * Make typecast_params plugin check for null bytes in strings by default, with :allow_null_bytes option for previous behavior (jeremyevans)
4
+
5
+ = 3.44.0 (2021-05-12)
6
+
7
+ * Add optimized_segment_matchers plugin for optimized matchers for a single String class argument (jeremyevans)
8
+
9
+ * Use RFC 5987 UTF-8 and ISO-8859-1 encoded filenames when using send_file and attachment in the sinatra_helpers plugin (jeremyevans)
10
+
11
+ = 3.43.1 (2021-04-13)
12
+
13
+ * [SECURITY] Fix issue where loading content_security_policy plugin after default_headers plugin had no effect (jeremyevans)
14
+
15
+ = 3.43.0 (2021-04-12)
16
+
17
+ * Add host_authorization plugin, for checking that requests are submitted using an approved host (jeremyevans)
18
+
19
+ = 3.42.0 (2021-03-12)
20
+
21
+ * Make Roda.plugin support plugins using keyword arguments in Ruby 3 (jeremyevans)
22
+
23
+ * Make Roda.use support middleware using keyword arguments in Ruby 3 (pat) (#207)
24
+
25
+ * Support common_logger plugin :method option for specifying the method to call on the logger (fnordfish, jeremyevans) (#206)
26
+
27
+ * Add recheck_precompiled_assets plugin for checking for updates to the precompiled asset metadata file (jeremyevans)
28
+
29
+ * Make compile_assets class method in assets plugin use an atomic approach to writing precompiled metadata file (jeremyevans)
30
+
1
31
  = 3.41.0 (2021-02-17)
2
32
 
3
33
  * Improve view performance with :content option up to 3x by calling compiled template methods directly (jeremyevans)
data/README.rdoc CHANGED
@@ -965,19 +965,17 @@ option. If you really want to turn path checking off, you can do so via the
965
965
  Roda does not ship with integrated support for code reloading, but there are rack-based
966
966
  reloaders that will work with Roda apps.
967
967
 
968
- For most applications, {rack-unreloader}[https://github.com/jeremyevans/rack-unreloader]
969
- is probably the fastest approach to reloading while still being fairly safe, as it
970
- reloads just files that have been modified, and unloads constants defined in the files
971
- before reloading them. However, it requires modifying your application code to use
972
- rack-unreloader specific APIs.
973
-
974
- A similar solution that reloads files and unloads constants is ActiveSupport::Dependencies.
975
- ActiveSupport::Dependencies doesn't require modifying your application code, but it modifies
976
- some core methods, including +require+ and +const_missing+. It requires less configuration,
977
- but depends that you follow Rails' file and class naming conventions. It also provides
978
- autoloading (on the fly) of files when a missing constant is accessed. If your application
979
- does not rely on autoloading then +require_dependency+ must be used to require the dependencies
980
- or they won't be reloaded.
968
+ {Zeitwerk}[https://github.com/fxn/zeitwerk] (which Rails now uses for reloading) can be used
969
+ with Roda. It requires minimal setup and handles most cases. It overrides +require+ when
970
+ activated. If it can meet the needs of your application, it's probably the best approach.
971
+
972
+ {rack-unreloader}[https://github.com/jeremyevans/rack-unreloader] uses a fast
973
+ approach to reloading while still being fairly safe, as it only reloads files that have
974
+ been modified, and unloads constants defined in the files before reloading them. It can handle
975
+ advanced cases that Zeitwerk does not support, such as classes defined in multiple files
976
+ (common when using separate route files for different routing branches in the same application).
977
+ However, rack-unreloader does not modify core classes and using it requires modifying your
978
+ application code to use rack-unreloader specific APIs, which may not be simple.
981
979
 
982
980
  {AutoReloader}[https://github.com/rosenfeld/auto_reloader] provides transparent reloading for
983
981
  all files reached from one of the +reloadable_paths+ option entries, by detecting new top-level
@@ -0,0 +1,21 @@
1
+ = New Features
2
+
3
+ * A recheck_precompiled_assets plugin has been added, which allows
4
+ for checking for updates to the precompiled asset metadata file,
5
+ and automatically using the updated data.
6
+
7
+ * The common_logger plugin now supports a :method plugin option to
8
+ specify the method to call on the logger.
9
+
10
+ = Other Improvements
11
+
12
+ * Plugins and middleware that use keyword arguments are now supported
13
+ in Ruby 3.
14
+
15
+ * The compile_assets class method in the assets plugin now uses an
16
+ atomic approach to writing the precompiled asset metadata file.
17
+
18
+ * Minor method visibility issues have been fixed. The custom_matchers
19
+ plugin no longer makes the unsupported_matcher request method
20
+ public, and the render plugin no longer makes the _layout_method
21
+ public when the application is frozen.
@@ -0,0 +1,34 @@
1
+ = New Features
2
+
3
+ * A host_authorization plugin has been added to verify the requested
4
+ Host header is authorized. Using it can prevent DNS rebinding
5
+ attacks in cases where the application can receive requests for
6
+ arbitrary hosts.
7
+
8
+ To check for authorized hosts in your routing tree, you call the
9
+ check_host_authorization! method. For example, if you want to
10
+ check for authorized hosts after serving requests for public
11
+ files, you could do:
12
+
13
+ plugin :public
14
+ plugin :host_authorization, 'my-domain-name.example.com'
15
+
16
+ route do |r|
17
+ r.public
18
+ check_host_authorized!
19
+
20
+ # ... rest of routing tree
21
+ end
22
+
23
+ In addition to handling single domain names via a string, you can
24
+ provide an array of domain names, a regexp to match again, or a
25
+ proc.
26
+
27
+ By default, requests using unauthorized hosts receive an empty 403
28
+ response. If you would like to customize the response, you can
29
+ pass a block when loading the plugin:
30
+
31
+ plugin :host_authorization, 'my-domain-name.example.com' do |r|
32
+ response.status = 403
33
+ "Response Body Here"
34
+ end
@@ -0,0 +1,23 @@
1
+ = New Features
2
+
3
+ * An optimized_segment_matchers plugin has been added that offers
4
+ very fast matchers for arbitrary segments (the same segments
5
+ that would be matched by the String class matcher). The
6
+ on_segment method it offers accepts no arguments and yields
7
+ the next segment if there is a segment. The is_segment method
8
+ is similar, but only yields if the next segment is the final
9
+ segment.
10
+
11
+ = Other Improvements
12
+
13
+ * The send_file and attachment methods in the sinatra_helpers plugin
14
+ now support RFC 5987 UTF-8 and ISO-8859-1 encoded filenames,
15
+ allowing modern browsers to save files with encoded chracters. For
16
+ older browsers that do not support RFC 5987, unsupported characters
17
+ in filenames are replaced with dashes. This is considered to be an
18
+ improvement over the previous behavior of using Ruby's inspect
19
+ output for the filename, which could contain backslashes (backslash
20
+ is not an allowed chracter in Windows filenames).
21
+
22
+ * The performance of the String class matcher has been slightly
23
+ improved.
@@ -0,0 +1,22 @@
1
+ = Improvements
2
+
3
+ * The typecast_params plugin checks now checks for null bytes by
4
+ default before typecasting. If null bytes are present, it raises
5
+ an error. Most applications do not require null bytes in
6
+ parameters, and in some cases allowing them can lead to security
7
+ issues, especially when parameters are passed to C extensions.
8
+ In general, the benefit of forbidding null bytes in parameters is
9
+ greater than the cost.
10
+
11
+ If you would like to continue allowing null bytes, use the
12
+ :allow_null_bytes option when loading the plugin.
13
+
14
+ Note that this change does not affect uploaded files, since those
15
+ are expected to contain null bytes.
16
+
17
+ = Backwards Compatibility
18
+
19
+ * The change to the typecast_params plugin to raise an error for
20
+ null bytes can break applications that are expecting null bytes
21
+ to be passed in parameters. Such applications should use the
22
+ :allow_null_bytes option when loading the plugin.
data/lib/roda.rb CHANGED
@@ -292,6 +292,9 @@ class Roda
292
292
  plugin.configure(self, *args, &block) if plugin.respond_to?(:configure)
293
293
  @app = nil
294
294
  end
295
+ # :nocov:
296
+ ruby2_keywords(:plugin) if respond_to?(:ruby2_keywords, true)
297
+ # :nocov:
295
298
 
296
299
  # Setup routing tree for the current Roda application, and build the
297
300
  # underlying rack application using the stored middleware. Requires
@@ -327,6 +330,9 @@ class Roda
327
330
  @middleware << [args, block].freeze
328
331
  @app = nil
329
332
  end
333
+ # :nocov:
334
+ ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
335
+ # :nocov:
330
336
 
331
337
  private
332
338
 
@@ -152,6 +152,22 @@ class Roda
152
152
  # together and not compressed during compilation. You can use the
153
153
  # :css_compressor and :js_compressor options to specify the compressor to use.
154
154
  #
155
+ # It is also possible to use the built-in compression options in the CSS or JS
156
+ # compiler, assuming the compiler supports such options. For example, with
157
+ # sass/sassc, you can use:
158
+ #
159
+ # plugin :assets,
160
+ # css_opts: {style: :compressed}
161
+ #
162
+ # === Source Maps (CSS)
163
+ #
164
+ # The assets plugin does not have direct support for source maps, so it is
165
+ # recommended you use embedded source maps if supported by the CSS compiler.
166
+ # For sass/sassc, you can use:
167
+ #
168
+ # plugin :assets,
169
+ # css_opts: {:source_map_embed=>true, source_map_contents: true, source_map_file: "."}
170
+ #
155
171
  # === With Asset Groups
156
172
  #
157
173
  # When using asset groups, a separate compiled file will be produced per
@@ -376,7 +392,7 @@ class Roda
376
392
 
377
393
  if opts[:precompiled] && !opts[:compiled] && ::File.exist?(opts[:precompiled])
378
394
  require 'json'
379
- opts[:compiled] = (app.opts[:json_parser] || ::JSON.method(:parse)).call(::File.read(opts[:precompiled]))
395
+ opts[:compiled] = app.send(:_precompiled_asset_metadata, opts[:precompiled])
380
396
  end
381
397
 
382
398
  if opts[:early_hints]
@@ -455,7 +471,7 @@ class Roda
455
471
  require 'fileutils'
456
472
 
457
473
  unless assets_opts[:compiled]
458
- opts[:assets] = assets_opts.merge(:compiled => {})
474
+ opts[:assets] = assets_opts.merge(:compiled => _compiled_assets_initial_hash).freeze
459
475
  end
460
476
 
461
477
  if type == nil
@@ -465,10 +481,12 @@ class Roda
465
481
  _compile_assets(type)
466
482
  end
467
483
 
468
- if assets_opts[:precompiled]
484
+ if precompile_file = assets_opts[:precompiled]
469
485
  require 'json'
470
- ::FileUtils.mkdir_p(File.dirname(assets_opts[:precompiled]))
471
- ::File.open(assets_opts[:precompiled], 'wb'){|f| f.write((opts[:json_serializer] || :to_json.to_proc).call(assets_opts[:compiled]))}
486
+ ::FileUtils.mkdir_p(File.dirname(precompile_file))
487
+ tmp_file = "#{precompile_file}.tmp"
488
+ ::File.open(tmp_file, 'wb'){|f| f.write((opts[:json_serializer] || :to_json.to_proc).call(assets_opts[:compiled]))}
489
+ ::File.rename(tmp_file, precompile_file)
472
490
  end
473
491
 
474
492
  assets_opts[:compiled]
@@ -476,6 +494,11 @@ class Roda
476
494
 
477
495
  private
478
496
 
497
+ # The initial hash to use to store compiled asset metadata.
498
+ def _compiled_assets_initial_hash
499
+ {}
500
+ end
501
+
479
502
  # Internals of compile_assets, handling recursive calls for loading
480
503
  # all asset groups under the given type.
481
504
  def _compile_assets(type)
@@ -493,6 +516,11 @@ class Roda
493
516
  end
494
517
  end
495
518
 
519
+ # The precompiled asset metadata stored in the given file
520
+ def _precompiled_asset_metadata(file)
521
+ (opts[:json_parser] || ::JSON.method(:parse)).call(::File.read(file))
522
+ end
523
+
496
524
  # Compile each array of files for the given type into a single
497
525
  # file. Dirs should be an array of asset group names, if these
498
526
  # are files in an asset group.
@@ -794,23 +822,32 @@ class Roda
794
822
  # handled.
795
823
  def assets_matchers
796
824
  @assets_matchers ||= [:css, :js].map do |t|
797
- [t, assets_regexp(t)].freeze if roda_class.assets_opts[t]
825
+ if regexp = assets_regexp(t)
826
+ [t, regexp].freeze
827
+ end
798
828
  end.compact.freeze
799
829
  end
800
830
 
801
831
  private
802
832
 
833
+ # A string for the asset filename for the asset type, key, and digest.
834
+ def _asset_regexp(type, key, digest)
835
+ "#{key.sub(/\A#{type}/, '')}.#{digest}.#{type}"
836
+ end
837
+
803
838
  # The regexp matcher to use for the given type. This handles any asset groups
804
839
  # for the asset types.
805
840
  def assets_regexp(type)
806
841
  o = roda_class.assets_opts
807
842
  if compiled = o[:compiled]
808
- assets = compiled.select{|k,_| k =~ /\A#{type}/}.map do |k, md|
809
- "#{k.sub(/\A#{type}/, '')}.#{md}.#{type}"
810
- end
843
+ assets = compiled.
844
+ select{|k,_| k =~ /\A#{type}/}.
845
+ map{|k, md| _asset_regexp(type, k, md)}
846
+ return if assets.empty?
811
847
  /#{o[:"compiled_#{type}_prefix"]}(#{Regexp.union(assets)})/
812
848
  else
813
- assets = unnest_assets_hash(o[type])
849
+ return unless assets = o[type]
850
+ assets = unnest_assets_hash(assets)
814
851
  ts = o[:timestamp_paths]
815
852
  /#{o[:"#{type}_prefix"]}#{"\\d+#{ts}" if ts}(#{Regexp.union(assets.uniq)})#{o[:"#{type}_suffix"]}/
816
853
  end
@@ -18,10 +18,11 @@ class Roda
18
18
  # plugin :common_logger
19
19
  # plugin :common_logger, $stdout
20
20
  # plugin :common_logger, Logger.new('filename')
21
+ # plugin :common_logger, Logger.new('filename'), method: :debug
21
22
  module CommonLogger
22
- def self.configure(app, logger=nil)
23
+ def self.configure(app, logger=nil, opts=OPTS)
23
24
  app.opts[:common_logger] = logger || app.opts[:common_logger] || $stderr
24
- app.opts[:common_logger_meth] = app.opts[:common_logger].method(logger.respond_to?(:write) ? :write : :<<)
25
+ app.opts[:common_logger_meth] = app.opts[:common_logger].method(opts.fetch(:method){logger.respond_to?(:write) ? :write : :<<})
25
26
  end
26
27
 
27
28
  if RUBY_VERSION >= '2.1'
@@ -68,6 +68,8 @@ class Roda
68
68
  end
69
69
 
70
70
  module RequestMethods
71
+ private
72
+
71
73
  # Try custom matchers before calling super
72
74
  def unsupported_matcher(matcher)
73
75
  roda_class.opts[:custom_matchers].each do |match_class, meth|
@@ -23,30 +23,31 @@ class Roda
23
23
  module DefaultHeaders
24
24
  # Merge the given headers into the existing default headers, if any.
25
25
  def self.configure(app, headers={})
26
- headers = app.opts[:default_headers] = (app.default_headers || app::RodaResponse::DEFAULT_HEADERS).merge(headers).freeze
26
+ app.opts[:default_headers] = (app.default_headers || app::RodaResponse::DEFAULT_HEADERS).merge(headers).freeze
27
+ end
28
+
29
+ module ClassMethods
30
+ # The default response headers to use for the current class.
31
+ def default_headers
32
+ opts[:default_headers]
33
+ end
27
34
 
28
- if headers.all?{|k, v| k.is_a?(String) && v.is_a?(String)}
29
- response_class = app::RodaResponse
30
- owner = response_class.instance_method(:set_default_headers).owner
31
- if owner == Base::ResponseMethods || (owner == response_class && app.opts[:set_default_headers_overridder] == response_class)
32
- app.opts[:set_default_headers_overridder] = response_class
33
- response_class.class_eval(<<-END, __FILE__, __LINE__+1)
35
+ # Optimize the response class set_default_headers method if it hasn't been
36
+ # overridden and all default headers are strings.
37
+ def freeze
38
+ if (headers = opts[:default_headers]).all?{|k, v| k.is_a?(String) && v.is_a?(String)} &&
39
+ (self::RodaResponse.instance_method(:set_default_headers).owner == Base::ResponseMethods)
40
+ self::RodaResponse.class_eval(<<-END, __FILE__, __LINE__+1)
34
41
  private
35
42
 
36
- alias set_default_headers set_default_headers
37
43
  def set_default_headers
38
44
  h = @headers
39
45
  #{headers.map{|k,v| "h[#{k.inspect}] ||= #{v.inspect}"}.join('; ')}
40
46
  end
41
47
  END
42
48
  end
43
- end
44
- end
45
49
 
46
- module ClassMethods
47
- # The default response headers to use for the current class.
48
- def default_headers
49
- opts[:default_headers]
50
+ super
50
51
  end
51
52
  end
52
53
 
@@ -0,0 +1,156 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The host_authorization plugin allows configuring an authorized host or
7
+ # an array of authorized hosts. Then in the routing tree, you can check
8
+ # whether the request uses an authorized host via the +check_host_authorized!+
9
+ # method.
10
+ #
11
+ # If the request doesn't match one of the authorized hosts, the
12
+ # request processing stops at that point. Using this plugin can prevent
13
+ # DNS rebinding attacks if the application can receive requests for
14
+ # arbitrary hosts.
15
+ #
16
+ # By default, an empty response using status 403 will be returned for requests
17
+ # with unauthorized hosts.
18
+ #
19
+ # Because +check_host_authorized!+ is an instance method, you can easily choose
20
+ # to only check for authorization in certain routes, or to check it after
21
+ # other processing. For example, you could check for authorized hosts after
22
+ # serving static files, since the serving of static files should not be
23
+ # vulnerable to DNS rebinding attacks.
24
+ #
25
+ # = Usage
26
+ #
27
+ # In your routing tree, call the +check_host_authorized!+ method at the point you
28
+ # want to check for authorized hosts:
29
+ #
30
+ # plugin :host_authorization, 'www.example.com'
31
+ # plugin :public
32
+ #
33
+ # route do |r|
34
+ # r.public
35
+ # check_host_authorized!
36
+ #
37
+ # # ...
38
+ # end
39
+ #
40
+ # = Specifying authorized hosts
41
+ #
42
+ # For applications hosted on a single domain name, you can use a single string:
43
+ #
44
+ # plugin :host_authorization, 'www.example.com'
45
+ #
46
+ # For applications hosted on multiple domain names, you can use an array of strings:
47
+ #
48
+ # plugin :host_authorization, %w'www.example.com www.example2.com'
49
+ #
50
+ # For applications supporting arbitrary subdomains, you can use a regexp. If using
51
+ # a regexp, make sure you use <tt>\A<tt> and <tt>\z</tt> in your regexp, and restrict
52
+ # the allowed characters to the minimum required, otherwise you can potentionally
53
+ # introduce a security issue:
54
+ #
55
+ # plugin :host_authorization, /\A[-0-9a-f]+\.example\.com\z/
56
+ #
57
+ # For applications with more complex requirements, you can use a proc. Similarly
58
+ # to the regexp case, the proc should be aware the host contains user-submitted
59
+ # values, and not assume it is in any particular format:
60
+ #
61
+ # plugin :host_authorization, proc{|host| ExternalService.allowed_host?(host)}
62
+ #
63
+ # If an array of values is passed as the host argument, the host is authorized if
64
+ # it matches any value in the array. All host authorization checks use the
65
+ # <tt>===</tt> method, which is why it works for strings, regexps, and procs.
66
+ # It can also work with arbitrary objects that support <tt>===</tt>.
67
+ #
68
+ # For security reasons, only the +Host+ header is checked by default. If you are
69
+ # sure that your application is being run behind a forwarding proxy that sets the
70
+ # <tt>X-Forwarded-Host</tt> header, you should enable support for checking that
71
+ # header using the +:check_forwarded+ option:
72
+ #
73
+ # plugin :host_authorization, 'www.example.com', check_forwarded: true
74
+ #
75
+ # In this case, the trailing host in the <tt>X-Forwarded-Host</tt> header is checked,
76
+ # which should be the host set by the forwarding proxy closest to the application.
77
+ # In cases where multiple forwarding proxies are used that append to the
78
+ # <tt>X-Forwarded-Host</tt> header, you should not use this plugin.
79
+ #
80
+ # = Customizing behavior
81
+ #
82
+ # By default, an unauthorized host will receive an empty 403 response. You can
83
+ # customize this by passing a block when loading the plugin. For example, for
84
+ # sites using the render plugin, you could return a page that uses your default
85
+ # layout:
86
+ #
87
+ # plugin :render
88
+ # plugin :host_authorization, 'www.example.com' do |r|
89
+ # response.status = 403
90
+ # view(:content=>"<h1>Forbidden</h1>")
91
+ # end
92
+ #
93
+ # The block passed to this plugin is treated as a match block.
94
+ module HostAuthorization
95
+ def self.configure(app, host, opts=OPTS, &block)
96
+ app.opts[:host_authorization_host] = host
97
+ app.opts[:host_authorization_check_forwarded] = opts[:check_forwarded] if opts.key?(:check_forwarded)
98
+
99
+ if block
100
+ app.define_roda_method(:host_authorization_unauthorized, 1, &block)
101
+ end
102
+ end
103
+
104
+ module InstanceMethods
105
+ # Check whether the host is authorized. If not authorized, return a response
106
+ # immediately based on the plugin block.
107
+ def check_host_authorization!
108
+ r = @_request
109
+ return if host_authorized?(_convert_host_for_authorization(r.env["HTTP_HOST"].to_s.dup))
110
+
111
+ if opts[:host_authorization_check_forwarded] && (host = r.env["HTTP_X_FORWARDED_HOST"])
112
+ if i = host.rindex(',')
113
+ host = host[i+1, 10000000].to_s
114
+ end
115
+ host = _convert_host_for_authorization(host.strip)
116
+
117
+ if !host.empty? && host_authorized?(host)
118
+ return
119
+ end
120
+ end
121
+
122
+ r.on do
123
+ host_authorization_unauthorized(r)
124
+ end
125
+ end
126
+
127
+ private
128
+
129
+ # Remove the port information from the passed string (mutates the passed argument).
130
+ def _convert_host_for_authorization(host)
131
+ host.sub!(/:\d+\z/, "")
132
+ host
133
+ end
134
+
135
+ # Whether the host given is one of the authorized hosts for this application.
136
+ def host_authorized?(host, authorized_host = opts[:host_authorization_host])
137
+ case authorized_host
138
+ when Array
139
+ authorized_host.any?{|auth_host| host_authorized?(host, auth_host)}
140
+ else
141
+ authorized_host === host
142
+ end
143
+ end
144
+
145
+ # Action to take for unauthorized hosts. Sets a 403 status by default.
146
+ def host_authorization_unauthorized(_)
147
+ @_response.status = 403
148
+ nil
149
+ end
150
+ end
151
+ end
152
+
153
+ register_plugin(:host_authorization, HostAuthorization)
154
+ end
155
+ end
156
+
@@ -0,0 +1,53 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The optimized_segment_matchers plugin adds two optimized matcher methods,
7
+ # +r.on_segment+ and +r.is_segment+. +r.on_segment+ is an optimized version of
8
+ # +r.on String+ that accepts no arguments and yields the next segment if there
9
+ # is a segment. +r.is_segment+ is an optimized version of +r.is String+ that accepts
10
+ # no arguments and yields the next segment only if it is the last segment.
11
+ #
12
+ # plugin :optimized_segment_matchers
13
+ #
14
+ # route do |r|
15
+ # r.on_segment do |x|
16
+ # # matches any segment (e.g. /a, /b, but not /)
17
+ # r.is_segment do |y|
18
+ # # matches only if final segment (e.g. /a/b, /b/c, but not /c, /c/d/, /c/d/e)
19
+ # end
20
+ # end
21
+ # end
22
+ module OptimizedSegmentMatchers
23
+ module RequestMethods
24
+ # Optimized version of +r.on String+ that yields the next segment if there
25
+ # is a segment.
26
+ def on_segment
27
+ rp = @remaining_path
28
+ if rp.getbyte(0) == 47
29
+ if last = rp.index('/', 1)
30
+ @remaining_path = rp[last, rp.length]
31
+ always{yield rp[1, last-1]}
32
+ elsif (len = rp.length) > 1
33
+ @remaining_path = ""
34
+ always{yield rp[1, len]}
35
+ end
36
+ end
37
+ end
38
+
39
+ # Optimized version of +r.is String+ that yields the next segment only if it
40
+ # is the final segment.
41
+ def is_segment
42
+ rp = @remaining_path
43
+ if rp.getbyte(0) == 47 && !rp.index('/', 1) && (len = rp.length) > 1
44
+ @remaining_path = ""
45
+ always{yield rp[1, len]}
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ register_plugin(:optimized_segment_matchers, OptimizedSegmentMatchers)
52
+ end
53
+ end
@@ -19,7 +19,8 @@ class Roda
19
19
  # end
20
20
  # end
21
21
  #
22
- # Note that both of these methods only work with plain strings, not
22
+ # If you are using the placeholder_string_matchers plugin, note
23
+ # that both of these methods only work with plain strings, not
23
24
  # with strings with embedded colons for capturing. Matching will work
24
25
  # correctly in such cases, but the captures will not be yielded to the
25
26
  # match blocks.
@@ -0,0 +1,107 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The recheck_precompiled_assets plugin enables checking for the precompiled asset metadata file.
7
+ # You need to have already loaded the assets plugin with the +:precompiled+ option and the file
8
+ # specified by the +:precompiled+ option must already exist in order to use the
9
+ # recheck_precompiled_assets plugin.
10
+ #
11
+ # Any time you want to check whether the precompiled asset metadata file has changed and should be
12
+ # reloaded, you can call the +recheck_precompiled_assets+ class method. This method will check
13
+ # whether the file has changed, and reload it if it has. If you want to check for modifications on
14
+ # every request, you can use +self.class.recheck_precompiled_assets+ inside your route block.
15
+ module RecheckPrecompiledAssets
16
+ # Thread safe wrapper for the compiled asset metadata hash. Does not wrap all
17
+ # hash methods, only a few that are used.
18
+ class CompiledAssetsHash
19
+ include Enumerable
20
+
21
+ def initialize
22
+ @hash = {}
23
+ @mutex = Mutex.new
24
+ end
25
+
26
+ def [](key)
27
+ @mutex.synchronize{@hash[key]}
28
+ end
29
+
30
+ def []=(key, value)
31
+ @mutex.synchronize{@hash[key] = value}
32
+ end
33
+
34
+ def replace(hash)
35
+ hash = hash.instance_variable_get(:@hash) if (CompiledAssetsHash === hash)
36
+ @mutex.synchronize{@hash.replace(hash)}
37
+ self
38
+ end
39
+
40
+ def each(&block)
41
+ @mutex.synchronize{@hash.dup}.each(&block)
42
+ self
43
+ end
44
+
45
+ def to_json(*args)
46
+ @mutex.synchronize{@hash.dup}.to_json(*args)
47
+ end
48
+ end
49
+
50
+ def self.load_dependencies(app)
51
+ unless app.respond_to?(:assets_opts) && app.assets_opts[:precompiled]
52
+ raise RodaError, "must load assets plugin with precompiled option before loading recheck_precompiled_assets plugin"
53
+ end
54
+ end
55
+
56
+ def self.configure(app)
57
+ precompiled_file = app.assets_opts[:precompiled]
58
+ prev_mtime = ::File.mtime(precompiled_file)
59
+ app.instance_exec do
60
+ opts[:assets] = opts[:assets].merge(:compiled=>_compiled_assets_initial_hash.replace(assets_opts[:compiled])).freeze
61
+
62
+ define_singleton_method(:recheck_precompiled_assets) do
63
+ new_mtime = ::File.mtime(precompiled_file)
64
+ if new_mtime != prev_mtime
65
+ prev_mtime = new_mtime
66
+ assets_opts[:compiled].replace(_precompiled_asset_metadata(precompiled_file))
67
+
68
+ # Unset the cached asset matchers, so new ones will be generated.
69
+ # This is needed in case the new precompiled metadata uses
70
+ # different files.
71
+ app::RodaRequest.instance_variable_set(:@assets_matchers, nil)
72
+ end
73
+ end
74
+ singleton_class.send(:alias_method, :recheck_precompiled_assets, :recheck_precompiled_assets)
75
+ end
76
+ end
77
+
78
+ module ClassMethods
79
+ private
80
+
81
+ # Wrap the precompiled asset metadata in a thread-safe hash.
82
+ def _precompiled_asset_metadata(file)
83
+ CompiledAssetsHash.new.replace(super)
84
+ end
85
+
86
+ # Use a thread-safe wrapper of a hash for the :compiled assets option, since
87
+ # the recheck_precompiled_asset_metadata can modify it at runtime.
88
+ def _compiled_assets_initial_hash
89
+ CompiledAssetsHash.new
90
+ end
91
+ end
92
+
93
+ module RequestClassMethods
94
+ private
95
+
96
+ # Use a regexp that matches any digest. When the precompiled asset metadata
97
+ # file is updated, this allows requests for a previous precompiled asset to
98
+ # still work.
99
+ def _asset_regexp(type, key, _)
100
+ /#{Regexp.escape(key.sub(/\A#{type}/, ''))}\.[0-9a-fA-F]+\.#{type}/
101
+ end
102
+ end
103
+ end
104
+
105
+ register_plugin(:recheck_precompiled_assets, RecheckPrecompiledAssets)
106
+ end
107
+ end
@@ -388,6 +388,8 @@ class Roda
388
388
  instance.send(:retrieve_template, :template=>layout_template, :cache_key=>nil, :template_method_cache_key => :_roda_layout)
389
389
  layout_method = opts[:render][:template_method_cache][:_roda_layout]
390
390
  define_method(:_layout_method){layout_method}
391
+ private :_layout_method
392
+ alias_method(:_layout_method, :_layout_method)
391
393
  opts[:render] = opts[:render].merge(:optimized_layout_method_created=>true)
392
394
  end
393
395
  end
@@ -211,6 +211,10 @@ class Roda
211
211
  # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
212
212
  # OTHER DEALINGS IN THE SOFTWARE.
213
213
  module SinatraHelpers
214
+ UTF8_ENCODING = Encoding.find('UTF-8')
215
+ ISO88591_ENCODING = Encoding.find('ISO-8859-1')
216
+ BINARY_ENCODING = Encoding.find('BINARY')
217
+
214
218
  # Depend on the status_303 plugin.
215
219
  def self.load_dependencies(app, _opts = nil)
216
220
  app.plugin :status_303
@@ -432,7 +436,25 @@ class Roda
432
436
  # instructing the user agents to prompt to save.
433
437
  def attachment(filename = nil, disposition='attachment')
434
438
  if filename
435
- params = "; filename=#{File.basename(filename).inspect}"
439
+ param_filename = File.basename(filename)
440
+ encoding = param_filename.encoding
441
+
442
+ needs_encoding = param_filename.gsub!(/[^ 0-9a-zA-Z!\#$&\+\.\^_`\|~]+/, '-')
443
+ params = "; filename=#{param_filename.inspect}"
444
+
445
+ if needs_encoding && (encoding == UTF8_ENCODING || encoding == ISO88591_ENCODING)
446
+ # File name contains non attr-char characters from RFC 5987 Section 3.2.1
447
+
448
+ encoded_filename = File.basename(filename).force_encoding(BINARY_ENCODING)
449
+ # Similar regexp as above, but treat each byte separately, and encode
450
+ # space characters, since those aren't allowed in attr-char
451
+ encoded_filename.gsub!(/[^0-9a-zA-Z!\#$&\+\.\^_`\|~]/) do |c|
452
+ "%%%X" % c.ord
453
+ end
454
+
455
+ encoded_params = "; filename*=#{encoding.to_s}''#{encoded_filename}"
456
+ end
457
+
436
458
  unless @headers["Content-Type"]
437
459
  ext = File.extname(filename)
438
460
  unless ext.empty?
@@ -440,7 +462,7 @@ class Roda
440
462
  end
441
463
  end
442
464
  end
443
- @headers["Content-Disposition"] = "#{disposition}#{params}"
465
+ @headers["Content-Disposition"] = "#{disposition}#{params}#{encoded_params}"
444
466
  end
445
467
 
446
468
  # Whether or not the status is set to 1xx. Returns nil if status not yet set.
@@ -260,6 +260,11 @@ class Roda
260
260
  # strip leading and trailing whitespace from parameter string values before processing, which
261
261
  # you can do by passing the <tt>strip: :all</tt> option when loading the plugin.
262
262
  #
263
+ # By default, the typecast_params conversion procs check that null bytes are not allowed
264
+ # in param string values. This check for null bytes occurs prior to any type conversion.
265
+ # If you would like to skip this check and allow null bytes in param string values,
266
+ # you can do by passing the <tt>:allow_null_bytes</tt> option when loading the plugin.
267
+ #
263
268
  # By design, typecast_params only deals with string keys, it is not possible to use
264
269
  # symbol keys as arguments to the conversion methods and have them converted.
265
270
  module TypecastParams
@@ -356,6 +361,14 @@ class Roda
356
361
  end
357
362
  end
358
363
 
364
+ module AllowNullByte
365
+ private
366
+
367
+ # Allow ASCII NUL bytes ("\0") in parameter string values.
368
+ def check_null_byte(v)
369
+ end
370
+ end
371
+
359
372
  module StringStripper
360
373
  private
361
374
 
@@ -391,7 +404,10 @@ class Roda
391
404
  convert_array_meth = :"_convert_array_#{type}"
392
405
  define_method(convert_array_meth) do |v|
393
406
  raise Error, "expected array but received #{v.inspect}" unless v.is_a?(Array)
394
- v.map!{|val| send(convert_meth, val)}
407
+ v.map! do |val|
408
+ check_null_byte(val)
409
+ send(convert_meth, val)
410
+ end
395
411
  end
396
412
 
397
413
  private convert_meth, convert_array_meth
@@ -927,12 +943,20 @@ class Roda
927
943
  end
928
944
  end
929
945
 
946
+ # Raise an Error if the value is a string containing a null byte.
947
+ def check_null_byte(v)
948
+ if v.is_a?(String) && v.index("\0")
949
+ handle_error(nil, :null_byte, "string parameter contains null byte", true)
950
+ end
951
+ end
952
+
930
953
  # Get the value of +key+ for the object, and convert it to the expected type using +meth+.
931
954
  # If the value either before or after conversion is nil, return the +default+ value.
932
955
  def process(meth, key, default)
933
956
  v = param_value(key)
934
957
 
935
958
  unless v.nil?
959
+ check_null_byte(v)
936
960
  v = send(meth, v)
937
961
  end
938
962
 
@@ -992,6 +1016,9 @@ class Roda
992
1016
  if opts[:strip] == :all
993
1017
  app::TypecastParams.send(:include, StringStripper)
994
1018
  end
1019
+ if opts[:allow_null_bytes]
1020
+ app::TypecastParams.send(:include, AllowNullByte)
1021
+ end
995
1022
  end
996
1023
 
997
1024
  module ClassMethods
data/lib/roda/request.rb CHANGED
@@ -468,8 +468,8 @@ class Roda
468
468
  if last = rp.index('/', 1)
469
469
  @captures << rp[1, last-1]
470
470
  @remaining_path = rp[last, rp.length]
471
- elsif rp.length > 1
472
- @captures << rp[1,rp.length]
471
+ elsif (len = rp.length) > 1
472
+ @captures << rp[1, len]
473
473
  @remaining_path = ""
474
474
  end
475
475
  end
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 = 41
7
+ RodaMinorVersion = 45
8
8
 
9
9
  # The patch version of Roda, updated only for bug fixes from the last
10
10
  # feature release.
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.41.0
4
+ version: 3.45.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: 2021-02-17 00:00:00.000000000 Z
11
+ date: 2021-06-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -212,6 +212,10 @@ extra_rdoc_files:
212
212
  - doc/release_notes/3.4.0.txt
213
213
  - doc/release_notes/3.40.0.txt
214
214
  - doc/release_notes/3.41.0.txt
215
+ - doc/release_notes/3.42.0.txt
216
+ - doc/release_notes/3.43.0.txt
217
+ - doc/release_notes/3.44.0.txt
218
+ - doc/release_notes/3.45.0.txt
215
219
  - doc/release_notes/3.5.0.txt
216
220
  - doc/release_notes/3.6.0.txt
217
221
  - doc/release_notes/3.7.0.txt
@@ -260,6 +264,10 @@ files:
260
264
  - doc/release_notes/3.4.0.txt
261
265
  - doc/release_notes/3.40.0.txt
262
266
  - doc/release_notes/3.41.0.txt
267
+ - doc/release_notes/3.42.0.txt
268
+ - doc/release_notes/3.43.0.txt
269
+ - doc/release_notes/3.44.0.txt
270
+ - doc/release_notes/3.45.0.txt
263
271
  - doc/release_notes/3.5.0.txt
264
272
  - doc/release_notes/3.6.0.txt
265
273
  - doc/release_notes/3.7.0.txt
@@ -310,6 +318,7 @@ files:
310
318
  - lib/roda/plugins/header_matchers.rb
311
319
  - lib/roda/plugins/heartbeat.rb
312
320
  - lib/roda/plugins/hooks.rb
321
+ - lib/roda/plugins/host_authorization.rb
313
322
  - lib/roda/plugins/indifferent_params.rb
314
323
  - lib/roda/plugins/json.rb
315
324
  - lib/roda/plugins/json_parser.rb
@@ -328,6 +337,7 @@ files:
328
337
  - lib/roda/plugins/named_templates.rb
329
338
  - lib/roda/plugins/not_allowed.rb
330
339
  - lib/roda/plugins/not_found.rb
340
+ - lib/roda/plugins/optimized_segment_matchers.rb
331
341
  - lib/roda/plugins/optimized_string_matchers.rb
332
342
  - lib/roda/plugins/padrino_render.rb
333
343
  - lib/roda/plugins/param_matchers.rb
@@ -341,6 +351,7 @@ files:
341
351
  - lib/roda/plugins/precompile_templates.rb
342
352
  - lib/roda/plugins/public.rb
343
353
  - lib/roda/plugins/r.rb
354
+ - lib/roda/plugins/recheck_precompiled_assets.rb
344
355
  - lib/roda/plugins/relative_path.rb
345
356
  - lib/roda/plugins/render.rb
346
357
  - lib/roda/plugins/render_each.rb
@@ -398,7 +409,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
398
409
  - !ruby/object:Gem::Version
399
410
  version: '0'
400
411
  requirements: []
401
- rubygems_version: 3.2.3
412
+ rubygems_version: 3.2.15
402
413
  signing_key:
403
414
  specification_version: 4
404
415
  summary: Routing tree web toolkit