actionpack 4.2.11.1 → 5.2.4

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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +279 -497
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -7
  5. data/lib/abstract_controller/asset_paths.rb +2 -0
  6. data/lib/abstract_controller/base.rb +45 -49
  7. data/lib/{action_controller → abstract_controller}/caching/fragments.rb +78 -15
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/abstract_controller/callbacks.rb +47 -31
  10. data/lib/abstract_controller/collector.rb +8 -11
  11. data/lib/abstract_controller/error.rb +6 -0
  12. data/lib/abstract_controller/helpers.rb +25 -25
  13. data/lib/abstract_controller/logger.rb +2 -0
  14. data/lib/abstract_controller/railties/routes_helpers.rb +4 -2
  15. data/lib/abstract_controller/rendering.rb +42 -41
  16. data/lib/abstract_controller/translation.rb +10 -7
  17. data/lib/abstract_controller/url_for.rb +2 -0
  18. data/lib/abstract_controller.rb +12 -5
  19. data/lib/action_controller/api/api_rendering.rb +16 -0
  20. data/lib/action_controller/api.rb +149 -0
  21. data/lib/action_controller/base.rb +27 -19
  22. data/lib/action_controller/caching.rb +14 -57
  23. data/lib/action_controller/form_builder.rb +50 -0
  24. data/lib/action_controller/log_subscriber.rb +10 -15
  25. data/lib/action_controller/metal/basic_implicit_render.rb +13 -0
  26. data/lib/action_controller/metal/conditional_get.rb +118 -44
  27. data/lib/action_controller/metal/content_security_policy.rb +52 -0
  28. data/lib/action_controller/metal/cookies.rb +3 -3
  29. data/lib/action_controller/metal/data_streaming.rb +27 -46
  30. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  31. data/lib/action_controller/metal/etag_with_template_digest.rb +20 -13
  32. data/lib/action_controller/metal/exceptions.rb +8 -14
  33. data/lib/action_controller/metal/flash.rb +4 -3
  34. data/lib/action_controller/metal/force_ssl.rb +23 -21
  35. data/lib/action_controller/metal/head.rb +21 -19
  36. data/lib/action_controller/metal/helpers.rb +24 -14
  37. data/lib/action_controller/metal/http_authentication.rb +64 -57
  38. data/lib/action_controller/metal/implicit_render.rb +62 -8
  39. data/lib/action_controller/metal/instrumentation.rb +19 -21
  40. data/lib/action_controller/metal/live.rb +90 -106
  41. data/lib/action_controller/metal/mime_responds.rb +33 -46
  42. data/lib/action_controller/metal/parameter_encoding.rb +51 -0
  43. data/lib/action_controller/metal/params_wrapper.rb +61 -53
  44. data/lib/action_controller/metal/redirecting.rb +49 -28
  45. data/lib/action_controller/metal/renderers.rb +87 -44
  46. data/lib/action_controller/metal/rendering.rb +72 -50
  47. data/lib/action_controller/metal/request_forgery_protection.rb +203 -92
  48. data/lib/action_controller/metal/rescue.rb +9 -16
  49. data/lib/action_controller/metal/streaming.rb +12 -10
  50. data/lib/action_controller/metal/strong_parameters.rb +582 -165
  51. data/lib/action_controller/metal/testing.rb +2 -17
  52. data/lib/action_controller/metal/url_for.rb +19 -10
  53. data/lib/action_controller/metal.rb +98 -83
  54. data/lib/action_controller/railtie.rb +28 -10
  55. data/lib/action_controller/railties/helpers.rb +2 -0
  56. data/lib/action_controller/renderer.rb +117 -0
  57. data/lib/action_controller/template_assertions.rb +11 -0
  58. data/lib/action_controller/test_case.rb +280 -411
  59. data/lib/action_controller.rb +29 -21
  60. data/lib/action_dispatch/http/cache.rb +93 -47
  61. data/lib/action_dispatch/http/content_security_policy.rb +272 -0
  62. data/lib/action_dispatch/http/filter_parameters.rb +26 -20
  63. data/lib/action_dispatch/http/filter_redirect.rb +10 -11
  64. data/lib/action_dispatch/http/headers.rb +55 -22
  65. data/lib/action_dispatch/http/mime_negotiation.rb +56 -41
  66. data/lib/action_dispatch/http/mime_type.rb +134 -121
  67. data/lib/action_dispatch/http/mime_types.rb +20 -6
  68. data/lib/action_dispatch/http/parameter_filter.rb +25 -11
  69. data/lib/action_dispatch/http/parameters.rb +98 -39
  70. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  71. data/lib/action_dispatch/http/request.rb +200 -118
  72. data/lib/action_dispatch/http/response.rb +225 -110
  73. data/lib/action_dispatch/http/upload.rb +12 -6
  74. data/lib/action_dispatch/http/url.rb +110 -28
  75. data/lib/action_dispatch/journey/formatter.rb +55 -32
  76. data/lib/action_dispatch/journey/gtg/builder.rb +7 -5
  77. data/lib/action_dispatch/journey/gtg/simulator.rb +3 -9
  78. data/lib/action_dispatch/journey/gtg/transition_table.rb +17 -16
  79. data/lib/action_dispatch/journey/nfa/builder.rb +5 -3
  80. data/lib/action_dispatch/journey/nfa/dot.rb +13 -13
  81. data/lib/action_dispatch/journey/nfa/simulator.rb +3 -1
  82. data/lib/action_dispatch/journey/nfa/transition_table.rb +5 -48
  83. data/lib/action_dispatch/journey/nodes/node.rb +18 -6
  84. data/lib/action_dispatch/journey/parser.rb +23 -22
  85. data/lib/action_dispatch/journey/parser.y +3 -2
  86. data/lib/action_dispatch/journey/parser_extras.rb +12 -4
  87. data/lib/action_dispatch/journey/path/pattern.rb +50 -44
  88. data/lib/action_dispatch/journey/route.rb +106 -28
  89. data/lib/action_dispatch/journey/router/utils.rb +20 -11
  90. data/lib/action_dispatch/journey/router.rb +35 -23
  91. data/lib/action_dispatch/journey/routes.rb +18 -16
  92. data/lib/action_dispatch/journey/scanner.rb +18 -15
  93. data/lib/action_dispatch/journey/visitors.rb +99 -52
  94. data/lib/action_dispatch/journey.rb +7 -5
  95. data/lib/action_dispatch/middleware/callbacks.rb +1 -2
  96. data/lib/action_dispatch/middleware/cookies.rb +304 -193
  97. data/lib/action_dispatch/middleware/debug_exceptions.rb +152 -57
  98. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  99. data/lib/action_dispatch/middleware/exception_wrapper.rb +68 -69
  100. data/lib/action_dispatch/middleware/executor.rb +21 -0
  101. data/lib/action_dispatch/middleware/flash.rb +78 -54
  102. data/lib/action_dispatch/middleware/public_exceptions.rb +27 -25
  103. data/lib/action_dispatch/middleware/reloader.rb +5 -91
  104. data/lib/action_dispatch/middleware/remote_ip.rb +41 -31
  105. data/lib/action_dispatch/middleware/request_id.rb +17 -9
  106. data/lib/action_dispatch/middleware/session/abstract_store.rb +28 -26
  107. data/lib/action_dispatch/middleware/session/cache_store.rb +14 -9
  108. data/lib/action_dispatch/middleware/session/cookie_store.rb +62 -67
  109. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +8 -2
  110. data/lib/action_dispatch/middleware/show_exceptions.rb +26 -22
  111. data/lib/action_dispatch/middleware/ssl.rb +114 -36
  112. data/lib/action_dispatch/middleware/stack.rb +31 -44
  113. data/lib/action_dispatch/middleware/static.rb +57 -50
  114. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +2 -14
  115. data/lib/action_dispatch/middleware/templates/rescues/{_source.erb → _source.html.erb} +0 -0
  116. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  117. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +21 -0
  118. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +13 -0
  119. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +1 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -1
  121. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
  122. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +4 -4
  123. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +64 -64
  124. data/lib/action_dispatch/railtie.rb +19 -11
  125. data/lib/action_dispatch/request/session.rb +99 -58
  126. data/lib/action_dispatch/request/utils.rb +67 -24
  127. data/lib/action_dispatch/routing/endpoint.rb +9 -2
  128. data/lib/action_dispatch/routing/inspector.rb +58 -67
  129. data/lib/action_dispatch/routing/mapper.rb +733 -447
  130. data/lib/action_dispatch/routing/polymorphic_routes.rb +161 -139
  131. data/lib/action_dispatch/routing/redirection.rb +36 -26
  132. data/lib/action_dispatch/routing/route_set.rb +321 -291
  133. data/lib/action_dispatch/routing/routes_proxy.rb +32 -5
  134. data/lib/action_dispatch/routing/url_for.rb +65 -25
  135. data/lib/action_dispatch/routing.rb +17 -18
  136. data/lib/action_dispatch/system_test_case.rb +147 -0
  137. data/lib/action_dispatch/system_testing/browser.rb +49 -0
  138. data/lib/action_dispatch/system_testing/driver.rb +59 -0
  139. data/lib/action_dispatch/system_testing/server.rb +31 -0
  140. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +96 -0
  141. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +31 -0
  142. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  143. data/lib/action_dispatch/testing/assertion_response.rb +47 -0
  144. data/lib/action_dispatch/testing/assertions/response.rb +45 -20
  145. data/lib/action_dispatch/testing/assertions/routing.rb +30 -26
  146. data/lib/action_dispatch/testing/assertions.rb +6 -4
  147. data/lib/action_dispatch/testing/integration.rb +347 -209
  148. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  149. data/lib/action_dispatch/testing/test_process.rb +28 -22
  150. data/lib/action_dispatch/testing/test_request.rb +27 -34
  151. data/lib/action_dispatch/testing/test_response.rb +35 -7
  152. data/lib/action_dispatch.rb +26 -19
  153. data/lib/action_pack/gem_version.rb +5 -3
  154. data/lib/action_pack/version.rb +3 -1
  155. data/lib/action_pack.rb +4 -2
  156. metadata +50 -38
  157. data/lib/action_controller/metal/hide_actions.rb +0 -40
  158. data/lib/action_controller/metal/rack_delegation.rb +0 -32
  159. data/lib/action_controller/middleware.rb +0 -39
  160. data/lib/action_controller/model_naming.rb +0 -12
  161. data/lib/action_dispatch/journey/backwards.rb +0 -5
  162. data/lib/action_dispatch/journey/router/strexp.rb +0 -27
  163. data/lib/action_dispatch/middleware/params_parser.rb +0 -60
  164. data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
  165. data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
  166. data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
