actionpack 5.2.3

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

Files changed (170) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +429 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +57 -0
  5. data/lib/abstract_controller.rb +27 -0
  6. data/lib/abstract_controller/asset_paths.rb +12 -0
  7. data/lib/abstract_controller/base.rb +265 -0
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/abstract_controller/caching/fragments.rb +166 -0
  10. data/lib/abstract_controller/callbacks.rb +212 -0
  11. data/lib/abstract_controller/collector.rb +43 -0
  12. data/lib/abstract_controller/error.rb +6 -0
  13. data/lib/abstract_controller/helpers.rb +194 -0
  14. data/lib/abstract_controller/logger.rb +14 -0
  15. data/lib/abstract_controller/railties/routes_helpers.rb +20 -0
  16. data/lib/abstract_controller/rendering.rb +127 -0
  17. data/lib/abstract_controller/translation.rb +31 -0
  18. data/lib/abstract_controller/url_for.rb +35 -0
  19. data/lib/action_controller.rb +66 -0
  20. data/lib/action_controller/api.rb +149 -0
  21. data/lib/action_controller/api/api_rendering.rb +16 -0
  22. data/lib/action_controller/base.rb +276 -0
  23. data/lib/action_controller/caching.rb +46 -0
  24. data/lib/action_controller/form_builder.rb +50 -0
  25. data/lib/action_controller/log_subscriber.rb +78 -0
  26. data/lib/action_controller/metal.rb +256 -0
  27. data/lib/action_controller/metal/basic_implicit_render.rb +13 -0
  28. data/lib/action_controller/metal/conditional_get.rb +274 -0
  29. data/lib/action_controller/metal/content_security_policy.rb +52 -0
  30. data/lib/action_controller/metal/cookies.rb +16 -0
  31. data/lib/action_controller/metal/data_streaming.rb +152 -0
  32. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  33. data/lib/action_controller/metal/etag_with_template_digest.rb +57 -0
  34. data/lib/action_controller/metal/exceptions.rb +53 -0
  35. data/lib/action_controller/metal/flash.rb +61 -0
  36. data/lib/action_controller/metal/force_ssl.rb +99 -0
  37. data/lib/action_controller/metal/head.rb +60 -0
  38. data/lib/action_controller/metal/helpers.rb +123 -0
  39. data/lib/action_controller/metal/http_authentication.rb +519 -0
  40. data/lib/action_controller/metal/implicit_render.rb +73 -0
  41. data/lib/action_controller/metal/instrumentation.rb +107 -0
  42. data/lib/action_controller/metal/live.rb +312 -0
  43. data/lib/action_controller/metal/mime_responds.rb +313 -0
  44. data/lib/action_controller/metal/parameter_encoding.rb +51 -0
  45. data/lib/action_controller/metal/params_wrapper.rb +293 -0
  46. data/lib/action_controller/metal/redirecting.rb +133 -0
  47. data/lib/action_controller/metal/renderers.rb +181 -0
  48. data/lib/action_controller/metal/rendering.rb +122 -0
  49. data/lib/action_controller/metal/request_forgery_protection.rb +445 -0
  50. data/lib/action_controller/metal/rescue.rb +28 -0
  51. data/lib/action_controller/metal/streaming.rb +223 -0
  52. data/lib/action_controller/metal/strong_parameters.rb +1086 -0
  53. data/lib/action_controller/metal/testing.rb +16 -0
  54. data/lib/action_controller/metal/url_for.rb +58 -0
  55. data/lib/action_controller/railtie.rb +89 -0
  56. data/lib/action_controller/railties/helpers.rb +24 -0
  57. data/lib/action_controller/renderer.rb +117 -0
  58. data/lib/action_controller/template_assertions.rb +11 -0
  59. data/lib/action_controller/test_case.rb +629 -0
  60. data/lib/action_dispatch.rb +112 -0
  61. data/lib/action_dispatch/http/cache.rb +222 -0
  62. data/lib/action_dispatch/http/content_security_policy.rb +272 -0
  63. data/lib/action_dispatch/http/filter_parameters.rb +84 -0
  64. data/lib/action_dispatch/http/filter_redirect.rb +37 -0
  65. data/lib/action_dispatch/http/headers.rb +132 -0
  66. data/lib/action_dispatch/http/mime_negotiation.rb +175 -0
  67. data/lib/action_dispatch/http/mime_type.rb +342 -0
  68. data/lib/action_dispatch/http/mime_types.rb +50 -0
  69. data/lib/action_dispatch/http/parameter_filter.rb +86 -0
  70. data/lib/action_dispatch/http/parameters.rb +126 -0
  71. data/lib/action_dispatch/http/rack_cache.rb +63 -0
  72. data/lib/action_dispatch/http/request.rb +430 -0
  73. data/lib/action_dispatch/http/response.rb +519 -0
  74. data/lib/action_dispatch/http/upload.rb +84 -0
  75. data/lib/action_dispatch/http/url.rb +350 -0
  76. data/lib/action_dispatch/journey.rb +7 -0
  77. data/lib/action_dispatch/journey/formatter.rb +189 -0
  78. data/lib/action_dispatch/journey/gtg/builder.rb +164 -0
  79. data/lib/action_dispatch/journey/gtg/simulator.rb +41 -0
  80. data/lib/action_dispatch/journey/gtg/transition_table.rb +158 -0
  81. data/lib/action_dispatch/journey/nfa/builder.rb +78 -0
  82. data/lib/action_dispatch/journey/nfa/dot.rb +36 -0
  83. data/lib/action_dispatch/journey/nfa/simulator.rb +49 -0
  84. data/lib/action_dispatch/journey/nfa/transition_table.rb +120 -0
  85. data/lib/action_dispatch/journey/nodes/node.rb +140 -0
  86. data/lib/action_dispatch/journey/parser.rb +199 -0
  87. data/lib/action_dispatch/journey/parser.y +50 -0
  88. data/lib/action_dispatch/journey/parser_extras.rb +31 -0
  89. data/lib/action_dispatch/journey/path/pattern.rb +198 -0
  90. data/lib/action_dispatch/journey/route.rb +203 -0
  91. data/lib/action_dispatch/journey/router.rb +156 -0
  92. data/lib/action_dispatch/journey/router/utils.rb +102 -0
  93. data/lib/action_dispatch/journey/routes.rb +82 -0
  94. data/lib/action_dispatch/journey/scanner.rb +64 -0
  95. data/lib/action_dispatch/journey/visitors.rb +268 -0
  96. data/lib/action_dispatch/journey/visualizer/fsm.css +30 -0
  97. data/lib/action_dispatch/journey/visualizer/fsm.js +134 -0
  98. data/lib/action_dispatch/journey/visualizer/index.html.erb +52 -0
  99. data/lib/action_dispatch/middleware/callbacks.rb +36 -0
  100. data/lib/action_dispatch/middleware/cookies.rb +685 -0
  101. data/lib/action_dispatch/middleware/debug_exceptions.rb +205 -0
  102. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  103. data/lib/action_dispatch/middleware/exception_wrapper.rb +147 -0
  104. data/lib/action_dispatch/middleware/executor.rb +21 -0
  105. data/lib/action_dispatch/middleware/flash.rb +300 -0
  106. data/lib/action_dispatch/middleware/public_exceptions.rb +57 -0
  107. data/lib/action_dispatch/middleware/reloader.rb +12 -0
  108. data/lib/action_dispatch/middleware/remote_ip.rb +183 -0
  109. data/lib/action_dispatch/middleware/request_id.rb +43 -0
  110. data/lib/action_dispatch/middleware/session/abstract_store.rb +92 -0
  111. data/lib/action_dispatch/middleware/session/cache_store.rb +54 -0
  112. data/lib/action_dispatch/middleware/session/cookie_store.rb +118 -0
  113. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +28 -0
  114. data/lib/action_dispatch/middleware/show_exceptions.rb +62 -0
  115. data/lib/action_dispatch/middleware/ssl.rb +150 -0
  116. data/lib/action_dispatch/middleware/stack.rb +116 -0
  117. data/lib/action_dispatch/middleware/static.rb +130 -0
  118. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +22 -0
  119. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +23 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +27 -0
  121. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  122. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +52 -0
  123. data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +9 -0
  124. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +16 -0
  125. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
  126. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +21 -0
  127. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +13 -0
  128. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +161 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +11 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb +3 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +32 -0
  132. data/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb +11 -0
  133. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +20 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +7 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +6 -0
  136. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +3 -0
  137. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +16 -0
  138. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +200 -0
  139. data/lib/action_dispatch/railtie.rb +55 -0
  140. data/lib/action_dispatch/request/session.rb +234 -0
  141. data/lib/action_dispatch/request/utils.rb +78 -0
  142. data/lib/action_dispatch/routing.rb +260 -0
  143. data/lib/action_dispatch/routing/endpoint.rb +17 -0
  144. data/lib/action_dispatch/routing/inspector.rb +225 -0
  145. data/lib/action_dispatch/routing/mapper.rb +2267 -0
  146. data/lib/action_dispatch/routing/polymorphic_routes.rb +352 -0
  147. data/lib/action_dispatch/routing/redirection.rb +201 -0
  148. data/lib/action_dispatch/routing/route_set.rb +890 -0
  149. data/lib/action_dispatch/routing/routes_proxy.rb +69 -0
  150. data/lib/action_dispatch/routing/url_for.rb +236 -0
  151. data/lib/action_dispatch/system_test_case.rb +147 -0
  152. data/lib/action_dispatch/system_testing/browser.rb +49 -0
  153. data/lib/action_dispatch/system_testing/driver.rb +59 -0
  154. data/lib/action_dispatch/system_testing/server.rb +31 -0
  155. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +96 -0
  156. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +31 -0
  157. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  158. data/lib/action_dispatch/testing/assertion_response.rb +47 -0
  159. data/lib/action_dispatch/testing/assertions.rb +24 -0
  160. data/lib/action_dispatch/testing/assertions/response.rb +107 -0
  161. data/lib/action_dispatch/testing/assertions/routing.rb +222 -0
  162. data/lib/action_dispatch/testing/integration.rb +652 -0
  163. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  164. data/lib/action_dispatch/testing/test_process.rb +50 -0
  165. data/lib/action_dispatch/testing/test_request.rb +71 -0
  166. data/lib/action_dispatch/testing/test_response.rb +53 -0
  167. data/lib/action_pack.rb +26 -0
  168. data/lib/action_pack/gem_version.rb +17 -0
  169. data/lib/action_pack/version.rb +10 -0
  170. metadata +318 -0
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionController
4
+ module Head
5
+ # Returns a response that has no content (merely headers). The options
6
+ # argument is interpreted to be a hash of header names and values.
7
+ # This allows you to easily return a response that consists only of
8
+ # significant headers:
9
+ #
10
+ # head :created, location: person_path(@person)
11
+ #
12
+ # head :created, location: @person
13
+ #
14
+ # It can also be used to return exceptional conditions:
15
+ #
16
+ # return head(:method_not_allowed) unless request.post?
17
+ # return head(:bad_request) unless valid_request?
18
+ # render
19
+ #
20
+ # See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list of valid +status+ symbols.
21
+ def head(status, options = {})
22
+ if status.is_a?(Hash)
23
+ raise ArgumentError, "#{status.inspect} is not a valid value for `status`."
24
+ end
25
+
26
+ status ||= :ok
27
+
28
+ location = options.delete(:location)
29
+ content_type = options.delete(:content_type)
30
+
31
+ options.each do |key, value|
32
+ headers[key.to_s.dasherize.split("-").each { |v| v[0] = v[0].chr.upcase }.join("-")] = value.to_s
33
+ end
34
+
35
+ self.status = status
36
+ self.location = url_for(location) if location
37
+
38
+ self.response_body = ""
39
+
40
+ if include_content?(response_code)
41
+ self.content_type = content_type || (Mime[formats.first] if formats)
42
+ response.charset = false
43
+ end
44
+
45
+ true
46
+ end
47
+
48
+ private
49
+ def include_content?(status)
50
+ case status
51
+ when 100..199
52
+ false
53
+ when 204, 205, 304
54
+ false
55
+ else
56
+ true
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionController
4
+ # The \Rails framework provides a large number of helpers for working with assets, dates, forms,
5
+ # numbers and model objects, to name a few. These helpers are available to all templates
6
+ # by default.
7
+ #
8
+ # In addition to using the standard template helpers provided, creating custom helpers to
9
+ # extract complicated logic or reusable functionality is strongly encouraged. By default, each controller
10
+ # will include all helpers. These helpers are only accessible on the controller through <tt>#helpers</tt>
11
+ #
12
+ # In previous versions of \Rails the controller will include a helper which
13
+ # matches the name of the controller, e.g., <tt>MyController</tt> will automatically
14
+ # include <tt>MyHelper</tt>. To return old behavior set +config.action_controller.include_all_helpers+ to +false+.
15
+ #
16
+ # Additional helpers can be specified using the +helper+ class method in ActionController::Base or any
17
+ # controller which inherits from it.
18
+ #
19
+ # The +to_s+ method from the \Time class can be wrapped in a helper method to display a custom message if
20
+ # a \Time object is blank:
21
+ #
22
+ # module FormattedTimeHelper
23
+ # def format_time(time, format=:long, blank_message="&nbsp;")
24
+ # time.blank? ? blank_message : time.to_s(format)
25
+ # end
26
+ # end
27
+ #
28
+ # FormattedTimeHelper can now be included in a controller, using the +helper+ class method:
29
+ #
30
+ # class EventsController < ActionController::Base
31
+ # helper FormattedTimeHelper
32
+ # def index
33
+ # @events = Event.all
34
+ # end
35
+ # end
36
+ #
37
+ # Then, in any view rendered by <tt>EventController</tt>, the <tt>format_time</tt> method can be called:
38
+ #
39
+ # <% @events.each do |event| -%>
40
+ # <p>
41
+ # <%= format_time(event.time, :short, "N/A") %> | <%= event.name %>
42
+ # </p>
43
+ # <% end -%>
44
+ #
45
+ # Finally, assuming we have two event instances, one which has a time and one which does not,
46
+ # the output might look like this:
47
+ #
48
+ # 23 Aug 11:30 | Carolina Railhawks Soccer Match
49
+ # N/A | Carolina Railhawks Training Workshop
50
+ #
51
+ module Helpers
52
+ extend ActiveSupport::Concern
53
+
54
+ class << self; attr_accessor :helpers_path; end
55
+ include AbstractController::Helpers
56
+
57
+ included do
58
+ class_attribute :helpers_path, default: []
59
+ class_attribute :include_all_helpers, default: true
60
+ end
61
+
62
+ module ClassMethods
63
+ # Declares helper accessors for controller attributes. For example, the
64
+ # following adds new +name+ and <tt>name=</tt> instance methods to a
65
+ # controller and makes them available to the view:
66
+ # attr_accessor :name
67
+ # helper_attr :name
68
+ #
69
+ # ==== Parameters
70
+ # * <tt>attrs</tt> - Names of attributes to be converted into helpers.
71
+ def helper_attr(*attrs)
72
+ attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") }
73
+ end
74
+
75
+ # Provides a proxy to access helper methods from outside the view.
76
+ def helpers
77
+ @helper_proxy ||= begin
78
+ proxy = ActionView::Base.new
79
+ proxy.config = config.inheritable_copy
80
+ proxy.extend(_helpers)
81
+ end
82
+ end
83
+
84
+ # Overwrite modules_for_helpers to accept :all as argument, which loads
85
+ # all helpers in helpers_path.
86
+ #
87
+ # ==== Parameters
88
+ # * <tt>args</tt> - A list of helpers
89
+ #
90
+ # ==== Returns
91
+ # * <tt>array</tt> - A normalized list of modules for the list of helpers provided.
92
+ def modules_for_helpers(args)
93
+ args += all_application_helpers if args.delete(:all)
94
+ super(args)
95
+ end
96
+
97
+ # Returns a list of helper names in a given path.
98
+ #
99
+ # ActionController::Base.all_helpers_from_path 'app/helpers'
100
+ # # => ["application", "chart", "rubygems"]
101
+ def all_helpers_from_path(path)
102
+ helpers = Array(path).flat_map do |_path|
103
+ extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/
104
+ names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1'.freeze) }
105
+ names.sort!
106
+ end
107
+ helpers.uniq!
108
+ helpers
109
+ end
110
+
111
+ private
112
+ # Extract helper names from files in <tt>app/helpers/**/*_helper.rb</tt>
113
+ def all_application_helpers
114
+ all_helpers_from_path(helpers_path)
115
+ end
116
+ end
117
+
118
+ # Provides a proxy to access helper methods from outside the view.
119
+ def helpers
120
+ @_helper_proxy ||= view_context
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,519 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "active_support/security_utils"
5
+
6
+ module ActionController
7
+ # Makes it dead easy to do HTTP Basic, Digest and Token authentication.
8
+ module HttpAuthentication
9
+ # Makes it dead easy to do HTTP \Basic authentication.
10
+ #
11
+ # === Simple \Basic example
12
+ #
13
+ # class PostsController < ApplicationController
14
+ # http_basic_authenticate_with name: "dhh", password: "secret", except: :index
15
+ #
16
+ # def index
17
+ # render plain: "Everyone can see me!"
18
+ # end
19
+ #
20
+ # def edit
21
+ # render plain: "I'm only accessible if you know the password"
22
+ # end
23
+ # end
24
+ #
25
+ # === Advanced \Basic example
26
+ #
27
+ # Here is a more advanced \Basic example where only Atom feeds and the XML API is protected by HTTP authentication,
28
+ # the regular HTML interface is protected by a session approach:
29
+ #
30
+ # class ApplicationController < ActionController::Base
31
+ # before_action :set_account, :authenticate
32
+ #
33
+ # private
34
+ # def set_account
35
+ # @account = Account.find_by(url_name: request.subdomains.first)
36
+ # end
37
+ #
38
+ # def authenticate
39
+ # case request.format
40
+ # when Mime[:xml], Mime[:atom]
41
+ # if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) }
42
+ # @current_user = user
43
+ # else
44
+ # request_http_basic_authentication
45
+ # end
46
+ # else
47
+ # if session_authenticated?
48
+ # @current_user = @account.users.find(session[:authenticated][:user_id])
49
+ # else
50
+ # redirect_to(login_url) and return false
51
+ # end
52
+ # end
53
+ # end
54
+ # end
55
+ #
56
+ # In your integration tests, you can do something like this:
57
+ #
58
+ # def test_access_granted_from_xml
59
+ # @request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
60
+ # get "/notes/1.xml"
61
+ #
62
+ # assert_equal 200, status
63
+ # end
64
+ module Basic
65
+ extend self
66
+
67
+ module ControllerMethods
68
+ extend ActiveSupport::Concern
69
+
70
+ module ClassMethods
71
+ def http_basic_authenticate_with(options = {})
72
+ before_action(options.except(:name, :password, :realm)) do
73
+ authenticate_or_request_with_http_basic(options[:realm] || "Application") do |name, password|
74
+ # This comparison uses & so that it doesn't short circuit and
75
+ # uses `secure_compare` so that length information
76
+ # isn't leaked.
77
+ ActiveSupport::SecurityUtils.secure_compare(name, options[:name]) &
78
+ ActiveSupport::SecurityUtils.secure_compare(password, options[:password])
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ def authenticate_or_request_with_http_basic(realm = "Application", message = nil, &login_procedure)
85
+ authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm, message)
86
+ end
87
+
88
+ def authenticate_with_http_basic(&login_procedure)
89
+ HttpAuthentication::Basic.authenticate(request, &login_procedure)
90
+ end
91
+
92
+ def request_http_basic_authentication(realm = "Application", message = nil)
93
+ HttpAuthentication::Basic.authentication_request(self, realm, message)
94
+ end
95
+ end
96
+
97
+ def authenticate(request, &login_procedure)
98
+ if has_basic_credentials?(request)
99
+ login_procedure.call(*user_name_and_password(request))
100
+ end
101
+ end
102
+
103
+ def has_basic_credentials?(request)
104
+ request.authorization.present? && (auth_scheme(request).downcase == "basic")
105
+ end
106
+
107
+ def user_name_and_password(request)
108
+ decode_credentials(request).split(":", 2)
109
+ end
110
+
111
+ def decode_credentials(request)
112
+ ::Base64.decode64(auth_param(request) || "")
113
+ end
114
+
115
+ def auth_scheme(request)
116
+ request.authorization.to_s.split(" ", 2).first
117
+ end
118
+
119
+ def auth_param(request)
120
+ request.authorization.to_s.split(" ", 2).second
121
+ end
122
+
123
+ def encode_credentials(user_name, password)
124
+ "Basic #{::Base64.strict_encode64("#{user_name}:#{password}")}"
125
+ end
126
+
127
+ def authentication_request(controller, realm, message)
128
+ message ||= "HTTP Basic: Access denied.\n"
129
+ controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"'.freeze, "".freeze)}")
130
+ controller.status = 401
131
+ controller.response_body = message
132
+ end
133
+ end
134
+
135
+ # Makes it dead easy to do HTTP \Digest authentication.
136
+ #
137
+ # === Simple \Digest example
138
+ #
139
+ # require 'digest/md5'
140
+ # class PostsController < ApplicationController
141
+ # REALM = "SuperSecret"
142
+ # USERS = {"dhh" => "secret", #plain text password
143
+ # "dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password
144
+ #
145
+ # before_action :authenticate, except: [:index]
146
+ #
147
+ # def index
148
+ # render plain: "Everyone can see me!"
149
+ # end
150
+ #
151
+ # def edit
152
+ # render plain: "I'm only accessible if you know the password"
153
+ # end
154
+ #
155
+ # private
156
+ # def authenticate
157
+ # authenticate_or_request_with_http_digest(REALM) do |username|
158
+ # USERS[username]
159
+ # end
160
+ # end
161
+ # end
162
+ #
163
+ # === Notes
164
+ #
165
+ # The +authenticate_or_request_with_http_digest+ block must return the user's password
166
+ # or the ha1 digest hash so the framework can appropriately hash to check the user's
167
+ # credentials. Returning +nil+ will cause authentication to fail.
168
+ #
169
+ # Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If
170
+ # the password file or database is compromised, the attacker would be able to use the ha1 hash to
171
+ # authenticate as the user at this +realm+, but would not have the user's password to try using at
172
+ # other sites.
173
+ #
174
+ # In rare instances, web servers or front proxies strip authorization headers before
175
+ # they reach your application. You can debug this situation by logging all environment
176
+ # variables, and check for HTTP_AUTHORIZATION, amongst others.
177
+ module Digest
178
+ extend self
179
+
180
+ module ControllerMethods
181
+ def authenticate_or_request_with_http_digest(realm = "Application", message = nil, &password_procedure)
182
+ authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm, message)
183
+ end
184
+
185
+ # Authenticate with HTTP Digest, returns true or false
186
+ def authenticate_with_http_digest(realm = "Application", &password_procedure)
187
+ HttpAuthentication::Digest.authenticate(request, realm, &password_procedure)
188
+ end
189
+
190
+ # Render output including the HTTP Digest authentication header
191
+ def request_http_digest_authentication(realm = "Application", message = nil)
192
+ HttpAuthentication::Digest.authentication_request(self, realm, message)
193
+ end
194
+ end
195
+
196
+ # Returns false on a valid response, true otherwise
197
+ def authenticate(request, realm, &password_procedure)
198
+ request.authorization && validate_digest_response(request, realm, &password_procedure)
199
+ end
200
+
201
+ # Returns false unless the request credentials response value matches the expected value.
202
+ # First try the password as a ha1 digest password. If this fails, then try it as a plain
203
+ # text password.
204
+ def validate_digest_response(request, realm, &password_procedure)
205
+ secret_key = secret_token(request)
206
+ credentials = decode_credentials_header(request)
207
+ valid_nonce = validate_nonce(secret_key, request, credentials[:nonce])
208
+
209
+ if valid_nonce && realm == credentials[:realm] && opaque(secret_key) == credentials[:opaque]
210
+ password = password_procedure.call(credentials[:username])
211
+ return false unless password
212
+
213
+ method = request.get_header("rack.methodoverride.original_method") || request.get_header("REQUEST_METHOD")
214
+ uri = credentials[:uri]
215
+
216
+ [true, false].any? do |trailing_question_mark|
217
+ [true, false].any? do |password_is_ha1|
218
+ _uri = trailing_question_mark ? uri + "?" : uri
219
+ expected = expected_response(method, _uri, credentials, password, password_is_ha1)
220
+ expected == credentials[:response]
221
+ end
222
+ end
223
+ end
224
+ end
225
+
226
+ # Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+
227
+ # Optional parameter +password_is_ha1+ is set to +true+ by default, since best practice is to store ha1 digest instead
228
+ # of a plain-text password.
229
+ def expected_response(http_method, uri, credentials, password, password_is_ha1 = true)
230
+ ha1 = password_is_ha1 ? password : ha1(credentials, password)
231
+ ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":"))
232
+ ::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(":"))
233
+ end
234
+
235
+ def ha1(credentials, password)
236
+ ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
237
+ end
238
+
239
+ def encode_credentials(http_method, credentials, password, password_is_ha1)
240
+ credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1)
241
+ "Digest " + credentials.sort_by { |x| x[0].to_s }.map { |v| "#{v[0]}='#{v[1]}'" }.join(", ")
242
+ end
243
+
244
+ def decode_credentials_header(request)
245
+ decode_credentials(request.authorization)
246
+ end
247
+
248
+ def decode_credentials(header)
249
+ ActiveSupport::HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/, "").split(",").map do |pair|
250
+ key, value = pair.split("=", 2)
251
+ [key.strip, value.to_s.gsub(/^"|"$/, "").delete("'")]
252
+ end]
253
+ end
254
+
255
+ def authentication_header(controller, realm)
256
+ secret_key = secret_token(controller.request)
257
+ nonce = self.nonce(secret_key)
258
+ opaque = opaque(secret_key)
259
+ controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce}", opaque="#{opaque}")
260
+ end
261
+
262
+ def authentication_request(controller, realm, message = nil)
263
+ message ||= "HTTP Digest: Access denied.\n"
264
+ authentication_header(controller, realm)
265
+ controller.status = 401
266
+ controller.response_body = message
267
+ end
268
+
269
+ def secret_token(request)
270
+ key_generator = request.key_generator
271
+ http_auth_salt = request.http_auth_salt
272
+ key_generator.generate_key(http_auth_salt)
273
+ end
274
+
275
+ # Uses an MD5 digest based on time to generate a value to be used only once.
276
+ #
277
+ # A server-specified data string which should be uniquely generated each time a 401 response is made.
278
+ # It is recommended that this string be base64 or hexadecimal data.
279
+ # Specifically, since the string is passed in the header lines as a quoted string, the double-quote character is not allowed.
280
+ #
281
+ # The contents of the nonce are implementation dependent.
282
+ # The quality of the implementation depends on a good choice.
283
+ # A nonce might, for example, be constructed as the base 64 encoding of
284
+ #
285
+ # time-stamp H(time-stamp ":" ETag ":" private-key)
286
+ #
287
+ # where time-stamp is a server-generated time or other non-repeating value,
288
+ # ETag is the value of the HTTP ETag header associated with the requested entity,
289
+ # and private-key is data known only to the server.
290
+ # With a nonce of this form a server would recalculate the hash portion after receiving the client authentication header and
291
+ # reject the request if it did not match the nonce from that header or
292
+ # if the time-stamp value is not recent enough. In this way the server can limit the time of the nonce's validity.
293
+ # The inclusion of the ETag prevents a replay request for an updated version of the resource.
294
+ # (Note: including the IP address of the client in the nonce would appear to offer the server the ability
295
+ # to limit the reuse of the nonce to the same client that originally got it.
296
+ # However, that would break proxy farms, where requests from a single user often go through different proxies in the farm.
297
+ # Also, IP address spoofing is not that hard.)
298
+ #
299
+ # An implementation might choose not to accept a previously used nonce or a previously used digest, in order to
300
+ # protect against a replay attack. Or, an implementation might choose to use one-time nonces or digests for
301
+ # POST, PUT, or PATCH requests and a time-stamp for GET requests. For more details on the issues involved see Section 4
302
+ # of this document.
303
+ #
304
+ # The nonce is opaque to the client. Composed of Time, and hash of Time with secret
305
+ # key from the Rails session secret generated upon creation of project. Ensures
306
+ # the time cannot be modified by client.
307
+ def nonce(secret_key, time = Time.now)
308
+ t = time.to_i
309
+ hashed = [t, secret_key]
310
+ digest = ::Digest::MD5.hexdigest(hashed.join(":"))
311
+ ::Base64.strict_encode64("#{t}:#{digest}")
312
+ end
313
+
314
+ # Might want a shorter timeout depending on whether the request
315
+ # is a PATCH, PUT, or POST, and if the client is a browser or web service.
316
+ # Can be much shorter if the Stale directive is implemented. This would
317
+ # allow a user to use new nonce without prompting the user again for their
318
+ # username and password.
319
+ def validate_nonce(secret_key, request, value, seconds_to_timeout = 5 * 60)
320
+ return false if value.nil?
321
+ t = ::Base64.decode64(value).split(":").first.to_i
322
+ nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout
323
+ end
324
+
325
+ # Opaque based on digest of secret key
326
+ def opaque(secret_key)
327
+ ::Digest::MD5.hexdigest(secret_key)
328
+ end
329
+ end
330
+
331
+ # Makes it dead easy to do HTTP Token authentication.
332
+ #
333
+ # Simple Token example:
334
+ #
335
+ # class PostsController < ApplicationController
336
+ # TOKEN = "secret"
337
+ #
338
+ # before_action :authenticate, except: [ :index ]
339
+ #
340
+ # def index
341
+ # render plain: "Everyone can see me!"
342
+ # end
343
+ #
344
+ # def edit
345
+ # render plain: "I'm only accessible if you know the password"
346
+ # end
347
+ #
348
+ # private
349
+ # def authenticate
350
+ # authenticate_or_request_with_http_token do |token, options|
351
+ # # Compare the tokens in a time-constant manner, to mitigate
352
+ # # timing attacks.
353
+ # ActiveSupport::SecurityUtils.secure_compare(token, TOKEN)
354
+ # end
355
+ # end
356
+ # end
357
+ #
358
+ #
359
+ # Here is a more advanced Token example where only Atom feeds and the XML API is protected by HTTP token authentication,
360
+ # the regular HTML interface is protected by a session approach:
361
+ #
362
+ # class ApplicationController < ActionController::Base
363
+ # before_action :set_account, :authenticate
364
+ #
365
+ # private
366
+ # def set_account
367
+ # @account = Account.find_by(url_name: request.subdomains.first)
368
+ # end
369
+ #
370
+ # def authenticate
371
+ # case request.format
372
+ # when Mime[:xml], Mime[:atom]
373
+ # if user = authenticate_with_http_token { |t, o| @account.users.authenticate(t, o) }
374
+ # @current_user = user
375
+ # else
376
+ # request_http_token_authentication
377
+ # end
378
+ # else
379
+ # if session_authenticated?
380
+ # @current_user = @account.users.find(session[:authenticated][:user_id])
381
+ # else
382
+ # redirect_to(login_url) and return false
383
+ # end
384
+ # end
385
+ # end
386
+ # end
387
+ #
388
+ #
389
+ # In your integration tests, you can do something like this:
390
+ #
391
+ # def test_access_granted_from_xml
392
+ # get(
393
+ # "/notes/1.xml", nil,
394
+ # 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token)
395
+ # )
396
+ #
397
+ # assert_equal 200, status
398
+ # end
399
+ #
400
+ #
401
+ # On shared hosts, Apache sometimes doesn't pass authentication headers to
402
+ # FCGI instances. If your environment matches this description and you cannot
403
+ # authenticate, try this rule in your Apache setup:
404
+ #
405
+ # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
406
+ module Token
407
+ TOKEN_KEY = "token="
408
+ TOKEN_REGEX = /^(Token|Bearer)\s+/
409
+ AUTHN_PAIR_DELIMITERS = /(?:,|;|\t+)/
410
+ extend self
411
+
412
+ module ControllerMethods
413
+ def authenticate_or_request_with_http_token(realm = "Application", message = nil, &login_procedure)
414
+ authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm, message)
415
+ end
416
+
417
+ def authenticate_with_http_token(&login_procedure)
418
+ Token.authenticate(self, &login_procedure)
419
+ end
420
+
421
+ def request_http_token_authentication(realm = "Application", message = nil)
422
+ Token.authentication_request(self, realm, message)
423
+ end
424
+ end
425
+
426
+ # If token Authorization header is present, call the login
427
+ # procedure with the present token and options.
428
+ #
429
+ # [controller]
430
+ # ActionController::Base instance for the current request.
431
+ #
432
+ # [login_procedure]
433
+ # Proc to call if a token is present. The Proc should take two arguments:
434
+ #
435
+ # authenticate(controller) { |token, options| ... }
436
+ #
437
+ # Returns the return value of <tt>login_procedure</tt> if a
438
+ # token is found. Returns <tt>nil</tt> if no token is found.
439
+
440
+ def authenticate(controller, &login_procedure)
441
+ token, options = token_and_options(controller.request)
442
+ unless token.blank?
443
+ login_procedure.call(token, options)
444
+ end
445
+ end
446
+
447
+ # Parses the token and options out of the token Authorization header.
448
+ # The value for the Authorization header is expected to have the prefix
449
+ # <tt>"Token"</tt> or <tt>"Bearer"</tt>. If the header looks like this:
450
+ # Authorization: Token token="abc", nonce="def"
451
+ # Then the returned token is <tt>"abc"</tt>, and the options are
452
+ # <tt>{nonce: "def"}</tt>
453
+ #
454
+ # request - ActionDispatch::Request instance with the current headers.
455
+ #
456
+ # Returns an +Array+ of <tt>[String, Hash]</tt> if a token is present.
457
+ # Returns +nil+ if no token is found.
458
+ def token_and_options(request)
459
+ authorization_request = request.authorization.to_s
460
+ if authorization_request[TOKEN_REGEX]
461
+ params = token_params_from authorization_request
462
+ [params.shift[1], Hash[params].with_indifferent_access]
463
+ end
464
+ end
465
+
466
+ def token_params_from(auth)
467
+ rewrite_param_values params_array_from raw_params auth
468
+ end
469
+
470
+ # Takes raw_params and turns it into an array of parameters
471
+ def params_array_from(raw_params)
472
+ raw_params.map { |param| param.split %r/=(.+)?/ }
473
+ end
474
+
475
+ # This removes the <tt>"</tt> characters wrapping the value.
476
+ def rewrite_param_values(array_params)
477
+ array_params.each { |param| (param[1] || "".dup).gsub! %r/^"|"$/, "" }
478
+ end
479
+
480
+ # This method takes an authorization body and splits up the key-value
481
+ # pairs by the standardized <tt>:</tt>, <tt>;</tt>, or <tt>\t</tt>
482
+ # delimiters defined in +AUTHN_PAIR_DELIMITERS+.
483
+ def raw_params(auth)
484
+ _raw_params = auth.sub(TOKEN_REGEX, "").split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/)
485
+
486
+ if !(_raw_params.first =~ %r{\A#{TOKEN_KEY}})
487
+ _raw_params[0] = "#{TOKEN_KEY}#{_raw_params.first}"
488
+ end
489
+
490
+ _raw_params
491
+ end
492
+
493
+ # Encodes the given token and options into an Authorization header value.
494
+ #
495
+ # token - String token.
496
+ # options - optional Hash of the options.
497
+ #
498
+ # Returns String.
499
+ def encode_credentials(token, options = {})
500
+ values = ["#{TOKEN_KEY}#{token.to_s.inspect}"] + options.map do |key, value|
501
+ "#{key}=#{value.to_s.inspect}"
502
+ end
503
+ "Token #{values * ", "}"
504
+ end
505
+
506
+ # Sets a WWW-Authenticate header to let the client know a token is desired.
507
+ #
508
+ # controller - ActionController::Base instance for the outgoing response.
509
+ # realm - String realm to use in the header.
510
+ #
511
+ # Returns nothing.
512
+ def authentication_request(controller, realm, message = nil)
513
+ message ||= "HTTP Token: Access denied.\n"
514
+ controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"'.freeze, "".freeze)}")
515
+ controller.__send__ :render, plain: message, status: :unauthorized
516
+ end
517
+ end
518
+ end
519
+ end