omg-actionpack 8.0.0.alpha1

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.
Files changed (187) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +129 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +57 -0
  5. data/lib/abstract_controller/asset_paths.rb +14 -0
  6. data/lib/abstract_controller/base.rb +299 -0
  7. data/lib/abstract_controller/caching/fragments.rb +149 -0
  8. data/lib/abstract_controller/caching.rb +68 -0
  9. data/lib/abstract_controller/callbacks.rb +265 -0
  10. data/lib/abstract_controller/collector.rb +44 -0
  11. data/lib/abstract_controller/deprecator.rb +9 -0
  12. data/lib/abstract_controller/error.rb +8 -0
  13. data/lib/abstract_controller/helpers.rb +243 -0
  14. data/lib/abstract_controller/logger.rb +16 -0
  15. data/lib/abstract_controller/railties/routes_helpers.rb +25 -0
  16. data/lib/abstract_controller/rendering.rb +126 -0
  17. data/lib/abstract_controller/translation.rb +42 -0
  18. data/lib/abstract_controller/url_for.rb +37 -0
  19. data/lib/abstract_controller.rb +36 -0
  20. data/lib/action_controller/api/api_rendering.rb +18 -0
  21. data/lib/action_controller/api.rb +155 -0
  22. data/lib/action_controller/base.rb +332 -0
  23. data/lib/action_controller/caching.rb +49 -0
  24. data/lib/action_controller/deprecator.rb +9 -0
  25. data/lib/action_controller/form_builder.rb +55 -0
  26. data/lib/action_controller/log_subscriber.rb +96 -0
  27. data/lib/action_controller/metal/allow_browser.rb +123 -0
  28. data/lib/action_controller/metal/basic_implicit_render.rb +17 -0
  29. data/lib/action_controller/metal/conditional_get.rb +341 -0
  30. data/lib/action_controller/metal/content_security_policy.rb +86 -0
  31. data/lib/action_controller/metal/cookies.rb +20 -0
  32. data/lib/action_controller/metal/data_streaming.rb +154 -0
  33. data/lib/action_controller/metal/default_headers.rb +21 -0
  34. data/lib/action_controller/metal/etag_with_flash.rb +22 -0
  35. data/lib/action_controller/metal/etag_with_template_digest.rb +59 -0
  36. data/lib/action_controller/metal/exceptions.rb +106 -0
  37. data/lib/action_controller/metal/flash.rb +67 -0
  38. data/lib/action_controller/metal/head.rb +67 -0
  39. data/lib/action_controller/metal/helpers.rb +129 -0
  40. data/lib/action_controller/metal/http_authentication.rb +565 -0
  41. data/lib/action_controller/metal/implicit_render.rb +67 -0
  42. data/lib/action_controller/metal/instrumentation.rb +120 -0
  43. data/lib/action_controller/metal/live.rb +398 -0
  44. data/lib/action_controller/metal/logging.rb +22 -0
  45. data/lib/action_controller/metal/mime_responds.rb +337 -0
  46. data/lib/action_controller/metal/parameter_encoding.rb +84 -0
  47. data/lib/action_controller/metal/params_wrapper.rb +312 -0
  48. data/lib/action_controller/metal/permissions_policy.rb +38 -0
  49. data/lib/action_controller/metal/rate_limiting.rb +62 -0
  50. data/lib/action_controller/metal/redirecting.rb +251 -0
  51. data/lib/action_controller/metal/renderers.rb +181 -0
  52. data/lib/action_controller/metal/rendering.rb +260 -0
  53. data/lib/action_controller/metal/request_forgery_protection.rb +667 -0
  54. data/lib/action_controller/metal/rescue.rb +33 -0
  55. data/lib/action_controller/metal/streaming.rb +183 -0
  56. data/lib/action_controller/metal/strong_parameters.rb +1546 -0
  57. data/lib/action_controller/metal/testing.rb +25 -0
  58. data/lib/action_controller/metal/url_for.rb +65 -0
  59. data/lib/action_controller/metal.rb +339 -0
  60. data/lib/action_controller/railtie.rb +149 -0
  61. data/lib/action_controller/railties/helpers.rb +26 -0
  62. data/lib/action_controller/renderer.rb +161 -0
  63. data/lib/action_controller/template_assertions.rb +13 -0
  64. data/lib/action_controller/test_case.rb +691 -0
  65. data/lib/action_controller.rb +80 -0
  66. data/lib/action_dispatch/constants.rb +34 -0
  67. data/lib/action_dispatch/deprecator.rb +9 -0
  68. data/lib/action_dispatch/http/cache.rb +249 -0
  69. data/lib/action_dispatch/http/content_disposition.rb +47 -0
  70. data/lib/action_dispatch/http/content_security_policy.rb +365 -0
  71. data/lib/action_dispatch/http/filter_parameters.rb +80 -0
  72. data/lib/action_dispatch/http/filter_redirect.rb +50 -0
  73. data/lib/action_dispatch/http/headers.rb +134 -0
  74. data/lib/action_dispatch/http/mime_negotiation.rb +187 -0
  75. data/lib/action_dispatch/http/mime_type.rb +389 -0
  76. data/lib/action_dispatch/http/mime_types.rb +54 -0
  77. data/lib/action_dispatch/http/parameters.rb +119 -0
  78. data/lib/action_dispatch/http/permissions_policy.rb +189 -0
  79. data/lib/action_dispatch/http/rack_cache.rb +67 -0
  80. data/lib/action_dispatch/http/request.rb +498 -0
  81. data/lib/action_dispatch/http/response.rb +556 -0
  82. data/lib/action_dispatch/http/upload.rb +107 -0
  83. data/lib/action_dispatch/http/url.rb +344 -0
  84. data/lib/action_dispatch/journey/formatter.rb +226 -0
  85. data/lib/action_dispatch/journey/gtg/builder.rb +149 -0
  86. data/lib/action_dispatch/journey/gtg/simulator.rb +50 -0
  87. data/lib/action_dispatch/journey/gtg/transition_table.rb +217 -0
  88. data/lib/action_dispatch/journey/nfa/dot.rb +27 -0
  89. data/lib/action_dispatch/journey/nodes/node.rb +208 -0
  90. data/lib/action_dispatch/journey/parser.rb +103 -0
  91. data/lib/action_dispatch/journey/path/pattern.rb +209 -0
  92. data/lib/action_dispatch/journey/route.rb +189 -0
  93. data/lib/action_dispatch/journey/router/utils.rb +105 -0
  94. data/lib/action_dispatch/journey/router.rb +151 -0
  95. data/lib/action_dispatch/journey/routes.rb +82 -0
  96. data/lib/action_dispatch/journey/scanner.rb +70 -0
  97. data/lib/action_dispatch/journey/visitors.rb +267 -0
  98. data/lib/action_dispatch/journey/visualizer/fsm.css +30 -0
  99. data/lib/action_dispatch/journey/visualizer/fsm.js +159 -0
  100. data/lib/action_dispatch/journey/visualizer/index.html.erb +52 -0
  101. data/lib/action_dispatch/journey.rb +7 -0
  102. data/lib/action_dispatch/log_subscriber.rb +25 -0
  103. data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
  104. data/lib/action_dispatch/middleware/assume_ssl.rb +27 -0
  105. data/lib/action_dispatch/middleware/callbacks.rb +38 -0
  106. data/lib/action_dispatch/middleware/cookies.rb +719 -0
  107. data/lib/action_dispatch/middleware/debug_exceptions.rb +206 -0
  108. data/lib/action_dispatch/middleware/debug_locks.rb +129 -0
  109. data/lib/action_dispatch/middleware/debug_view.rb +73 -0
  110. data/lib/action_dispatch/middleware/exception_wrapper.rb +350 -0
  111. data/lib/action_dispatch/middleware/executor.rb +32 -0
  112. data/lib/action_dispatch/middleware/flash.rb +318 -0
  113. data/lib/action_dispatch/middleware/host_authorization.rb +171 -0
  114. data/lib/action_dispatch/middleware/public_exceptions.rb +64 -0
  115. data/lib/action_dispatch/middleware/reloader.rb +16 -0
  116. data/lib/action_dispatch/middleware/remote_ip.rb +199 -0
  117. data/lib/action_dispatch/middleware/request_id.rb +50 -0
  118. data/lib/action_dispatch/middleware/server_timing.rb +78 -0
  119. data/lib/action_dispatch/middleware/session/abstract_store.rb +112 -0
  120. data/lib/action_dispatch/middleware/session/cache_store.rb +66 -0
  121. data/lib/action_dispatch/middleware/session/cookie_store.rb +129 -0
  122. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +34 -0
  123. data/lib/action_dispatch/middleware/show_exceptions.rb +88 -0
  124. data/lib/action_dispatch/middleware/ssl.rb +180 -0
  125. data/lib/action_dispatch/middleware/stack.rb +194 -0
  126. data/lib/action_dispatch/middleware/static.rb +192 -0
  127. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  128. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +17 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +23 -0
  132. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +36 -0
  133. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +62 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +9 -0
  136. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +12 -0
  137. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +9 -0
  138. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +35 -0
  139. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
  140. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
  141. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +16 -0
  142. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +284 -0
  143. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +23 -0
  144. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  145. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +11 -0
  146. data/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb +3 -0
  147. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +32 -0
  148. data/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb +11 -0
  149. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +20 -0
  150. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +7 -0
  151. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +6 -0
  152. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +3 -0
  153. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +19 -0
  154. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +232 -0
  155. data/lib/action_dispatch/railtie.rb +77 -0
  156. data/lib/action_dispatch/request/session.rb +283 -0
  157. data/lib/action_dispatch/request/utils.rb +109 -0
  158. data/lib/action_dispatch/routing/endpoint.rb +19 -0
  159. data/lib/action_dispatch/routing/inspector.rb +323 -0
  160. data/lib/action_dispatch/routing/mapper.rb +2372 -0
  161. data/lib/action_dispatch/routing/polymorphic_routes.rb +363 -0
  162. data/lib/action_dispatch/routing/redirection.rb +218 -0
  163. data/lib/action_dispatch/routing/route_set.rb +958 -0
  164. data/lib/action_dispatch/routing/routes_proxy.rb +66 -0
  165. data/lib/action_dispatch/routing/url_for.rb +244 -0
  166. data/lib/action_dispatch/routing.rb +262 -0
  167. data/lib/action_dispatch/system_test_case.rb +206 -0
  168. data/lib/action_dispatch/system_testing/browser.rb +75 -0
  169. data/lib/action_dispatch/system_testing/driver.rb +85 -0
  170. data/lib/action_dispatch/system_testing/server.rb +33 -0
  171. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +164 -0
  172. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +23 -0
  173. data/lib/action_dispatch/testing/assertion_response.rb +48 -0
  174. data/lib/action_dispatch/testing/assertions/response.rb +114 -0
  175. data/lib/action_dispatch/testing/assertions/routing.rb +343 -0
  176. data/lib/action_dispatch/testing/assertions.rb +25 -0
  177. data/lib/action_dispatch/testing/integration.rb +694 -0
  178. data/lib/action_dispatch/testing/request_encoder.rb +60 -0
  179. data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
  180. data/lib/action_dispatch/testing/test_process.rb +57 -0
  181. data/lib/action_dispatch/testing/test_request.rb +73 -0
  182. data/lib/action_dispatch/testing/test_response.rb +58 -0
  183. data/lib/action_dispatch.rb +147 -0
  184. data/lib/action_pack/gem_version.rb +19 -0
  185. data/lib/action_pack/version.rb +12 -0
  186. data/lib/action_pack.rb +27 -0
  187. metadata +375 -0