@@ -1,13 +1,18 @@
1
- require 'action_dispatch/middleware/session/abstract_store'
1
+ # frozen_string_literal: true
2
+
3
+ require "action_dispatch/middleware/session/abstract_store"
2
4
 
3
5
  module ActionDispatch
4
6
  module Session
5
- # Session store that uses an ActiveSupport::Cache::Store to store the sessions. This store is most useful
7
+ # A session store that uses an ActiveSupport::Cache::Store to store the sessions. This store is most useful
6
8
  # if you don't store critical data in your sessions and you don't need them to live for extended periods
7
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.
8
15
  class CacheStore < AbstractStore
9
- # Create a new store. The cache to use can be passed in the <tt>:cache</tt> option. If it is
10
- # not specified, <tt>Rails.cache</tt> will be used.
11
16
  def initialize(app, options = {})
12
17
  @cache = options[:cache] || Rails.cache
13
18
  options[:expire_after] ||= @cache.options[:expires_in]
@@ -15,18 +20,18 @@ module ActionDispatch
15
20
  end
16
21
 
17
22
  # Get a session from the cache.
18
- def get_session(env, sid)
19
- unless sid and session = @cache.read(cache_key(sid))
23
+ def find_session(env, sid)
24
+ unless sid && (session = @cache.read(cache_key(sid)))
20
25
  sid, session = generate_sid, {}
