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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +86 -600
- data/MIT-LICENSE +1 -1
- data/README.rdoc +13 -14
- data/lib/abstract_controller/asset_paths.rb +5 -1
- data/lib/abstract_controller/base.rb +166 -136
- data/lib/abstract_controller/caching/fragments.rb +149 -0
- data/lib/abstract_controller/caching.rb +68 -0
- data/lib/abstract_controller/callbacks.rb +126 -57
- data/lib/abstract_controller/collector.rb +13 -15
- data/lib/abstract_controller/deprecator.rb +9 -0
- data/lib/abstract_controller/error.rb +8 -0
- data/lib/abstract_controller/helpers.rb +181 -132
- data/lib/abstract_controller/logger.rb +5 -1
- data/lib/abstract_controller/railties/routes_helpers.rb +10 -3
- data/lib/abstract_controller/rendering.rb +56 -56
- data/lib/abstract_controller/translation.rb +29 -15
- data/lib/abstract_controller/url_for.rb +15 -11
- data/lib/abstract_controller.rb +21 -5
- data/lib/action_controller/api/api_rendering.rb +18 -0
- data/lib/action_controller/api.rb +154 -0
- data/lib/action_controller/base.rb +219 -155
- data/lib/action_controller/caching.rb +28 -68
- data/lib/action_controller/deprecator.rb +9 -0
- data/lib/action_controller/form_builder.rb +55 -0
- data/lib/action_controller/log_subscriber.rb +35 -22
- data/lib/action_controller/metal/allow_browser.rb +119 -0
- data/lib/action_controller/metal/basic_implicit_render.rb +17 -0
- data/lib/action_controller/metal/conditional_get.rb +259 -122
- data/lib/action_controller/metal/content_security_policy.rb +86 -0
- data/lib/action_controller/metal/cookies.rb +9 -5
- data/lib/action_controller/metal/data_streaming.rb +87 -104
- data/lib/action_controller/metal/default_headers.rb +21 -0
- data/lib/action_controller/metal/etag_with_flash.rb +22 -0
- data/lib/action_controller/metal/etag_with_template_digest.rb +35 -26
- data/lib/action_controller/metal/exceptions.rb +71 -24
- data/lib/action_controller/metal/flash.rb +26 -19
- data/lib/action_controller/metal/head.rb +45 -36
- data/lib/action_controller/metal/helpers.rb +80 -64
- data/lib/action_controller/metal/http_authentication.rb +297 -244
- data/lib/action_controller/metal/implicit_render.rb +57 -9
- data/lib/action_controller/metal/instrumentation.rb +76 -64
- data/lib/action_controller/metal/live.rb +238 -176
- data/lib/action_controller/metal/logging.rb +22 -0
- data/lib/action_controller/metal/mime_responds.rb +177 -166
- data/lib/action_controller/metal/parameter_encoding.rb +84 -0
- data/lib/action_controller/metal/params_wrapper.rb +145 -118
- data/lib/action_controller/metal/permissions_policy.rb +38 -0
- data/lib/action_controller/metal/rate_limiting.rb +62 -0
- data/lib/action_controller/metal/redirecting.rb +203 -64
- data/lib/action_controller/metal/renderers.rb +108 -65
- data/lib/action_controller/metal/rendering.rb +216 -56
- data/lib/action_controller/metal/request_forgery_protection.rb +496 -163
- data/lib/action_controller/metal/rescue.rb +19 -21
- data/lib/action_controller/metal/streaming.rb +179 -138
- data/lib/action_controller/metal/strong_parameters.rb +1058 -382
- data/lib/action_controller/metal/testing.rb +11 -17
- data/lib/action_controller/metal/url_for.rb +37 -21
- data/lib/action_controller/metal.rb +236 -138
- data/lib/action_controller/railtie.rb +89 -11
- data/lib/action_controller/railties/helpers.rb +5 -1
- data/lib/action_controller/renderer.rb +161 -0
- data/lib/action_controller/template_assertions.rb +13 -0
- data/lib/action_controller/test_case.rb +425 -497
- data/lib/action_controller.rb +44 -22
- data/lib/action_dispatch/constants.rb +34 -0
- data/lib/action_dispatch/deprecator.rb +9 -0
- data/lib/action_dispatch/http/cache.rb +119 -63
- data/lib/action_dispatch/http/content_disposition.rb +47 -0
- data/lib/action_dispatch/http/content_security_policy.rb +364 -0
- data/lib/action_dispatch/http/filter_parameters.rb +36 -34
- data/lib/action_dispatch/http/filter_redirect.rb +24 -12
- data/lib/action_dispatch/http/headers.rb +66 -31
- data/lib/action_dispatch/http/mime_negotiation.rb +106 -75
- data/lib/action_dispatch/http/mime_type.rb +196 -136
- data/lib/action_dispatch/http/mime_types.rb +25 -7
- data/lib/action_dispatch/http/parameters.rb +97 -45
- data/lib/action_dispatch/http/permissions_policy.rb +187 -0
- data/lib/action_dispatch/http/rack_cache.rb +6 -0
- data/lib/action_dispatch/http/request.rb +299 -170
- data/lib/action_dispatch/http/response.rb +311 -160
- data/lib/action_dispatch/http/upload.rb +52 -23
- data/lib/action_dispatch/http/url.rb +201 -125
- data/lib/action_dispatch/journey/formatter.rb +110 -50
- data/lib/action_dispatch/journey/gtg/builder.rb +37 -50
- data/lib/action_dispatch/journey/gtg/simulator.rb +20 -17
- data/lib/action_dispatch/journey/gtg/transition_table.rb +96 -36
- data/lib/action_dispatch/journey/nfa/dot.rb +5 -14
- data/lib/action_dispatch/journey/nodes/node.rb +100 -20
- data/lib/action_dispatch/journey/parser.rb +19 -17
- data/lib/action_dispatch/journey/parser.y +4 -3
- data/lib/action_dispatch/journey/parser_extras.rb +14 -4
- data/lib/action_dispatch/journey/path/pattern.rb +79 -63
- data/lib/action_dispatch/journey/route.rb +108 -44
- data/lib/action_dispatch/journey/router/utils.rb +41 -29
- data/lib/action_dispatch/journey/router.rb +64 -57
- data/lib/action_dispatch/journey/routes.rb +23 -21
- data/lib/action_dispatch/journey/scanner.rb +28 -17
- data/lib/action_dispatch/journey/visitors.rb +100 -54
- data/lib/action_dispatch/journey/visualizer/fsm.js +49 -24
- data/lib/action_dispatch/journey/visualizer/index.html.erb +1 -1
- data/lib/action_dispatch/journey.rb +7 -5
- data/lib/action_dispatch/log_subscriber.rb +25 -0
- data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
- data/lib/action_dispatch/middleware/assume_ssl.rb +27 -0
- data/lib/action_dispatch/middleware/callbacks.rb +7 -6
- data/lib/action_dispatch/middleware/cookies.rb +471 -328
- data/lib/action_dispatch/middleware/debug_exceptions.rb +149 -66
- data/lib/action_dispatch/middleware/debug_locks.rb +129 -0
- data/lib/action_dispatch/middleware/debug_view.rb +73 -0
- data/lib/action_dispatch/middleware/exception_wrapper.rb +275 -73
- data/lib/action_dispatch/middleware/executor.rb +32 -0
- data/lib/action_dispatch/middleware/flash.rb +143 -101
- data/lib/action_dispatch/middleware/host_authorization.rb +171 -0
- data/lib/action_dispatch/middleware/public_exceptions.rb +36 -27
- data/lib/action_dispatch/middleware/reloader.rb +10 -92
- data/lib/action_dispatch/middleware/remote_ip.rb +133 -107
- data/lib/action_dispatch/middleware/request_id.rb +29 -15
- data/lib/action_dispatch/middleware/server_timing.rb +78 -0
- data/lib/action_dispatch/middleware/session/abstract_store.rb +49 -27
- data/lib/action_dispatch/middleware/session/cache_store.rb +33 -16
- data/lib/action_dispatch/middleware/session/cookie_store.rb +86 -80
- data/lib/action_dispatch/middleware/session/mem_cache_store.rb +15 -3
- data/lib/action_dispatch/middleware/show_exceptions.rb +66 -36
- data/lib/action_dispatch/middleware/ssl.rb +134 -36
- data/lib/action_dispatch/middleware/stack.rb +109 -44
- data/lib/action_dispatch/middleware/static.rb +159 -90
- data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
- data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
- data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +7 -24
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
- data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +36 -0
- data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
- data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +46 -36
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +12 -0
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +9 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +26 -7
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +3 -3
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +16 -0
- data/lib/action_dispatch/middleware/templates/rescues/layout.erb +139 -15
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +23 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +6 -6
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +7 -7
- data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +9 -9
- data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +4 -4
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +1 -1
- data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +7 -4
- data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +125 -93
- data/lib/action_dispatch/railtie.rb +44 -16
- data/lib/action_dispatch/request/session.rb +159 -69
- data/lib/action_dispatch/request/utils.rb +97 -23
- data/lib/action_dispatch/routing/endpoint.rb +11 -2
- data/lib/action_dispatch/routing/inspector.rb +195 -106
- data/lib/action_dispatch/routing/mapper.rb +1338 -955
- data/lib/action_dispatch/routing/polymorphic_routes.rb +234 -201
- data/lib/action_dispatch/routing/redirection.rb +78 -51
- data/lib/action_dispatch/routing/route_set.rb +460 -374
- data/lib/action_dispatch/routing/routes_proxy.rb +36 -12
- data/lib/action_dispatch/routing/url_for.rb +172 -124
- data/lib/action_dispatch/routing.rb +159 -158
- data/lib/action_dispatch/system_test_case.rb +206 -0
- data/lib/action_dispatch/system_testing/browser.rb +84 -0
- data/lib/action_dispatch/system_testing/driver.rb +85 -0
- data/lib/action_dispatch/system_testing/server.rb +33 -0
- data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +164 -0
- data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +23 -0
- data/lib/action_dispatch/testing/assertion_response.rb +48 -0
- data/lib/action_dispatch/testing/assertions/response.rb +71 -39
- data/lib/action_dispatch/testing/assertions/routing.rb +228 -103
- data/lib/action_dispatch/testing/assertions.rb +9 -6
- data/lib/action_dispatch/testing/integration.rb +486 -306
- data/lib/action_dispatch/testing/request_encoder.rb +60 -0
- data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
- data/lib/action_dispatch/testing/test_process.rb +35 -22
- data/lib/action_dispatch/testing/test_request.rb +29 -34
- data/lib/action_dispatch/testing/test_response.rb +48 -15
- data/lib/action_dispatch.rb +82 -40
- data/lib/action_pack/gem_version.rb +8 -4
- data/lib/action_pack/version.rb +6 -2
- data/lib/action_pack.rb +21 -18
- metadata +146 -56
- data/lib/action_controller/caching/fragments.rb +0 -103
- data/lib/action_controller/metal/force_ssl.rb +0 -97
- data/lib/action_controller/metal/hide_actions.rb +0 -40
- data/lib/action_controller/metal/rack_delegation.rb +0 -32
- data/lib/action_controller/middleware.rb +0 -39
- data/lib/action_controller/model_naming.rb +0 -12
- data/lib/action_dispatch/http/parameter_filter.rb +0 -72
- data/lib/action_dispatch/journey/backwards.rb +0 -5
- data/lib/action_dispatch/journey/nfa/builder.rb +0 -76
- data/lib/action_dispatch/journey/nfa/simulator.rb +0 -47
- data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -163
- data/lib/action_dispatch/journey/router/strexp.rb +0 -27
- data/lib/action_dispatch/middleware/params_parser.rb +0 -60
- data/lib/action_dispatch/middleware/templates/rescues/_source.erb +0 -27
- data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
- data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
- data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
@@ -1,99 +1,211 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
require
|
6
|
-
require
|
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
|
-
|
12
|
+
module RequestCookieMethods
|
10
13
|
def cookie_jar
|
11
|
-
|
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
|
-
|
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
|
-
#
|
18
|
-
#
|
19
|
-
#
|
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
|
-
#
|
24
|
-
#
|
25
|
-
#
|
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
|
-
#
|
28
|
-
#
|
112
|
+
# # Sets a cookie that expires in 1 hour.
|
113
|
+
# cookies[:login] = { value: "XJ-122", expires: 1.hour }
|
29
114
|
#
|
30
|
-
#
|
31
|
-
#
|
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
|
-
#
|
34
|
-
#
|
35
|
-
#
|
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
|
-
#
|
39
|
-
#
|
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
|
-
#
|
42
|
-
#
|
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
|
-
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
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
|
-
#
|
143
|
+
# cookies.delete :user_name
|
54
144
|
#
|
55
|
-
# Please note that if you specify a
|
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
|
-
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
#
|
61
|
-
#
|
148
|
+
# cookies[:name] = {
|
149
|
+
# value: 'a yummy cookie',
|
150
|
+
# expires: 1.year,
|
151
|
+
# domain: 'domain.com'
|
152
|
+
# }
|
62
153
|
#
|
63
|
-
#
|
154
|
+
# cookies.delete(:name, domain: 'domain.com')
|
64
155
|
#
|
65
156
|
# The option symbols for setting cookies are:
|
66
157
|
#
|
67
|
-
# *
|
68
|
-
# *
|
69
|
-
#
|
70
|
-
# *
|
71
|
-
#
|
72
|
-
#
|
73
|
-
#
|
74
|
-
#
|
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
|
-
#
|
77
|
-
#
|
78
|
-
#
|
79
|
-
#
|
80
|
-
#
|
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"
|
89
|
-
GENERATOR_KEY = "action_dispatch.key_generator"
|
90
|
-
SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt"
|
91
|
-
ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt"
|
92
|
-
ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt"
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
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
|
-
#
|
109
|
-
#
|
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
|
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
|
227
|
+
# This jar allows chaining with the signed jar as well, so you can set
|
228
|
+
# permanent, signed cookies. Examples:
|
114
229
|
#
|
115
|
-
#
|
116
|
-
#
|
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
|
233
|
+
@permanent ||= PermanentCookieJar.new(self)
|
119
234
|
end
|
120
235
|
|
121
|
-
# Returns a jar that'll automatically generate a signed representation of cookie
|
122
|
-
# the cookie again. This is useful for
|
123
|
-
#
|
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
|
-
#
|
126
|
-
#
|
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
|
-
#
|
133
|
-
#
|
247
|
+
# cookies.signed[:discount] = 45
|
248
|
+
# # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
|
134
249
|
#
|
135
|
-
#
|
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
|
146
|
-
#
|
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
|
149
|
-
#
|
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
|
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
|
-
#
|
156
|
-
#
|
268
|
+
# cookies.encrypted[:discount] = 45
|
269
|
+
# # => Set-Cookie: discount=DIQ7fw==--K3n//8vvnSbGq9dA--7Xh91HfLpwzbj1czhBiwOg==; path=/
|
157
270
|
#
|
158
|
-
#
|
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
|
169
|
-
# Used by ActionDispatch::Session::CookieStore to
|
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
|
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
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
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
|
-
|
191
|
-
|
192
|
-
|
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
|
311
|
+
class CookieJar # :nodoc:
|
200
312
|
include Enumerable, ChainedCookieJars
|
201
313
|
|
202
|
-
|
203
|
-
|
204
|
-
|
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
|
-
|
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(
|
242
|
-
@key_generator = key_generator
|
322
|
+
def initialize(request)
|
243
323
|
@set_cookies = {}
|
244
324
|
@delete_cookies = {}
|
245
|
-
@
|
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
|
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
|
284
|
-
|
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
|
-
|
287
|
-
|
288
|
-
domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
|
368
|
+
@cookies.update set_cookies if set_cookies
|
369
|
+
end
|
289
370
|
|
290
|
-
|
291
|
-
|
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
|
302
|
-
#
|
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 = { :
|
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
|
324
|
-
# and the expiration date in the past. Like
|
325
|
-
#
|
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
|
-
#
|
339
|
-
#
|
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
|
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(
|
352
|
-
@set_cookies.each
|
353
|
-
|
354
|
-
|
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
|
-
|
357
|
-
|
358
|
-
|
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
|
-
|
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
|
504
|
+
class AbstractCookieJar # :nodoc:
|
371
505
|
include ChainedCookieJars
|
372
506
|
|
373
|
-
def initialize(parent_jar
|
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 = { :
|
527
|
+
options = { value: options }
|
388
528
|
end
|
389
529
|
|
390
|
-
options
|
530
|
+
commit(name, options)
|
391
531
|
@parent_jar[name] = options
|
392
532
|
end
|
393
|
-
end
|
394
533
|
|
395
|
-
|
396
|
-
|
397
|
-
ActiveSupport::JSON.decode(value)
|
398
|
-
end
|
534
|
+
protected
|
535
|
+
def request; @parent_jar.request; end
|
399
536
|
|
400
|
-
|
401
|
-
|
402
|
-
|
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
|
-
|
564
|
+
SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer
|
407
565
|
|
408
566
|
protected
|
409
|
-
def
|
410
|
-
|
567
|
+
def digest
|
568
|
+
request.cookies_digest || "SHA1"
|
411
569
|
end
|
412
570
|
|
413
|
-
|
414
|
-
|
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
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
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
|
430
|
-
|
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
|
442
|
-
|
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
|
447
|
-
include ChainedCookieJars
|
617
|
+
class SignedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
|
448
618
|
include SerializedCookieJars
|
449
619
|
|
450
|
-
def initialize(parent_jar
|
451
|
-
|
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
|
-
|
458
|
-
|
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
|
-
|
464
|
-
|
465
|
-
options
|
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
|
477
|
-
|
478
|
-
|
479
|
-
|
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
|
-
|
491
|
-
|
492
|
-
|
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
|
498
|
-
include ChainedCookieJars
|
646
|
+
class EncryptedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
|
499
647
|
include SerializedCookieJars
|
500
648
|
|
501
|
-
def initialize(parent_jar
|
502
|
-
|
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
|
-
|
515
|
-
|
516
|
-
|
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
|
-
|
521
|
-
|
522
|
-
options
|
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
|
-
|
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
|
-
|
530
|
-
|
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
|
535
|
-
|
536
|
-
|
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
|
-
|
549
|
-
|
550
|
-
|
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
|
-
|
703
|
+
request = ActionDispatch::Request.new(env)
|
704
|
+
response = @app.call(env)
|
561
705
|
|
562
|
-
if
|
706
|
+
if request.have_cookie_jar?
|
707
|
+
cookie_jar = request.cookie_jar
|
563
708
|
unless cookie_jar.committed?
|
564
|
-
|
565
|
-
|
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
|
-
|
714
|
+
response.to_a
|
572
715
|
end
|
573
716
|
end
|
574
717
|
end
|