actionpack 4.2.10 → 7.2.0.rc1

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 (202) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +86 -600
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +13 -14
  5. data/lib/abstract_controller/asset_paths.rb +5 -1
  6. data/lib/abstract_controller/base.rb +166 -136
  7. data/lib/abstract_controller/caching/fragments.rb +149 -0
  8. data/lib/abstract_controller/caching.rb +68 -0
  9. data/lib/abstract_controller/callbacks.rb +126 -57
  10. data/lib/abstract_controller/collector.rb +13 -15
  11. data/lib/abstract_controller/deprecator.rb +9 -0
  12. data/lib/abstract_controller/error.rb +8 -0
  13. data/lib/abstract_controller/helpers.rb +181 -132
  14. data/lib/abstract_controller/logger.rb +5 -1
  15. data/lib/abstract_controller/railties/routes_helpers.rb +10 -3
  16. data/lib/abstract_controller/rendering.rb +56 -56
  17. data/lib/abstract_controller/translation.rb +29 -15
  18. data/lib/abstract_controller/url_for.rb +15 -11
  19. data/lib/abstract_controller.rb +21 -5
  20. data/lib/action_controller/api/api_rendering.rb +18 -0
  21. data/lib/action_controller/api.rb +154 -0
  22. data/lib/action_controller/base.rb +219 -155
  23. data/lib/action_controller/caching.rb +28 -68
  24. data/lib/action_controller/deprecator.rb +9 -0
  25. data/lib/action_controller/form_builder.rb +55 -0
  26. data/lib/action_controller/log_subscriber.rb +35 -22
  27. data/lib/action_controller/metal/allow_browser.rb +119 -0
  28. data/lib/action_controller/metal/basic_implicit_render.rb +17 -0
  29. data/lib/action_controller/metal/conditional_get.rb +259 -122
  30. data/lib/action_controller/metal/content_security_policy.rb +86 -0
  31. data/lib/action_controller/metal/cookies.rb +9 -5
  32. data/lib/action_controller/metal/data_streaming.rb +87 -104
  33. data/lib/action_controller/metal/default_headers.rb +21 -0
  34. data/lib/action_controller/metal/etag_with_flash.rb +22 -0
  35. data/lib/action_controller/metal/etag_with_template_digest.rb +35 -26
  36. data/lib/action_controller/metal/exceptions.rb +71 -24
  37. data/lib/action_controller/metal/flash.rb +26 -19
  38. data/lib/action_controller/metal/head.rb +45 -36
  39. data/lib/action_controller/metal/helpers.rb +80 -64
  40. data/lib/action_controller/metal/http_authentication.rb +297 -244
  41. data/lib/action_controller/metal/implicit_render.rb +57 -9
  42. data/lib/action_controller/metal/instrumentation.rb +76 -64
  43. data/lib/action_controller/metal/live.rb +238 -176
  44. data/lib/action_controller/metal/logging.rb +22 -0
  45. data/lib/action_controller/metal/mime_responds.rb +177 -166
  46. data/lib/action_controller/metal/parameter_encoding.rb +84 -0
  47. data/lib/action_controller/metal/params_wrapper.rb +145 -118
  48. data/lib/action_controller/metal/permissions_policy.rb +38 -0
  49. data/lib/action_controller/metal/rate_limiting.rb +62 -0
  50. data/lib/action_controller/metal/redirecting.rb +203 -64
  51. data/lib/action_controller/metal/renderers.rb +108 -65
  52. data/lib/action_controller/metal/rendering.rb +216 -56
  53. data/lib/action_controller/metal/request_forgery_protection.rb +496 -163
  54. data/lib/action_controller/metal/rescue.rb +19 -21
  55. data/lib/action_controller/metal/streaming.rb +179 -138
  56. data/lib/action_controller/metal/strong_parameters.rb +1058 -382
  57. data/lib/action_controller/metal/testing.rb +11 -17
  58. data/lib/action_controller/metal/url_for.rb +37 -21
  59. data/lib/action_controller/metal.rb +236 -138
  60. data/lib/action_controller/railtie.rb +89 -11
  61. data/lib/action_controller/railties/helpers.rb +5 -1
  62. data/lib/action_controller/renderer.rb +161 -0
  63. data/lib/action_controller/template_assertions.rb +13 -0
  64. data/lib/action_controller/test_case.rb +425 -497
  65. data/lib/action_controller.rb +44 -22
  66. data/lib/action_dispatch/constants.rb +34 -0
  67. data/lib/action_dispatch/deprecator.rb +9 -0
  68. data/lib/action_dispatch/http/cache.rb +119 -63
  69. data/lib/action_dispatch/http/content_disposition.rb +47 -0
  70. data/lib/action_dispatch/http/content_security_policy.rb +364 -0
  71. data/lib/action_dispatch/http/filter_parameters.rb +36 -34
  72. data/lib/action_dispatch/http/filter_redirect.rb +24 -12
  73. data/lib/action_dispatch/http/headers.rb +66 -31
  74. data/lib/action_dispatch/http/mime_negotiation.rb +106 -75
  75. data/lib/action_dispatch/http/mime_type.rb +196 -136
  76. data/lib/action_dispatch/http/mime_types.rb +25 -7
  77. data/lib/action_dispatch/http/parameters.rb +97 -45
  78. data/lib/action_dispatch/http/permissions_policy.rb +187 -0
  79. data/lib/action_dispatch/http/rack_cache.rb +6 -0
  80. data/lib/action_dispatch/http/request.rb +299 -170
  81. data/lib/action_dispatch/http/response.rb +311 -160
  82. data/lib/action_dispatch/http/upload.rb +52 -23
  83. data/lib/action_dispatch/http/url.rb +201 -125
  84. data/lib/action_dispatch/journey/formatter.rb +110 -50
  85. data/lib/action_dispatch/journey/gtg/builder.rb +37 -50
  86. data/lib/action_dispatch/journey/gtg/simulator.rb +20 -17
  87. data/lib/action_dispatch/journey/gtg/transition_table.rb +96 -36
  88. data/lib/action_dispatch/journey/nfa/dot.rb +5 -14
  89. data/lib/action_dispatch/journey/nodes/node.rb +100 -20
  90. data/lib/action_dispatch/journey/parser.rb +19 -17
  91. data/lib/action_dispatch/journey/parser.y +4 -3
  92. data/lib/action_dispatch/journey/parser_extras.rb +14 -4
  93. data/lib/action_dispatch/journey/path/pattern.rb +79 -63
  94. data/lib/action_dispatch/journey/route.rb +108 -44
  95. data/lib/action_dispatch/journey/router/utils.rb +41 -29
  96. data/lib/action_dispatch/journey/router.rb +64 -57
  97. data/lib/action_dispatch/journey/routes.rb +23 -21
  98. data/lib/action_dispatch/journey/scanner.rb +28 -17
  99. data/lib/action_dispatch/journey/visitors.rb +100 -54
  100. data/lib/action_dispatch/journey/visualizer/fsm.js +49 -24
  101. data/lib/action_dispatch/journey/visualizer/index.html.erb +1 -1
  102. data/lib/action_dispatch/journey.rb +7 -5
  103. data/lib/action_dispatch/log_subscriber.rb +25 -0
  104. data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
  105. data/lib/action_dispatch/middleware/assume_ssl.rb +27 -0
  106. data/lib/action_dispatch/middleware/callbacks.rb +7 -6
  107. data/lib/action_dispatch/middleware/cookies.rb +471 -328
  108. data/lib/action_dispatch/middleware/debug_exceptions.rb +149 -66
  109. data/lib/action_dispatch/middleware/debug_locks.rb +129 -0
  110. data/lib/action_dispatch/middleware/debug_view.rb +73 -0
  111. data/lib/action_dispatch/middleware/exception_wrapper.rb +275 -73
  112. data/lib/action_dispatch/middleware/executor.rb +32 -0
  113. data/lib/action_dispatch/middleware/flash.rb +143 -101
  114. data/lib/action_dispatch/middleware/host_authorization.rb +171 -0
  115. data/lib/action_dispatch/middleware/public_exceptions.rb +36 -27
  116. data/lib/action_dispatch/middleware/reloader.rb +10 -92
  117. data/lib/action_dispatch/middleware/remote_ip.rb +133 -107
  118. data/lib/action_dispatch/middleware/request_id.rb +29 -15
  119. data/lib/action_dispatch/middleware/server_timing.rb +78 -0
  120. data/lib/action_dispatch/middleware/session/abstract_store.rb +49 -27
  121. data/lib/action_dispatch/middleware/session/cache_store.rb +33 -16
  122. data/lib/action_dispatch/middleware/session/cookie_store.rb +86 -80
  123. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +15 -3
  124. data/lib/action_dispatch/middleware/show_exceptions.rb +66 -36
  125. data/lib/action_dispatch/middleware/ssl.rb +134 -36
  126. data/lib/action_dispatch/middleware/stack.rb +109 -44
  127. data/lib/action_dispatch/middleware/static.rb +159 -90
  128. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +7 -24
  132. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
  133. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +36 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +46 -36
  136. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +12 -0
  137. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +9 -0
  138. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +26 -7
  139. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +3 -3
  140. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
  141. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +16 -0
  142. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +139 -15
  143. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +23 -0
  144. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  145. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +6 -6
  146. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +7 -7
  147. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +9 -9
  148. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
  149. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +4 -4
  150. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +1 -1
  151. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +7 -4
  152. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +125 -93
  153. data/lib/action_dispatch/railtie.rb +44 -16
  154. data/lib/action_dispatch/request/session.rb +159 -69
  155. data/lib/action_dispatch/request/utils.rb +97 -23
  156. data/lib/action_dispatch/routing/endpoint.rb +11 -2
  157. data/lib/action_dispatch/routing/inspector.rb +195 -106
  158. data/lib/action_dispatch/routing/mapper.rb +1338 -955
  159. data/lib/action_dispatch/routing/polymorphic_routes.rb +234 -201
  160. data/lib/action_dispatch/routing/redirection.rb +78 -51
  161. data/lib/action_dispatch/routing/route_set.rb +460 -374
  162. data/lib/action_dispatch/routing/routes_proxy.rb +36 -12
  163. data/lib/action_dispatch/routing/url_for.rb +172 -124
  164. data/lib/action_dispatch/routing.rb +159 -158
  165. data/lib/action_dispatch/system_test_case.rb +206 -0
  166. data/lib/action_dispatch/system_testing/browser.rb +84 -0
  167. data/lib/action_dispatch/system_testing/driver.rb +85 -0
  168. data/lib/action_dispatch/system_testing/server.rb +33 -0
  169. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +164 -0
  170. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +23 -0
  171. data/lib/action_dispatch/testing/assertion_response.rb +48 -0
  172. data/lib/action_dispatch/testing/assertions/response.rb +71 -39
  173. data/lib/action_dispatch/testing/assertions/routing.rb +228 -103
  174. data/lib/action_dispatch/testing/assertions.rb +9 -6
  175. data/lib/action_dispatch/testing/integration.rb +486 -306
  176. data/lib/action_dispatch/testing/request_encoder.rb +60 -0
  177. data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
  178. data/lib/action_dispatch/testing/test_process.rb +35 -22
  179. data/lib/action_dispatch/testing/test_request.rb +29 -34
  180. data/lib/action_dispatch/testing/test_response.rb +48 -15
  181. data/lib/action_dispatch.rb +82 -40
  182. data/lib/action_pack/gem_version.rb +8 -4
  183. data/lib/action_pack/version.rb +6 -2
  184. data/lib/action_pack.rb +21 -18
  185. metadata +146 -56
  186. data/lib/action_controller/caching/fragments.rb +0 -103
  187. data/lib/action_controller/metal/force_ssl.rb +0 -97
  188. data/lib/action_controller/metal/hide_actions.rb +0 -40
  189. data/lib/action_controller/metal/rack_delegation.rb +0 -32
  190. data/lib/action_controller/middleware.rb +0 -39
  191. data/lib/action_controller/model_naming.rb +0 -12
  192. data/lib/action_dispatch/http/parameter_filter.rb +0 -72
  193. data/lib/action_dispatch/journey/backwards.rb +0 -5
  194. data/lib/action_dispatch/journey/nfa/builder.rb +0 -76
  195. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -47
  196. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -163
  197. data/lib/action_dispatch/journey/router/strexp.rb +0 -27
  198. data/lib/action_dispatch/middleware/params_parser.rb +0 -60
  199. data/lib/action_dispatch/middleware/templates/rescues/_source.erb +0 -27
  200. data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
  201. data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
  202. data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