21
26
  end
22
27
  [sid, session]
23
28
  end
24
29
 
25
30
  # Set a session in the cache.
26
- def set_session(env, sid, session, options)
31
+ def write_session(env, sid, session, options)
27
32
  key = cache_key(sid)
28
33
  if session
29
- @cache.write(key, session, :expires_in => options[:expire_after])
34
+ @cache.write(key, session, expires_in: options[:expire_after])
30
35
  else
31
36
  @cache.delete(key)
32
37
  end
@@ -34,7 +39,7 @@ module ActionDispatch
34
39
  end
35
40
 
36
41
  # Remove a session from the cache.
37
- def destroy_session(env, sid, options)
42
+ def delete_session(env, sid, options)
38
43
  @cache.delete(cache_key(sid))
39
44
  generate_sid
40
45
  end
@@ -1,6 +1,8 @@
1
- require 'active_support/core_ext/hash/keys'
2
- require 'action_dispatch/middleware/session/abstract_store'
3
- require 'rack/session/cookie'
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"
4
6
 
5
7
  module ActionDispatch
6
8
  module Session
@@ -19,58 +21,51 @@ module ActionDispatch
19
21
  # knowing your app's secret key, but can easily read their +user_id+. This
20
22
  # was the default for Rails 3 apps.
21
23
  #
