actionpack 4.2.8 → 5.2.4.2

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 +5 -5
  2. data/CHANGELOG.md +285 -444
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -7
  5. data/lib/abstract_controller.rb +12 -5
  6. data/lib/abstract_controller/asset_paths.rb +2 -0
  7. data/lib/abstract_controller/base.rb +45 -49
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/{action_controller → abstract_controller}/caching/fragments.rb +78 -15
  10. data/lib/abstract_controller/callbacks.rb +47 -31
  11. data/lib/abstract_controller/collector.rb +8 -11
  12. data/lib/abstract_controller/error.rb +6 -0
  13. data/lib/abstract_controller/helpers.rb +25 -25
  14. data/lib/abstract_controller/logger.rb +2 -0
  15. data/lib/abstract_controller/railties/routes_helpers.rb +4 -2
  16. data/lib/abstract_controller/rendering.rb +42 -41
  17. data/lib/abstract_controller/translation.rb +10 -7
  18. data/lib/abstract_controller/url_for.rb +2 -0
  19. data/lib/action_controller.rb +29 -21
  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 +27 -19
  23. data/lib/action_controller/caching.rb +14 -57
  24. data/lib/action_controller/form_builder.rb +50 -0
  25. data/lib/action_controller/log_subscriber.rb +10 -15
  26. data/lib/action_controller/metal.rb +98 -83
  27. data/lib/action_controller/metal/basic_implicit_render.rb +13 -0
  28. data/lib/action_controller/metal/conditional_get.rb +118 -44
  29. data/lib/action_controller/metal/content_security_policy.rb +52 -0
  30. data/lib/action_controller/metal/cookies.rb +3 -3
  31. data/lib/action_controller/metal/data_streaming.rb +27 -46
  32. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  33. data/lib/action_controller/metal/etag_with_template_digest.rb +20 -13
  34. data/lib/action_controller/metal/exceptions.rb +8 -14
  35. data/lib/action_controller/metal/flash.rb +4 -3
  36. data/lib/action_controller/metal/force_ssl.rb +23 -21
  37. data/lib/action_controller/metal/head.rb +21 -19
  38. data/lib/action_controller/metal/helpers.rb +24 -14
  39. data/lib/action_controller/metal/http_authentication.rb +64 -57
  40. data/lib/action_controller/metal/implicit_render.rb +62 -8
  41. data/lib/action_controller/metal/instrumentation.rb +19 -21
  42. data/lib/action_controller/metal/live.rb +90 -106
  43. data/lib/action_controller/metal/mime_responds.rb +33 -46
  44. data/lib/action_controller/metal/parameter_encoding.rb +51 -0
  45. data/lib/action_controller/metal/params_wrapper.rb +61 -53
  46. data/lib/action_controller/metal/redirecting.rb +49 -28
  47. data/lib/action_controller/metal/renderers.rb +87 -44
  48. data/lib/action_controller/metal/rendering.rb +72 -50
  49. data/lib/action_controller/metal/request_forgery_protection.rb +203 -92
  50. data/lib/action_controller/metal/rescue.rb +9 -16
  51. data/lib/action_controller/metal/streaming.rb +12 -10
  52. data/lib/action_controller/metal/strong_parameters.rb +582 -165
  53. data/lib/action_controller/metal/testing.rb +2 -17
  54. data/lib/action_controller/metal/url_for.rb +19 -10
  55. data/lib/action_controller/railtie.rb +28 -10
  56. data/lib/action_controller/railties/helpers.rb +2 -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 +280 -411
  60. data/lib/action_dispatch.rb +27 -19
  61. data/lib/action_dispatch/http/cache.rb +93 -47
  62. data/lib/action_dispatch/http/content_security_policy.rb +272 -0
  63. data/lib/action_dispatch/http/filter_parameters.rb +26 -20
  64. data/lib/action_dispatch/http/filter_redirect.rb +10 -11
  65. data/lib/action_dispatch/http/headers.rb +55 -22
  66. data/lib/action_dispatch/http/mime_negotiation.rb +60 -41
  67. data/lib/action_dispatch/http/mime_type.rb +134 -121
  68. data/lib/action_dispatch/http/mime_types.rb +20 -6
  69. data/lib/action_dispatch/http/parameter_filter.rb +25 -11
  70. data/lib/action_dispatch/http/parameters.rb +98 -39
  71. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  72. data/lib/action_dispatch/http/request.rb +200 -118
  73. data/lib/action_dispatch/http/response.rb +225 -110
  74. data/lib/action_dispatch/http/upload.rb +12 -6
  75. data/lib/action_dispatch/http/url.rb +110 -28
  76. data/lib/action_dispatch/journey.rb +7 -5
  77. data/lib/action_dispatch/journey/formatter.rb +55 -32
  78. data/lib/action_dispatch/journey/gtg/builder.rb +7 -5
  79. data/lib/action_dispatch/journey/gtg/simulator.rb +3 -9
  80. data/lib/action_dispatch/journey/gtg/transition_table.rb +17 -16
  81. data/lib/action_dispatch/journey/nfa/builder.rb +5 -3
  82. data/lib/action_dispatch/journey/nfa/dot.rb +13 -13
  83. data/lib/action_dispatch/journey/nfa/simulator.rb +3 -1
  84. data/lib/action_dispatch/journey/nfa/transition_table.rb +5 -48
  85. data/lib/action_dispatch/journey/nodes/node.rb +18 -6
  86. data/lib/action_dispatch/journey/parser.rb +23 -22
  87. data/lib/action_dispatch/journey/parser.y +3 -2
  88. data/lib/action_dispatch/journey/parser_extras.rb +12 -4
  89. data/lib/action_dispatch/journey/path/pattern.rb +50 -44
  90. data/lib/action_dispatch/journey/route.rb +106 -28
  91. data/lib/action_dispatch/journey/router.rb +35 -23
  92. data/lib/action_dispatch/journey/router/utils.rb +20 -11
  93. data/lib/action_dispatch/journey/routes.rb +18 -16
  94. data/lib/action_dispatch/journey/scanner.rb +18 -15
  95. data/lib/action_dispatch/journey/visitors.rb +99 -52
  96. data/lib/action_dispatch/middleware/callbacks.rb +1 -2
  97. data/lib/action_dispatch/middleware/cookies.rb +304 -193
  98. data/lib/action_dispatch/middleware/debug_exceptions.rb +152 -57
  99. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  100. data/lib/action_dispatch/middleware/exception_wrapper.rb +68 -69
  101. data/lib/action_dispatch/middleware/executor.rb +21 -0
  102. data/lib/action_dispatch/middleware/flash.rb +78 -54
  103. data/lib/action_dispatch/middleware/public_exceptions.rb +27 -25
  104. data/lib/action_dispatch/middleware/reloader.rb +5 -91
  105. data/lib/action_dispatch/middleware/remote_ip.rb +41 -31
  106. data/lib/action_dispatch/middleware/request_id.rb +17 -9
  107. data/lib/action_dispatch/middleware/session/abstract_store.rb +41 -25
  108. data/lib/action_dispatch/middleware/session/cache_store.rb +24 -14
  109. data/lib/action_dispatch/middleware/session/cookie_store.rb +72 -67
  110. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +8 -2
  111. data/lib/action_dispatch/middleware/show_exceptions.rb +26 -22
  112. data/lib/action_dispatch/middleware/ssl.rb +114 -36
  113. data/lib/action_dispatch/middleware/stack.rb +31 -44
  114. data/lib/action_dispatch/middleware/static.rb +57 -50
  115. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +2 -14
  116. data/lib/action_dispatch/middleware/templates/rescues/{_source.erb → _source.html.erb} +0 -0
  117. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  118. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +21 -0
  119. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +13 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +1 -0
  121. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -1
  122. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
  123. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +4 -4
  124. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +64 -64
  125. data/lib/action_dispatch/railtie.rb +19 -11
  126. data/lib/action_dispatch/request/session.rb +106 -59
  127. data/lib/action_dispatch/request/utils.rb +67 -24
  128. data/lib/action_dispatch/routing.rb +17 -18
  129. data/lib/action_dispatch/routing/endpoint.rb +9 -2
  130. data/lib/action_dispatch/routing/inspector.rb +58 -67
  131. data/lib/action_dispatch/routing/mapper.rb +734 -447
  132. data/lib/action_dispatch/routing/polymorphic_routes.rb +161 -139
  133. data/lib/action_dispatch/routing/redirection.rb +36 -26
  134. data/lib/action_dispatch/routing/route_set.rb +321 -291
  135. data/lib/action_dispatch/routing/routes_proxy.rb +32 -5
  136. data/lib/action_dispatch/routing/url_for.rb +65 -25
  137. data/lib/action_dispatch/system_test_case.rb +147 -0
  138. data/lib/action_dispatch/system_testing/browser.rb +49 -0
  139. data/lib/action_dispatch/system_testing/driver.rb +59 -0
  140. data/lib/action_dispatch/system_testing/server.rb +31 -0
  141. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +96 -0
  142. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +31 -0
  143. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  144. data/lib/action_dispatch/testing/assertion_response.rb +47 -0
  145. data/lib/action_dispatch/testing/assertions.rb +6 -4
  146. data/lib/action_dispatch/testing/assertions/response.rb +45 -20
  147. data/lib/action_dispatch/testing/assertions/routing.rb +30 -26
  148. data/lib/action_dispatch/testing/integration.rb +347 -209
  149. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  150. data/lib/action_dispatch/testing/test_process.rb +28 -22
  151. data/lib/action_dispatch/testing/test_request.rb +27 -34
  152. data/lib/action_dispatch/testing/test_response.rb +35 -7
  153. data/lib/action_pack.rb +4 -2
  154. data/lib/action_pack/gem_version.rb +5 -3
  155. data/lib/action_pack/version.rb +3 -1
  156. metadata +56 -39
  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,15 +1,87 @@