@@ -1,99 +1,211 @@
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
+ # :markup: markdown
4
+
5
+ require "active_support/core_ext/hash/keys"
6
+ require "active_support/key_generator"
7
+ require "active_support/message_verifier"
8
+ require "active_support/json"
9
+ require "rack/utils"
7
10
 
8
11
  module ActionDispatch
9
- class Request < Rack::Request
12
+ module RequestCookieMethods
10
13
  def cookie_jar
11
- env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(self)
14
+ fetch_header("action_dispatch.cookies") do
15
+ self.cookie_jar = Cookies::CookieJar.build(self, cookies)
16
+ end
17
+ end
18
+
19
+ # :stopdoc:
20
+ prepend Module.new {
21
+ def commit_cookie_jar!
22
+ cookie_jar.commit!
23
+ end
24
+ }
25
+
26
+ def have_cookie_jar?
27
+ has_header? "action_dispatch.cookies"
28
+ end
29
+
30
+ def cookie_jar=(jar)
31
+ set_header "action_dispatch.cookies", jar
32
+ end
33
+
34
+ def key_generator
35
+ get_header Cookies::GENERATOR_KEY
12
36
  end
37
+
38
+ def signed_cookie_salt
39
+ get_header Cookies::SIGNED_COOKIE_SALT
40
+ end
41
+
42
+ def encrypted_cookie_salt
43
+ get_header Cookies::ENCRYPTED_COOKIE_SALT
44
+ end
45
+
46
+ def encrypted_signed_cookie_salt
47
+ get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT
48
+ end
49
+
50
+ def authenticated_encrypted_cookie_salt
51
+ get_header Cookies::AUTHENTICATED_ENCRYPTED_COOKIE_SALT
52
+ end
53
+
54
+ def use_authenticated_cookie_encryption
55
+ get_header Cookies::USE_AUTHENTICATED_COOKIE_ENCRYPTION
56
+ end
57
+
58
+ def encrypted_cookie_cipher
59
+ get_header Cookies::ENCRYPTED_COOKIE_CIPHER
60
+ end
61
+
62
+ def signed_cookie_digest
63
+ get_header Cookies::SIGNED_COOKIE_DIGEST
64
+ end
65
+
66
+ def secret_key_base
67
+ get_header Cookies::SECRET_KEY_BASE
68
+ end
69
+
70
+ def cookies_serializer
71
+ get_header Cookies::COOKIES_SERIALIZER
72
+ end
73
+
74
+ def cookies_same_site_protection
75
+ get_header(Cookies::COOKIES_SAME_SITE_PROTECTION)&.call(self)
76
+ end
77
+
78
+ def cookies_digest
79
+ get_header Cookies::COOKIES_DIGEST
80
+ end
81
+
82
+ def cookies_rotations
83
+ get_header Cookies::COOKIES_ROTATIONS
84
+ end
85
+
86
+ def use_cookies_with_metadata
87
+ get_header Cookies::USE_COOKIES_WITH_METADATA
88
+ end
89
+
90
+ # :startdoc:
13
91
  end