22
- # If you have secret_key_base set, your cookies will be encrypted. This
24
+ # Your cookies will be encrypted using your apps secret_key_base. This
23
25
  # goes a step further than signed cookies in that encrypted cookies cannot
24
26
  # be altered or read by users. This is the default starting in Rails 4.
25
27
  #
26
- # If you have both secret_token and secret_key base set, your cookies will
27
- # be encrypted, and signed cookies generated by Rails 3 will be
28
- # transparently read and encrypted to provide a smooth upgrade path.
29
- #
30
- # Configure your session store in config/initializers/session_store.rb:
28
+ # Configure your session store in <tt>config/initializers/session_store.rb</tt>:
31
29
  #
32
30
  # Rails.application.config.session_store :cookie_store, key: '_your_app_session'
33
31
  #
34
- # Configure your secret key in config/secrets.yml:
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.
35
36
  #
36
- # development:
37
- # secret_key_base: 'secret key'
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.
38
39
  #
39
- # To generate a secret key for an existing application, run `rake secret`.
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.
40
44
  #
41
- # If you are upgrading an existing Rails 3 app, you should leave your
42
- # existing secret_token in place and simply add the new secret_key_base.
43
- # Note that you should wait to set secret_key_base until you have 100% of
44
- # your userbase on Rails 4 and are reasonably sure you will not need to
45
- # rollback to Rails 3. This is because cookies signed based on the new
46
- # secret_key_base in Rails 4 are not backwards compatible with Rails 3.
47
- # You are free to leave your existing secret_token in place, not set the
48
- # new secret_key_base, and ignore the deprecation warnings until you are
49
- # reasonably sure that your upgrade is otherwise complete. Additionally,
50
- # you should take care to make sure you are not relying on the ability to
51
- # decode signed cookies generated by your app in external applications or
52
- # JavaScript before upgrading.
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:
53
48
  #
54
- # Note that changing the secret key will invalidate all existing sessions!
55
- class CookieStore < Rack::Session::Abstract::ID
56
- include Compatibility
57
- include StaleSessionCheck
58
- include SessionObject
59
-
60
- def initialize(app, options={})
61
- super(app, options.merge!(:cookie_only => true))
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))
62
57
  end
63
58
 
64
- def destroy_session(env, session_id, options)
59
+ def delete_session(req, session_id, options)
65
60
  new_sid = generate_sid unless options[:drop]
66
61
  # Reset hash and Assign the new session id
67
- env["action_dispatch.request.unsigned_session_cookie"] = new_sid ? { "session_id" => new_sid } : {}
62
+ req.set_header("action_dispatch.request.unsigned_session_cookie", new_sid ? { "session_id" => new_sid } : {})
68
63
  new_sid
69
64
  end
70
65
 
71
- def load_session(env)
66
+ def load_session(req)
72
67
  stale_session_check! do
73
- data = unpacked_cookie_data(env)
68
+ data = unpacked_cookie_data(req)
74
69
  data = persistent_session_id!(data)
75
70
  [data["session_id"], data]
76
71
  end
@@ -78,46 +73,46 @@ module ActionDispatch
78
73
 
79
74
  private
80
75
 
81
- def extract_session_id(env)
82
- stale_session_check! do
83
- unpacked_cookie_data(env)["session_id"]
76
+ def extract_session_id(req)
77
+ stale_session_check! do
78
+ unpacked_cookie_data(req)["session_id"]
79
+ end
84
80
  end
85
- end
86
81
 
87
- def unpacked_cookie_data(env)
88
- env["action_dispatch.request.unsigned_session_cookie"] ||= begin
89
- stale_session_check! do
90
- if data = get_cookie(env)
91
- data.stringify_keys!
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 || {}
92
89
  end