1
- require 'active_support/core_ext/hash/keys'
2
- require 'active_support/core_ext/module/attribute_accessors'
3
- require 'active_support/core_ext/object/blank'
4
- require 'active_support/key_generator'
5
- require 'active_support/message_verifier'
6
- require 'active_support/json'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/keys"
4
+ require "active_support/key_generator"
5
+ require "active_support/message_verifier"
6
+ require "active_support/json"
7
+ require "rack/utils"
7
8
 
8
9
  module ActionDispatch
9
- class Request < Rack::Request
10
+ class Request
10
11
  def cookie_jar
11
- env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(self)
12
+ fetch_header("action_dispatch.cookies".freeze) do
13
+ self.cookie_jar = Cookies::CookieJar.build(self, cookies)
14
+ end
15
+ end
16
+
17
+ # :stopdoc:
18
+ prepend Module.new {
19
+ def commit_cookie_jar!
20
+ cookie_jar.commit!
21
+ end
22
+ }
23
+
24
+ def have_cookie_jar?
25
+ has_header? "action_dispatch.cookies".freeze
26
+ end
27
+
28
+ def cookie_jar=(jar)
29
+ set_header "action_dispatch.cookies".freeze, jar
30
+ end
31
+
32
+ def key_generator
33
+ get_header Cookies::GENERATOR_KEY
34
+ end
35
+
36
+ def signed_cookie_salt
37
+ get_header Cookies::SIGNED_COOKIE_SALT
38
+ end
39
+
40
+ def encrypted_cookie_salt
41
+ get_header Cookies::ENCRYPTED_COOKIE_SALT
42
+ end
43
+
44
+ def encrypted_signed_cookie_salt
45
+ get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT
46
+ end
47
+
48
+ def authenticated_encrypted_cookie_salt
49
+ get_header Cookies::AUTHENTICATED_ENCRYPTED_COOKIE_SALT
12
50
  end