14
92
 
15
- # \Cookies are read and written through ActionController#cookies.
93
+ ActiveSupport.on_load(:action_dispatch_request) do
94
+ include RequestCookieMethods
95
+ end
96
+
97
+ # Read and write data to cookies through ActionController::Cookies#cookies.
16
98
  #
17
- # The cookies being read are the ones received along with the request, the cookies
18
- # being written will be sent out with the response. Reading a cookie does not get
19
- # the cookie object itself back, just the value it holds.
99
+ # When reading cookie data, the data is read from the HTTP request header,
100
+ # Cookie. When writing cookie data, the data is sent out in the HTTP response
101
+ # header, `Set-Cookie`.
20
102
  #
21
103
  # Examples of writing:
22
104
  #
23
- # # Sets a simple session cookie.
24
- # # This cookie will be deleted when the user's browser is closed.
25
- # cookies[:user_name] = "david"
105
+ # # Sets a simple session cookie.
106
+ # # This cookie will be deleted when the user's browser is closed.
107
+ # cookies[:user_name] = "david"
108
+ #
109
+ # # Cookie values are String-based. Other data types need to be serialized.
110
+ # cookies[:lat_lon] = JSON.generate([47.68, -122.37])
26
111
  #
27
- # # Cookie values are String based. Other data types need to be serialized.
28
- # cookies[:lat_lon] = JSON.generate([47.68, -122.37])
112
+ # # Sets a cookie that expires in 1 hour.
113
+ # cookies[:login] = { value: "XJ-122", expires: 1.hour }
29
114
  #
30
- # # Sets a cookie that expires in 1 hour.
31
- # cookies[:login] = { value: "XJ-122", expires: 1.hour.from_now }
115
+ # # Sets a cookie that expires at a specific time.
116
+ # cookies[:login] = { value: "XJ-122", expires: Time.utc(2020, 10, 15, 5) }
32
117
  #
33
- # # 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
- # # It can be read using the signed method `cookies.signed[:name]`
36
- # cookies.signed[:user_id] = current_user.id
118
+ # # Sets a signed cookie, which prevents users from tampering with its value.
119
+ # # It can be read using the signed method `cookies.signed[:name]`
120
+ # cookies.signed[:user_id] = current_user.id
37
121
  #
38
- # # Sets a "permanent" cookie (which expires in 20 years from now).
39
- # cookies.permanent[:login] = "XJ-122"
122
+ # # Sets an encrypted cookie value before sending it to the client which
123
+ # # prevent users from reading and tampering with its value.
124
+ # # It can be read using the encrypted method `cookies.encrypted[:name]`
125
+ # cookies.encrypted[:discount] = 45
40
126
  #