93
- data || {}
90
+ req.set_header k, v
94
91
  end
95
92
  end
96
- end
97
93
 
98
- def persistent_session_id!(data, sid=nil)
99
- data ||= {}
100
- data["session_id"] ||= sid || generate_sid
101
- data
102
- end
94
+ def persistent_session_id!(data, sid = nil)
95
+ data ||= {}
96
+ data["session_id"] ||= sid || generate_sid
97
+ data
98
+ end
103
99
 
104
- def set_session(env, sid, session_data, options)
105
- session_data["session_id"] = sid
106
- session_data
107
- end
100
+ def write_session(req, sid, session_data, options)
101
+ session_data["session_id"] = sid
102
+ session_data
103
+ end
108
104
 
109
- def set_cookie(env, session_id, cookie)
110
- cookie_jar(env)[@key] = cookie
111
- end
105
+ def set_cookie(request, session_id, cookie)
106
+ cookie_jar(request)[@key] = cookie
107
+ end
112
108
 
113
- def get_cookie(env)
114
- cookie_jar(env)[@key]
115
- end
109
+ def get_cookie(req)
110
+ cookie_jar(req)[@key]
111
+ end
116
112
 
117
- def cookie_jar(env)
118
- request = ActionDispatch::Request.new(env)
119
- request.cookie_jar.signed_or_encrypted
120
- end
113
+ def cookie_jar(request)
114
+ request.cookie_jar.signed_or_encrypted
115
+ end
121
116
  end
122
117
  end
123
118
  end
@@ -1,6 +1,8 @@
1
- require 'action_dispatch/middleware/session/abstract_store'
1
+ # frozen_string_literal: true
2
+
3
+ require "action_dispatch/middleware/session/abstract_store"
2
4
  begin
3
- require 'rack/session/dalli'
5
+ require "rack/session/dalli"
4
6
  rescue LoadError => e
5
7
  $stderr.puts "You don't have dalli installed in your application. Please add it to your Gemfile and run bundle install"
6
8
  raise e
@@ -8,6 +10,10 @@ end
8
10
 
9
11
  module ActionDispatch
10
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.
11
17
  class MemCacheStore < Rack::Session::Dalli
12
18
  include Compatibility
13
19
  include StaleSessionCheck
@@ -1,5 +1,7 @@
1
- require 'action_dispatch/http/request'
2
- require 'action_dispatch/middleware/exception_wrapper'
1
+ # frozen_string_literal: true
2
+
3
+ require "action_dispatch/http/request"
4
+ require "action_dispatch/middleware/exception_wrapper"
3
5
 
4
6
  module ActionDispatch
5
7
  # This middleware rescues any exception returned by the application
@@ -8,14 +10,14 @@ module ActionDispatch
8
10
  # The exceptions app should be passed as parameter on initialization
9
11
  # of ShowExceptions. Every time there is an exception, ShowExceptions will
10
12
  # store the exception in env["action_dispatch.exception"], rewrite the
11
- # PATH_INFO to the exception status code and call the rack app.
13
+ # PATH_INFO to the exception status code and call the Rack app.
12
14
  #
13
15
  # If the application returns a "X-Cascade" pass response, this middleware
14
16
  # will send an empty response as result with the correct status code.
15
17
  # If any exception happens inside the exceptions app, this middleware
16
18
  # catches the exceptions and returns a FAILSAFE_RESPONSE.
17
19
  class ShowExceptions