51
+
52
+ def use_authenticated_cookie_encryption
53
+ get_header Cookies::USE_AUTHENTICATED_COOKIE_ENCRYPTION
54
+ end
55
+
56
+ def encrypted_cookie_cipher
57
+ get_header Cookies::ENCRYPTED_COOKIE_CIPHER
58
+ end
59
+
60
+ def signed_cookie_digest
61
+ get_header Cookies::SIGNED_COOKIE_DIGEST
62
+ end
63
+
64
+ def secret_token
65
+ get_header Cookies::SECRET_TOKEN
66
+ end
67
+
68
+ def secret_key_base
69
+ get_header Cookies::SECRET_KEY_BASE
70
+ end
71
+
72
+ def cookies_serializer
73
+ get_header Cookies::COOKIES_SERIALIZER
74
+ end
75
+
76
+ def cookies_digest
77
+ get_header Cookies::COOKIES_DIGEST
78
+ end
79
+
80
+ def cookies_rotations
81
+ get_header Cookies::COOKIES_ROTATIONS
82
+ end
83
+
84
+ # :startdoc:
13
85
  end
14
86
 
15
87
  # \Cookies are read and written through ActionController#cookies.
@@ -28,18 +100,25 @@ module ActionDispatch
28
100
  # cookies[:lat_lon] = JSON.generate([47.68, -122.37])
29
101
  #
30
102
  # # Sets a cookie that expires in 1 hour.
31
- # cookies[:login] = { value: "XJ-122", expires: 1.hour.from_now }
103
+ # cookies[:login] = { value: "XJ-122", expires: 1.hour }
104
+ #
105
+ # # Sets a cookie that expires at a specific time.
106
+ # cookies[:login] = { value: "XJ-122", expires: Time.utc(2020, 10, 15, 5) }
32
107
  #
33
108
  # # Sets a signed cookie, which prevents users from tampering with its value.
34
- # # The cookie is signed by your app's `secrets.secret_key_base` value.
35
109
  # # It can be read using the signed method `cookies.signed[:name]`
36
110
  # cookies.signed[:user_id] = current_user.id
37
111
  #
112
+ # # Sets an encrypted cookie value before sending it to the client which
113
+ # # prevent users from reading and tampering with its value.
114
+ # # It can be read using the encrypted method `cookies.encrypted[:name]`
115
+ # cookies.encrypted[:discount] = 45
116
+ #
38
117
  # # Sets a "permanent" cookie (which expires in 20 years from now).
39
118
  # cookies.permanent[:login] = "XJ-122"
40
119
  #
41
120
  # # You can also chain these methods:
42
- # cookies.permanent.signed[:login] = "XJ-122"
121
+ # cookies.signed.permanent[:login] = "XJ-122"
43
122
  #
44
123
  # Examples of reading:
45
124
  #
@@ -47,6 +126,7 @@ module ActionDispatch
47
126
  # cookies.size # => 2
48
127
  # JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37]
49
128
  # cookies.signed[:login] # => "XJ-122"
129
+ # cookies.encrypted[:discount] # => 45
50
130
  #
51
131
  # Example for deleting:
52
132
  #
@@ -56,7 +136,7 @@ module ActionDispatch
56
136
  #
57
137
  # cookies[:name] = {
58
138
  # value: 'a yummy cookie',
59
- # expires: 1.year.from_now,
139
+ # expires: 1.year,
60
140
  # domain: 'domain.com'
61
141
  # }
62
142
  #
@@ -73,13 +153,16 @@ module ActionDispatch
73
153
  # to <tt>:all</tt>. Make sure to specify the <tt>:domain</tt> option with
74
154
  # <tt>:all</tt> or <tt>Array</tt> again when deleting cookies.
75
155
  #
76
- # domain: nil # Does not sets cookie domain. (default)
156
+ # domain: nil # Does not set cookie domain. (default)
77
157
  # domain: :all # Allow the cookie for the top most level
78
158
  # # domain and subdomains.
79
159
  # domain: %w(.example.com .example.org) # Allow the cookie
80
160
  # # for concrete domain names.
81
161
  #
82
- # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object.
162
+ # * <tt>:tld_length</tt> - When using <tt>:domain => :all</tt>, this option can be used to explicitly
163
+ # set the TLD length when using a short (<= 3 character) domain that is being interpreted as part of a TLD.
164
+ # For example, to share cookies between user1.lvh.me and user2.lvh.me, set <tt>:tld_length</tt> to 2.
165
+ # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time or ActiveSupport::Duration object.
83
166
  # * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers.
84
167
  # Default is +false+.
85
168
  # * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or
@@ -90,10 +173,15 @@ module ActionDispatch
90
173
  SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze
91
174
  ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze
92
175
  ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze
176
+ AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt".freeze
177
+ USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption".freeze
178
+ ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher".freeze
179
+ SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest".freeze
93
180
  SECRET_TOKEN = "action_dispatch.secret_token".freeze
94
181
  SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
95
182
  COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze
96
183
  COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze
184
+ COOKIES_ROTATIONS = "action_dispatch.cookies_rotations".freeze
97
185
 
98
186
  # Cookies can typically store 4096 bytes.
99
187
  MAX_COOKIE_SIZE = 4096
@@ -101,7 +189,7 @@ module ActionDispatch
101
189
  # Raised when storing more than 4K of session data.
102
190
  CookieOverflow = Class.new StandardError
103
191
 
104
- # Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed
192
+ # Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed.
105
193
  module ChainedCookieJars
106
194
  # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example:
107
195
  #
@@ -115,17 +203,17 @@ module ActionDispatch
115
203
  # cookies.permanent.signed[:remember_me] = current_user.id
116
204
  # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
117
205
  def permanent
118
- @permanent ||= PermanentCookieJar.new(self, @key_generator, @options)
206
+ @permanent ||= PermanentCookieJar.new(self)
119
207
  end
120
208
 
121
209
  # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
122
210
  # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
123
- # cookie was tampered with by the user (or a 3rd party), nil will be returned.
211
+ # cookie was tampered with by the user (or a 3rd party), +nil+ will be returned.
124
212
  #
125
- # If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
213
+ # If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
126
214
  # legacy cookies signed with the old key generator will be transparently upgraded.
127
215
  #
128
- # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+.
216
+ # This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+.
129
217
  #
130
218
  # Example:
131
219
  #
@@ -134,66 +222,61 @@ module ActionDispatch
134
222
  #
135
223
  # cookies.signed[:discount] # => 45
136
224
  def signed
137
- @signed ||=
138
- if @options[:upgrade_legacy_signed_cookies]
139
- UpgradeLegacySignedCookieJar.new(self, @key_generator, @options)
140
- else
141
- SignedCookieJar.new(self, @key_generator, @options)
142
- end
225
+ @signed ||= SignedKeyRotatingCookieJar.new(self)
143
226
  end
144
227
 
145
228
  # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read.
146
- # If the cookie was tampered with by the user (or a 3rd party), nil will be returned.
229
+ # If the cookie was tampered with by the user (or a 3rd party), +nil+ will be returned.
147
230
  #
148
- # If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
231
+ # If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
149
232
  # legacy cookies signed with the old key generator will be transparently upgraded.
150
233
  #
151
- # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+.
234
+ # If +config.action_dispatch.encrypted_cookie_salt+ and +config.action_dispatch.encrypted_signed_cookie_salt+
235
+ # are both set, legacy cookies encrypted with HMAC AES-256-CBC will be transparently upgraded.
236
+ #
237
+ # This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+.
152
238
  #