41
- # # You can also chain these methods:
42
- # cookies.permanent.signed[:login] = "XJ-122"
127
+ # # Sets a "permanent" cookie (which expires in 20 years from now).
128
+ # cookies.permanent[:login] = "XJ-122"
129
+ #
130
+ # # You can also chain these methods:
131
+ # cookies.signed.permanent[:login] = "XJ-122"
43
132
  #
44
133
  # Examples of reading:
45
134
  #
46
- # cookies[:user_name] # => "david"
47
- # cookies.size # => 2
48
- # JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37]
49
- # cookies.signed[:login] # => "XJ-122"
135
+ # cookies[:user_name] # => "david"
136
+ # cookies.size # => 2
137
+ # JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37]
138
+ # cookies.signed[:login] # => "XJ-122"
139
+ # cookies.encrypted[:discount] # => 45
50
140
  #
51
141
  # Example for deleting:
52
142
  #
53
- # cookies.delete :user_name
143
+ # cookies.delete :user_name
54
144
  #
55
- # Please note that if you specify a :domain when setting a cookie, you must also specify the domain when deleting the cookie:
145
+ # Please note that if you specify a `:domain` when setting a cookie, you must
146
+ # also specify the domain when deleting the cookie:
56
147
  #
57
- # cookies[:name] = {
58
- # value: 'a yummy cookie',
59
- # expires: 1.year.from_now,
60
- # domain: 'domain.com'
61
- # }
148
+ # cookies[:name] = {
149
+ # value: 'a yummy cookie',
150
+ # expires: 1.year,
151
+ # domain: 'domain.com'
152
+ # }
62
153
  #
63
- # cookies.delete(:name, domain: 'domain.com')
154
+ # cookies.delete(:name, domain: 'domain.com')
64
155
  #
65
156
  # The option symbols for setting cookies are:
66
157
  #
67
- # * <tt>:value</tt> - The cookie's value.
68
- # * <tt>:path</tt> - The path for which this cookie applies. Defaults to the root
69
- # of the application.
70
- # * <tt>:domain</tt> - The domain for which this cookie applies so you can
71
- # restrict to the domain level. If you use a schema like www.example.com
72
- # and want to share session with user.example.com set <tt>:domain</tt>
73
- # to <tt>:all</tt>. Make sure to specify the <tt>:domain</tt> option with
74
- # <tt>:all</tt> or <tt>Array</tt> again when deleting cookies.
158
+ # * `:value` - The cookie's value.
159
+ # * `:path` - The path for which this cookie applies. Defaults to the root of
160
+ # the application.
161
+ # * `:domain` - The domain for which this cookie applies so you can restrict
162
+ # to the domain level. If you use a schema like www.example.com and want to
163
+ # share session with user.example.com set `:domain` to `:all`. To support
164
+ # multiple domains, provide an array, and the first domain matching
165
+ # `request.host` will be used. Make sure to specify the `:domain` option
166
+ # with `:all` or `Array` again when deleting cookies. For more flexibility
167
+ # you can set the domain on a per-request basis by specifying `:domain` with
168
+ # a proc.
169
+ #
170
+ # domain: nil # Does not set cookie domain. (default)
171
+ # domain: :all # Allow the cookie for the top most level
172
+ # # domain and subdomains.
173
+ # domain: %w(.example.com .example.org) # Allow the cookie
174
+ # # for concrete domain names.
175
+ # domain: proc { Tenant.current.cookie_domain } # Set cookie domain dynamically
176
+ # domain: proc { |req| ".sub.#{req.host}" } # Set cookie domain dynamically based on request
75
177
  #
76
- # domain: nil # Does not sets cookie domain. (default)
77
- # domain: :all # Allow the cookie for the top most level
78
- # # domain and subdomains.
79
- # domain: %w(.example.com .example.org) # Allow the cookie
80
- # # for concrete domain names.
178
+ # * `:tld_length` - When using `:domain => :all`, this option can be used to
179
+ # explicitly set the TLD length when using a short (<= 3 character) domain
180
+ # that is being interpreted as part of a TLD. For example, to share cookies
181
+ # between user1.lvh.me and user2.lvh.me, set `:tld_length` to 2.
182
+ # * `:expires` - The time at which this cookie expires, as a Time or
183
+ # ActiveSupport::Duration object.
184
+ # * `:secure` - Whether this cookie is only transmitted to HTTPS servers.
185
+ # Default is `false`.
186
+ # * `:httponly` - Whether this cookie is accessible via scripting or only
187
+ # HTTP. Defaults to `false`.
188
+ # * `:same_site` - The value of the `SameSite` cookie attribute, which
189
+ # determines how this cookie should be restricted in cross-site contexts.
190
+ # Possible values are `nil`, `:none`, `:lax`, and `:strict`. Defaults to
191
+ # `:lax`.
81
192
  #
82
- # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object.
83
- # * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers.
84
- # Default is +false+.
85
- # * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or
86
- # only HTTP. Defaults to +false+.
87
193
  class Cookies
88
- HTTP_HEADER = "Set-Cookie".freeze
89
- GENERATOR_KEY = "action_dispatch.key_generator".freeze
90
- SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze
91
- ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze
92
- ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze
93
- SECRET_TOKEN = "action_dispatch.secret_token".freeze
94
- SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
95
- COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze
96
- COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze
194
+ HTTP_HEADER = "Set-Cookie"
195
+ GENERATOR_KEY = "action_dispatch.key_generator"
196
+ SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt"
197
+ ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt"
198
+ ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt"
199
+ AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt"
200
+ USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption"
201
+ ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher"
202
+ SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest"
203
+ SECRET_KEY_BASE = "action_dispatch.secret_key_base"
204
+ COOKIES_SERIALIZER = "action_dispatch.cookies_serializer"
205
+ COOKIES_DIGEST = "action_dispatch.cookies_digest"
206
+ COOKIES_ROTATIONS = "action_dispatch.cookies_rotations"
207
+ COOKIES_SAME_SITE_PROTECTION = "action_dispatch.cookies_same_site_protection"
208
+ USE_COOKIES_WITH_METADATA = "action_dispatch.use_cookies_with_metadata"
97
209
 
98
210
  # Cookies can typically store 4096 bytes.
99
211
  MAX_COOKIE_SIZE = 4096
