actionpack 5.1.7 → 5.2.0.beta1
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 +132 -490
- data/README.rdoc +1 -1
- data/lib/abstract_controller.rb +2 -0
- data/lib/abstract_controller/asset_paths.rb +2 -0
- data/lib/abstract_controller/base.rb +10 -2
- data/lib/abstract_controller/caching.rb +3 -2
- data/lib/abstract_controller/caching/fragments.rb +30 -7
- data/lib/abstract_controller/callbacks.rb +25 -3
- data/lib/abstract_controller/collector.rb +2 -0
- data/lib/abstract_controller/error.rb +2 -0
- data/lib/abstract_controller/helpers.rb +4 -5
- data/lib/abstract_controller/logger.rb +2 -0
- data/lib/abstract_controller/railties/routes_helpers.rb +2 -0
- data/lib/abstract_controller/rendering.rb +9 -16
- data/lib/abstract_controller/translation.rb +2 -0
- data/lib/abstract_controller/url_for.rb +2 -0
- data/lib/action_controller.rb +3 -0
- data/lib/action_controller/api.rb +2 -0
- data/lib/action_controller/api/api_rendering.rb +2 -0
- data/lib/action_controller/base.rb +3 -0
- data/lib/action_controller/caching.rb +2 -0
- data/lib/action_controller/form_builder.rb +2 -0
- data/lib/action_controller/log_subscriber.rb +5 -3
- data/lib/action_controller/metal.rb +3 -2
- data/lib/action_controller/metal/basic_implicit_render.rb +2 -0
- data/lib/action_controller/metal/conditional_get.rb +4 -3
- data/lib/action_controller/metal/content_security_policy.rb +26 -0
- data/lib/action_controller/metal/cookies.rb +2 -0
- data/lib/action_controller/metal/data_streaming.rb +7 -5
- data/lib/action_controller/metal/etag_with_flash.rb +2 -0
- data/lib/action_controller/metal/etag_with_template_digest.rb +3 -2
- data/lib/action_controller/metal/exceptions.rb +2 -3
- data/lib/action_controller/metal/flash.rb +3 -2
- data/lib/action_controller/metal/force_ssl.rb +2 -0
- data/lib/action_controller/metal/head.rb +2 -0
- data/lib/action_controller/metal/helpers.rb +4 -3
- data/lib/action_controller/metal/http_authentication.rb +8 -9
- data/lib/action_controller/metal/implicit_render.rb +2 -0
- data/lib/action_controller/metal/instrumentation.rb +4 -6
- data/lib/action_controller/metal/live.rb +3 -1
- data/lib/action_controller/metal/mime_responds.rb +3 -1
- data/lib/action_controller/metal/parameter_encoding.rb +2 -0
- data/lib/action_controller/metal/params_wrapper.rb +13 -9
- data/lib/action_controller/metal/redirecting.rb +21 -10
- data/lib/action_controller/metal/renderers.rb +4 -3
- data/lib/action_controller/metal/rendering.rb +2 -2
- data/lib/action_controller/metal/request_forgery_protection.rb +22 -6
- data/lib/action_controller/metal/rescue.rb +5 -3
- data/lib/action_controller/metal/streaming.rb +2 -0
- data/lib/action_controller/metal/strong_parameters.rb +19 -11
- data/lib/action_controller/metal/testing.rb +2 -6
- data/lib/action_controller/metal/url_for.rb +2 -0
- data/lib/action_controller/railtie.rb +16 -4
- data/lib/action_controller/railties/helpers.rb +2 -0
- data/lib/action_controller/renderer.rb +2 -0
- data/lib/action_controller/template_assertions.rb +2 -0
- data/lib/action_controller/test_case.rb +4 -1
- data/lib/action_dispatch.rb +3 -0
- data/lib/action_dispatch/http/cache.rb +15 -9
- data/lib/action_dispatch/http/content_security_policy.rb +233 -0
- data/lib/action_dispatch/http/filter_parameters.rb +4 -2
- data/lib/action_dispatch/http/filter_redirect.rb +2 -0
- data/lib/action_dispatch/http/headers.rb +2 -0
- data/lib/action_dispatch/http/mime_negotiation.rb +4 -13
- data/lib/action_dispatch/http/mime_type.rb +15 -13
- data/lib/action_dispatch/http/mime_types.rb +4 -2
- data/lib/action_dispatch/http/parameter_filter.rb +2 -0
- data/lib/action_dispatch/http/parameters.rb +6 -9
- data/lib/action_dispatch/http/rack_cache.rb +2 -0
- data/lib/action_dispatch/http/request.rb +36 -16
- data/lib/action_dispatch/http/response.rb +11 -9
- data/lib/action_dispatch/http/upload.rb +2 -0
- data/lib/action_dispatch/http/url.rb +4 -5
- data/lib/action_dispatch/journey.rb +2 -0
- data/lib/action_dispatch/journey/formatter.rb +4 -2
- data/lib/action_dispatch/journey/gtg/builder.rb +2 -0
- data/lib/action_dispatch/journey/gtg/simulator.rb +2 -8
- data/lib/action_dispatch/journey/gtg/transition_table.rb +3 -2
- data/lib/action_dispatch/journey/nfa/builder.rb +2 -0
- data/lib/action_dispatch/journey/nfa/dot.rb +2 -0
- data/lib/action_dispatch/journey/nfa/simulator.rb +2 -0
- data/lib/action_dispatch/journey/nfa/transition_table.rb +2 -0
- data/lib/action_dispatch/journey/nodes/node.rb +2 -0
- data/lib/action_dispatch/journey/parser_extras.rb +2 -0
- data/lib/action_dispatch/journey/path/pattern.rb +2 -0
- data/lib/action_dispatch/journey/route.rb +15 -6
- data/lib/action_dispatch/journey/router.rb +3 -1
- data/lib/action_dispatch/journey/router/utils.rb +14 -7
- data/lib/action_dispatch/journey/routes.rb +2 -1
- data/lib/action_dispatch/journey/scanner.rb +1 -0
- data/lib/action_dispatch/journey/visitors.rb +5 -3
- data/lib/action_dispatch/middleware/callbacks.rb +2 -0
- data/lib/action_dispatch/middleware/cookies.rb +141 -91
- data/lib/action_dispatch/middleware/debug_exceptions.rb +4 -2
- data/lib/action_dispatch/middleware/debug_locks.rb +9 -7
- data/lib/action_dispatch/middleware/exception_wrapper.rb +4 -6
- data/lib/action_dispatch/middleware/executor.rb +2 -0
- data/lib/action_dispatch/middleware/flash.rb +3 -1
- data/lib/action_dispatch/middleware/public_exceptions.rb +6 -4
- data/lib/action_dispatch/middleware/reloader.rb +2 -0
- data/lib/action_dispatch/middleware/remote_ip.rb +7 -5
- data/lib/action_dispatch/middleware/request_id.rb +2 -0
- data/lib/action_dispatch/middleware/session/abstract_store.rb +3 -1
- data/lib/action_dispatch/middleware/session/cache_store.rb +2 -0
- data/lib/action_dispatch/middleware/session/cookie_store.rb +13 -25
- data/lib/action_dispatch/middleware/session/mem_cache_store.rb +2 -0
- data/lib/action_dispatch/middleware/show_exceptions.rb +3 -1
- data/lib/action_dispatch/middleware/ssl.rb +42 -37
- data/lib/action_dispatch/middleware/stack.rb +2 -0
- data/lib/action_dispatch/middleware/static.rb +10 -8
- data/lib/action_dispatch/middleware/templates/rescues/layout.erb +1 -0
- data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +6 -2
- data/lib/action_dispatch/railtie.rb +7 -0
- data/lib/action_dispatch/request/session.rb +8 -4
- data/lib/action_dispatch/request/utils.rb +4 -4
- data/lib/action_dispatch/routing.rb +3 -1
- data/lib/action_dispatch/routing/endpoint.rb +8 -4
- data/lib/action_dispatch/routing/inspector.rb +5 -3
- data/lib/action_dispatch/routing/mapper.rb +62 -51
- data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -0
- data/lib/action_dispatch/routing/redirection.rb +7 -5
- data/lib/action_dispatch/routing/route_set.rb +26 -33
- data/lib/action_dispatch/routing/routes_proxy.rb +5 -2
- data/lib/action_dispatch/routing/url_for.rb +6 -4
- data/lib/action_dispatch/system_test_case.rb +14 -6
- data/lib/action_dispatch/system_testing/driver.rb +20 -2
- data/lib/action_dispatch/system_testing/server.rb +2 -16
- data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +6 -4
- data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +2 -0
- data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
- data/lib/action_dispatch/testing/assertion_response.rb +2 -0
- data/lib/action_dispatch/testing/assertions.rb +2 -0
- data/lib/action_dispatch/testing/assertions/response.rb +4 -2
- data/lib/action_dispatch/testing/assertions/routing.rb +5 -5
- data/lib/action_dispatch/testing/integration.rb +24 -21
- data/lib/action_dispatch/testing/request_encoder.rb +2 -0
- data/lib/action_dispatch/testing/test_process.rb +2 -0
- data/lib/action_dispatch/testing/test_request.rb +3 -1
- data/lib/action_dispatch/testing/test_response.rb +23 -3
- data/lib/action_pack.rb +2 -0
- data/lib/action_pack/gem_version.rb +5 -3
- data/lib/action_pack/version.rb +2 -0
- metadata +17 -13
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "action_controller/metal/exceptions"
|
2
4
|
|
3
5
|
module ActionDispatch
|
@@ -15,7 +17,7 @@ module ActionDispatch
|
|
15
17
|
|
16
18
|
def generate(name, options, path_parameters, parameterize = nil)
|
17
19
|
constraints = path_parameters.merge(options)
|
18
|
-
missing_keys = nil
|
20
|
+
missing_keys = nil
|
19
21
|
|
20
22
|
match_route(name, constraints) do |route|
|
21
23
|
parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize)
|
@@ -48,7 +50,7 @@ module ActionDispatch
|
|
48
50
|
unmatched_keys = (missing_keys || []) & constraints.keys
|
49
51
|
missing_keys = (missing_keys || []) - unmatched_keys
|
50
52
|
|
51
|
-
message = "No route matches #{Hash[constraints.sort_by { |k, v| k.to_s }].inspect}"
|
53
|
+
message = "No route matches #{Hash[constraints.sort_by { |k, v| k.to_s }].inspect}".dup
|
52
54
|
message << ", missing required keys: #{missing_keys.sort.inspect}" if missing_keys && !missing_keys.empty?
|
53
55
|
message << ", possible unmatched constraints: #{unmatched_keys.sort.inspect}" if unmatched_keys && !unmatched_keys.empty?
|
54
56
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "strscan"
|
2
4
|
|
3
5
|
module ActionDispatch
|
@@ -18,14 +20,6 @@ module ActionDispatch
|
|
18
20
|
@tt = transition_table
|
19
21
|
end
|
20
22
|
|
21
|
-
def simulate(string)
|
22
|
-
ms = memos(string) { return }
|
23
|
-
MatchData.new(ms)
|
24
|
-
end
|
25
|
-
|
26
|
-
alias :=~ :simulate
|
27
|
-
alias :match :simulate
|
28
|
-
|
29
23
|
def memos(string)
|
30
24
|
input = StringScanner.new(string)
|
31
25
|
state = [0]
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "action_dispatch/journey/nfa/dot"
|
2
4
|
|
3
5
|
module ActionDispatch
|
@@ -82,7 +84,7 @@ module ActionDispatch
|
|
82
84
|
end
|
83
85
|
|
84
86
|
def visualizer(paths, title = "FSM")
|
85
|
-
viz_dir = File.join
|
87
|
+
viz_dir = File.join __dir__, "..", "visualizer"
|
86
88
|
fsm_js = File.read File.join(viz_dir, "fsm.js")
|
87
89
|
fsm_css = File.read File.join(viz_dir, "fsm.css")
|
88
90
|
erb = File.read File.join(viz_dir, "index.html.erb")
|
@@ -109,7 +111,6 @@ module ActionDispatch
|
|
109
111
|
svg = to_svg
|
110
112
|
javascripts = [states, fsm_js]
|
111
113
|
|
112
|
-
# Annoying hack warnings
|
113
114
|
fun_routes = fun_routes
|
114
115
|
stylesheets = stylesheets
|
115
116
|
svg = svg
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActionDispatch
|
2
4
|
# :stopdoc:
|
3
5
|
module Journey
|
@@ -10,11 +12,11 @@ module ActionDispatch
|
|
10
12
|
module VerbMatchers
|
11
13
|
VERBS = %w{ DELETE GET HEAD OPTIONS LINK PATCH POST PUT TRACE UNLINK }
|
12
14
|
VERBS.each do |v|
|
13
|
-
class_eval <<-eoc
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
class_eval <<-eoc, __FILE__, __LINE__ + 1
|
16
|
+
class #{v}
|
17
|
+
def self.verb; name.split("::").last; end
|
18
|
+
def self.call(req); req.#{v.downcase}?; end
|
19
|
+
end
|
18
20
|
eoc
|
19
21
|
end
|
20
22
|
|
@@ -89,8 +91,15 @@ module ActionDispatch
|
|
89
91
|
end
|
90
92
|
end
|
91
93
|
|
94
|
+
# Needed for `rails routes`. Picks up succinctly defined requirements
|
95
|
+
# for a route, for example route
|
96
|
+
#
|
97
|
+
# get 'photo/:id', :controller => 'photos', :action => 'show',
|
98
|
+
# :id => /[A-Z]\d{5}/
|
99
|
+
#
|
100
|
+
# will have {:controller=>"photos", :action=>"show", :id=>/[A-Z]\d{5}/}
|
101
|
+
# as requirements.
|
92
102
|
def requirements
|
93
|
-
# needed for rails `rails routes`
|
94
103
|
@defaults.merge(path.requirements).delete_if { |_, v|
|
95
104
|
/.+?/ == v
|
96
105
|
}
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "action_dispatch/journey/router/utils"
|
2
4
|
require "action_dispatch/journey/routes"
|
3
5
|
require "action_dispatch/journey/formatter"
|
@@ -59,7 +61,7 @@ module ActionDispatch
|
|
59
61
|
return [status, headers, body]
|
60
62
|
end
|
61
63
|
|
62
|
-
|
64
|
+
[404, { "X-Cascade" => "pass" }, ["Not Found"]]
|
63
65
|
end
|
64
66
|
|
65
67
|
def recognize(rails_req)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActionDispatch
|
2
4
|
module Journey # :nodoc:
|
3
5
|
class Router # :nodoc:
|
@@ -5,7 +7,7 @@ module ActionDispatch
|
|
5
7
|
# Normalizes URI path.
|
6
8
|
#
|
7
9
|
# Strips off trailing slash and ensures there is a leading slash.
|
8
|
-
# Also converts downcase
|
10
|
+
# Also converts downcase URL encoded string to uppercase.
|
9
11
|
#
|
10
12
|
# normalize_path("/foo") # => "/foo"
|
11
13
|
# normalize_path("/foo/") # => "/foo"
|
@@ -13,23 +15,24 @@ module ActionDispatch
|
|
13
15
|
# normalize_path("") # => "/"
|
14
16
|
# normalize_path("/%ab") # => "/%AB"
|
15
17
|
def self.normalize_path(path)
|
18
|
+
path ||= ""
|
16
19
|
encoding = path.encoding
|
17
|
-
path = "/#{path}"
|
20
|
+
path = "/#{path}".dup
|
18
21
|
path.squeeze!("/".freeze)
|
19
22
|
path.sub!(%r{/+\Z}, "".freeze)
|
20
23
|
path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase }
|
21
|
-
path = "/" if path == "".freeze
|
24
|
+
path = "/".dup if path == "".freeze
|
22
25
|
path.force_encoding(encoding)
|
23
26
|
path
|
24
27
|
end
|
25
28
|
|
26
29
|
# URI path and fragment escaping
|
27
|
-
#
|
30
|
+
# https://tools.ietf.org/html/rfc3986
|
28
31
|
class UriEncoder # :nodoc:
|
29
32
|
ENCODE = "%%%02X".freeze
|
30
33
|
US_ASCII = Encoding::US_ASCII
|
31
34
|
UTF_8 = Encoding::UTF_8
|
32
|
-
EMPTY = "".force_encoding(US_ASCII).freeze
|
35
|
+
EMPTY = "".dup.force_encoding(US_ASCII).freeze
|
33
36
|
DEC2HEX = (0..255).to_a.map { |i| ENCODE % i }.map { |s| s.force_encoding(US_ASCII) }
|
34
37
|
|
35
38
|
ALPHA = "a-zA-Z".freeze
|
@@ -61,11 +64,11 @@ module ActionDispatch
|
|
61
64
|
end
|
62
65
|
|
63
66
|
private
|
64
|
-
def escape(component, pattern)
|
67
|
+
def escape(component, pattern)
|
65
68
|
component.gsub(pattern) { |unsafe| percent_encode(unsafe) }.force_encoding(US_ASCII)
|
66
69
|
end
|
67
70
|
|
68
|
-
def percent_encode(unsafe)
|
71
|
+
def percent_encode(unsafe)
|
69
72
|
safe = EMPTY.dup
|
70
73
|
unsafe.each_byte { |b| safe << DEC2HEX[b] }
|
71
74
|
safe
|
@@ -86,6 +89,10 @@ module ActionDispatch
|
|
86
89
|
ENCODER.escape_fragment(fragment.to_s)
|
87
90
|
end
|
88
91
|
|
92
|
+
# Replaces any escaped sequences with their unescaped representations.
|
93
|
+
#
|
94
|
+
# uri = "/topics?title=Ruby%20on%20Rails"
|
95
|
+
# unescape_uri(uri) #=> "/topics?title=Ruby on Rails"
|
89
96
|
def self.unescape_uri(uri)
|
90
97
|
ENCODER.unescape_uri(uri)
|
91
98
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActionDispatch
|
2
4
|
module Journey # :nodoc:
|
3
5
|
# The Routing table. Contains all routes for a system. Routes can be
|
@@ -54,7 +56,6 @@ module ActionDispatch
|
|
54
56
|
end
|
55
57
|
|
56
58
|
def simulator
|
57
|
-
return if ast.nil?
|
58
59
|
@simulator ||= begin
|
59
60
|
gtg = GTG::Builder.new(ast).transition_table
|
60
61
|
GTG::Simulator.new(gtg)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActionDispatch
|
2
4
|
# :stopdoc:
|
3
5
|
module Journey
|
@@ -154,7 +156,7 @@ module ActionDispatch
|
|
154
156
|
end
|
155
157
|
end
|
156
158
|
|
157
|
-
# Loop through the requirements AST
|
159
|
+
# Loop through the requirements AST.
|
158
160
|
class Each < FunctionalVisitor # :nodoc:
|
159
161
|
def visit(node, block)
|
160
162
|
block.call(node)
|
@@ -175,7 +177,7 @@ module ActionDispatch
|
|
175
177
|
last_child = node.children.last
|
176
178
|
node.children.inject(seed) { |s, c|
|
177
179
|
string = visit(c, s)
|
178
|
-
string << "|"
|
180
|
+
string << "|" unless last_child == c
|
179
181
|
string
|
180
182
|
}
|
181
183
|
end
|
@@ -185,7 +187,7 @@ module ActionDispatch
|
|
185
187
|
end
|
186
188
|
|
187
189
|
def visit_GROUP(node, seed)
|
188
|
-
visit(node.left, seed << "("
|
190
|
+
visit(node.left, seed.dup << "(") << ")"
|
189
191
|
end
|
190
192
|
|
191
193
|
INSTANCE = new
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "active_support/core_ext/hash/keys"
|
2
4
|
require "active_support/key_generator"
|
3
5
|
require "active_support/message_verifier"
|
@@ -43,6 +45,22 @@ module ActionDispatch
|
|
43
45
|
get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT
|
44
46
|
end
|
45
47
|
|
48
|
+
def authenticated_encrypted_cookie_salt
|
49
|
+
get_header Cookies::AUTHENTICATED_ENCRYPTED_COOKIE_SALT
|
50
|
+
end
|
51
|
+
|
52
|
+
def use_authenticated_cookie_encryption
|
53
|
+
get_header Cookies::USE_AUTHENTICATED_COOKIE_ENCRYPTION
|
54
|
+
end
|
55
|
+
|
56
|
+
def encrypted_cookie_cipher
|
57
|
+
get_header Cookies::ENCRYPTED_COOKIE_CIPHER
|
58
|
+
end
|
59
|
+
|
60
|
+
def signed_cookie_digest
|
61
|
+
get_header Cookies::SIGNED_COOKIE_DIGEST
|
62
|
+
end
|
63
|
+
|
46
64
|
def secret_token
|
47
65
|
get_header Cookies::SECRET_TOKEN
|
48
66
|
end
|
@@ -58,6 +76,11 @@ module ActionDispatch
|
|
58
76
|
def cookies_digest
|
59
77
|
get_header Cookies::COOKIES_DIGEST
|
60
78
|
end
|
79
|
+
|
80
|
+
def cookies_rotations
|
81
|
+
get_header Cookies::COOKIES_ROTATIONS
|
82
|
+
end
|
83
|
+
|
61
84
|
# :startdoc:
|
62
85
|
end
|
63
86
|
|
@@ -77,16 +100,17 @@ module ActionDispatch
|
|
77
100
|
# cookies[:lat_lon] = JSON.generate([47.68, -122.37])
|
78
101
|
#
|
79
102
|
# # Sets a cookie that expires in 1 hour.
|
80
|
-
# cookies[:login] = { value: "XJ-122", expires: 1.hour
|
103
|
+
# cookies[:login] = { value: "XJ-122", expires: 1.hour }
|
104
|
+
#
|
105
|
+
# # Sets a cookie that expires at a specific time.
|
106
|
+
# cookies[:login] = { value: "XJ-122", expires: Time.utc(2020, 10, 15, 5) }
|
81
107
|
#
|
82
108
|
# # Sets a signed cookie, which prevents users from tampering with its value.
|
83
|
-
# # The cookie is signed by your app's `secrets.secret_key_base` value.
|
84
109
|
# # It can be read using the signed method `cookies.signed[:name]`
|
85
110
|
# cookies.signed[:user_id] = current_user.id
|
86
111
|
#
|
87
112
|
# # Sets an encrypted cookie value before sending it to the client which
|
88
113
|
# # prevent users from reading and tampering with its value.
|
89
|
-
# # The cookie is signed by your app's `secrets.secret_key_base` value.
|
90
114
|
# # It can be read using the encrypted method `cookies.encrypted[:name]`
|
91
115
|
# cookies.encrypted[:discount] = 45
|
92
116
|
#
|
@@ -94,7 +118,7 @@ module ActionDispatch
|
|
94
118
|
# cookies.permanent[:login] = "XJ-122"
|
95
119
|
#
|
96
120
|
# # You can also chain these methods:
|
97
|
-
# cookies.permanent
|
121
|
+
# cookies.signed.permanent[:login] = "XJ-122"
|
98
122
|
#
|
99
123
|
# Examples of reading:
|
100
124
|
#
|
@@ -112,7 +136,7 @@ module ActionDispatch
|
|
112
136
|
#
|
113
137
|
# cookies[:name] = {
|
114
138
|
# value: 'a yummy cookie',
|
115
|
-
# expires: 1.year
|
139
|
+
# expires: 1.year,
|
116
140
|
# domain: 'domain.com'
|
117
141
|
# }
|
118
142
|
#
|
@@ -137,8 +161,8 @@ module ActionDispatch
|
|
137
161
|
#
|
138
162
|
# * <tt>:tld_length</tt> - When using <tt>:domain => :all</tt>, this option can be used to explicitly
|
139
163
|
# set the TLD length when using a short (<= 3 character) domain that is being interpreted as part of a TLD.
|
140
|
-
# For example, to share cookies between user1.lvh.me and user2.lvh.me, set <tt>:tld_length</tt> to
|
141
|
-
# * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object.
|
164
|
+
# For example, to share cookies between user1.lvh.me and user2.lvh.me, set <tt>:tld_length</tt> to 2.
|
165
|
+
# * <tt>:expires</tt> - The time at which this cookie expires, as a \Time or ActiveSupport::Duration object.
|
142
166
|
# * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers.
|
143
167
|
# Default is +false+.
|
144
168
|
# * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or
|
@@ -149,10 +173,15 @@ module ActionDispatch
|
|
149
173
|
SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze
|
150
174
|
ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze
|
151
175
|
ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze
|
176
|
+
AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt".freeze
|
177
|
+
USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption".freeze
|
178
|
+
ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher".freeze
|
179
|
+
SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest".freeze
|
152
180
|
SECRET_TOKEN = "action_dispatch.secret_token".freeze
|
153
181
|
SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
|
154
182
|
COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze
|
155
183
|
COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze
|
184
|
+
COOKIES_ROTATIONS = "action_dispatch.cookies_rotations".freeze
|
156
185
|
|
157
186
|
# Cookies can typically store 4096 bytes.
|
158
187
|
MAX_COOKIE_SIZE = 4096
|
@@ -160,7 +189,7 @@ module ActionDispatch
|
|
160
189
|
# Raised when storing more than 4K of session data.
|
161
190
|
CookieOverflow = Class.new StandardError
|
162
191
|
|
163
|
-
# Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed
|
192
|
+
# Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed.
|
164
193
|
module ChainedCookieJars
|
165
194
|
# Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example:
|
166
195
|
#
|
@@ -181,10 +210,10 @@ module ActionDispatch
|
|
181
210
|
# the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
|
182
211
|
# cookie was tampered with by the user (or a 3rd party), +nil+ will be returned.
|
183
212
|
#
|
184
|
-
# If +
|
213
|
+
# If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
|
185
214
|
# legacy cookies signed with the old key generator will be transparently upgraded.
|
186
215
|
#
|
187
|
-
# This jar requires that you set a suitable secret for the verification on your app's +
|
216
|
+
# This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+.
|
188
217
|
#
|
189
218
|
# Example:
|
190
219
|
#
|
@@ -193,35 +222,28 @@ module ActionDispatch
|
|
193
222
|
#
|
194
223
|
# cookies.signed[:discount] # => 45
|
195
224
|
def signed
|
196
|
-
@signed ||=
|
197
|
-
if upgrade_legacy_signed_cookies?
|
198
|
-
UpgradeLegacySignedCookieJar.new(self)
|
199
|
-
else
|
200
|
-
SignedCookieJar.new(self)
|
201
|
-
end
|
225
|
+
@signed ||= SignedKeyRotatingCookieJar.new(self)
|
202
226
|
end
|
203
227
|
|
204
228
|
# Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read.
|
205
229
|
# If the cookie was tampered with by the user (or a 3rd party), +nil+ will be returned.
|
206
230
|
#
|
207
|
-
# If +
|
231
|
+
# If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
|
208
232
|
# legacy cookies signed with the old key generator will be transparently upgraded.
|
209
233
|
#
|
210
|
-
#
|
234
|
+
# If +config.action_dispatch.encrypted_cookie_salt+ and +config.action_dispatch.encrypted_signed_cookie_salt+
|
235
|
+
# are both set, legacy cookies encrypted with HMAC AES-256-CBC will be transparently upgraded.
|
236
|
+
#
|
237
|
+
# This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+.
|
211
238
|
#
|
212
239
|
# Example:
|
213
240
|
#
|
214
241
|
# cookies.encrypted[:discount] = 45
|
215
|
-
# # => Set-Cookie: discount=
|
242
|
+
# # => Set-Cookie: discount=DIQ7fw==--K3n//8vvnSbGq9dA--7Xh91HfLpwzbj1czhBiwOg==; path=/
|
216
243
|
#
|
217
244
|
# cookies.encrypted[:discount] # => 45
|
218
245
|
def encrypted
|
219
|
-
@encrypted ||=
|
220
|
-
if upgrade_legacy_signed_cookies?
|
221
|
-
UpgradeLegacyEncryptedCookieJar.new(self)
|
222
|
-
else
|
223
|
-
EncryptedCookieJar.new(self)
|
224
|
-
end
|
246
|
+
@encrypted ||= EncryptedKeyRotatingCookieJar.new(self)
|
225
247
|
end
|
226
248
|
|
227
249
|
# Returns the +signed+ or +encrypted+ jar, preferring +encrypted+ if +secret_key_base+ is set.
|
@@ -240,29 +262,20 @@ module ActionDispatch
|
|
240
262
|
def upgrade_legacy_signed_cookies?
|
241
263
|
request.secret_token.present? && request.secret_key_base.present?
|
242
264
|
end
|
243
|
-
end
|
244
265
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
super
|
252
|
-
@legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
|
253
|
-
end
|
266
|
+
def upgrade_legacy_hmac_aes_cbc_cookies?
|
267
|
+
request.secret_key_base.present? &&
|
268
|
+
request.encrypted_signed_cookie_salt.present? &&
|
269
|
+
request.encrypted_cookie_salt.present? &&
|
270
|
+
request.use_authenticated_cookie_encryption
|
271
|
+
end
|
254
272
|
|
255
|
-
|
256
|
-
|
257
|
-
self[name] = { value: value }
|
273
|
+
def encrypted_cookie_cipher
|
274
|
+
request.encrypted_cookie_cipher || "aes-256-gcm"
|
258
275
|
end
|
259
|
-
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
260
|
-
nil
|
261
|
-
end
|
262
276
|
|
263
|
-
|
264
|
-
|
265
|
-
super || verify_and_upgrade_legacy_signed_message(name, signed_message)
|
277
|
+
def signed_cookie_digest
|
278
|
+
request.signed_cookie_digest || "SHA1"
|
266
279
|
end
|
267
280
|
end
|
268
281
|
|
@@ -341,20 +354,24 @@ module ActionDispatch
|
|
341
354
|
@cookies.map { |k, v| "#{escape(k)}=#{escape(v)}" }.join "; "
|
342
355
|
end
|
343
356
|
|
344
|
-
def handle_options(options)
|
357
|
+
def handle_options(options) # :nodoc:
|
358
|
+
if options[:expires].respond_to?(:from_now)
|
359
|
+
options[:expires] = options[:expires].from_now
|
360
|
+
end
|
361
|
+
|
345
362
|
options[:path] ||= "/"
|
346
363
|
|
347
364
|
if options[:domain] == :all || options[:domain] == "all"
|
348
|
-
#
|
365
|
+
# If there is a provided tld length then we use it otherwise default domain regexp.
|
349
366
|
domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
|
350
367
|
|
351
|
-
#
|
368
|
+
# If host is not ip and matches domain regexp.
|
352
369
|
# (ip confirms to domain regexp so we explicitly check for ip)
|
353
370
|
options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
|
354
371
|
".#{$&}"
|
355
372
|
end
|
356
373
|
elsif options[:domain].is_a? Array
|
357
|
-
#
|
374
|
+
# If host matches one of the supplied domains without a dot in front of it.
|
358
375
|
options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
|
359
376
|
end
|
360
377
|
end
|
@@ -404,7 +421,7 @@ module ActionDispatch
|
|
404
421
|
@delete_cookies[name.to_s] == options
|
405
422
|
end
|
406
423
|
|
407
|
-
# Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie
|
424
|
+
# Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie.
|
408
425
|
def clear(options = {})
|
409
426
|
@cookies.each_key { |k| delete(k, options) }
|
410
427
|
end
|
@@ -415,8 +432,7 @@ module ActionDispatch
|
|
415
432
|
end
|
416
433
|
end
|
417
434
|
|
418
|
-
mattr_accessor :always_write_cookie
|
419
|
-
self.always_write_cookie = false
|
435
|
+
mattr_accessor :always_write_cookie, default: false
|
420
436
|
|
421
437
|
private
|
422
438
|
|
@@ -470,6 +486,14 @@ module ActionDispatch
|
|
470
486
|
def request; @parent_jar.request; end
|
471
487
|
|
472
488
|
private
|
489
|
+
def expiry_options(options)
|
490
|
+
if options[:expires].respond_to?(:from_now)
|
491
|
+
{ expires_in: options[:expires] }
|
492
|
+
else
|
493
|
+
{ expires_at: options[:expires] }
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
473
497
|
def parse(name, data); data; end
|
474
498
|
def commit(options); end
|
475
499
|
end
|
@@ -493,6 +517,7 @@ module ActionDispatch
|
|
493
517
|
|
494
518
|
module SerializedCookieJars # :nodoc:
|
495
519
|
MARSHAL_SIGNATURE = "\x04\x08".freeze
|
520
|
+
SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer
|
496
521
|
|
497
522
|
protected
|
498
523
|
def needs_migration?(value)
|
@@ -503,12 +528,16 @@ module ActionDispatch
|
|
503
528
|
serializer.dump(value)
|
504
529
|
end
|
505
530
|
|
506
|
-
def deserialize(name
|
531
|
+
def deserialize(name)
|
532
|
+
rotate = false
|
533
|
+
value = yield -> { rotate = true }
|
534
|
+
|
507
535
|
if value
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
536
|
+
case
|
537
|
+
when needs_migration?(value)
|
538
|
+
self[name] = Marshal.load(value)
|
539
|
+
when rotate
|
540
|
+
self[name] = serializer.load(value)
|
512
541
|
else
|
513
542
|
serializer.load(value)
|
514
543
|
end
|
@@ -530,77 +559,98 @@ module ActionDispatch
|
|
530
559
|
def digest
|
531
560
|
request.cookies_digest || "SHA1"
|
532
561
|
end
|
533
|
-
|
534
|
-
def key_generator
|
535
|
-
request.key_generator
|
536
|
-
end
|
537
562
|
end
|
538
563
|
|
539
|
-
class
|
564
|
+
class SignedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
|
540
565
|
include SerializedCookieJars
|
541
566
|
|
542
567
|
def initialize(parent_jar)
|
543
568
|
super
|
544
|
-
|
545
|
-
|
569
|
+
|
570
|
+
secret = request.key_generator.generate_key(request.signed_cookie_salt)
|
571
|
+
@verifier = ActiveSupport::MessageVerifier.new(secret, digest: signed_cookie_digest, serializer: SERIALIZER)
|
572
|
+
|
573
|
+
request.cookies_rotations.signed.each do |*secrets, **options|
|
574
|
+
@verifier.rotate(*secrets, serializer: SERIALIZER, **options)
|
575
|
+
end
|
576
|
+
|
577
|
+
if upgrade_legacy_signed_cookies?
|
578
|
+
@verifier.rotate request.secret_token, serializer: SERIALIZER
|
579
|
+
end
|
546
580
|
end
|
547
581
|
|
548
582
|
private
|
549
583
|
def parse(name, signed_message)
|
550
|
-
deserialize
|
584
|
+
deserialize(name) do |rotate|
|
585
|
+
@verifier.verified(signed_message, on_rotation: rotate)
|
586
|
+
end
|
551
587
|
end
|
552
588
|
|
553
589
|
def commit(options)
|
554
|
-
options[:value] = @verifier.generate(serialize(options[:value]))
|
590
|
+
options[:value] = @verifier.generate(serialize(options[:value]), expiry_options(options))
|
555
591
|
|
556
592
|
raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
|
557
593
|
end
|
558
594
|
end
|
559
595
|
|
560
|
-
|
561
|
-
# secrets.secret_token and secrets.secret_key_base are both set. It reads
|
562
|
-
# legacy cookies signed with the old dummy key generator and signs and
|
563
|
-
# re-saves them using the new key generator to provide a smooth upgrade path.
|
564
|
-
class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc:
|
565
|
-
include VerifyAndUpgradeLegacySignedMessage
|
566
|
-
end
|
567
|
-
|
568
|
-
class EncryptedCookieJar < AbstractCookieJar # :nodoc:
|
596
|
+
class EncryptedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
|
569
597
|
include SerializedCookieJars
|
570
598
|
|
571
599
|
def initialize(parent_jar)
|
572
600
|
super
|
573
601
|
|
574
|
-
if
|
575
|
-
|
576
|
-
|
602
|
+
if request.use_authenticated_cookie_encryption
|
603
|
+
key_len = ActiveSupport::MessageEncryptor.key_len(encrypted_cookie_cipher)
|
604
|
+
secret = request.key_generator.generate_key(request.authenticated_encrypted_cookie_salt, key_len)
|
605
|
+
@encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: encrypted_cookie_cipher, serializer: SERIALIZER)
|
606
|
+
else
|
607
|
+
key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-cbc")
|
608
|
+
secret = request.key_generator.generate_key(request.encrypted_cookie_salt, key_len)
|
609
|
+
sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
|
610
|
+
@encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: SERIALIZER)
|
611
|
+
end
|
612
|
+
|
613
|
+
request.cookies_rotations.encrypted.each do |*secrets, **options|
|
614
|
+
@encryptor.rotate(*secrets, serializer: SERIALIZER, **options)
|
577
615
|
end
|
578
616
|
|
579
|
-
|
580
|
-
|
581
|
-
|
617
|
+
if upgrade_legacy_hmac_aes_cbc_cookies?
|
618
|
+
legacy_cipher = "aes-256-cbc"
|
619
|
+
secret = request.key_generator.generate_key(request.encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len(legacy_cipher))
|
620
|
+
sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
|
621
|
+
|
622
|
+
@encryptor.rotate(secret, sign_secret, cipher: legacy_cipher, digest: digest, serializer: SERIALIZER)
|
623
|
+
end
|
624
|
+
|
625
|
+
if upgrade_legacy_signed_cookies?
|
626
|
+
@legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, digest: digest, serializer: SERIALIZER)
|
627
|
+
end
|
582
628
|
end
|
583
629
|
|
584
630
|
private
|
585
631
|
def parse(name, encrypted_message)
|
586
|
-
deserialize
|
587
|
-
|
588
|
-
|
632
|
+
deserialize(name) do |rotate|
|
633
|
+
@encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate)
|
634
|
+
end
|
635
|
+
rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature
|
636
|
+
parse_legacy_signed_message(name, encrypted_message)
|
589
637
|
end
|
590
638
|
|
591
639
|
def commit(options)
|
592
|
-
options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]))
|
640
|
+
options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), expiry_options(options))
|
593
641
|
|
594
642
|
raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
|
595
643
|
end
|
596
|
-
end
|
597
644
|
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
645
|
+
def parse_legacy_signed_message(name, legacy_signed_message)
|
646
|
+
if defined?(@legacy_verifier)
|
647
|
+
deserialize(name) do |rotate|
|
648
|
+
rotate.call
|
649
|
+
|
650
|
+
@legacy_verifier.verified(legacy_signed_message)
|
651
|
+
end
|
652
|
+
end
|
653
|
+
end
|
604
654
|
end
|
605
655
|
|
606
656
|
def initialize(app)
|