153
239
  # Example:
154
240
  #
155
241
  # cookies.encrypted[:discount] = 45
156
- # # => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/
242
+ # # => Set-Cookie: discount=DIQ7fw==--K3n//8vvnSbGq9dA--7Xh91HfLpwzbj1czhBiwOg==; path=/
157
243
  #
158
244
  # cookies.encrypted[:discount] # => 45
159
245
  def encrypted
160
- @encrypted ||=
161
- if @options[:upgrade_legacy_signed_cookies]
162
- UpgradeLegacyEncryptedCookieJar.new(self, @key_generator, @options)
163
- else
164
- EncryptedCookieJar.new(self, @key_generator, @options)
165
- end
246
+ @encrypted ||= EncryptedKeyRotatingCookieJar.new(self)
166
247
  end
167
248
 
168
249
  # Returns the +signed+ or +encrypted+ jar, preferring +encrypted+ if +secret_key_base+ is set.
169
250
  # Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores.
170
251
  def signed_or_encrypted
171
252
  @signed_or_encrypted ||=
172
- if @options[:secret_key_base].present?
253
+ if request.secret_key_base.present?
173
254
  encrypted
174
255
  else
175
256
  signed
176
257
  end
177
258
  end
178
- end
179
259
 
180
- # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream
181
- # to the Message{Encryptor,Verifier} allows us to handle the
182
- # (de)serialization step within the cookie jar, which gives us the
183
- # opportunity to detect and migrate legacy cookies.
184
- module VerifyAndUpgradeLegacySignedMessage # :nodoc:
185
- def initialize(*args)
186
- super
187
- @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: ActiveSupport::MessageEncryptor::NullSerializer)
188
- end
260
+ private
189
261
 
190
- def verify_and_upgrade_legacy_signed_message(name, signed_message)
191
- deserialize(name, @legacy_verifier.verify(signed_message)).tap do |value|
192
- self[name] = { value: value }
262
+ def upgrade_legacy_signed_cookies?
263
+ request.secret_token.present? && request.secret_key_base.present?
264
+ end
265
+
266
+ def upgrade_legacy_hmac_aes_cbc_cookies?
267
+ request.secret_key_base.present? &&
268
+ request.encrypted_signed_cookie_salt.present? &&
269
+ request.encrypted_cookie_salt.present? &&
270
+ request.use_authenticated_cookie_encryption
271
+ end
272
+
273
+ def encrypted_cookie_cipher
274
+ request.encrypted_cookie_cipher || "aes-256-gcm"
275
+ end
276
+
277
+ def signed_cookie_digest
278
+ request.signed_cookie_digest || "SHA1"
193
279
  end
194
- rescue ActiveSupport::MessageVerifier::InvalidSignature
195
- nil
196
- end
197
280
  end
198
281
 
199
282
  class CookieJar #:nodoc:
@@ -213,38 +296,18 @@ module ActionDispatch
213
296
  # $& => example.local
214
297
  DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/
215
298
 
216
- def self.options_for_env(env) #:nodoc:
217
- { signed_cookie_salt: env[SIGNED_COOKIE_SALT] || '',
218
- encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT] || '',
219
- encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '',
220
- secret_token: env[SECRET_TOKEN],
221
- secret_key_base: env[SECRET_KEY_BASE],
222
- upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?,
223
- serializer: env[COOKIES_SERIALIZER],
224
- digest: env[COOKIES_DIGEST]
225
- }
226
- end
227
-
228
- def self.build(request)
229
- env = request.env
230
- key_generator = env[GENERATOR_KEY]
231
- options = options_for_env env
232
-
233
- host = request.host
234
- secure = request.ssl?
235
-
236
- new(key_generator, host, secure, options).tap do |hash|
237
- hash.update(request.cookies)
299
+ def self.build(req, cookies)
300
+ new(req).tap do |hash|
301
+ hash.update(cookies)
238
302
  end
239
303
  end
240
304
 
241
- def initialize(key_generator, host = nil, secure = false, options = {})
242
- @key_generator = key_generator
305
+ attr_reader :request
306
+
307
+ def initialize(request)
243
308
  @set_cookies = {}
244
309
  @delete_cookies = {}
245
- @host = host
246
- @secure = secure
247
- @options = options
310
+ @request = request
248
311
  @cookies = {}
249
312
  @committed = false
250
313
  end
@@ -275,26 +338,44 @@ module ActionDispatch
275
338
  end
276
339
  alias :has_key? :key?
277
340
 
341
+ # Returns the cookies as Hash.
342
+ alias :to_hash :to_h
343
+
278
344
  def update(other_hash)
279
345
  @cookies.update other_hash.stringify_keys
280
346
  self
281
347
  end