@@ -101,150 +213,116 @@ module ActionDispatch
101
213
  # Raised when storing more than 4K of session data.
102
214
  CookieOverflow = Class.new StandardError
103
215
 
104
- # Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed
216
+ # Include in a cookie jar to allow chaining, e.g. `cookies.permanent.signed`.
105
217
  module ChainedCookieJars
106
- # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example:
218
+ # Returns a jar that'll automatically set the assigned cookies to have an
219
+ # expiration date 20 years from now. Example:
107
220
  #
108
- # cookies.permanent[:prefers_open_id] = true
109
- # # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
221
+ # cookies.permanent[:prefers_open_id] = true
222
+ # # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
110
223
  #
111
- # This jar is only meant for writing. You'll read permanent cookies through the regular accessor.
224
+ # This jar is only meant for writing. You'll read permanent cookies through the
225
+ # regular accessor.
112
226
  #
113
- # This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples:
227
+ # This jar allows chaining with the signed jar as well, so you can set
228
+ # permanent, signed cookies. Examples:
114
229
  #
115
- # cookies.permanent.signed[:remember_me] = current_user.id
116
- # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
230
+ # cookies.permanent.signed[:remember_me] = current_user.id
231
+ # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
117
232
  def permanent
118
- @permanent ||= PermanentCookieJar.new(self, @key_generator, @options)
233
+ @permanent ||= PermanentCookieJar.new(self)
119
234
  end
120
235
 
121
- # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
122
- # 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.
236
+ # Returns a jar that'll automatically generate a signed representation of cookie
237
+ # value and verify it when reading from the cookie again. This is useful for
238
+ # creating cookies with values that the user is not supposed to change. If a
239
+ # signed cookie was tampered with by the user (or a 3rd party), `nil` will be
240
+ # returned.
124
241
  #
125
- # If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
126
- # legacy cookies signed with the old key generator will be transparently upgraded.
127
- #
128
- # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+.
242
+ # This jar requires that you set a suitable secret for the verification on your
243
+ # app's `secret_key_base`.
129
244
  #
130
245
  # Example:
131
246
  #
132
- # cookies.signed[:discount] = 45
133
- # # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
247
+ # cookies.signed[:discount] = 45
248
+ # # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
134
249
  #
135
- # cookies.signed[:discount] # => 45
250
+ # cookies.signed[:discount] # => 45
136
251
  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
252
+ @signed ||= SignedKeyRotatingCookieJar.new(self)
143
253
  end
144
254
 
145
- # 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.
255
+ # Returns a jar that'll automatically encrypt cookie values before sending them
256
+ # to the client and will decrypt them for read. If the cookie was tampered with
257
+ # by the user (or a 3rd party), `nil` will be returned.
147
258
  #
148
- # If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
149
- # legacy cookies signed with the old key generator will be transparently upgraded.
259
+ # If `config.action_dispatch.encrypted_cookie_salt` and
260
+ # `config.action_dispatch.encrypted_signed_cookie_salt` are both set, legacy
261
+ # cookies encrypted with HMAC AES-256-CBC will be transparently upgraded.
150
262
  #
151
- # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+.
263
+ # This jar requires that you set a suitable secret for the verification on your
264
+ # app's `secret_key_base`.
152
265
  #
153
266
  # Example:
154
267
  #
155
- # cookies.encrypted[:discount] = 45
156
- # # => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/
268
+ # cookies.encrypted[:discount] = 45
269
+ # # => Set-Cookie: discount=DIQ7fw==--K3n//8vvnSbGq9dA--7Xh91HfLpwzbj1czhBiwOg==; path=/
157
270
  #
158
- # cookies.encrypted[:discount] # => 45
271
+ # cookies.encrypted[:discount] # => 45
159
272
  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
273
+ @encrypted ||= EncryptedKeyRotatingCookieJar.new(self)
166
274
  end
167
275
 
168
- # Returns the +signed+ or +encrypted+ jar, preferring +encrypted+ if +secret_key_base+ is set.
169
- # Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores.
276
+ # Returns the `signed` or `encrypted` jar, preferring `encrypted` if
277
+ # `secret_key_base` is set. Used by ActionDispatch::Session::CookieStore to
278
+ # avoid the need to introduce new cookie stores.
170
279
  def signed_or_encrypted
171
280
  @signed_or_encrypted ||=
172
- if @options[:secret_key_base].present?
281
+ if request.secret_key_base.present?
173
282
  encrypted
174
283
  else
175
284
  signed
176
285
  end
177
286
  end
178
- end
179
287
 
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
288
+ private
289
+ def upgrade_legacy_hmac_aes_cbc_cookies?
290
+ request.secret_key_base.present? &&
291
+ request.encrypted_signed_cookie_salt.present? &&
292
+ request.encrypted_cookie_salt.present? &&
293
+ request.use_authenticated_cookie_encryption
294
+ end
189
295
 
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 }
296
+ def prepare_upgrade_legacy_hmac_aes_cbc_cookies?
297
+ request.secret_key_base.present? &&
298
+ request.authenticated_encrypted_cookie_salt.present? &&
299
+ !request.use_authenticated_cookie_encryption
300
+ end
301
+
302
+ def encrypted_cookie_cipher
303
+ request.encrypted_cookie_cipher || "aes-256-gcm"
304
+ end
305
+
306
+ def signed_cookie_digest
307
+ request.signed_cookie_digest || "SHA1"
193
308
  end
194
- rescue ActiveSupport::MessageVerifier::InvalidSignature
195
- nil
196
- end
197
309
  end
198
310
 
199
- class CookieJar #:nodoc:
311
+ class CookieJar # :nodoc:
200
312
  include Enumerable, ChainedCookieJars
201
313
 
202
- # This regular expression is used to split the levels of a domain.
203
- # The top level domain can be any string without a period or
204
- # **.**, ***.** style TLDs like co.uk or com.au
205
- #
206
- # www.example.co.uk gives:
207
- # $& => example.co.uk
208
- #
209
- # example.com gives:
210
- # $& => example.com
211
- #
212
- # lots.of.subdomains.example.local gives:
213
- # $& => example.local
214
- DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/
215
-
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
- }
314
+ def self.build(req, cookies)
315
+ jar = new(req)
316
+ jar.update(cookies)
317
+ jar
226
318
  end
