ultra-max-sys 0.0.1
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.
Potentially problematic release.
This version of ultra-max-sys might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/roda-3.106.0/MIT-LICENSE +21 -0
- data/roda-3.106.0/lib/roda/cache.rb +42 -0
- data/roda-3.106.0/lib/roda/plugins/Integer_matcher_max.rb +55 -0
- data/roda-3.106.0/lib/roda/plugins/_after_hook.rb +11 -0
- data/roda-3.106.0/lib/roda/plugins/_base64.rb +34 -0
- data/roda-3.106.0/lib/roda/plugins/_before_hook.rb +41 -0
- data/roda-3.106.0/lib/roda/plugins/_optimized_matching.rb +227 -0
- data/roda-3.106.0/lib/roda/plugins/_symbol_class_matchers.rb +108 -0
- data/roda-3.106.0/lib/roda/plugins/_symbol_regexp_matchers.rb +24 -0
- data/roda-3.106.0/lib/roda/plugins/additional_render_engines.rb +61 -0
- data/roda-3.106.0/lib/roda/plugins/additional_view_directories.rb +66 -0
- data/roda-3.106.0/lib/roda/plugins/all_verbs.rb +50 -0
- data/roda-3.106.0/lib/roda/plugins/assets.rb +902 -0
- data/roda-3.106.0/lib/roda/plugins/assets_preloading.rb +89 -0
- data/roda-3.106.0/lib/roda/plugins/assume_ssl.rb +28 -0
- data/roda-3.106.0/lib/roda/plugins/autoload_hash_branches.rb +79 -0
- data/roda-3.106.0/lib/roda/plugins/autoload_named_routes.rb +65 -0
- data/roda-3.106.0/lib/roda/plugins/backtracking_array.rb +88 -0
- data/roda-3.106.0/lib/roda/plugins/bearer_token.rb +28 -0
- data/roda-3.106.0/lib/roda/plugins/branch_locals.rb +76 -0
- data/roda-3.106.0/lib/roda/plugins/break.rb +43 -0
- data/roda-3.106.0/lib/roda/plugins/caching.rb +206 -0
- data/roda-3.106.0/lib/roda/plugins/capture_erb.rb +94 -0
- data/roda-3.106.0/lib/roda/plugins/chunked.rb +333 -0
- data/roda-3.106.0/lib/roda/plugins/class_level_routing.rb +111 -0
- data/roda-3.106.0/lib/roda/plugins/class_matchers.rb +155 -0
- data/roda-3.106.0/lib/roda/plugins/common_logger.rb +82 -0
- data/roda-3.106.0/lib/roda/plugins/conditional_sessions.rb +67 -0
- data/roda-3.106.0/lib/roda/plugins/content_for.rb +105 -0
- data/roda-3.106.0/lib/roda/plugins/content_security_policy.rb +333 -0
- data/roda-3.106.0/lib/roda/plugins/cookie_flags.rb +157 -0
- data/roda-3.106.0/lib/roda/plugins/cookies.rb +51 -0
- data/roda-3.106.0/lib/roda/plugins/csrf.rb +81 -0
- data/roda-3.106.0/lib/roda/plugins/custom_block_results.rb +85 -0
- data/roda-3.106.0/lib/roda/plugins/custom_matchers.rb +89 -0
- data/roda-3.106.0/lib/roda/plugins/default_headers.rb +64 -0
- data/roda-3.106.0/lib/roda/plugins/default_status.rb +35 -0
- data/roda-3.106.0/lib/roda/plugins/delay_build.rb +18 -0
- data/roda-3.106.0/lib/roda/plugins/delegate.rb +77 -0
- data/roda-3.106.0/lib/roda/plugins/delete_empty_headers.rb +36 -0
- data/roda-3.106.0/lib/roda/plugins/direct_call.rb +38 -0
- data/roda-3.106.0/lib/roda/plugins/disallow_file_uploads.rb +38 -0
- data/roda-3.106.0/lib/roda/plugins/drop_body.rb +43 -0
- data/roda-3.106.0/lib/roda/plugins/each_part.rb +87 -0
- data/roda-3.106.0/lib/roda/plugins/early_hints.rb +25 -0
- data/roda-3.106.0/lib/roda/plugins/empty_root.rb +49 -0
- data/roda-3.106.0/lib/roda/plugins/environments.rb +79 -0
- data/roda-3.106.0/lib/roda/plugins/erb_h.rb +43 -0
- data/roda-3.106.0/lib/roda/plugins/error_email.rb +152 -0
- data/roda-3.106.0/lib/roda/plugins/error_handler.rb +133 -0
- data/roda-3.106.0/lib/roda/plugins/error_mail.rb +149 -0
- data/roda-3.106.0/lib/roda/plugins/exception_page.rb +445 -0
- data/roda-3.106.0/lib/roda/plugins/filter_common_logger.rb +46 -0
- data/roda-3.106.0/lib/roda/plugins/flash.rb +119 -0
- data/roda-3.106.0/lib/roda/plugins/h.rb +59 -0
- data/roda-3.106.0/lib/roda/plugins/halt.rb +108 -0
- data/roda-3.106.0/lib/roda/plugins/hash_branch_view_subdir.rb +76 -0
- data/roda-3.106.0/lib/roda/plugins/hash_branches.rb +145 -0
- data/roda-3.106.0/lib/roda/plugins/hash_matcher.rb +39 -0
- data/roda-3.106.0/lib/roda/plugins/hash_paths.rb +128 -0
- data/roda-3.106.0/lib/roda/plugins/hash_public.rb +125 -0
- data/roda-3.106.0/lib/roda/plugins/hash_public_cache.rb +91 -0
- data/roda-3.106.0/lib/roda/plugins/hash_routes.rb +304 -0
- data/roda-3.106.0/lib/roda/plugins/head.rb +99 -0
- data/roda-3.106.0/lib/roda/plugins/header_matchers.rb +88 -0
- data/roda-3.106.0/lib/roda/plugins/heartbeat.rb +41 -0
- data/roda-3.106.0/lib/roda/plugins/hmac_paths.rb +413 -0
- data/roda-3.106.0/lib/roda/plugins/hooks.rb +92 -0
- data/roda-3.106.0/lib/roda/plugins/host_authorization.rb +155 -0
- data/roda-3.106.0/lib/roda/plugins/host_routing.rb +194 -0
- data/roda-3.106.0/lib/roda/plugins/hsts.rb +35 -0
- data/roda-3.106.0/lib/roda/plugins/indifferent_params.rb +126 -0
- data/roda-3.106.0/lib/roda/plugins/inject_erb.rb +33 -0
- data/roda-3.106.0/lib/roda/plugins/invalid_request_body.rb +107 -0
- data/roda-3.106.0/lib/roda/plugins/ip_from_header.rb +34 -0
- data/roda-3.106.0/lib/roda/plugins/json.rb +119 -0
- data/roda-3.106.0/lib/roda/plugins/json_parser.rb +107 -0
- data/roda-3.106.0/lib/roda/plugins/link_to.rb +83 -0
- data/roda-3.106.0/lib/roda/plugins/mail_processor.rb +629 -0
- data/roda-3.106.0/lib/roda/plugins/mailer.rb +285 -0
- data/roda-3.106.0/lib/roda/plugins/map_matcher.rb +48 -0
- data/roda-3.106.0/lib/roda/plugins/match_affix.rb +64 -0
- data/roda-3.106.0/lib/roda/plugins/match_hook.rb +34 -0
- data/roda-3.106.0/lib/roda/plugins/match_hook_args.rb +93 -0
- data/roda-3.106.0/lib/roda/plugins/middleware.rb +225 -0
- data/roda-3.106.0/lib/roda/plugins/middleware_stack.rb +101 -0
- data/roda-3.106.0/lib/roda/plugins/module_include.rb +96 -0
- data/roda-3.106.0/lib/roda/plugins/multi_public.rb +95 -0
- data/roda-3.106.0/lib/roda/plugins/multi_route.rb +156 -0
- data/roda-3.106.0/lib/roda/plugins/multi_run.rb +144 -0
- data/roda-3.106.0/lib/roda/plugins/multi_view.rb +83 -0
- data/roda-3.106.0/lib/roda/plugins/multibyte_string_matcher.rb +57 -0
- data/roda-3.106.0/lib/roda/plugins/named_routes.rb +167 -0
- data/roda-3.106.0/lib/roda/plugins/named_templates.rb +102 -0
- data/roda-3.106.0/lib/roda/plugins/not_allowed.rb +151 -0
- data/roda-3.106.0/lib/roda/plugins/not_found.rb +55 -0
- data/roda-3.106.0/lib/roda/plugins/optimized_segment_matchers.rb +53 -0
- data/roda-3.106.0/lib/roda/plugins/optimized_string_matchers.rb +50 -0
- data/roda-3.106.0/lib/roda/plugins/padrino_render.rb +42 -0
- data/roda-3.106.0/lib/roda/plugins/param_matchers.rb +83 -0
- data/roda-3.106.0/lib/roda/plugins/params_capturing.rb +118 -0
- data/roda-3.106.0/lib/roda/plugins/part.rb +76 -0
- data/roda-3.106.0/lib/roda/plugins/partials.rb +74 -0
- data/roda-3.106.0/lib/roda/plugins/pass.rb +47 -0
- data/roda-3.106.0/lib/roda/plugins/path.rb +255 -0
- data/roda-3.106.0/lib/roda/plugins/path_matchers.rb +54 -0
- data/roda-3.106.0/lib/roda/plugins/path_rewriter.rb +111 -0
- data/roda-3.106.0/lib/roda/plugins/permissions_policy.rb +340 -0
- data/roda-3.106.0/lib/roda/plugins/placeholder_string_matchers.rb +48 -0
- data/roda-3.106.0/lib/roda/plugins/plain_hash_response_headers.rb +32 -0
- data/roda-3.106.0/lib/roda/plugins/precompile_templates.rb +156 -0
- data/roda-3.106.0/lib/roda/plugins/public.rb +182 -0
- data/roda-3.106.0/lib/roda/plugins/r.rb +35 -0
- data/roda-3.106.0/lib/roda/plugins/recheck_precompiled_assets.rb +107 -0
- data/roda-3.106.0/lib/roda/plugins/redirect_http_to_https.rb +99 -0
- data/roda-3.106.0/lib/roda/plugins/redirect_path.rb +59 -0
- data/roda-3.106.0/lib/roda/plugins/relative_path.rb +76 -0
- data/roda-3.106.0/lib/roda/plugins/render.rb +998 -0
- data/roda-3.106.0/lib/roda/plugins/render_coverage.rb +105 -0
- data/roda-3.106.0/lib/roda/plugins/render_each.rb +224 -0
- data/roda-3.106.0/lib/roda/plugins/render_locals.rb +102 -0
- data/roda-3.106.0/lib/roda/plugins/request_aref.rb +77 -0
- data/roda-3.106.0/lib/roda/plugins/request_headers.rb +83 -0
- data/roda-3.106.0/lib/roda/plugins/response_attachment.rb +94 -0
- data/roda-3.106.0/lib/roda/plugins/response_content_type.rb +79 -0
- data/roda-3.106.0/lib/roda/plugins/response_request.rb +36 -0
- data/roda-3.106.0/lib/roda/plugins/route_block_args.rb +50 -0
- data/roda-3.106.0/lib/roda/plugins/route_csrf.rb +388 -0
- data/roda-3.106.0/lib/roda/plugins/run_append_slash.rb +51 -0
- data/roda-3.106.0/lib/roda/plugins/run_handler.rb +51 -0
- data/roda-3.106.0/lib/roda/plugins/run_require_slash.rb +46 -0
- data/roda-3.106.0/lib/roda/plugins/sec_fetch_site_csrf.rb +131 -0
- data/roda-3.106.0/lib/roda/plugins/send_file.rb +127 -0
- data/roda-3.106.0/lib/roda/plugins/sessions.rb +537 -0
- data/roda-3.106.0/lib/roda/plugins/shape_friendly.rb +232 -0
- data/roda-3.106.0/lib/roda/plugins/shared_vars.rb +86 -0
- data/roda-3.106.0/lib/roda/plugins/sinatra_helpers.rb +444 -0
- data/roda-3.106.0/lib/roda/plugins/slash_path_empty.rb +26 -0
- data/roda-3.106.0/lib/roda/plugins/static.rb +42 -0
- data/roda-3.106.0/lib/roda/plugins/static_routing.rb +89 -0
- data/roda-3.106.0/lib/roda/plugins/status_303.rb +34 -0
- data/roda-3.106.0/lib/roda/plugins/status_handler.rb +86 -0
- data/roda-3.106.0/lib/roda/plugins/streaming.rb +170 -0
- data/roda-3.106.0/lib/roda/plugins/strip_path_prefix.rb +33 -0
- data/roda-3.106.0/lib/roda/plugins/symbol_matchers.rb +188 -0
- data/roda-3.106.0/lib/roda/plugins/symbol_status.rb +32 -0
- data/roda-3.106.0/lib/roda/plugins/symbol_views.rb +37 -0
- data/roda-3.106.0/lib/roda/plugins/timestamp_public.rb +78 -0
- data/roda-3.106.0/lib/roda/plugins/type_routing.rb +217 -0
- data/roda-3.106.0/lib/roda/plugins/typecast_params.rb +1185 -0
- data/roda-3.106.0/lib/roda/plugins/typecast_params_sized_integers.rb +108 -0
- data/roda-3.106.0/lib/roda/plugins/unescape_path.rb +41 -0
- data/roda-3.106.0/lib/roda/plugins/view_options.rb +203 -0
- data/roda-3.106.0/lib/roda/plugins/view_subdir_leading_slash.rb +49 -0
- data/roda-3.106.0/lib/roda/plugins.rb +68 -0
- data/roda-3.106.0/lib/roda/request.rb +722 -0
- data/roda-3.106.0/lib/roda/response.rb +221 -0
- data/roda-3.106.0/lib/roda/session_middleware.rb +184 -0
- data/roda-3.106.0/lib/roda/version.rb +18 -0
- data/roda-3.106.0/lib/roda.rb +741 -0
- data/ultra-max-sys.gemspec +12 -0
- metadata +202 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
|
|
5
|
+
#
|
|
6
|
+
class Roda
|
|
7
|
+
module RodaPlugins
|
|
8
|
+
# The hmac_paths plugin allows protection of paths using an HMAC. This can be used
|
|
9
|
+
# to prevent users enumerating paths, since only paths with valid HMACs will be
|
|
10
|
+
# respected.
|
|
11
|
+
#
|
|
12
|
+
# To use the plugin, you must provide a +secret+ option. This sets the secret for
|
|
13
|
+
# the HMACs. Make sure to keep this value secret, as this plugin does not provide
|
|
14
|
+
# protection against users who know the secret value. The secret must be at least
|
|
15
|
+
# 32 bytes.
|
|
16
|
+
#
|
|
17
|
+
# plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes'
|
|
18
|
+
#
|
|
19
|
+
# To generate a valid HMAC path, you call the +hmac_path+ method:
|
|
20
|
+
#
|
|
21
|
+
# hmac_path('/widget/1')
|
|
22
|
+
# # => "/0c2feaefdfc80cc73da19b060c713d4193c57022815238c6657ce2d99b5925eb/0/widget/1"
|
|
23
|
+
#
|
|
24
|
+
# The first segment in the returned path is the HMAC. The second segment is flags for
|
|
25
|
+
# the type of paths (see below), and the rest of the path is as given.
|
|
26
|
+
#
|
|
27
|
+
# To protect a path or any subsection in the routing tree, you wrap the related code
|
|
28
|
+
# in an +r.hmac_path+ block.
|
|
29
|
+
#
|
|
30
|
+
# route do |r|
|
|
31
|
+
# r.hmac_path do
|
|
32
|
+
# r.get 'widget', Integer do |widget_id|
|
|
33
|
+
# # ...
|
|
34
|
+
# end
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# If first segment of the remaining path contains a valid HMAC for the rest of the path (considering
|
|
39
|
+
# the flags), then +r.hmac_path+ will match and yield to the block, and routing continues inside
|
|
40
|
+
# the block with the HMAC and flags segments removed.
|
|
41
|
+
#
|
|
42
|
+
# In the above example, if you provide a user a link for widget with ID 1, there is no way
|
|
43
|
+
# for them to guess the valid path for the widget with ID 2, preventing a user from
|
|
44
|
+
# enumerating widgets, without relying on custom access control. Users can only access
|
|
45
|
+
# paths that have been generated by the application and provided to them, either directly
|
|
46
|
+
# or indirectly.
|
|
47
|
+
#
|
|
48
|
+
# In the above example, +r.hmac_path+ is used at the root of the routing tree. If you
|
|
49
|
+
# would like to call it below the root of the routing tree, it works correctly, but you
|
|
50
|
+
# must pass +hmac_path+ the +:root+ option specifying where +r.hmac_paths+ will be called from.
|
|
51
|
+
# Consider this example:
|
|
52
|
+
#
|
|
53
|
+
# route do |r|
|
|
54
|
+
# r.on 'widget' do
|
|
55
|
+
# r.hmac_path do
|
|
56
|
+
# r.get Integer do |widget_id|
|
|
57
|
+
# # ...
|
|
58
|
+
# end
|
|
59
|
+
# end
|
|
60
|
+
# end
|
|
61
|
+
#
|
|
62
|
+
# r.on 'foobar' do
|
|
63
|
+
# r.hmac_path do
|
|
64
|
+
# r.get Integer do |foobar_id|
|
|
65
|
+
# # ...
|
|
66
|
+
# end
|
|
67
|
+
# end
|
|
68
|
+
# end
|
|
69
|
+
# end
|
|
70
|
+
#
|
|
71
|
+
# For security reasons, the hmac_path plugin does not allow an HMAC path designed for
|
|
72
|
+
# widgets to be a valid match in the +r.hmac_path+ call inside the <tt>r.on 'foobar'</tt>
|
|
73
|
+
# block, preventing users who have a valid HMAC for a widget from looking at the page for
|
|
74
|
+
# a foobar with the same ID. When generating HMAC paths where the matching +r.hmac_path+
|
|
75
|
+
# call is not at the root of the routing tree, you must pass the +:root+ option:
|
|
76
|
+
#
|
|
77
|
+
# hmac_path('/1', root: '/widget')
|
|
78
|
+
# # => "/widget/daccafce3ce0df52e5ce774626779eaa7286085fcbde1e4681c74175ff0bbacd/0/1"
|
|
79
|
+
#
|
|
80
|
+
# hmac_path('/1', root: '/foobar')
|
|
81
|
+
# # => "/foobar/c5fdaf482771d4f9f38cc13a1b2832929026a4ceb05e98ed6a0cd5a00bf180b7/0/1"
|
|
82
|
+
#
|
|
83
|
+
# Note how the HMAC changes even though the path is the same.
|
|
84
|
+
#
|
|
85
|
+
# In addition to the +:root+ option, there are additional options that further constrain
|
|
86
|
+
# use of the generated paths.
|
|
87
|
+
#
|
|
88
|
+
# The +:method+ option creates a path that can only be called with a certain request
|
|
89
|
+
# method:
|
|
90
|
+
#
|
|
91
|
+
# hmac_path('/widget/1', method: :get)
|
|
92
|
+
# # => "/d38c1e634ecf9a3c0ab9d0832555b035d91b35069efcbf2670b0dfefd4b62fdd/m/widget/1"
|
|
93
|
+
#
|
|
94
|
+
# Note how this results in a different HMAC than the original <tt>hmac_path('/widget/1')</tt>
|
|
95
|
+
# call. This sets the flags segment to +m+, which means +r.hmac_path+ will consider the
|
|
96
|
+
# request mehod when checking the HMAC, and will only match if the provided request method
|
|
97
|
+
# is GET. This allows you to provide a user the ability to submit a GET request for the
|
|
98
|
+
# underlying path, without providing them the ability to submit a POST request for the
|
|
99
|
+
# underlying path, with no other access control.
|
|
100
|
+
#
|
|
101
|
+
# The +:params+ option accepts a hash of params, converts it into a query string, and
|
|
102
|
+
# includes the query string in the returned path. It sets the flags segment to +p+, which
|
|
103
|
+
# means +r.hmac_path+ will check for that exact query string. Requests with an empty query
|
|
104
|
+
# string or a different string will not match.
|
|
105
|
+
#
|
|
106
|
+
# hmac_path('/widget/1', params: {foo: 'bar'})
|
|
107
|
+
# # => "/fe8d03f9572d5af6c2866295bd3c12c2ea11d290b1cbd016c3b68ee36a678139/p/widget/1?foo=bar"
|
|
108
|
+
#
|
|
109
|
+
# For GET requests, which cannot have request bodies, that is sufficient to ensure that the
|
|
110
|
+
# submitted params are exactly as specified. However, POST requests can have request bodies,
|
|
111
|
+
# and request body params override query string params in +r.params+. So if you are using
|
|
112
|
+
# this for POST requests (or other HTTP verbs that can have request bodies), use +r.GET+
|
|
113
|
+
# instead of +r.params+ to specifically check query string parameters.
|
|
114
|
+
#
|
|
115
|
+
# The generated paths can be timestamped, so that they are only valid until a given time
|
|
116
|
+
# or for a given number of seconds after they are generated, using the :until or :seconds
|
|
117
|
+
# options:
|
|
118
|
+
#
|
|
119
|
+
# hmac_path('/widget/1', until: Time.utc(2100))
|
|
120
|
+
# # => "/dc8b6e56e4cbe7815df7880d42f0e02956b2e4c49881b6060ceb0e49745a540d/t/4102444800/widget/1"
|
|
121
|
+
#
|
|
122
|
+
# hmac_path('/widget/1', seconds: Time.utc(2100).to_i - Time.now.to_i)
|
|
123
|
+
# # => "/dc8b6e56e4cbe7815df7880d42f0e02956b2e4c49881b6060ceb0e49745a540d/t/4102444800/widget/1"
|
|
124
|
+
#
|
|
125
|
+
# The :namespace option, if provided, should be a string, and it modifies the generated HMACs
|
|
126
|
+
# to only match those in the same namespace. This can be used to provide different paths to
|
|
127
|
+
# different users or groups of users.
|
|
128
|
+
#
|
|
129
|
+
# hmac_path('/widget/1', namespace: '1')
|
|
130
|
+
# # => "/3793ac2a72ea399c40cbd63f154d19f0fe34cdf8d347772134c506a0b756d590/n/widget/1"
|
|
131
|
+
#
|
|
132
|
+
# hmac_path('/widget/1', namespace: '2')
|
|
133
|
+
# # => "/0e1e748860d4fd17fe9b7c8259b1e26996502c38e465f802c2c9a0a13000087c/n/widget/1"
|
|
134
|
+
#
|
|
135
|
+
# The +r.hmac_path+ method accepts a :namespace option, and if a :namespace option is
|
|
136
|
+
# provided, it will only match an hmac path if the namespace given matches the one used
|
|
137
|
+
# when the hmac path was created.
|
|
138
|
+
#
|
|
139
|
+
# r.hmac_path(namespace: '1'){}
|
|
140
|
+
# # will match "/3793ac2a72ea399c40cbd63f154d19f0fe34cdf8d347772134c506a0b756d590/n/widget/1"
|
|
141
|
+
# # will not match "/0e1e748860d4fd17fe9b7c8259b1e26996502c38e465f802c2c9a0a13000087c/n/widget/1"
|
|
142
|
+
#
|
|
143
|
+
# The most common use of the :namespace option is to reference session values, so the value of
|
|
144
|
+
# each path depends on the logged in user. You can use the +:namespace_session_key+ plugin
|
|
145
|
+
# option to set the default namespace for both +hmac_path+ and +r.hmac_path+:
|
|
146
|
+
#
|
|
147
|
+
# plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes',
|
|
148
|
+
# namespace_session_key: 'account_id'
|
|
149
|
+
#
|
|
150
|
+
# This will use <tt>session['account_id']</tt> as the default namespace for both +hmac_path+
|
|
151
|
+
# and +r.hmac_path+ (if the session value is not nil, it is converted to a string using +to_s+).
|
|
152
|
+
# You can override the default namespace by passing a +:namespace+ option when calling +hmac_path+
|
|
153
|
+
# and +r.hmac_path+.
|
|
154
|
+
#
|
|
155
|
+
# You can use +:root+, +:method+, +:params+, and +:namespace+ at the same time:
|
|
156
|
+
#
|
|
157
|
+
# hmac_path('/1', root: '/widget', method: :get, params: {foo: 'bar'}, namespace: '1')
|
|
158
|
+
# # => "/widget/c14c78a81d34d766cf334a3ddbb7a6b231bc2092ef50a77ded0028586027b14e/mpn/1?foo=bar"
|
|
159
|
+
#
|
|
160
|
+
# This gives you a path only valid for a GET request with a root of <tt>/widget</tt> and
|
|
161
|
+
# a query string of <tt>foo=bar</tt>, using namespace +1+.
|
|
162
|
+
#
|
|
163
|
+
# To handle secret rotation, you can provide an +:old_secret+ option when loading the
|
|
164
|
+
# plugin.
|
|
165
|
+
#
|
|
166
|
+
# plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes',
|
|
167
|
+
# old_secret: 'previous-secret-value-with-at-least-32-bytes'
|
|
168
|
+
#
|
|
169
|
+
# This will use +:secret+ for constructing new paths, but will respect paths generated by
|
|
170
|
+
# +:old_secret+.
|
|
171
|
+
#
|
|
172
|
+
# = HMAC Construction
|
|
173
|
+
#
|
|
174
|
+
# This describes the internals for how HMACs are constructed based on the options provided
|
|
175
|
+
# to +hmac_path+. In the examples below:
|
|
176
|
+
#
|
|
177
|
+
# * +HMAC+ is the raw HMAC-SHA256 output (first argument is secret, second is data)
|
|
178
|
+
# * +HMAC_hex+ is the hexidecimal version of +HMAC+
|
|
179
|
+
# * +secret+ is the plugin :secret option
|
|
180
|
+
#
|
|
181
|
+
# The +:secret+ plugin option is never used directly as the HMAC secret. All HMACs are
|
|
182
|
+
# generated with a root-specific secret. The root will be the empty if no +:root+ option
|
|
183
|
+
# is given. The hmac path flags are always included in the hmac calculation, prepended to the
|
|
184
|
+
# path:
|
|
185
|
+
#
|
|
186
|
+
# r.hmac_path('/1')
|
|
187
|
+
# HMAC_hex(HMAC_hex(secret, ''), '/0/1')
|
|
188
|
+
#
|
|
189
|
+
# r.hmac_path('/1', root: '/2')
|
|
190
|
+
# HMAC_hex(HMAC_hex(secret, '/2'), '/0/1')
|
|
191
|
+
#
|
|
192
|
+
# The +:method+ option uses an uppercase version of the method prepended to the path. This
|
|
193
|
+
# cannot conflict with the path itself, since paths must start with a slash.
|
|
194
|
+
#
|
|
195
|
+
# r.hmac_path('/1', method: :get)
|
|
196
|
+
# HMAC_hex(HMAC_hex(secret, ''), 'GET:/m/1')
|
|
197
|
+
#
|
|
198
|
+
# The +:params+ option includes the query string for the params in the HMAC:
|
|
199
|
+
#
|
|
200
|
+
# r.hmac_path('/1', params: {k: 2})
|
|
201
|
+
# HMAC_hex(HMAC_hex(secret, ''), '/p/1?k=2')
|
|
202
|
+
#
|
|
203
|
+
# The +:until+ and +:seconds+ option include the timestamp in the HMAC:
|
|
204
|
+
#
|
|
205
|
+
# r.hmac_path('/1', until: Time.utc(2100))
|
|
206
|
+
# HMAC_hex(HMAC_hex(secret, ''), '/t/4102444800/1')
|
|
207
|
+
#
|
|
208
|
+
# If a +:namespace+ option is provided, the original secret used before the +:root+ option is
|
|
209
|
+
# an HMAC of the +:secret+ plugin option and the given namespace.
|
|
210
|
+
#
|
|
211
|
+
# r.hmac_path('/1', namespace: '2')
|
|
212
|
+
# HMAC_hex(HMAC_hex(HMAC(secret, '2'), ''), '/n/1')
|
|
213
|
+
module HmacPaths
|
|
214
|
+
def self.configure(app, opts=OPTS)
|
|
215
|
+
hmac_secret = opts[:secret]
|
|
216
|
+
unless hmac_secret.is_a?(String) && hmac_secret.bytesize >= 32
|
|
217
|
+
raise RodaError, "hmac_paths plugin :secret option must be a string containing at least 32 bytes"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
if hmac_old_secret = opts[:old_secret]
|
|
221
|
+
unless hmac_old_secret.is_a?(String) && hmac_old_secret.bytesize >= 32
|
|
222
|
+
raise RodaError, "hmac_paths plugin :old_secret option must be a string containing at least 32 bytes if present"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
app.opts[:hmac_paths_secret] = hmac_secret
|
|
227
|
+
app.opts[:hmac_paths_old_secret] = hmac_old_secret
|
|
228
|
+
|
|
229
|
+
if opts[:namespace_session_key]
|
|
230
|
+
app.opts[:hmac_paths_namespace_session_key] = opts[:namespace_session_key]
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
module InstanceMethods
|
|
235
|
+
# Return a path with an HMAC. Designed to be used with r.hmac_path, to make sure
|
|
236
|
+
# users can only request paths that they have been provided by the application
|
|
237
|
+
# (directly or indirectly). This can prevent users of a site from enumerating
|
|
238
|
+
# valid paths. The given path should be a string starting with +/+. Options:
|
|
239
|
+
#
|
|
240
|
+
# :method :: Limits the returned path to only be valid for the given request method.
|
|
241
|
+
# :namespace :: Make the HMAC value depend on the given namespace. If this is not
|
|
242
|
+
# provided, the default namespace is used. To explicitly not use a
|
|
243
|
+
# namespace when there is a default namespace, pass a nil value.
|
|
244
|
+
# :params :: Includes parameters in the query string of the returned path, and
|
|
245
|
+
# limits the returned path to only be valid for that exact query string.
|
|
246
|
+
# :root :: Should be an empty string or string starting with +/+. This will be
|
|
247
|
+
# the already matched path of the routing tree using r.hmac_path. Defaults
|
|
248
|
+
# to the empty string, which will returns paths valid for r.hmac_path at
|
|
249
|
+
# the top level of the routing tree.
|
|
250
|
+
# :seconds :: Make the given path valid for the given integer number of seconds.
|
|
251
|
+
# :until :: Make the given path valid until the given Time.
|
|
252
|
+
def hmac_path(path, opts=OPTS)
|
|
253
|
+
unless path.is_a?(String) && path.getbyte(0) == 47
|
|
254
|
+
raise RodaError, "path must be a string starting with /"
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
root = opts[:root] || ''
|
|
258
|
+
unless root.is_a?(String) && ((root_byte = root.getbyte(0)) == 47 || root_byte == nil)
|
|
259
|
+
raise RodaError, "root must be empty string or string starting with /"
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
if valid_until = opts[:until]
|
|
263
|
+
valid_until = valid_until.to_i
|
|
264
|
+
elsif seconds = opts[:seconds]
|
|
265
|
+
valid_until = Time.now.to_i + seconds
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
flags = String.new
|
|
269
|
+
path = path.dup
|
|
270
|
+
|
|
271
|
+
if method = opts[:method]
|
|
272
|
+
flags << 'm'
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
if params = opts[:params]
|
|
276
|
+
flags << 'p'
|
|
277
|
+
path << '?' << Rack::Utils.build_query(params)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
if hmac_path_namespace(opts)
|
|
281
|
+
flags << 'n'
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
if valid_until
|
|
285
|
+
flags << 't'
|
|
286
|
+
path = "/#{valid_until}#{path}"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
flags << '0' if flags.empty?
|
|
290
|
+
|
|
291
|
+
hmac_path = if method
|
|
292
|
+
"#{method.to_s.upcase}:/#{flags}#{path}"
|
|
293
|
+
else
|
|
294
|
+
"/#{flags}#{path}"
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
"#{root}/#{hmac_path_hmac(root, hmac_path, opts)}/#{flags}#{path}"
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# The HMAC to use in hmac_path, for the given root, path, and options.
|
|
301
|
+
def hmac_path_hmac(root, path, opts=OPTS)
|
|
302
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, hmac_path_hmac_secret(root, opts), path)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# The namespace to use for the hmac path. If a :namespace option is not
|
|
306
|
+
# provided, and a :namespace_session_key option was provided, this will
|
|
307
|
+
# use the value of the related session key, if present.
|
|
308
|
+
def hmac_path_namespace(opts=OPTS)
|
|
309
|
+
opts.fetch(:namespace){hmac_path_default_namespace}
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
private
|
|
313
|
+
|
|
314
|
+
# The secret used to calculate the HMAC in hmac_path. This is itself an HMAC, created
|
|
315
|
+
# using the secret given in the plugin, for the given root and options.
|
|
316
|
+
# This always returns a hexidecimal string.
|
|
317
|
+
def hmac_path_hmac_secret(root, opts=OPTS)
|
|
318
|
+
secret = opts[:secret] || self.opts[:hmac_paths_secret]
|
|
319
|
+
|
|
320
|
+
if namespace = hmac_path_namespace(opts)
|
|
321
|
+
secret = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, secret, namespace)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, secret, root)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# The default namespace to use for hmac_path, if a :namespace option is not provided.
|
|
328
|
+
def hmac_path_default_namespace
|
|
329
|
+
if (key = opts[:hmac_paths_namespace_session_key]) && (value = session[key])
|
|
330
|
+
value.to_s
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
module RequestMethods
|
|
336
|
+
# Looks at the first segment of the remaining path, and if it contains a valid HMAC for the
|
|
337
|
+
# rest of the path considering the flags in the second segment and the given options, the
|
|
338
|
+
# block matches and is yielded to, and the result of the block is returned. Otherwise, the
|
|
339
|
+
# block does not matches and routing continues after the call.
|
|
340
|
+
def hmac_path(opts=OPTS, &block)
|
|
341
|
+
orig_path = remaining_path
|
|
342
|
+
mpath = matched_path
|
|
343
|
+
|
|
344
|
+
on String do |submitted_hmac|
|
|
345
|
+
rpath = remaining_path
|
|
346
|
+
|
|
347
|
+
if submitted_hmac.bytesize == 64
|
|
348
|
+
on String do |flags|
|
|
349
|
+
if flags.bytesize >= 1
|
|
350
|
+
if flags.include?('n') ^ !scope.hmac_path_namespace(opts).nil?
|
|
351
|
+
# Namespace required and not provided, or provided and not required.
|
|
352
|
+
# Bail early to avoid unnecessary HMAC calculation.
|
|
353
|
+
@remaining_path = orig_path
|
|
354
|
+
return
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
if flags.include?('m')
|
|
358
|
+
rpath = "#{env['REQUEST_METHOD'].to_s.upcase}:#{rpath}"
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
if flags.include?('p')
|
|
362
|
+
rpath = "#{rpath}?#{env["QUERY_STRING"]}"
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
if hmac_path_valid?(mpath, rpath, submitted_hmac, opts)
|
|
366
|
+
if flags.include?('t')
|
|
367
|
+
on Integer do |int|
|
|
368
|
+
if int >= Time.now.to_i
|
|
369
|
+
always(&block)
|
|
370
|
+
else
|
|
371
|
+
# Return from method without matching
|
|
372
|
+
@remaining_path = orig_path
|
|
373
|
+
return
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
else
|
|
377
|
+
always(&block)
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Return from method without matching
|
|
383
|
+
@remaining_path = orig_path
|
|
384
|
+
return
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Return from method without matching
|
|
389
|
+
@remaining_path = orig_path
|
|
390
|
+
return
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
private
|
|
395
|
+
|
|
396
|
+
# Determine whether the provided hmac matches.
|
|
397
|
+
def hmac_path_valid?(root, path, hmac, opts=OPTS)
|
|
398
|
+
if Rack::Utils.secure_compare(scope.hmac_path_hmac(root, path, opts), hmac)
|
|
399
|
+
true
|
|
400
|
+
elsif old_secret = roda_class.opts[:hmac_paths_old_secret]
|
|
401
|
+
opts = opts.dup
|
|
402
|
+
opts[:secret] = old_secret
|
|
403
|
+
Rack::Utils.secure_compare(scope.hmac_path_hmac(root, path, opts), hmac)
|
|
404
|
+
else
|
|
405
|
+
false
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
register_plugin(:hmac_paths, HmacPaths)
|
|
412
|
+
end
|
|
413
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
class Roda
|
|
5
|
+
module RodaPlugins
|
|
6
|
+
# The hooks plugin adds before and after hooks to the request cycle.
|
|
7
|
+
#
|
|
8
|
+
# plugin :hooks
|
|
9
|
+
#
|
|
10
|
+
# before do
|
|
11
|
+
# request.redirect('/login') unless logged_in?
|
|
12
|
+
# @time = Time.now
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# after do |res|
|
|
16
|
+
# logger.notice("Took #{Time.now - @time} seconds")
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# Note that in general, before hooks are not needed, since you can just
|
|
20
|
+
# run code at the top of the route block:
|
|
21
|
+
#
|
|
22
|
+
# route do |r|
|
|
23
|
+
# r.redirect('/login') unless logged_in?
|
|
24
|
+
# # ...
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# However, this code makes it easier to write after hooks, as well as
|
|
28
|
+
# handle cases where before hooks are added after the route block.
|
|
29
|
+
#
|
|
30
|
+
# Note that the after hook is called with the rack response array
|
|
31
|
+
# of status, headers, and body. If it wants to change the response,
|
|
32
|
+
# it must mutate this argument, calling <tt>response.status=</tt> inside
|
|
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.
|
|
35
|
+
module Hooks
|
|
36
|
+
def self.configure(app)
|
|
37
|
+
app.opts[:after_hooks] ||= []
|
|
38
|
+
app.opts[:before_hooks] ||= []
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
module ClassMethods
|
|
42
|
+
# Freeze the array of hook methods when freezing the app.
|
|
43
|
+
def freeze
|
|
44
|
+
opts[:after_hooks].freeze
|
|
45
|
+
opts[:before_hooks].freeze
|
|
46
|
+
|
|
47
|
+
super
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Add an after hook.
|
|
51
|
+
def after(&block)
|
|
52
|
+
opts[:after_hooks] << define_roda_method("after_hook", 1, &block)
|
|
53
|
+
if opts[:after_hooks].length == 1
|
|
54
|
+
class_eval("alias _roda_after_80__hooks #{opts[:after_hooks].first}", __FILE__, __LINE__)
|
|
55
|
+
else
|
|
56
|
+
class_eval("def _roda_after_80__hooks(res) #{opts[:after_hooks].map{|m| "#{m}(res)"}.join(';')} end", __FILE__, __LINE__)
|
|
57
|
+
end
|
|
58
|
+
private :_roda_after_80__hooks
|
|
59
|
+
def_roda_after
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Add a before hook.
|
|
64
|
+
def before(&block)
|
|
65
|
+
opts[:before_hooks].unshift(define_roda_method("before_hook", 0, &block))
|
|
66
|
+
if opts[:before_hooks].length == 1
|
|
67
|
+
class_eval("alias _roda_before_10__hooks #{opts[:before_hooks].first}", __FILE__, __LINE__)
|
|
68
|
+
else
|
|
69
|
+
class_eval("def _roda_before_10__hooks; #{opts[:before_hooks].join(';')} end", __FILE__, __LINE__)
|
|
70
|
+
end
|
|
71
|
+
private :_roda_before_10__hooks
|
|
72
|
+
def_roda_before
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
module InstanceMethods
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# Default method if no after hooks are defined.
|
|
81
|
+
def _roda_after_80__hooks(res)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Default method if no before hooks are defined.
|
|
85
|
+
def _roda_before_10__hooks
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
register_plugin(:hooks, Hooks)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
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_authorization!+
|
|
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_authorization!+ 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_authorization!+ 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_authorization!
|
|
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
|