282
348
 
283
- def handle_options(options) #:nodoc:
349
+ def update_cookies_from_jar
350
+ request_jar = @request.cookie_jar.instance_variable_get(:@cookies)
351
+ set_cookies = request_jar.reject { |k, _| @delete_cookies.key?(k) }
352
+
353
+ @cookies.update set_cookies if set_cookies
354
+ end
355
+
356
+ def to_header
357
+ @cookies.map { |k, v| "#{escape(k)}=#{escape(v)}" }.join "; "
358
+ end
359
+
360
+ def handle_options(options) # :nodoc:
361
+ if options[:expires].respond_to?(:from_now)
362
+ options[:expires] = options[:expires].from_now
363
+ end
364
+
284
365
  options[:path] ||= "/"
285
366
 
286
- if options[:domain] == :all
287
- # if there is a provided tld length then we use it otherwise default domain regexp
367
+ if options[:domain] == :all || options[:domain] == "all"
368
+ # If there is a provided tld length then we use it otherwise default domain regexp.
288
369
  domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
289
370
 
290
- # if host is not ip and matches domain regexp
371
+ # If host is not ip and matches domain regexp.
291
372
  # (ip confirms to domain regexp so we explicitly check for ip)
292
- options[:domain] = if (@host !~ /^[\d.]+$/) && (@host =~ domain_regexp)
373
+ options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
293
374
  ".#{$&}"
294
375
  end
295
376
  elsif options[:domain].is_a? Array
296
- # if host matches one of the supplied domains without a dot in front of it
297
- options[:domain] = options[:domain].find {|domain| @host.include? domain.sub(/^\./, '') }
377
+ # If host matches one of the supplied domains without a dot in front of it.
378
+ options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
298
379
  end
299
380
  end
300
381
 
@@ -306,7 +387,7 @@ module ActionDispatch
306
387
  value = options[:value]
307
388
  else
308
389
  value = options
309
- options = { :value => value }
390
+ options = { value: value }
310
391
  end
311
392
 
312
393
  handle_options(options)
@@ -343,53 +424,92 @@ module ActionDispatch
343
424
  @delete_cookies[name.to_s] == options
344
425
  end
345
426
 
346
- # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie
427
+ # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie.
347
428
  def clear(options = {})
348
- @cookies.each_key{ |k| delete(k, options) }
429
+ @cookies.each_key { |k| delete(k, options) }
349
430
  end
350
431
 
351
432
  def write(headers)
352
- @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) }
353
- @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) }
354
- end
355
-
356
- def recycle! #:nodoc:
357
- @set_cookies = {}
358
- @delete_cookies = {}
433
+ if header = make_set_cookie_header(headers[HTTP_HEADER])
434
+ headers[HTTP_HEADER] = header
435
+ end
359
436
  end
360
437
 
361
- mattr_accessor :always_write_cookie
362
- self.always_write_cookie = false
438
+ mattr_accessor :always_write_cookie, default: false
363
439
 
364
440
  private
441
+
442
+ def escape(string)
443
+ ::Rack::Utils.escape(string)
444
+ end
445
+
446
+ def make_set_cookie_header(header)
447
+ header = @set_cookies.inject(header) { |m, (k, v)|
448
+ if write_cookie?(v)
449
+ ::Rack::Utils.add_cookie_to_header(m, k, v)
450
+ else
451
+ m
452
+ end
453
+ }
454
+ @delete_cookies.inject(header) { |m, (k, v)|
455
+ ::Rack::Utils.add_remove_cookie_to_header(m, k, v)
456
+ }
457
+ end
458
+
365
459
  def write_cookie?(cookie)
366
- @secure || !cookie[:secure] || always_write_cookie
460
+ request.ssl? || !cookie[:secure] || always_write_cookie
367
461
  end
368
462
  end
369
463
 
370
- class PermanentCookieJar #:nodoc:
464
+ class AbstractCookieJar # :nodoc:
371
465
  include ChainedCookieJars
372
466
 
373
- def initialize(parent_jar, key_generator, options = {})
467
+ def initialize(parent_jar)
374
468
  @parent_jar = parent_jar
375
- @key_generator = key_generator
376
- @options = options
377
469
  end
378
470
 
379
471
  def [](name)
380
- @parent_jar[name.to_s]
472
+ if data = @parent_jar[name.to_s]
473
+ parse name, data
474
+ end
381
475
  end
382
476
 
383
477
  def []=(name, options)
384
478
  if options.is_a?(Hash)
385
479
  options.symbolize_keys!
386
480
  else
387
- options = { :value => options }
481
+ options = { value: options }
388
482
  end
389
483
 
390
- options[:expires] = 20.years.from_now
484
+ commit(options)
391
485
  @parent_jar[name] = options
392
486
  end