227
319
 
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)
238
- end
239
- end
320
+ attr_reader :request
240
321
 
241
- def initialize(key_generator, host = nil, secure = false, options = {})
242
- @key_generator = key_generator
322
+ def initialize(request)
243
323
  @set_cookies = {}
244
324
  @delete_cookies = {}
245
- @host = host
246
- @secure = secure
247
- @options = options
325
+ @request = request
248
326
  @cookies = {}
249
327
  @committed = false
250
328
  end
@@ -261,7 +339,7 @@ module ActionDispatch
261
339
  @cookies.each(&block)
262
340
  end
263
341
 
264
- # Returns the value of the cookie by +name+, or +nil+ if no such cookie exists.
342
+ # Returns the value of the cookie by `name`, or `nil` if no such cookie exists.
265
343
  def [](name)
266
344
  @cookies[name.to_s]
267
345
  end
@@ -275,38 +353,34 @@ module ActionDispatch
275
353
  end
276
354
  alias :has_key? :key?
277
355
 
356
+ # Returns the cookies as Hash.
357
+ alias :to_hash :to_h
358
+
278
359
  def update(other_hash)
279
360
  @cookies.update other_hash.stringify_keys
280
361
  self
281
362
  end
282
363
 
283
- def handle_options(options) #:nodoc:
284
- options[:path] ||= "/"
364
+ def update_cookies_from_jar
365
+ request_jar = @request.cookie_jar.instance_variable_get(:@cookies)
366
+ set_cookies = request_jar.reject { |k, _| @delete_cookies.key?(k) || @set_cookies.key?(k) }
285
367
 
286
- if options[:domain] == :all
287
- # if there is a provided tld length then we use it otherwise default domain regexp
288
- domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
368
+ @cookies.update set_cookies if set_cookies
369
+ end
289
370
 
290
- # if host is not ip and matches domain regexp
291
- # (ip confirms to domain regexp so we explicitly check for ip)
292
- options[:domain] = if (@host !~ /^[\d.]+$/) && (@host =~ domain_regexp)
293
- ".#{$&}"
294
- end
295
- 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(/^\./, '') }
298
- end
371
+ def to_header
372
+ @cookies.map { |k, v| "#{escape(k)}=#{escape(v)}" }.join "; "
299
373
  end
300
374
 
301
- # Sets the cookie named +name+. The second argument may be the cookie's
302
- # value or a hash of options as documented above.
375
+ # Sets the cookie named `name`. The second argument may be the cookie's value or
376
+ # a hash of options as documented above.
303
377
  def []=(name, options)
304
378
  if options.is_a?(Hash)
305
379
  options.symbolize_keys!
306
380
  value = options[:value]
307
381
  else
308
382
  value = options
309
- options = { :value => value }
383
+ options = { value: value }
310
384
  end
311
385
 
312
386
  handle_options(options)
@@ -320,9 +394,11 @@ module ActionDispatch
320
394
  value
321
395
  end
322
396
 
323
- # Removes the cookie on the client machine by setting the value to an empty string
324
- # and the expiration date in the past. Like <tt>[]=</tt>, you can pass in
325
- # an options hash to delete cookies with extra data such as a <tt>:path</tt>.
397
+ # Removes the cookie on the client machine by setting the value to an empty
398
+ # string and the expiration date in the past. Like `[]=`, you can pass in an
399
+ # options hash to delete cookies with extra data such as a `:path`.
400
+ #
401
+ # Returns the value of the cookie, or `nil` if the cookie does not exist.
326
402
  def delete(name, options = {})
327
403
  return unless @cookies.has_key? name.to_s
328
404
 
@@ -334,222 +410,289 @@ module ActionDispatch
334
410
  value
335
411
  end
336
412
 
337
- # Whether the given cookie is to be deleted by this CookieJar.
338
- # Like <tt>[]=</tt>, you can pass in an options hash to test if a
339
- # deletion applies to a specific <tt>:path</tt>, <tt>:domain</tt> etc.
413
+ # Whether the given cookie is to be deleted by this CookieJar. Like `[]=`, you
414
+ # can pass in an options hash to test if a deletion applies to a specific
415
+ # `:path`, `:domain` etc.
340
416
  def deleted?(name, options = {})
341
417
  options.symbolize_keys!
342
418
  handle_options(options)
343
419
  @delete_cookies[name.to_s] == options
344
420
  end
345
421
 
346
- # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie
422
+ # Removes all cookies on the client machine by calling `delete` for each cookie.
347
423
  def clear(options = {})
348
- @cookies.each_key{ |k| delete(k, options) }
424
+ @cookies.each_key { |k| delete(k, options) }
349
425
  end
350
426
 
351
- 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
427
+ def write(response)
428
+ @set_cookies.each do |name, value|
429
+ if write_cookie?(value)
430
+ response.set_cookie(name, value)
431
+ end
432
+ end
355
433
 
356
- def recycle! #:nodoc:
357
- @set_cookies = {}
358
- @delete_cookies = {}
434
+ @delete_cookies.each do |name, value|
435
+ response.delete_cookie(name, value)
436
+ end
359
437
  end
360
438
 
361
- mattr_accessor :always_write_cookie
362
- self.always_write_cookie = false
439
+ mattr_accessor :always_write_cookie, default: false
363
440
 
364
441
  private
442
+ def escape(string)
443
+ ::Rack::Utils.escape(string)
444
+ end
445
+
365
446
  def write_cookie?(cookie)
