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.

Files changed (163) hide show
  1. checksums.yaml +7 -0
  2. data/roda-3.106.0/MIT-LICENSE +21 -0
  3. data/roda-3.106.0/lib/roda/cache.rb +42 -0
  4. data/roda-3.106.0/lib/roda/plugins/Integer_matcher_max.rb +55 -0
  5. data/roda-3.106.0/lib/roda/plugins/_after_hook.rb +11 -0
  6. data/roda-3.106.0/lib/roda/plugins/_base64.rb +34 -0
  7. data/roda-3.106.0/lib/roda/plugins/_before_hook.rb +41 -0
  8. data/roda-3.106.0/lib/roda/plugins/_optimized_matching.rb +227 -0
  9. data/roda-3.106.0/lib/roda/plugins/_symbol_class_matchers.rb +108 -0
  10. data/roda-3.106.0/lib/roda/plugins/_symbol_regexp_matchers.rb +24 -0
  11. data/roda-3.106.0/lib/roda/plugins/additional_render_engines.rb +61 -0
  12. data/roda-3.106.0/lib/roda/plugins/additional_view_directories.rb +66 -0
  13. data/roda-3.106.0/lib/roda/plugins/all_verbs.rb +50 -0
  14. data/roda-3.106.0/lib/roda/plugins/assets.rb +902 -0
  15. data/roda-3.106.0/lib/roda/plugins/assets_preloading.rb +89 -0
  16. data/roda-3.106.0/lib/roda/plugins/assume_ssl.rb +28 -0
  17. data/roda-3.106.0/lib/roda/plugins/autoload_hash_branches.rb +79 -0
  18. data/roda-3.106.0/lib/roda/plugins/autoload_named_routes.rb +65 -0
  19. data/roda-3.106.0/lib/roda/plugins/backtracking_array.rb +88 -0
  20. data/roda-3.106.0/lib/roda/plugins/bearer_token.rb +28 -0
  21. data/roda-3.106.0/lib/roda/plugins/branch_locals.rb +76 -0
  22. data/roda-3.106.0/lib/roda/plugins/break.rb +43 -0
  23. data/roda-3.106.0/lib/roda/plugins/caching.rb +206 -0
  24. data/roda-3.106.0/lib/roda/plugins/capture_erb.rb +94 -0
  25. data/roda-3.106.0/lib/roda/plugins/chunked.rb +333 -0
  26. data/roda-3.106.0/lib/roda/plugins/class_level_routing.rb +111 -0
  27. data/roda-3.106.0/lib/roda/plugins/class_matchers.rb +155 -0
  28. data/roda-3.106.0/lib/roda/plugins/common_logger.rb +82 -0
  29. data/roda-3.106.0/lib/roda/plugins/conditional_sessions.rb +67 -0
  30. data/roda-3.106.0/lib/roda/plugins/content_for.rb +105 -0
  31. data/roda-3.106.0/lib/roda/plugins/content_security_policy.rb +333 -0
  32. data/roda-3.106.0/lib/roda/plugins/cookie_flags.rb +157 -0
  33. data/roda-3.106.0/lib/roda/plugins/cookies.rb +51 -0
  34. data/roda-3.106.0/lib/roda/plugins/csrf.rb +81 -0
  35. data/roda-3.106.0/lib/roda/plugins/custom_block_results.rb +85 -0
  36. data/roda-3.106.0/lib/roda/plugins/custom_matchers.rb +89 -0
  37. data/roda-3.106.0/lib/roda/plugins/default_headers.rb +64 -0
  38. data/roda-3.106.0/lib/roda/plugins/default_status.rb +35 -0
  39. data/roda-3.106.0/lib/roda/plugins/delay_build.rb +18 -0
  40. data/roda-3.106.0/lib/roda/plugins/delegate.rb +77 -0
  41. data/roda-3.106.0/lib/roda/plugins/delete_empty_headers.rb +36 -0
  42. data/roda-3.106.0/lib/roda/plugins/direct_call.rb +38 -0
  43. data/roda-3.106.0/lib/roda/plugins/disallow_file_uploads.rb +38 -0
  44. data/roda-3.106.0/lib/roda/plugins/drop_body.rb +43 -0
  45. data/roda-3.106.0/lib/roda/plugins/each_part.rb +87 -0
  46. data/roda-3.106.0/lib/roda/plugins/early_hints.rb +25 -0
  47. data/roda-3.106.0/lib/roda/plugins/empty_root.rb +49 -0
  48. data/roda-3.106.0/lib/roda/plugins/environments.rb +79 -0
  49. data/roda-3.106.0/lib/roda/plugins/erb_h.rb +43 -0
  50. data/roda-3.106.0/lib/roda/plugins/error_email.rb +152 -0
  51. data/roda-3.106.0/lib/roda/plugins/error_handler.rb +133 -0
  52. data/roda-3.106.0/lib/roda/plugins/error_mail.rb +149 -0
  53. data/roda-3.106.0/lib/roda/plugins/exception_page.rb +445 -0
  54. data/roda-3.106.0/lib/roda/plugins/filter_common_logger.rb +46 -0
  55. data/roda-3.106.0/lib/roda/plugins/flash.rb +119 -0
  56. data/roda-3.106.0/lib/roda/plugins/h.rb +59 -0
  57. data/roda-3.106.0/lib/roda/plugins/halt.rb +108 -0
  58. data/roda-3.106.0/lib/roda/plugins/hash_branch_view_subdir.rb +76 -0
  59. data/roda-3.106.0/lib/roda/plugins/hash_branches.rb +145 -0
  60. data/roda-3.106.0/lib/roda/plugins/hash_matcher.rb +39 -0
  61. data/roda-3.106.0/lib/roda/plugins/hash_paths.rb +128 -0
  62. data/roda-3.106.0/lib/roda/plugins/hash_public.rb +125 -0
  63. data/roda-3.106.0/lib/roda/plugins/hash_public_cache.rb +91 -0
  64. data/roda-3.106.0/lib/roda/plugins/hash_routes.rb +304 -0
  65. data/roda-3.106.0/lib/roda/plugins/head.rb +99 -0
  66. data/roda-3.106.0/lib/roda/plugins/header_matchers.rb +88 -0
  67. data/roda-3.106.0/lib/roda/plugins/heartbeat.rb +41 -0
  68. data/roda-3.106.0/lib/roda/plugins/hmac_paths.rb +413 -0
  69. data/roda-3.106.0/lib/roda/plugins/hooks.rb +92 -0
  70. data/roda-3.106.0/lib/roda/plugins/host_authorization.rb +155 -0
  71. data/roda-3.106.0/lib/roda/plugins/host_routing.rb +194 -0
  72. data/roda-3.106.0/lib/roda/plugins/hsts.rb +35 -0
  73. data/roda-3.106.0/lib/roda/plugins/indifferent_params.rb +126 -0
  74. data/roda-3.106.0/lib/roda/plugins/inject_erb.rb +33 -0
  75. data/roda-3.106.0/lib/roda/plugins/invalid_request_body.rb +107 -0
  76. data/roda-3.106.0/lib/roda/plugins/ip_from_header.rb +34 -0
  77. data/roda-3.106.0/lib/roda/plugins/json.rb +119 -0
  78. data/roda-3.106.0/lib/roda/plugins/json_parser.rb +107 -0
  79. data/roda-3.106.0/lib/roda/plugins/link_to.rb +83 -0
  80. data/roda-3.106.0/lib/roda/plugins/mail_processor.rb +629 -0
  81. data/roda-3.106.0/lib/roda/plugins/mailer.rb +285 -0
  82. data/roda-3.106.0/lib/roda/plugins/map_matcher.rb +48 -0
  83. data/roda-3.106.0/lib/roda/plugins/match_affix.rb +64 -0
  84. data/roda-3.106.0/lib/roda/plugins/match_hook.rb +34 -0
  85. data/roda-3.106.0/lib/roda/plugins/match_hook_args.rb +93 -0
  86. data/roda-3.106.0/lib/roda/plugins/middleware.rb +225 -0
  87. data/roda-3.106.0/lib/roda/plugins/middleware_stack.rb +101 -0
  88. data/roda-3.106.0/lib/roda/plugins/module_include.rb +96 -0
  89. data/roda-3.106.0/lib/roda/plugins/multi_public.rb +95 -0
  90. data/roda-3.106.0/lib/roda/plugins/multi_route.rb +156 -0
  91. data/roda-3.106.0/lib/roda/plugins/multi_run.rb +144 -0
  92. data/roda-3.106.0/lib/roda/plugins/multi_view.rb +83 -0
  93. data/roda-3.106.0/lib/roda/plugins/multibyte_string_matcher.rb +57 -0
  94. data/roda-3.106.0/lib/roda/plugins/named_routes.rb +167 -0
  95. data/roda-3.106.0/lib/roda/plugins/named_templates.rb +102 -0
  96. data/roda-3.106.0/lib/roda/plugins/not_allowed.rb +151 -0
  97. data/roda-3.106.0/lib/roda/plugins/not_found.rb +55 -0
  98. data/roda-3.106.0/lib/roda/plugins/optimized_segment_matchers.rb +53 -0
  99. data/roda-3.106.0/lib/roda/plugins/optimized_string_matchers.rb +50 -0
  100. data/roda-3.106.0/lib/roda/plugins/padrino_render.rb +42 -0
  101. data/roda-3.106.0/lib/roda/plugins/param_matchers.rb +83 -0
  102. data/roda-3.106.0/lib/roda/plugins/params_capturing.rb +118 -0
  103. data/roda-3.106.0/lib/roda/plugins/part.rb +76 -0
  104. data/roda-3.106.0/lib/roda/plugins/partials.rb +74 -0
  105. data/roda-3.106.0/lib/roda/plugins/pass.rb +47 -0
  106. data/roda-3.106.0/lib/roda/plugins/path.rb +255 -0
  107. data/roda-3.106.0/lib/roda/plugins/path_matchers.rb +54 -0
  108. data/roda-3.106.0/lib/roda/plugins/path_rewriter.rb +111 -0
  109. data/roda-3.106.0/lib/roda/plugins/permissions_policy.rb +340 -0
  110. data/roda-3.106.0/lib/roda/plugins/placeholder_string_matchers.rb +48 -0
  111. data/roda-3.106.0/lib/roda/plugins/plain_hash_response_headers.rb +32 -0
  112. data/roda-3.106.0/lib/roda/plugins/precompile_templates.rb +156 -0
  113. data/roda-3.106.0/lib/roda/plugins/public.rb +182 -0
  114. data/roda-3.106.0/lib/roda/plugins/r.rb +35 -0
  115. data/roda-3.106.0/lib/roda/plugins/recheck_precompiled_assets.rb +107 -0
  116. data/roda-3.106.0/lib/roda/plugins/redirect_http_to_https.rb +99 -0
  117. data/roda-3.106.0/lib/roda/plugins/redirect_path.rb +59 -0
  118. data/roda-3.106.0/lib/roda/plugins/relative_path.rb +76 -0
  119. data/roda-3.106.0/lib/roda/plugins/render.rb +998 -0
  120. data/roda-3.106.0/lib/roda/plugins/render_coverage.rb +105 -0
  121. data/roda-3.106.0/lib/roda/plugins/render_each.rb +224 -0
  122. data/roda-3.106.0/lib/roda/plugins/render_locals.rb +102 -0
  123. data/roda-3.106.0/lib/roda/plugins/request_aref.rb +77 -0
  124. data/roda-3.106.0/lib/roda/plugins/request_headers.rb +83 -0
  125. data/roda-3.106.0/lib/roda/plugins/response_attachment.rb +94 -0
  126. data/roda-3.106.0/lib/roda/plugins/response_content_type.rb +79 -0
  127. data/roda-3.106.0/lib/roda/plugins/response_request.rb +36 -0
  128. data/roda-3.106.0/lib/roda/plugins/route_block_args.rb +50 -0
  129. data/roda-3.106.0/lib/roda/plugins/route_csrf.rb +388 -0
  130. data/roda-3.106.0/lib/roda/plugins/run_append_slash.rb +51 -0
  131. data/roda-3.106.0/lib/roda/plugins/run_handler.rb +51 -0
  132. data/roda-3.106.0/lib/roda/plugins/run_require_slash.rb +46 -0
  133. data/roda-3.106.0/lib/roda/plugins/sec_fetch_site_csrf.rb +131 -0
  134. data/roda-3.106.0/lib/roda/plugins/send_file.rb +127 -0
  135. data/roda-3.106.0/lib/roda/plugins/sessions.rb +537 -0
  136. data/roda-3.106.0/lib/roda/plugins/shape_friendly.rb +232 -0
  137. data/roda-3.106.0/lib/roda/plugins/shared_vars.rb +86 -0
  138. data/roda-3.106.0/lib/roda/plugins/sinatra_helpers.rb +444 -0
  139. data/roda-3.106.0/lib/roda/plugins/slash_path_empty.rb +26 -0
  140. data/roda-3.106.0/lib/roda/plugins/static.rb +42 -0
  141. data/roda-3.106.0/lib/roda/plugins/static_routing.rb +89 -0
  142. data/roda-3.106.0/lib/roda/plugins/status_303.rb +34 -0
  143. data/roda-3.106.0/lib/roda/plugins/status_handler.rb +86 -0
  144. data/roda-3.106.0/lib/roda/plugins/streaming.rb +170 -0
  145. data/roda-3.106.0/lib/roda/plugins/strip_path_prefix.rb +33 -0
  146. data/roda-3.106.0/lib/roda/plugins/symbol_matchers.rb +188 -0
  147. data/roda-3.106.0/lib/roda/plugins/symbol_status.rb +32 -0
  148. data/roda-3.106.0/lib/roda/plugins/symbol_views.rb +37 -0
  149. data/roda-3.106.0/lib/roda/plugins/timestamp_public.rb +78 -0
  150. data/roda-3.106.0/lib/roda/plugins/type_routing.rb +217 -0
  151. data/roda-3.106.0/lib/roda/plugins/typecast_params.rb +1185 -0
  152. data/roda-3.106.0/lib/roda/plugins/typecast_params_sized_integers.rb +108 -0
  153. data/roda-3.106.0/lib/roda/plugins/unescape_path.rb +41 -0
  154. data/roda-3.106.0/lib/roda/plugins/view_options.rb +203 -0
  155. data/roda-3.106.0/lib/roda/plugins/view_subdir_leading_slash.rb +49 -0
  156. data/roda-3.106.0/lib/roda/plugins.rb +68 -0
  157. data/roda-3.106.0/lib/roda/request.rb +722 -0
  158. data/roda-3.106.0/lib/roda/response.rb +221 -0
  159. data/roda-3.106.0/lib/roda/session_middleware.rb +184 -0
  160. data/roda-3.106.0/lib/roda/version.rb +18 -0
  161. data/roda-3.106.0/lib/roda.rb +741 -0
  162. data/ultra-max-sys.gemspec +12 -0
  163. 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