actionpack 4.2.11.1 → 5.2.4.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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +287 -488
  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 +229 -93
  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 +583 -164
  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 +41 -25
  107. data/lib/action_dispatch/middleware/session/cache_store.rb +24 -14
  108. data/lib/action_dispatch/middleware/session/cookie_store.rb +72 -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 +106 -59
  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 +27 -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 +56 -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.
8
- 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.
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 < AbstractSecureStore
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 = get_session_with_fallback(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)
27
- key = cache_key(sid)
31
+ def write_session(env, sid, session, options)
32
+ key = cache_key(sid.private_id)
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,15 +39,20 @@ module ActionDispatch
34
39
  end
35
40
 
36
41
  # Remove a session from the cache.
37
- def destroy_session(env, sid, options)
38
- @cache.delete(cache_key(sid))
42
+ def delete_session(env, sid, options)
43
+ @cache.delete(cache_key(sid.private_id))
44
+ @cache.delete(cache_key(sid.public_id))
39
45
  generate_sid
40
46
  end
41
47
 
42
48
  private
43
49
  # Turn the session id into a cache key.
44
- def cache_key(sid)
45
- "_session_id:#{sid}"
50
+ def cache_key(id)
51
+ "_session_id:#{id}"
52
+ end
53
+
54
+ def get_session_with_fallback(sid)
55
+ @cache.read(cache_key(sid.private_id)) || @cache.read(cache_key(sid.public_id))
46
56
  end
47
57
  end
48
58
  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,105 +21,108 @@ 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.
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.
35
39
  #
36
- # development:
37
- # secret_key_base: 'secret key'
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.
38
44
  #
39
- # To generate a secret key for an existing application, run `rake secret`.
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:
40
48
  #
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.
49
+ # Rails.application.config.session_store :cookie_store, expire_after: 14.days
53
50
  #
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
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 < AbstractSecureStore
55
+ class SessionId < DelegateClass(Rack::Session::SessionId)
56
+ attr_reader :cookie_value
57
+
58
+ def initialize(session_id, cookie_value = {})
59
+ super(session_id)
60
+ @cookie_value = cookie_value
61
+ end
62
+ end
59
63
 
60
- def initialize(app, options={})
61
- super(app, options.merge!(:cookie_only => true))
64
+ def initialize(app, options = {})
65
+ super(app, options.merge!(cookie_only: true))
62
66
  end
63
67
 
64
- def destroy_session(env, session_id, options)
68
+ def delete_session(req, session_id, options)
65
69
  new_sid = generate_sid unless options[:drop]
66
70
  # Reset hash and Assign the new session id
67
- env["action_dispatch.request.unsigned_session_cookie"] = new_sid ? { "session_id" => new_sid } : {}
71
+ req.set_header("action_dispatch.request.unsigned_session_cookie", new_sid ? { "session_id" => new_sid.public_id } : {})
68
72
  new_sid
69
73
  end
70
74
 
71
- def load_session(env)
75
+ def load_session(req)
72
76
  stale_session_check! do
73
- data = unpacked_cookie_data(env)
77
+ data = unpacked_cookie_data(req)
74
78
  data = persistent_session_id!(data)
75
- [data["session_id"], data]
79
+ [Rack::Session::SessionId.new(data["session_id"]), data]
76
80
  end
77
81
  end
78
82
 
79
83
  private
80
84
 
81
- def extract_session_id(env)
82
- stale_session_check! do
83
- unpacked_cookie_data(env)["session_id"]
85
+ def extract_session_id(req)
86
+ stale_session_check! do
87
+ sid = unpacked_cookie_data(req)["session_id"]
88
+ sid && Rack::Session::SessionId.new(sid)
89
+ end
84
90
  end
85
- end
86
91
 
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!
92
+ def unpacked_cookie_data(req)
93
+ req.fetch_header("action_dispatch.request.unsigned_session_cookie") do |k|
94
+ v = stale_session_check! do
95
+ if data = get_cookie(req)
96
+ data.stringify_keys!
97
+ end
98
+ data || {}
92
99
  end
93
- data || {}
100
+ req.set_header k, v
94
101
  end
95
102
  end
96
- end
97
103
 
98
- def persistent_session_id!(data, sid=nil)
99
- data ||= {}
100
- data["session_id"] ||= sid || generate_sid
101
- data
102
- end
104
+ def persistent_session_id!(data, sid = nil)
105
+ data ||= {}
106
+ data["session_id"] ||= sid || generate_sid.public_id
107
+ data
108
+ end
103
109
 
104
- def set_session(env, sid, session_data, options)
105
- session_data["session_id"] = sid
106
- session_data
107
- end
110
+ def write_session(req, sid, session_data, options)
111
+ session_data["session_id"] = sid.public_id
112
+ SessionId.new(sid, session_data)
113
+ end
108
114
 
109
- def set_cookie(env, session_id, cookie)
110
- cookie_jar(env)[@key] = cookie
111
- end
115
+ def set_cookie(request, session_id, cookie)
116
+ cookie_jar(request)[@key] = cookie
117
+ end
112
118
 
113
- def get_cookie(env)
114
- cookie_jar(env)[@key]
115
- end
119
+ def get_cookie(req)
120
+ cookie_jar(req)[@key]
121
+ end
116
122
 
117
- def cookie_jar(env)
118
- request = ActionDispatch::Request.new(env)
119
- request.cookie_jar.signed_or_encrypted
120
- end
123
+ def cookie_jar(request)
124
+ request.cookie_jar.signed_or_encrypted
125
+ end
121
126
  end
122
127
  end
123
128
  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