366
- @secure || !cookie[:secure] || always_write_cookie
447
+ request.ssl? || !cookie[:secure] || always_write_cookie || request.host.end_with?(".onion")
448
+ end
449
+
450
+ def handle_options(options)
451
+ if options[:expires].respond_to?(:from_now)
452
+ options[:expires] = options[:expires].from_now
453
+ end
454
+
455
+ options[:path] ||= "/"
456
+
457
+ unless options.key?(:same_site)
458
+ options[:same_site] = request.cookies_same_site_protection
459
+ end
460
+
461
+ if options[:domain] == :all || options[:domain] == "all"
462
+ cookie_domain = ""
463
+ dot_splitted_host = request.host.split(".", -1)
464
+
465
+ # Case where request.host is not an IP address or it's an invalid domain (ip
466
+ # confirms to the domain structure we expect so we explicitly check for ip)
467
+ if request.host.match?(/^[\d.]+$/) || dot_splitted_host.include?("") || dot_splitted_host.length == 1
468
+ options[:domain] = nil
469
+ return
470
+ end
471
+
472
+ # If there is a provided tld length then we use it otherwise default domain.
473
+ if options[:tld_length].present?
474
+ # Case where the tld_length provided is valid
475
+ if dot_splitted_host.length >= options[:tld_length]
476
+ cookie_domain = dot_splitted_host.last(options[:tld_length]).join(".")
477
+ end
478
+ # Case where tld_length is not provided
479
+ else
480
+ # Regular TLDs
481
+ if !(/\.[^.]{2,3}\.[^.]{2}\z/.match?(request.host))
482
+ cookie_domain = dot_splitted_host.last(2).join(".")
483
+ # **.**, ***.** style TLDs like co.uk and com.au
484
+ else
485
+ cookie_domain = dot_splitted_host.last(3).join(".")
486
+ end
487
+ end
488
+
489
+ options[:domain] = if cookie_domain.present?
490
+ cookie_domain
491
+ end
492
+ elsif options[:domain].is_a? Array
493
+ # If host matches one of the supplied domains.
494
+ options[:domain] = options[:domain].find do |domain|
495
+ domain = domain.delete_prefix(".")
496
+ request.host == domain || request.host.end_with?(".#{domain}")
497
+ end
498
+ elsif options[:domain].respond_to?(:call)
499
+ options[:domain] = options[:domain].call(request)
500
+ end
367
501
  end
368
502
  end
369
503
 
370
- class PermanentCookieJar #:nodoc:
504
+ class AbstractCookieJar # :nodoc:
371
505
  include ChainedCookieJars
372
506
 
373
- def initialize(parent_jar, key_generator, options = {})
507
+ def initialize(parent_jar)
374
508
  @parent_jar = parent_jar
375
- @key_generator = key_generator
376
- @options = options
377
509
  end
378
510
 
379
511
  def [](name)
380
- @parent_jar[name.to_s]
512
+ if data = @parent_jar[name.to_s]
513
+ result = parse(name, data, purpose: "cookie.#{name}")
514
+
515
+ if result.nil?
516
+ parse(name, data)
517
+ else
518
+ result
519
+ end
520
+ end
381
521
  end
382
522
 
383
523
  def []=(name, options)
384
524
  if options.is_a?(Hash)
385
525
  options.symbolize_keys!
386
526
  else
387
- options = { :value => options }
527
+ options = { value: options }
388
528
  end
389
529
 
390
- options[:expires] = 20.years.from_now
530
+ commit(name, options)
391
531
  @parent_jar[name] = options
392
532
  end
393
- end
394
533
 
395
- class JsonSerializer # :nodoc:
396
- def self.load(value)
397
- ActiveSupport::JSON.decode(value)
398
- end
534
+ protected
535
+ def request; @parent_jar.request; end
399
536
 
400
- def self.dump(value)
401
- ActiveSupport::JSON.encode(value)
402
- end
537
+ private
538
+ def expiry_options(options)
539
+ if options[:expires].respond_to?(:from_now)
540
+ { expires_in: options[:expires] }
541
+ else
542
+ { expires_at: options[:expires] }
543
+ end
544
+ end
545
+
546
+ def cookie_metadata(name, options)
547
+ expiry_options(options).tap do |metadata|
548
+ metadata[:purpose] = "cookie.#{name}" if request.use_cookies_with_metadata
549
+ end
550
+ end
551
+
552
+ def parse(name, data, purpose: nil); data; end
553
+ def commit(name, options); end
554
+ end
555
+
556
+ class PermanentCookieJar < AbstractCookieJar # :nodoc:
557
+ private
558
+ def commit(name, options)
559
+ options[:expires] = 20.years.from_now
560
+ end
403
561
  end
404
562
 
405
563
  module SerializedCookieJars # :nodoc:
406
- MARSHAL_SIGNATURE = "\x04\x08".freeze
564
+ SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer
407
565
 
408
566
  protected
409
- def needs_migration?(value)
410
- @options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE)
567
+ def digest
568
+ request.cookies_digest || "SHA1"
411
569
  end
412
570
 
413
- def serialize(name, value)
414
- serializer.dump(value)
571
+ private
572
+ def serializer
573
+ @serializer ||=
574
+ case request.cookies_serializer
575
+ when nil
576
+ ActiveSupport::Messages::SerializerWithFallback[:marshal]
577
+ when :hybrid
578
+ ActiveSupport::Messages::SerializerWithFallback[:json_allow_marshal]
579
+ when Symbol
580
+ ActiveSupport::Messages::SerializerWithFallback[request.cookies_serializer]
581
+ else
582
+ request.cookies_serializer
583
+ end
415
584
  end
416
585
 
417
- def deserialize(name, value)
418
- if value
419
- if needs_migration?(value)
420
- Marshal.load(value).tap do |v|
421
- self[name] = { value: v }
422
- end
423
- else
424
- serializer.load(value)
586
+ def reserialize?(dumped)
587
+ serializer.is_a?(ActiveSupport::Messages::SerializerWithFallback) &&
588
+ serializer != ActiveSupport::Messages::SerializerWithFallback[:marshal] &&
589
+ !serializer.dumped?(dumped)
590
+ end
591
+
592
+ def parse(name, dumped, force_reserialize: false, **)
593
+ if dumped
594
+ begin
595
+ value = serializer.load(dumped)
596
+ rescue StandardError
597
+ return
425
598
  end
599
+
600
+ self[name] = { value: value } if force_reserialize || reserialize?(dumped)
601
+
602
+ value
426
603
  end
427
604
  end
428
605
 
429
- def serializer
430
- serializer = @options[:serializer] || :marshal
431
- case serializer
432
- when :marshal
433
- Marshal
434
- when :json, :hybrid
435
- JsonSerializer
436
- else
437
- serializer
438
- end
606
+ def commit(name, options)
607
+ options[:value] = serializer.dump(options[:value])
439
608
  end