487
+
488
+ protected
489
+ def request; @parent_jar.request; end
490
+
491
+ private
492
+ def expiry_options(options)
493
+ if request.use_authenticated_cookie_encryption
494
+ if options[:expires].respond_to?(:from_now)
495
+ { expires_in: options[:expires] }
496
+ else
497
+ { expires_at: options[:expires] }
498
+ end
499
+ else
500
+ {}
501
+ end
502
+ end
503
+
504
+ def parse(name, data); data; end
505
+ def commit(options); end
506
+ end
507
+
508
+ class PermanentCookieJar < AbstractCookieJar # :nodoc:
509
+ private
510
+ def commit(options)
511
+ options[:expires] = 20.years.from_now
512
+ end
393
513
  end
394
514
 
395
515
  class JsonSerializer # :nodoc:
@@ -404,22 +524,27 @@ module ActionDispatch
404
524
 
405
525
  module SerializedCookieJars # :nodoc:
406
526
  MARSHAL_SIGNATURE = "\x04\x08".freeze
527
+ SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer
407
528
 
408
529
  protected
409
530
  def needs_migration?(value)
410
- @options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE)
531
+ request.cookies_serializer == :hybrid && value.start_with?(MARSHAL_SIGNATURE)
411
532
  end
412
533
 
413
- def serialize(name, value)
534
+ def serialize(value)
414
535
  serializer.dump(value)
415
536
  end
416
537
 
417
- def deserialize(name, value)
538
+ def deserialize(name)
539
+ rotate = false
540
+ value = yield -> { rotate = true }
541
+
418
542
  if value
419
- if needs_migration?(value)
420
- Marshal.load(value).tap do |v|
421
- self[name] = { value: v }
422
- end
543
+ case
544
+ when needs_migration?(value)
545
+ self[name] = Marshal.load(value)
546
+ when rotate
547
+ self[name] = serializer.load(value)
423
548
  else
424
549
  serializer.load(value)
425
550
  end
@@ -427,7 +552,7 @@ module ActionDispatch
427
552
  end
428
553
 
429
554
  def serializer
430
- serializer = @options[:serializer] || :marshal
555
+ serializer = request.cookies_serializer || :marshal
431
556
  case serializer
432
557
  when :marshal
433
558
  Marshal
@@ -439,117 +564,100 @@ module ActionDispatch
439
564
  end
440
565
 
441
566
  def digest
442
- @options[:digest] || 'SHA1'
567
+ request.cookies_digest || "SHA1"
443
568
  end
444
569
  end
445
570
 
446
- class SignedCookieJar #:nodoc:
447
- include ChainedCookieJars
571
+ class SignedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
448
572
  include SerializedCookieJars
449
573
 
450
- def initialize(parent_jar, key_generator, options = {})
451
- @parent_jar = parent_jar
452
- @options = options
453
- secret = key_generator.generate_key(@options[:signed_cookie_salt])
454
- @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
455
- end
574
+ def initialize(parent_jar)
575
+ super
456
576
 
457
- def [](name)
458
- if signed_message = @parent_jar[name]
459
- deserialize name, verify(signed_message)
460
- end
461
- end
577
+ secret = request.key_generator.generate_key(request.signed_cookie_salt)
578
+ @verifier = ActiveSupport::MessageVerifier.new(secret, digest: signed_cookie_digest, serializer: SERIALIZER)
462
579
 
463
- def []=(name, options)
464
- if options.is_a?(Hash)
465
- options.symbolize_keys!
466
- options[:value] = @verifier.generate(serialize(name, options[:value]))
467
- else
468
- options = { :value => @verifier.generate(serialize(name, options)) }
580
+ request.cookies_rotations.signed.each do |*secrets, **options|
581
+ @verifier.rotate(*secrets, serializer: SERIALIZER, **options)
469
582
  end
470
583
 
471
- raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
472
- @parent_jar[name] = options
584
+ if upgrade_legacy_signed_cookies?
585
+ @verifier.rotate request.secret_token, serializer: SERIALIZER
586
+ end
473
587
  end
474
588
 
475
589
  private
476
- def verify(signed_message)
477
- @verifier.verify(signed_message)
478
- rescue ActiveSupport::MessageVerifier::InvalidSignature
479
- nil
590
+ def parse(name, signed_message)
591
+ deserialize(name) do |rotate|
592
+ @verifier.verified(signed_message, on_rotation: rotate)
593
+ end
480
594
  end
481
- end
482
595
 
483
- # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if
484
- # secrets.secret_token and secrets.secret_key_base are both set. It reads
485
- # legacy cookies signed with the old dummy key generator and re-saves
486
- # them using the new key generator to provide a smooth upgrade path.
487
- class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc:
488
- include VerifyAndUpgradeLegacySignedMessage
596
+ def commit(options)
597
+ options[:value] = @verifier.generate(serialize(options[:value]), expiry_options(options))
489
598
 