18
- FAILSAFE_RESPONSE = [500, { 'Content-Type' => 'text/plain' },
20
+ FAILSAFE_RESPONSE = [500, { "Content-Type" => "text/plain" },
19
21
  ["500 Internal Server Error\n" \
20
22
  "If you are the administrator of this website, then please read this web " \
21
23
  "application's log file and/or the web server's log file to find out what " \
@@ -27,32 +29,34 @@ module ActionDispatch
27
29
  end
28
30
 
29
31
  def call(env)
32
+ request = ActionDispatch::Request.new env
30
33
  @app.call(env)
31
34
  rescue Exception => exception
32
- if env['action_dispatch.show_exceptions'] == false
33
- raise exception
35
+ if request.show_exceptions?
36
+ render_exception(request, exception)
34
37
  else
35
- render_exception(env, exception)
38
+ raise exception
36
39
  end
37
40
  end
38
41
 
39
42
  private
40
43
 
41
- def render_exception(env, exception)
42
- wrapper = ExceptionWrapper.new(env, exception)
43
- status = wrapper.status_code
44
- env["action_dispatch.exception"] = wrapper.exception
45
- env["action_dispatch.original_path"] = env["PATH_INFO"]
46
- env["PATH_INFO"] = "/#{status}"
47
- response = @exceptions_app.call(env)
48
- response[1]['X-Cascade'] == 'pass' ? pass_response(status) : response
49
- rescue Exception => failsafe_error
50
- $stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}"
51
- FAILSAFE_RESPONSE
52
- end
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
53
57
 
54
- def pass_response(status)
55
- [status, {"Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0"}, []]
56
- end
58
+ def pass_response(status)
59
+ [status, { "Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0" }, []]
60
+ end
57
61
  end
58
62
  end
@@ -1,72 +1,150 @@
1
+ # frozen_string_literal: true
2
+
1
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>.
2
49
  class SSL
3
- YEAR = 31536000
50
+ # :stopdoc:
51
+
52
+ # Default to 1 year, the minimum for browser preload lists.
53
+ HSTS_EXPIRES_IN = 31536000
4
54
 
5
55
  def self.default_hsts_options
6
- { :expires => YEAR, :subdomains => false }
56
+ { expires: HSTS_EXPIRES_IN, subdomains: true, preload: false }
7
57
  end
8
58
 
9
- def initialize(app, options = {})
59
+ def initialize(app, redirect: {}, hsts: {}, secure_cookies: true)
10
60
  @app = app
11
61
 
12
- @hsts = options.fetch(:hsts, {})
13
- @hsts = {} if @hsts == true
14
- @hsts = self.class.default_hsts_options.merge(@hsts) if @hsts
62
+ @redirect = redirect
63
+
64
+ @exclude = @redirect && @redirect[:exclude] || proc { !@redirect }
65
+ @secure_cookies = secure_cookies
15
66
 
16
- @host = options[:host]
17
- @port = options[:port]
67
+ @hsts_header = build_hsts_header(normalize_hsts_options(hsts))
18
68
  end
19
69
 
20
70
  def call(env)
21
- request = Request.new(env)
71
+ request = Request.new env
22
72
 
23
73
  if request.ssl?
24
- status, headers, body = @app.call(env)
25
- headers.reverse_merge!(hsts_headers)
26
- flag_cookies_as_secure!(headers)
27
- [status, headers, body]
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
28
78
  else
29
- redirect_to_https(request)
79
+ return redirect_to_https request unless @exclude.call(request)
80
+ @app.call(env)
30
81
  end
31
82
  end
32
83
 
33
84
  private
34
- def redirect_to_https(request)
35
- host = @host || request.host
36
- port = @port || request.port
37
-
38
- location = "https://#{host}"
39
- location << ":#{port}" if port != 80
40
- location << request.fullpath
41
-
42
- headers = { 'Content-Type' => 'text/html', 'Location' => location }
43
-
44
- [301, headers, []]
85
+ def set_hsts_header!(headers)
86
+ headers["Strict-Transport-Security".freeze] ||= @hsts_header
45
87
  end
46
88
 
47
- # http://tools.ietf.org/html/draft-hodges-strict-transport-sec-02
48
- def hsts_headers
49
- if @hsts
50
- value = "max-age=#{@hsts[:expires].to_i}"
51
- value += "; includeSubDomains" if @hsts[:subdomains]
52
- { 'Strict-Transport-Security' => value }
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
53
98
  else
54
- {}
99
+ self.class.default_hsts_options.merge(options)
55
100
  end
56
101
  end
57
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
+
58
111
  def flag_cookies_as_secure!(headers)
59
- if cookies = headers['Set-Cookie']
60
- cookies = cookies.split("\n")
112
+ if cookies = headers["Set-Cookie".freeze]
113
+ cookies = cookies.split("\n".freeze)
61
114
 
62
- headers['Set-Cookie'] = cookies.map { |cookie|
115
+ headers["Set-Cookie".freeze] = cookies.map { |cookie|
63
116
  if cookie !~ /;\s*secure\s*(;|$)/i
64
117
  "#{cookie}; secure"
65
118
  else
66
119
  cookie
67
120
  end
68
- }.join("\n")
121
+ }.join("\n".freeze)
69
122
  end
70
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
71
149
  end
72
150
  end
@@ -1,28 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/inflector/methods"
2
4
  require "active_support/dependencies"
3
5
 
4
6
  module ActionDispatch
5
7
  class MiddlewareStack
6
8
  class Middleware
7
- attr_reader :args, :block, :name, :classcache
8
-
9
- def initialize(klass_or_name, *args, &block)
10
- @klass = nil
11
-
12
- if klass_or_name.respond_to?(:name)
13
- @klass = klass_or_name
14
- @name = @klass.name
15
- else
16
- @name = klass_or_name.to_s
17
- end
9
+ attr_reader :args, :block, :klass
18
10
 
19
- @classcache = ActiveSupport::Dependencies::Reference
20
- @args, @block = args, block
11
+ def initialize(klass, args, block)
12
+ @klass = klass
13
+ @args = args
14
+ @block = block
21
15
  end
22
16
 
23
- def klass
24
- @klass || classcache[@name]
25
- end
17
+ def name; klass.name; end
26
18
 
27
19
  def ==(middleware)
28
20
  case middleware
@@ -30,24 +22,20 @@ module ActionDispatch
30
22
  klass == middleware.klass
31
23
  when Class
32
24
  klass == middleware
33
- else
34
- normalize(@name) == normalize(middleware)
35
25
  end
36
26
  end
37
27
 
38
28
  def inspect
39
- klass.to_s
29
+ if klass.is_a?(Class)
30
+ klass.to_s
31
+ else
32
+ klass.class.to_s
33
+ end
40
34
  end
41
35
 
42
36
  def build(app)
43
37
  klass.new(app, *args, &block)
44
38
  end
45
-
46
- private
47
-
48
- def normalize(object)
49
- object.to_s.strip.sub(/^::/, '')
50
- end
51
39
  end
52
40
 
53
41
  include Enumerable
@@ -75,19 +63,17 @@ module ActionDispatch
75
63
  middlewares[i]
76
64
  end
77
65
 
78
- def unshift(*args, &block)
79
- middleware = self.class::Middleware.new(*args, &block)
80
- middlewares.unshift(middleware)
66
+ def unshift(klass, *args, &block)
67
+ middlewares.unshift(build_middleware(klass, args, block))
81
68
  end
82
69
 
83
70
  def initialize_copy(other)
84
71
  self.middlewares = other.middlewares.dup
85
72
  end
86
73
 
87
- def insert(index, *args, &block)
74
+ def insert(index, klass, *args, &block)
88
75
  index = assert_index(index, :before)
89
- middleware = self.class::Middleware.new(*args, &block)
90
- middlewares.insert(index, middleware)
76
+ middlewares.insert(index, build_middleware(klass, args, block))
91
77
  end
92
78
 
93
79
  alias_method :insert_before, :insert
@@ -104,26 +90,27 @@ module ActionDispatch
104
90
  end
105
91
 
106
92
  def delete(target)
107
- middlewares.delete target
93
+ middlewares.delete_if { |m| m.klass == target }
108
94
  end
109
95
 
110
- def use(*args, &block)
111
- middleware = self.class::Middleware.new(*args, &block)
112
- middlewares.push(middleware)
96
+ def use(klass, *args, &block)
97
+ middlewares.push(build_middleware(klass, args, block))
113
98
  end
114
99
 
115
100
  def build(app = nil, &block)
116
- app ||= block
117
- raise "MiddlewareStack#build requires an app" unless app
118
- middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }
101
+ middlewares.freeze.reverse.inject(app || block) { |a, e| e.build(a) }
119
102
  end
120
103
 
121
- protected
104
+ private
122
105
 
123
- def assert_index(index, where)
124
- i = index.is_a?(Integer) ? index : middlewares.index(index)
125
- raise "No such middleware to insert #{where}: #{index.inspect}" unless i
126
- i
127
- end
106
+ def assert_index(index, where)
107
+ i = index.is_a?(Integer) ? index : middlewares.index { |m| m.klass == index }
108
+ raise "No such middleware to insert #{where}: #{index.inspect}" unless i
109
+ i
110
+ end
111
+
112
+ def build_middleware(klass, args, block)
113
+ Middleware.new(klass, args, block)
114
+ end
128
115
  end
129
116
  end