440
609
 
441
- def digest
442
- @options[:digest] || 'SHA1'
610
+ def check_for_overflow!(name, options)
611
+ if options[:value].bytesize > MAX_COOKIE_SIZE
612
+ raise CookieOverflow, "#{name} cookie overflowed with size #{options[:value].bytesize} bytes"
613
+ end
443
614
  end
444
615
  end
445
616
 
446
- class SignedCookieJar #:nodoc:
447
- include ChainedCookieJars
617
+ class SignedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
448
618
  include SerializedCookieJars
449
619
 
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
620
+ def initialize(parent_jar)
621
+ super
456
622
 
457
- def [](name)
458
- if signed_message = @parent_jar[name]
459
- deserialize name, verify(signed_message)
460
- end
461
- end
623
+ secret = request.key_generator.generate_key(request.signed_cookie_salt)
624
+ @verifier = ActiveSupport::MessageVerifier.new(secret, digest: signed_cookie_digest, serializer: SERIALIZER)
462
625
 
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)) }
626
+ request.cookies_rotations.signed.each do |(*secrets)|
627
+ options = secrets.extract_options!
628
+ @verifier.rotate(*secrets, serializer: SERIALIZER, **options)
469
629
  end
470
-
471
- raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
472
- @parent_jar[name] = options
473
630
  end
474
631
 
475
632
  private
476
- def verify(signed_message)
477
- @verifier.verify(signed_message)
478
- rescue ActiveSupport::MessageVerifier::InvalidSignature
479
- nil
633
+ def parse(name, signed_message, purpose: nil)
634
+ rotated = false
635
+ data = @verifier.verified(signed_message, purpose: purpose, on_rotation: -> { rotated = true })
636
+ super(name, data, force_reserialize: rotated)
480
637
  end
481
- end
482
-
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
489
638
 
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)
639
+ def commit(name, options)
640
+ super
641
+ options[:value] = @verifier.generate(options[:value], **cookie_metadata(name, options))
642
+ check_for_overflow!(name, options)
493
643
  end
494
- end
495
644
  end
496
645
 
497
- class EncryptedCookieJar #:nodoc:
498
- include ChainedCookieJars
646
+ class EncryptedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
499
647
  include SerializedCookieJars
500
648
 
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
649
+ def initialize(parent_jar)
650
+ super
513
651
 
514
- def [](name)
515
- if encrypted_message = @parent_jar[name]
516
- deserialize name, decrypt_and_verify(encrypted_message)
652
+ if request.use_authenticated_cookie_encryption
653
+ key_len = ActiveSupport::MessageEncryptor.key_len(encrypted_cookie_cipher)
654
+ secret = request.key_generator.generate_key(request.authenticated_encrypted_cookie_salt, key_len)
655
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: encrypted_cookie_cipher, serializer: SERIALIZER)
656
+ else
657
+ key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-cbc")
658
+ secret = request.key_generator.generate_key(request.encrypted_cookie_salt, key_len)
659
+ sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
660
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: SERIALIZER)
517
661
  end
518
- end
519
662
 
520
- def []=(name, options)
521
- if options.is_a?(Hash)
522
- options.symbolize_keys!
523
- else
524
- options = { :value => options }
663
+ request.cookies_rotations.encrypted.each do |(*secrets)|
664
+ options = secrets.extract_options!
665
+ @encryptor.rotate(*secrets, serializer: SERIALIZER, **options)
525
666
  end
526
667
 
527
- options[:value] = @encryptor.encrypt_and_sign(serialize(name, options[:value]))
668
+ if upgrade_legacy_hmac_aes_cbc_cookies?
669
+ legacy_cipher = "aes-256-cbc"
670
+ secret = request.key_generator.generate_key(request.encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len(legacy_cipher))
671
+ sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
528
672
 
529
- raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
530
- @parent_jar[name] = options
673
+ @encryptor.rotate(secret, sign_secret, cipher: legacy_cipher, digest: digest, serializer: SERIALIZER)
674
+ elsif prepare_upgrade_legacy_hmac_aes_cbc_cookies?
675
+ future_cipher = encrypted_cookie_cipher
676
+ secret = request.key_generator.generate_key(request.authenticated_encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len(future_cipher))
677
+
678
+ @encryptor.rotate(secret, nil, cipher: future_cipher, serializer: SERIALIZER)
679
+ end
531
680
  end
532
681
 
533
682
  private
534
- def decrypt_and_verify(encrypted_message)
535
- @encryptor.decrypt_and_verify(encrypted_message)
536
- rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
683
+ def parse(name, encrypted_message, purpose: nil)
684
+ rotated = false
685
+ data = @encryptor.decrypt_and_verify(encrypted_message, purpose: purpose, on_rotation: -> { rotated = true })
686
+ super(name, data, force_reserialize: rotated)
687
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature
537
688
  nil
538
689
  end
539
- end
540
-
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
547
690
 
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)
691
+ def commit(name, options)
692
+ super
693
+ options[:value] = @encryptor.encrypt_and_sign(options[:value], **cookie_metadata(name, options))
694
+ check_for_overflow!(name, options)
551
695
  end
552
- end
553
696
  end
554
697
 
555
698
  def initialize(app)
@@ -557,18 +700,18 @@ module ActionDispatch
557
700
  end
558
701
 
559
702
  def call(env)
560
- status, headers, body = @app.call(env)
703
+ request = ActionDispatch::Request.new(env)
704
+ response = @app.call(env)
561
705
 
562
- if cookie_jar = env['action_dispatch.cookies']
706
+ if request.have_cookie_jar?
707
+ cookie_jar = request.cookie_jar
563
708
  unless cookie_jar.committed?
564
- cookie_jar.write(headers)
565
- if headers[HTTP_HEADER].respond_to?(:join)
566
- headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
567
- end
709
+ response = Rack::Response[*response]
710
+ cookie_jar.write(response)
568
711
  end
569
712
  end
570
713
 
571
- [status, headers, body]
714
+ response.to_a
572
715
  end
573
716
  end
574
717
  end