490
- def [](name)
491
- if signed_message = @parent_jar[name]
492
- deserialize(name, verify(signed_message)) || verify_and_upgrade_legacy_signed_message(name, signed_message)
599
+ raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
493
600
  end
494
- end
495
601
  end
496
602
 
497
- class EncryptedCookieJar #:nodoc:
498
- include ChainedCookieJars
603
+ class EncryptedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
499
604
  include SerializedCookieJars
500
605
 
501
- def initialize(parent_jar, key_generator, options = {})
502
- if ActiveSupport::LegacyKeyGenerator === key_generator
503
- raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " +
504
- "Read the upgrade documentation to learn more about this new config option."
505
- end
506
-
507
- @parent_jar = parent_jar
508
- @options = options
509
- secret = key_generator.generate_key(@options[:encrypted_cookie_salt])[0, ActiveSupport::MessageEncryptor.key_len]
510
- sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt])
511
- @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
512
- end
606
+ def initialize(parent_jar)
607
+ super
513
608
 
514
- def [](name)
515
- if encrypted_message = @parent_jar[name]
516
- deserialize name, decrypt_and_verify(encrypted_message)
609
+ if request.use_authenticated_cookie_encryption
610
+ key_len = ActiveSupport::MessageEncryptor.key_len(encrypted_cookie_cipher)
611
+ secret = request.key_generator.generate_key(request.authenticated_encrypted_cookie_salt, key_len)
612
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: encrypted_cookie_cipher, serializer: SERIALIZER)
613
+ else
614
+ key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-cbc")
615
+ secret = request.key_generator.generate_key(request.encrypted_cookie_salt, key_len)
616
+ sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
617
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: SERIALIZER)
517
618
  end
518
- end
519
619
 
520
- def []=(name, options)
521
- if options.is_a?(Hash)
522
- options.symbolize_keys!
523
- else
524
- options = { :value => options }
620
+ request.cookies_rotations.encrypted.each do |*secrets, **options|
621
+ @encryptor.rotate(*secrets, serializer: SERIALIZER, **options)
525
622
  end
526
623
 
527
- options[:value] = @encryptor.encrypt_and_sign(serialize(name, options[:value]))
624
+ if upgrade_legacy_hmac_aes_cbc_cookies?
625
+ legacy_cipher = "aes-256-cbc"
626
+ secret = request.key_generator.generate_key(request.encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len(legacy_cipher))
627
+ sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
528
628
 
529
- raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
530
- @parent_jar[name] = options
629
+ @encryptor.rotate(secret, sign_secret, cipher: legacy_cipher, digest: digest, serializer: SERIALIZER)
630
+ end
631
+
632
+ if upgrade_legacy_signed_cookies?
633
+ @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, digest: digest, serializer: SERIALIZER)
634
+ end
531
635
  end
532
636
 
533
637
  private
534
- def decrypt_and_verify(encrypted_message)
535
- @encryptor.decrypt_and_verify(encrypted_message)
536
- rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
537
- nil
638
+ def parse(name, encrypted_message)
639
+ deserialize(name) do |rotate|
640
+ @encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate)
641
+ end
642
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature
643
+ parse_legacy_signed_message(name, encrypted_message)
538
644
  end
539
- end
540
645
 
541
- # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore
542
- # instead of EncryptedCookieJar if secrets.secret_token and secrets.secret_key_base
543
- # are both set. It reads legacy cookies signed with the old dummy key generator and
544
- # encrypts and re-saves them using the new key generator to provide a smooth upgrade path.
545
- class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc:
546
- include VerifyAndUpgradeLegacySignedMessage
646
+ def commit(options)
647
+ options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), expiry_options(options))
547
648
 
548
- def [](name)
549
- if encrypted_or_signed_message = @parent_jar[name]
550
- deserialize(name, decrypt_and_verify(encrypted_or_signed_message)) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message)
649
+ raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
650
+ end
651
+
652
+ def parse_legacy_signed_message(name, legacy_signed_message)
653
+ if defined?(@legacy_verifier)
654
+ deserialize(name) do |rotate|
655
+ rotate.call
656
+
657
+ @legacy_verifier.verified(legacy_signed_message)
658
+ end
659
+ end
551
660
  end
552
- end
553
661
  end
554
662
 
555
663
  def initialize(app)
@@ -557,9 +665,12 @@ module ActionDispatch
557
665
  end
558
666
 
559
667
  def call(env)
668
+ request = ActionDispatch::Request.new env
669
+
560
670
  status, headers, body = @app.call(env)
561
671
 
562
- if cookie_jar = env['action_dispatch.cookies']
672
+ if request.have_cookie_jar?
673
+ cookie_jar = request.cookie_jar
563
674
  unless cookie_jar.committed?
564
675
  cookie_jar.write(headers)
565
676
  if headers[HTTP_HEADER].respond_to?(:join)