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,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "active_support/core_ext/string/access"
5
+
6
+ module ActionDispatch
7
+ # Makes a unique request id available to the +action_dispatch.request_id+ env variable (which is then accessible
8
+ # through <tt>ActionDispatch::Request#request_id</tt> or the alias <tt>ActionDispatch::Request#uuid</tt>) and sends
9
+ # the same id to the client via the X-Request-Id header.
10
+ #
11
+ # The unique request id is either based on the X-Request-Id header in the request, which would typically be generated
12
+ # by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the
13
+ # header is accepted from the outside world, we sanitize it to a max of 255 chars and alphanumeric and dashes only.
14
+ #
15
+ # The unique request id can be used to trace a request end-to-end and would typically end up being part of log files
16
+ # from multiple pieces of the stack.
17
+ class RequestId
18
+ X_REQUEST_ID = "X-Request-Id".freeze #:nodoc:
19
+
20
+ def initialize(app)
21
+ @app = app
22
+ end
23
+
24
+ def call(env)
25
+ req = ActionDispatch::Request.new env
26
+ req.request_id = make_request_id(req.x_request_id)
27
+ @app.call(env).tap { |_status, headers, _body| headers[X_REQUEST_ID] = req.request_id }
28
+ end
29
+
30
+ private
31
+ def make_request_id(request_id)
32
+ if request_id.presence
33
+ request_id.gsub(/[^\w\-@]/, "".freeze).first(255)
34
+ else
35
+ internal_request_id
36
+ end
37
+ end
38
+
39
+ def internal_request_id
40
+ SecureRandom.uuid
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/utils"
4
+ require "rack/request"
5
+ require "rack/session/abstract/id"
6
+ require "action_dispatch/middleware/cookies"
7
+ require "action_dispatch/request/session"
8
+
9
+ module ActionDispatch
10
+ module Session
11
+ class SessionRestoreError < StandardError #:nodoc:
12
+ def initialize
13
+ super("Session contains objects whose class definition isn't available.\n" \
14
+ "Remember to require the classes for all objects kept in the session.\n" \
15
+ "(Original exception: #{$!.message} [#{$!.class}])\n")
16
+ set_backtrace $!.backtrace
17
+ end
18
+ end
19
+
20
+ module Compatibility
21
+ def initialize(app, options = {})
22
+ options[:key] ||= "_session_id"
23
+ super
24
+ end
25
+
26
+ def generate_sid
27
+ sid = SecureRandom.hex(16)
28
+ sid.encode!(Encoding::UTF_8)
29
+ sid
30
+ end
31
+
32
+ private
33
+
34
+ def initialize_sid # :doc:
35
+ @default_options.delete(:sidbits)
36
+ @default_options.delete(:secure_random)
37
+ end
38
+
39
+ def make_request(env)
40
+ ActionDispatch::Request.new env
41
+ end
42
+ end
43
+
44
+ module StaleSessionCheck
45
+ def load_session(env)
46
+ stale_session_check! { super }
47
+ end
48
+
49
+ def extract_session_id(env)
50
+ stale_session_check! { super }
51
+ end
52
+
53
+ def stale_session_check!
54
+ yield
55
+ rescue ArgumentError => argument_error
56
+ if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
57
+ begin
58
+ # Note that the regexp does not allow $1 to end with a ':'.
59
+ $1.constantize
60
+ rescue LoadError, NameError
61
+ raise ActionDispatch::Session::SessionRestoreError
62
+ end
63
+ retry
64
+ else
65
+ raise
66
+ end
67
+ end
68
+ end
69
+
70
+ module SessionObject # :nodoc:
71
+ def prepare_session(req)
72
+ Request::Session.create(self, req, @default_options)
73
+ end
74
+
75
+ def loaded_session?(session)
76
+ !session.is_a?(Request::Session) || session.loaded?
77
+ end
78
+ end
79
+
80
+ class AbstractStore < Rack::Session::Abstract::Persisted
81
+ include Compatibility
82
+ include StaleSessionCheck
83
+ include SessionObject
84
+
85
+ private
86
+
87
+ def set_cookie(request, session_id, cookie)
88
+ request.cookie_jar[key] = cookie
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_dispatch/middleware/session/abstract_store"
4
+
5
+ module ActionDispatch
6
+ module Session
7
+ # A session store that uses an ActiveSupport::Cache::Store to store the sessions. This store is most useful
8
+ # if you don't store critical data in your sessions and you don't need them to live for extended periods
9
+ # of time.
10
+ #
11
+ # ==== Options
12
+ # * <tt>cache</tt> - The cache to use. If it is not specified, <tt>Rails.cache</tt> will be used.
13
+ # * <tt>expire_after</tt> - The length of time a session will be stored before automatically expiring.
14
+ # By default, the <tt>:expires_in</tt> option of the cache is used.
15
+ class CacheStore < AbstractStore
16
+ def initialize(app, options = {})
17
+ @cache = options[:cache] || Rails.cache
18
+ options[:expire_after] ||= @cache.options[:expires_in]
19
+ super
20
+ end
21
+
22
+ # Get a session from the cache.
23
+ def find_session(env, sid)
24
+ unless sid && (session = @cache.read(cache_key(sid)))
25
+ sid, session = generate_sid, {}
26
+ end
27
+ [sid, session]
28
+ end
29
+
30
+ # Set a session in the cache.
31
+ def write_session(env, sid, session, options)
32
+ key = cache_key(sid)
33
+ if session
34
+ @cache.write(key, session, expires_in: options[:expire_after])
35
+ else
36
+ @cache.delete(key)
37
+ end
38
+ sid
39
+ end
40
+
41
+ # Remove a session from the cache.
42
+ def delete_session(env, sid, options)
43
+ @cache.delete(cache_key(sid))
44
+ generate_sid
45
+ end
46
+
47
+ private
48
+ # Turn the session id into a cache key.
49
+ def cache_key(sid)
50
+ "_session_id:#{sid}"
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/keys"
4
+ require "action_dispatch/middleware/session/abstract_store"
5
+ require "rack/session/cookie"
6
+
7
+ module ActionDispatch
8
+ module Session
9
+ # This cookie-based session store is the Rails default. It is
10
+ # dramatically faster than the alternatives.
11
+ #
12
+ # Sessions typically contain at most a user_id and flash message; both fit
13
+ # within the 4K cookie size limit. A CookieOverflow exception is raised if
14
+ # you attempt to store more than 4K of data.
15
+ #
16
+ # The cookie jar used for storage is automatically configured to be the
17
+ # best possible option given your application's configuration.
18
+ #
19
+ # If you only have secret_token set, your cookies will be signed, but
20
+ # not encrypted. This means a user cannot alter their +user_id+ without
21
+ # knowing your app's secret key, but can easily read their +user_id+. This
22
+ # was the default for Rails 3 apps.
23
+ #
24
+ # Your cookies will be encrypted using your apps secret_key_base. This
25
+ # goes a step further than signed cookies in that encrypted cookies cannot
26
+ # be altered or read by users. This is the default starting in Rails 4.
27
+ #
28
+ # Configure your session store in <tt>config/initializers/session_store.rb</tt>:
29
+ #
30
+ # Rails.application.config.session_store :cookie_store, key: '_your_app_session'
31
+ #
32
+ # In the development and test environments your application's secret key base is
33
+ # generated by Rails and stored in a temporary file in <tt>tmp/development_secret.txt</tt>.
34
+ # In all other environments, it is stored encrypted in the
35
+ # <tt>config/credentials.yml.enc</tt> file.
36
+ #
37
+ # If your application was not updated to Rails 5.2 defaults, the secret_key_base
38
+ # will be found in the old <tt>config/secrets.yml</tt> file.
39
+ #
40
+ # Note that changing your secret_key_base will invalidate all existing session.
41
+ # Additionally, you should take care to make sure you are not relying on the
42
+ # ability to decode signed cookies generated by your app in external
43
+ # applications or JavaScript before changing it.
44
+ #
45
+ # Because CookieStore extends Rack::Session::Abstract::Persisted, many of the
46
+ # options described there can be used to customize the session cookie that
47
+ # is generated. For example:
48
+ #
49
+ # Rails.application.config.session_store :cookie_store, expire_after: 14.days
50
+ #
51
+ # would set the session cookie to expire automatically 14 days after creation.
52
+ # Other useful options include <tt>:key</tt>, <tt>:secure</tt> and
53
+ # <tt>:httponly</tt>.
54
+ class CookieStore < AbstractStore
55
+ def initialize(app, options = {})
56
+ super(app, options.merge!(cookie_only: true))
57
+ end
58
+
59
+ def delete_session(req, session_id, options)
60
+ new_sid = generate_sid unless options[:drop]
61
+ # Reset hash and Assign the new session id
62
+ req.set_header("action_dispatch.request.unsigned_session_cookie", new_sid ? { "session_id" => new_sid } : {})
63
+ new_sid
64
+ end
65
+
66
+ def load_session(req)
67
+ stale_session_check! do
68
+ data = unpacked_cookie_data(req)
69
+ data = persistent_session_id!(data)
70
+ [data["session_id"], data]
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def extract_session_id(req)
77
+ stale_session_check! do
78
+ unpacked_cookie_data(req)["session_id"]
79
+ end
80
+ end
81
+
82
+ def unpacked_cookie_data(req)
83
+ req.fetch_header("action_dispatch.request.unsigned_session_cookie") do |k|
84
+ v = stale_session_check! do
85
+ if data = get_cookie(req)
86
+ data.stringify_keys!
87
+ end
88
+ data || {}
89
+ end
90
+ req.set_header k, v
91
+ end
92
+ end
93
+
94
+ def persistent_session_id!(data, sid = nil)
95
+ data ||= {}
96
+ data["session_id"] ||= sid || generate_sid
97
+ data
98
+ end
99
+
100
+ def write_session(req, sid, session_data, options)
101
+ session_data["session_id"] = sid
102
+ session_data
103
+ end
104
+
105
+ def set_cookie(request, session_id, cookie)
106
+ cookie_jar(request)[@key] = cookie
107
+ end
108
+
109
+ def get_cookie(req)
110
+ cookie_jar(req)[@key]
111
+ end
112
+
113
+ def cookie_jar(request)
114
+ request.cookie_jar.signed_or_encrypted
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_dispatch/middleware/session/abstract_store"
4
+ begin
5
+ require "rack/session/dalli"
6
+ rescue LoadError => e
7
+ $stderr.puts "You don't have dalli installed in your application. Please add it to your Gemfile and run bundle install"
8
+ raise e
9
+ end
10
+
11
+ module ActionDispatch
12
+ module Session
13
+ # A session store that uses MemCache to implement storage.
14
+ #
15
+ # ==== Options
16
+ # * <tt>expire_after</tt> - The length of time a session will be stored before automatically expiring.
17
+ class MemCacheStore < Rack::Session::Dalli
18
+ include Compatibility
19
+ include StaleSessionCheck
20
+ include SessionObject
21
+
22
+ def initialize(app, options = {})
23
+ options[:expire_after] ||= options[:expires]
24
+ super
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_dispatch/http/request"
4
+ require "action_dispatch/middleware/exception_wrapper"
5
+
6
+ module ActionDispatch
7
+ # This middleware rescues any exception returned by the application
8
+ # and calls an exceptions app that will wrap it in a format for the end user.
9
+ #
10
+ # The exceptions app should be passed as parameter on initialization
11
+ # of ShowExceptions. Every time there is an exception, ShowExceptions will
12
+ # store the exception in env["action_dispatch.exception"], rewrite the
13
+ # PATH_INFO to the exception status code and call the Rack app.
14
+ #
15
+ # If the application returns a "X-Cascade" pass response, this middleware
16
+ # will send an empty response as result with the correct status code.
17
+ # If any exception happens inside the exceptions app, this middleware
18
+ # catches the exceptions and returns a FAILSAFE_RESPONSE.
19
+ class ShowExceptions
20
+ FAILSAFE_RESPONSE = [500, { "Content-Type" => "text/plain" },
21
+ ["500 Internal Server Error\n" \
22
+ "If you are the administrator of this website, then please read this web " \
23
+ "application's log file and/or the web server's log file to find out what " \
24
+ "went wrong."]]
25
+
26
+ def initialize(app, exceptions_app)
27
+ @app = app
28
+ @exceptions_app = exceptions_app
29
+ end
30
+
31
+ def call(env)
32
+ request = ActionDispatch::Request.new env
33
+ @app.call(env)
34
+ rescue Exception => exception
35
+ if request.show_exceptions?
36
+ render_exception(request, exception)
37
+ else
38
+ raise exception
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def render_exception(request, exception)
45
+ backtrace_cleaner = request.get_header "action_dispatch.backtrace_cleaner"
46
+ wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
47
+ status = wrapper.status_code
48
+ request.set_header "action_dispatch.exception", wrapper.exception
49
+ request.set_header "action_dispatch.original_path", request.path_info
50
+ request.path_info = "/#{status}"
51
+ response = @exceptions_app.call(request.env)
52
+ response[1]["X-Cascade"] == "pass" ? pass_response(status) : response
53
+ rescue Exception => failsafe_error
54
+ $stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}"
55
+ FAILSAFE_RESPONSE
56
+ end
57
+
58
+ def pass_response(status)
59
+ [status, { "Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0" }, []]
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionDispatch
4
+ # This middleware is added to the stack when <tt>config.force_ssl = true</tt>, and is passed
5
+ # the options set in +config.ssl_options+. It does three jobs to enforce secure HTTP
6
+ # requests:
7
+ #
8
+ # 1. <b>TLS redirect</b>: Permanently redirects +http://+ requests to +https://+
9
+ # with the same URL host, path, etc. Enabled by default. Set +config.ssl_options+
10
+ # to modify the destination URL
11
+ # (e.g. <tt>redirect: { host: "secure.widgets.com", port: 8080 }</tt>), or set
12
+ # <tt>redirect: false</tt> to disable this feature.
13
+ #
14
+ # Requests can opt-out of redirection with +exclude+:
15
+ #
16
+ # config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } }
17
+ #
18
+ # Cookies will not be flagged as secure for excluded requests.
19
+ #
20
+ # 2. <b>Secure cookies</b>: Sets the +secure+ flag on cookies to tell browsers they
21
+ # must not be sent along with +http://+ requests. Enabled by default. Set
22
+ # +config.ssl_options+ with <tt>secure_cookies: false</tt> to disable this feature.
23
+ #
24
+ # 3. <b>HTTP Strict Transport Security (HSTS)</b>: Tells the browser to remember
25
+ # this site as TLS-only and automatically redirect non-TLS requests.
26
+ # Enabled by default. Configure +config.ssl_options+ with <tt>hsts: false</tt> to disable.
27
+ #
28
+ # Set +config.ssl_options+ with <tt>hsts: { ... }</tt> to configure HSTS:
29
+ #
30
+ # * +expires+: How long, in seconds, these settings will stick. The minimum
31
+ # required to qualify for browser preload lists is 1 year. Defaults to
32
+ # 1 year (recommended).
33
+ #
34
+ # * +subdomains+: Set to +true+ to tell the browser to apply these settings
35
+ # to all subdomains. This protects your cookies from interception by a
36
+ # vulnerable site on a subdomain. Defaults to +true+.
37
+ #
38
+ # * +preload+: Advertise that this site may be included in browsers'
39
+ # preloaded HSTS lists. HSTS protects your site on every visit <i>except the
40
+ # first visit</i> since it hasn't seen your HSTS header yet. To close this
41
+ # gap, browser vendors include a baked-in list of HSTS-enabled sites.
42
+ # Go to https://hstspreload.org to submit your site for inclusion.
43
+ # Defaults to +false+.
44
+ #
45
+ # To turn off HSTS, omitting the header is not enough. Browsers will remember the
46
+ # original HSTS directive until it expires. Instead, use the header to tell browsers to
47
+ # expire HSTS immediately. Setting <tt>hsts: false</tt> is a shortcut for
48
+ # <tt>hsts: { expires: 0 }</tt>.
49
+ class SSL
50
+ # :stopdoc:
51
+
52
+ # Default to 1 year, the minimum for browser preload lists.
53
+ HSTS_EXPIRES_IN = 31536000
54
+
55
+ def self.default_hsts_options
56
+ { expires: HSTS_EXPIRES_IN, subdomains: true, preload: false }
57
+ end
58
+
59
+ def initialize(app, redirect: {}, hsts: {}, secure_cookies: true)
60
+ @app = app
61
+
62
+ @redirect = redirect
63
+
64
+ @exclude = @redirect && @redirect[:exclude] || proc { !@redirect }
65
+ @secure_cookies = secure_cookies
66
+
67
+ @hsts_header = build_hsts_header(normalize_hsts_options(hsts))
68
+ end
69
+
70
+ def call(env)
71
+ request = Request.new env
72
+
73
+ if request.ssl?
74
+ @app.call(env).tap do |status, headers, body|
75
+ set_hsts_header! headers
76
+ flag_cookies_as_secure! headers if @secure_cookies && !@exclude.call(request)
77
+ end
78
+ else
79
+ return redirect_to_https request unless @exclude.call(request)
80
+ @app.call(env)
81
+ end
82
+ end
83
+
84
+ private
85
+ def set_hsts_header!(headers)
86
+ headers["Strict-Transport-Security".freeze] ||= @hsts_header
87
+ end
88
+
89
+ def normalize_hsts_options(options)
90
+ case options
91
+ # Explicitly disabling HSTS clears the existing setting from browsers
92
+ # by setting expiry to 0.
93
+ when false
94
+ self.class.default_hsts_options.merge(expires: 0)
95
+ # Default to enabled, with default options.
96
+ when nil, true
97
+ self.class.default_hsts_options
98
+ else
99
+ self.class.default_hsts_options.merge(options)
100
+ end
101
+ end
102
+
103
+ # https://tools.ietf.org/html/rfc6797#section-6.1
104
+ def build_hsts_header(hsts)
105
+ value = "max-age=#{hsts[:expires].to_i}".dup
106
+ value << "; includeSubDomains" if hsts[:subdomains]
107
+ value << "; preload" if hsts[:preload]
108
+ value
109
+ end
110
+
111
+ def flag_cookies_as_secure!(headers)
112
+ if cookies = headers["Set-Cookie".freeze]
113
+ cookies = cookies.split("\n".freeze)
114
+
115
+ headers["Set-Cookie".freeze] = cookies.map { |cookie|
116
+ if cookie !~ /;\s*secure\s*(;|$)/i
117
+ "#{cookie}; secure"
118
+ else
119
+ cookie
120
+ end
121
+ }.join("\n".freeze)
122
+ end
123
+ end
124
+
125
+ def redirect_to_https(request)
126
+ [ @redirect.fetch(:status, redirection_status(request)),
127
+ { "Content-Type" => "text/html",
128
+ "Location" => https_location_for(request) },
129
+ @redirect.fetch(:body, []) ]
130
+ end
131
+
132
+ def redirection_status(request)
133
+ if request.get? || request.head?
134
+ 301 # Issue a permanent redirect via a GET request.
135
+ else
136
+ 307 # Issue a fresh request redirect to preserve the HTTP method.
137
+ end
138
+ end
139
+
140
+ def https_location_for(request)
141
+ host = @redirect[:host] || request.host
142
+ port = @redirect[:port] || request.port
143
+
144
+ location = "https://#{host}".dup
145
+ location << ":#{port}" if port != 80 && port != 443
146
+ location << request.fullpath
147
+ location
148
+ end
149
+ end
150
+ end