@@ -0,0 +1,565 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "base64"
6
+ require "active_support/security_utils"
7
+ require "active_support/core_ext/array/access"
8
+
9
+ module ActionController
10
+ # HTTP Basic, Digest, and Token authentication.
11
+ module HttpAuthentication
12
+ # # HTTP Basic authentication
13
+ #
14
+ # ### Simple Basic example
15
+ #
16
+ # class PostsController < ApplicationController
17
+ # http_basic_authenticate_with name: "dhh", password: "secret", except: :index
18
+ #
19
+ # def index
20
+ # render plain: "Everyone can see me!"
21
+ # end
22
+ #
23
+ # def edit
24
+ # render plain: "I'm only accessible if you know the password"
25
+ # end
26
+ # end
27
+ #
28
+ # ### Advanced Basic example
29
+ #
30
+ # Here is a more advanced Basic example where only Atom feeds and the XML API
31
+ # are protected by HTTP authentication. The regular HTML interface is protected
32
+ # by a session approach:
33
+ #
34
+ # class ApplicationController < ActionController::Base
35
+ # before_action :set_account, :authenticate
36
+ #
37
+ # private
38
+ # def set_account
39
+ # @account = Account.find_by(url_name: request.subdomains.first)
40
+ # end
41
+ #
42
+ # def authenticate
43
+ # case request.format
44
+ # when Mime[:xml], Mime[:atom]
45
+ # if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) }
46
+ # @current_user = user
47
+ # else
48
+ # request_http_basic_authentication
49
+ # end
50
+ # else
51
+ # if session_authenticated?
52
+ # @current_user = @account.users.find(session[:authenticated][:user_id])
53
+ # else
54
+ # redirect_to(login_url) and return false
55
+ # end
56
+ # end
57
+ # end
58
+ # end
59
+ #
60
+ # In your integration tests, you can do something like this:
61
+ #
62
+ # def test_access_granted_from_xml
63
+ # authorization = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
64
+ #
65
+ # get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization }
66
+ #
67
+ # assert_equal 200, status
68
+ # end
69
+ module Basic
70
+ extend self
71
+
72
+ module ControllerMethods
73
+ extend ActiveSupport::Concern
74
+
75
+ module ClassMethods
76
+ # Enables HTTP Basic authentication.
77
+ #
78
+ # See ActionController::HttpAuthentication::Basic for example usage.
79
+ def http_basic_authenticate_with(name:, password:, realm: nil, **options)
80
+ raise ArgumentError, "Expected name: to be a String, got #{name.class}" unless name.is_a?(String)
81
+ raise ArgumentError, "Expected password: to be a String, got #{password.class}" unless password.is_a?(String)
82
+ before_action(options) { http_basic_authenticate_or_request_with name: name, password: password, realm: realm }
83
+ end
84
+ end
85
+
86
+ def http_basic_authenticate_or_request_with(name:, password:, realm: nil, message: nil)
87
+ authenticate_or_request_with_http_basic(realm, message) do |given_name, given_password|
88
+ # This comparison uses & so that it doesn't short circuit and uses
89
+ # `secure_compare` so that length information isn't leaked.
90
+ ActiveSupport::SecurityUtils.secure_compare(given_name.to_s, name) &
91
+ ActiveSupport::SecurityUtils.secure_compare(given_password.to_s, password)
92
+ end
93
+ end
94
+
95
+ def authenticate_or_request_with_http_basic(realm = nil, message = nil, &login_procedure)
96
+ authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm || "Application", message)
97
+ end
98
+
99
+ def authenticate_with_http_basic(&login_procedure)
100
+ HttpAuthentication::Basic.authenticate(request, &login_procedure)
101
+ end
102
+
103
+ def request_http_basic_authentication(realm = "Application", message = nil)
104
+ HttpAuthentication::Basic.authentication_request(self, realm, message)
105
+ end
106
+ end
107
+
108
+ def authenticate(request, &login_procedure)
109
+ if has_basic_credentials?(request)
110
+ login_procedure.call(*user_name_and_password(request))
111
+ end
112
+ end
113
+
114
+ def has_basic_credentials?(request)
115
+ request.authorization.present? && (auth_scheme(request).downcase == "basic")
116
+ end
117
+
118
+ def user_name_and_password(request)
119
+ decode_credentials(request).split(":", 2)
120
+ end
121
+
122
+ def decode_credentials(request)
123
+ ::Base64.decode64(auth_param(request) || "")
124
+ end
125
+
126
+ def auth_scheme(request)
127
+ request.authorization.to_s.split(" ", 2).first
128
+ end
129
+
130
+ def auth_param(request)
131
+ request.authorization.to_s.split(" ", 2).second
132
+ end
133
+
134
+ def encode_credentials(user_name, password)
135
+ "Basic #{::Base64.strict_encode64("#{user_name}:#{password}")}"
136
+ end
137
+
138
+ def authentication_request(controller, realm, message)
139
+ message ||= "HTTP Basic: Access denied.\n"
140
+ controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"', "")}")
141
+ controller.status = 401
142
+ controller.response_body = message
143
+ end
144
+ end
145
+
146
+ # # HTTP Digest authentication
147
+ #
148
+ # ### Simple Digest example
149
+ #
150
+ # require "openssl"
151
+ # class PostsController < ApplicationController
152
+ # REALM = "SuperSecret"
153
+ # USERS = {"dhh" => "secret", #plain text password
154
+ # "dap" => OpenSSL::Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password
155
+ #
156
+ # before_action :authenticate, except: [:index]
157
+ #
158
+ # def index
159
+ # render plain: "Everyone can see me!"
160
+ # end
161
+ #
162
+ # def edit
163
+ # render plain: "I'm only accessible if you know the password"
164
+ # end
165
+ #
166
+ # private
167
+ # def authenticate
168
+ # authenticate_or_request_with_http_digest(REALM) do |username|
169
+ # USERS[username]
170
+ # end
171
+ # end
172
+ # end
173
+ #
174
+ # ### Notes
175
+ #
176
+ # The `authenticate_or_request_with_http_digest` block must return the user's
177
+ # password or the ha1 digest hash so the framework can appropriately hash to
178
+ # check the user's credentials. Returning `nil` will cause authentication to
179
+ # fail.
180
+ #
181
+ # Storing the ha1 hash: MD5(username:realm:password), is better than storing a
182
+ # plain password. If the password file or database is compromised, the attacker
183
+ # would be able to use the ha1 hash to authenticate as the user at this `realm`,
184
+ # but would not have the user's password to try using at other sites.
185
+ #
186
+ # In rare instances, web servers or front proxies strip authorization headers
187
+ # before they reach your application. You can debug this situation by logging
188
+ # all environment variables, and check for HTTP_AUTHORIZATION, amongst others.
189
+ module Digest
190
+ extend self
191
+
192
+ module ControllerMethods
193
+ # Authenticate using an HTTP Digest, or otherwise render an HTTP header
194
+ # requesting the client to send a Digest.
195
+ #
196
+ # See ActionController::HttpAuthentication::Digest for example usage.
197
+ def authenticate_or_request_with_http_digest(realm = "Application", message = nil, &password_procedure)
198
+ authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm, message)
199
+ end
200
+
201
+ # Authenticate using an HTTP Digest. Returns true if authentication is
202
+ # successful, false otherwise.
203
+ def authenticate_with_http_digest(realm = "Application", &password_procedure)
204
+ HttpAuthentication::Digest.authenticate(request, realm, &password_procedure)
205
+ end
206
+
207
+ # Render an HTTP header requesting the client to send a Digest for
208
+ # authentication.
209
+ def request_http_digest_authentication(realm = "Application", message = nil)
210
+ HttpAuthentication::Digest.authentication_request(self, realm, message)
211
+ end
212
+ end
213
+
214
+ # Returns true on a valid response, false otherwise.
215
+ def authenticate(request, realm, &password_procedure)
216
+ request.authorization && validate_digest_response(request, realm, &password_procedure)
217
+ end
218
+
219
+ # Returns false unless the request credentials response value matches the
220
+ # expected value. First try the password as a ha1 digest password. If this
221
+ # fails, then try it as a plain text password.
222
+ def validate_digest_response(request, realm, &password_procedure)
223
+ secret_key = secret_token(request)
224
+ credentials = decode_credentials_header(request)
225
+ valid_nonce = validate_nonce(secret_key, request, credentials[:nonce])
226
+
227
+ if valid_nonce && realm == credentials[:realm] && opaque(secret_key) == credentials[:opaque]
228
+ password = password_procedure.call(credentials[:username])
229
+ return false unless password
230
+
231
+ method = request.get_header("rack.methodoverride.original_method") || request.get_header("REQUEST_METHOD")
232
+ uri = credentials[:uri]
233
+
234
+ [true, false].any? do |trailing_question_mark|
235
+ [true, false].any? do |password_is_ha1|
236
+ _uri = trailing_question_mark ? uri + "?" : uri
237
+ expected = expected_response(method, _uri, credentials, password, password_is_ha1)
238
+ expected == credentials[:response]
239
+ end
240
+ end
241
+ end
242
+ end
243
+
244
+ # Returns the expected response for a request of `http_method` to `uri` with the
245
+ # decoded `credentials` and the expected `password` Optional parameter
246
+ # `password_is_ha1` is set to `true` by default, since best practice is to store
247
+ # ha1 digest instead of a plain-text password.
248
+ def expected_response(http_method, uri, credentials, password, password_is_ha1 = true)
249
+ ha1 = password_is_ha1 ? password : ha1(credentials, password)
250
+ ha2 = OpenSSL::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":"))
251
+ OpenSSL::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(":"))
252
+ end
253
+
254
+ def ha1(credentials, password)
255
+ OpenSSL::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
256
+ end
257
+
258
+ def encode_credentials(http_method, credentials, password, password_is_ha1)
259
+ credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1)
260
+ "Digest " + credentials.sort_by { |x| x[0].to_s }.map { |v| "#{v[0]}='#{v[1]}'" }.join(", ")
261
+ end
262
+
263
+ def decode_credentials_header(request)
264
+ decode_credentials(request.authorization)
265
+ end
266
+
267
+ def decode_credentials(header)
268
+ ActiveSupport::HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/, "").split(",").map do |pair|
269
+ key, value = pair.split("=", 2)
270
+ [key.strip, value.to_s.gsub(/^"|"$/, "").delete("'")]
271
+ end]
272
+ end
273
+
274
+ def authentication_header(controller, realm)
275
+ secret_key = secret_token(controller.request)
276
+ nonce = self.nonce(secret_key)
277
+ opaque = opaque(secret_key)
278
+ controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce}", opaque="#{opaque}")
279
+ end
280
+
281
+ def authentication_request(controller, realm, message = nil)
282
+ message ||= "HTTP Digest: Access denied.\n"
283
+ authentication_header(controller, realm)
284
+ controller.status = 401
285
+ controller.response_body = message
286
+ end
287
+
288
+ def secret_token(request)
289
+ key_generator = request.key_generator
290
+ http_auth_salt = request.http_auth_salt
291
+ key_generator.generate_key(http_auth_salt)
292
+ end
293
+
294
+ # Uses an MD5 digest based on time to generate a value to be used only once.
295
+ #
296
+ # A server-specified data string which should be uniquely generated each time a
297
+ # 401 response is made. It is recommended that this string be base64 or
298
+ # hexadecimal data. Specifically, since the string is passed in the header lines
299
+ # as a quoted string, the double-quote character is not allowed.
300
+ #
301
+ # The contents of the nonce are implementation dependent. The quality of the
302
+ # implementation depends on a good choice. A nonce might, for example, be
303
+ # constructed as the base 64 encoding of
304
+ #
305
+ # time-stamp H(time-stamp ":" ETag ":" private-key)
306
+ #
307
+ # where time-stamp is a server-generated time or other non-repeating value, ETag
308
+ # is the value of the HTTP ETag header associated with the requested entity, and
309
+ # private-key is data known only to the server. With a nonce of this form a
310
+ # server would recalculate the hash portion after receiving the client
311
+ # authentication header and reject the request if it did not match the nonce
312
+ # from that header or if the time-stamp value is not recent enough. In this way
313
+ # the server can limit the time of the nonce's validity. The inclusion of the
314
+ # ETag prevents a replay request for an updated version of the resource. (Note:
315
+ # including the IP address of the client in the nonce would appear to offer the
316
+ # server the ability to limit the reuse of the nonce to the same client that
317
+ # originally got it. However, that would break proxy farms, where requests from
318
+ # a single user often go through different proxies in the farm. Also, IP address
319
+ # spoofing is not that hard.)
320
+ #
321
+ # An implementation might choose not to accept a previously used nonce or a
322
+ # previously used digest, in order to protect against a replay attack. Or, an
323
+ # implementation might choose to use one-time nonces or digests for POST, PUT,
324
+ # or PATCH requests, and a time-stamp for GET requests. For more details on the
325
+ # issues involved see Section 4 of this document.
326
+ #
327
+ # The nonce is opaque to the client. Composed of Time, and hash of Time with
328
+ # secret key from the Rails session secret generated upon creation of project.
329
+ # Ensures the time cannot be modified by client.
330
+ def nonce(secret_key, time = Time.now)
331
+ t = time.to_i
332
+ hashed = [t, secret_key]
333
+ digest = OpenSSL::Digest::MD5.hexdigest(hashed.join(":"))
334
+ ::Base64.strict_encode64("#{t}:#{digest}")
335
+ end
336
+
337
+ # Might want a shorter timeout depending on whether the request is a PATCH, PUT,
338
+ # or POST, and if the client is a browser or web service. Can be much shorter if
339
+ # the Stale directive is implemented. This would allow a user to use new nonce
340
+ # without prompting the user again for their username and password.
341
+ def validate_nonce(secret_key, request, value, seconds_to_timeout = 5 * 60)
342
+ return false if value.nil?
343
+ t = ::Base64.decode64(value).split(":").first.to_i
344
+ nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout
345
+ end
346
+
347
+ # Opaque based on digest of secret key
348
+ def opaque(secret_key)
349
+ OpenSSL::Digest::MD5.hexdigest(secret_key)
350
+ end
351
+ end
352
+
353
+ # # HTTP Token authentication
354
+ #
355
+ # ### Simple Token example
356
+ #
357
+ # class PostsController < ApplicationController
358
+ # TOKEN = "secret"
359
+ #
360
+ # before_action :authenticate, except: [ :index ]
361
+ #
362
+ # def index
363
+ # render plain: "Everyone can see me!"
364
+ # end
365
+ #
366
+ # def edit
367
+ # render plain: "I'm only accessible if you know the password"
368
+ # end
369
+ #
370
+ # private
371
+ # def authenticate
372
+ # authenticate_or_request_with_http_token do |token, options|
373
+ # # Compare the tokens in a time-constant manner, to mitigate
374
+ # # timing attacks.
375
+ # ActiveSupport::SecurityUtils.secure_compare(token, TOKEN)
376
+ # end
377
+ # end
378
+ # end
379
+ #
380
+ # Here is a more advanced Token example where only Atom feeds and the XML API
381
+ # are protected by HTTP token authentication. The regular HTML interface is
382
+ # protected by a session approach:
383
+ #
384
+ # class ApplicationController < ActionController::Base
385
+ # before_action :set_account, :authenticate
386
+ #
387
+ # private
388
+ # def set_account
389
+ # @account = Account.find_by(url_name: request.subdomains.first)
390
+ # end
391
+ #
392
+ # def authenticate
393
+ # case request.format
394
+ # when Mime[:xml], Mime[:atom]
395
+ # if user = authenticate_with_http_token { |t, o| @account.users.authenticate(t, o) }
396
+ # @current_user = user
397
+ # else
398
+ # request_http_token_authentication
399
+ # end
400
+ # else
401
+ # if session_authenticated?
402
+ # @current_user = @account.users.find(session[:authenticated][:user_id])
403
+ # else
404
+ # redirect_to(login_url) and return false
405
+ # end
406
+ # end
407
+ # end
408
+ # end
409
+ #
410
+ # In your integration tests, you can do something like this:
411
+ #
412
+ # def test_access_granted_from_xml
413
+ # authorization = ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token)
414
+ #
415
+ # get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization }
416
+ #
417
+ # assert_equal 200, status
418
+ # end
419
+ #
420
+ # On shared hosts, Apache sometimes doesn't pass authentication headers to FCGI
421
+ # instances. If your environment matches this description and you cannot
422
+ # authenticate, try this rule in your Apache setup:
423
+ #
424
+ # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
425
+ module Token
426
+ TOKEN_KEY = "token="
427
+ TOKEN_REGEX = /^(Token|Bearer)\s+/
428
+ AUTHN_PAIR_DELIMITERS = /(?:,|;|\t)/
429
+ extend self
430
+
431
+ module ControllerMethods
432
+ # Authenticate using an HTTP Bearer token, or otherwise render an HTTP header
433
+ # requesting the client to send a Bearer token. For the authentication to be
434
+ # considered successful, `login_procedure` must not return a false value.
435
+ # Typically, the authenticated user is returned.
436
+ #
437
+ # See ActionController::HttpAuthentication::Token for example usage.
438
+ def authenticate_or_request_with_http_token(realm = "Application", message = nil, &login_procedure)
439
+ authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm, message)
440
+ end
441
+
442
+ # Authenticate using an HTTP Bearer token. Returns the return value of
443
+ # `login_procedure` if a token is found. Returns `nil` if no token is found.
444
+ #
445
+ # See ActionController::HttpAuthentication::Token for example usage.
446
+ def authenticate_with_http_token(&login_procedure)
447
+ Token.authenticate(self, &login_procedure)
448
+ end
449
+
450
+ # Render an HTTP header requesting the client to send a Bearer token for
451
+ # authentication.
452
+ def request_http_token_authentication(realm = "Application", message = nil)
453
+ Token.authentication_request(self, realm, message)
454
+ end
455
+ end
456
+
457
+ # If token Authorization header is present, call the login procedure with the
458
+ # present token and options.
459
+ #
460
+ # Returns the return value of `login_procedure` if a token is found. Returns
461
+ # `nil` if no token is found.
462
+ #
463
+ # #### Parameters
464
+ #
465
+ # * `controller` - ActionController::Base instance for the current request.
466
+ # * `login_procedure` - Proc to call if a token is present. The Proc should
467
+ # take two arguments:
468
+ #
469
+ # authenticate(controller) { |token, options| ... }
470
+ #
471
+ #
472
+ def authenticate(controller, &login_procedure)
473
+ token, options = token_and_options(controller.request)
474
+ unless token.blank?
475
+ login_procedure.call(token, options)
476
+ end
477
+ end
478
+
479
+ # Parses the token and options out of the token Authorization header. The value
480
+ # for the Authorization header is expected to have the prefix `"Token"` or
481
+ # `"Bearer"`. If the header looks like this:
482
+ #
483
+ # Authorization: Token token="abc", nonce="def"
484
+ #
485
+ # Then the returned token is `"abc"`, and the options are `{nonce: "def"}`.
486
+ #
487
+ # Returns an `Array` of `[String, Hash]` if a token is present. Returns `nil` if
488
+ # no token is found.
489
+ #
490
+ # #### Parameters
491
+ #
492
+ # * `request` - ActionDispatch::Request instance with the current headers.
493
+ #
494
+ def token_and_options(request)
495
+ authorization_request = request.authorization.to_s
496
+ if authorization_request[TOKEN_REGEX]
497
+ params = token_params_from authorization_request
498
+ [params.shift[1], Hash[params].with_indifferent_access]
499
+ end
500
+ end
501
+
502
+ def token_params_from(auth)
503
+ rewrite_param_values params_array_from raw_params auth
504
+ end
505
+
506
+ # Takes `raw_params` and turns it into an array of parameters.
507
+ def params_array_from(raw_params)
508
+ raw_params.map { |param| param.split %r/=(.+)?/ }
509
+ end
510
+
511
+ # This removes the `"` characters wrapping the value.
512
+ def rewrite_param_values(array_params)
513
+ array_params.each { |param| (param[1] || +"").gsub! %r/^"|"$/, "" }
514
+ end
515
+
516
+ WHITESPACED_AUTHN_PAIR_DELIMITERS = /\s*#{AUTHN_PAIR_DELIMITERS}\s*/
517
+ private_constant :WHITESPACED_AUTHN_PAIR_DELIMITERS
518
+
519
+ # This method takes an authorization body and splits up the key-value pairs by
520
+ # the standardized `:`, `;`, or `\t` delimiters defined in
521
+ # `AUTHN_PAIR_DELIMITERS`.
522
+ def raw_params(auth)
523
+ _raw_params = auth.sub(TOKEN_REGEX, "").split(WHITESPACED_AUTHN_PAIR_DELIMITERS)
524
+ _raw_params.reject!(&:empty?)
525
+
526
+ if !_raw_params.first&.start_with?(TOKEN_KEY)
527
+ _raw_params[0] = "#{TOKEN_KEY}#{_raw_params.first}"
528
+ end
529
+
530
+ _raw_params
531
+ end
532
+
533
+ # Encodes the given token and options into an Authorization header value.
534
+ #
535
+ # Returns String.
536
+ #
537
+ # #### Parameters
538
+ #
539
+ # * `token` - String token.
540
+ # * `options` - Optional Hash of the options.
541
+ #
542
+ def encode_credentials(token, options = {})
543
+ values = ["#{TOKEN_KEY}#{token.to_s.inspect}"] + options.map do |key, value|
544
+ "#{key}=#{value.to_s.inspect}"
545
+ end
546
+ "Token #{values * ", "}"
547
+ end
548
+
549
+ # Sets a WWW-Authenticate header to let the client know a token is desired.
550
+ #
551
+ # Returns nothing.
552
+ #
553
+ # #### Parameters
554
+ #
555
+ # * `controller` - ActionController::Base instance for the outgoing response.
556
+ # * `realm` - String realm to use in the header.
557
+ #
558
+ def authentication_request(controller, realm, message = nil)
559
+ message ||= "HTTP Token: Access denied.\n"
560
+ controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"', "")}")
561
+ controller.__send__ :render, plain: message, status: :unauthorized
562
+ end
563
+ end
564
+ end
565
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionController
6
+ # # Action Controller Implicit Render
7
+ #
8
+ # Handles implicit rendering for a controller action that does not explicitly
9
+ # respond with `render`, `respond_to`, `redirect`, or `head`.
10
+ #
11
+ # For API controllers, the implicit response is always `204 No Content`.
12
+ #
13
+ # For all other controllers, we use these heuristics to decide whether to render
14
+ # a template, raise an error for a missing template, or respond with `204 No
15
+ # Content`:
16
+ #
17
+ # First, if we DO find a template, it's rendered. Template lookup accounts for
18
+ # the action name, locales, format, variant, template handlers, and more (see
19
+ # `render` for details).
20
+ #
21
+ # Second, if we DON'T find a template but the controller action does have
22
+ # templates for other formats, variants, etc., then we trust that you meant to
23
+ # provide a template for this response, too, and we raise
24
+ # ActionController::UnknownFormat with an explanation.
25
+ #
26
+ # Third, if we DON'T find a template AND the request is a page load in a web
27
+ # browser (technically, a non-XHR GET request for an HTML response) where you
28
+ # reasonably expect to have rendered a template, then we raise
29
+ # ActionController::MissingExactTemplate with an explanation.
30
+ #
31
+ # Finally, if we DON'T find a template AND the request isn't a browser page
32
+ # load, then we implicitly respond with `204 No Content`.
33
+ module ImplicitRender
34
+ # :stopdoc:
35
+ include BasicImplicitRender
36
+
37
+ def default_render
38
+ if template_exists?(action_name.to_s, _prefixes, variants: request.variant)
39
+ render
40
+ elsif any_templates?(action_name.to_s, _prefixes)
41
+ message = "#{self.class.name}\##{action_name} is missing a template " \
42
+ "for this request format and variant.\n" \
43
+ "\nrequest.formats: #{request.formats.map(&:to_s).inspect}" \
44
+ "\nrequest.variant: #{request.variant.inspect}"
45
+
46
+ raise ActionController::UnknownFormat, message
47
+ elsif interactive_browser_request?
48
+ message = "#{self.class.name}\##{action_name} is missing a template for request formats: #{request.formats.map(&:to_s).join(',')}"
49
+ raise ActionController::MissingExactTemplate.new(message, self.class, action_name)
50
+ else
51
+ logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger
52
+ super
53
+ end
54
+ end
55
+
56
+ def method_for_action(action_name)
57
+ super || if template_exists?(action_name.to_s, _prefixes)
58
+ "default_render"
59
+ end
60
+ end
61
+
62
+ private
63
+ def interactive_browser_request?
64
+ request.get? && request.format == Mime[:html] && !request.xhr?
65
+ end
66
+ end
67
+ end