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.

Files changed (144) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +132 -490
  3. data/README.rdoc +1 -1
  4. data/lib/abstract_controller.rb +2 -0
  5. data/lib/abstract_controller/asset_paths.rb +2 -0
  6. data/lib/abstract_controller/base.rb +10 -2
  7. data/lib/abstract_controller/caching.rb +3 -2
  8. data/lib/abstract_controller/caching/fragments.rb +30 -7
  9. data/lib/abstract_controller/callbacks.rb +25 -3
  10. data/lib/abstract_controller/collector.rb +2 -0
  11. data/lib/abstract_controller/error.rb +2 -0
  12. data/lib/abstract_controller/helpers.rb +4 -5
  13. data/lib/abstract_controller/logger.rb +2 -0
  14. data/lib/abstract_controller/railties/routes_helpers.rb +2 -0
  15. data/lib/abstract_controller/rendering.rb +9 -16
  16. data/lib/abstract_controller/translation.rb +2 -0
  17. data/lib/abstract_controller/url_for.rb +2 -0
  18. data/lib/action_controller.rb +3 -0
  19. data/lib/action_controller/api.rb +2 -0
  20. data/lib/action_controller/api/api_rendering.rb +2 -0
  21. data/lib/action_controller/base.rb +3 -0
  22. data/lib/action_controller/caching.rb +2 -0
  23. data/lib/action_controller/form_builder.rb +2 -0
  24. data/lib/action_controller/log_subscriber.rb +5 -3
  25. data/lib/action_controller/metal.rb +3 -2
  26. data/lib/action_controller/metal/basic_implicit_render.rb +2 -0
  27. data/lib/action_controller/metal/conditional_get.rb +4 -3
  28. data/lib/action_controller/metal/content_security_policy.rb +26 -0
  29. data/lib/action_controller/metal/cookies.rb +2 -0
  30. data/lib/action_controller/metal/data_streaming.rb +7 -5
  31. data/lib/action_controller/metal/etag_with_flash.rb +2 -0
  32. data/lib/action_controller/metal/etag_with_template_digest.rb +3 -2
  33. data/lib/action_controller/metal/exceptions.rb +2 -3
  34. data/lib/action_controller/metal/flash.rb +3 -2
  35. data/lib/action_controller/metal/force_ssl.rb +2 -0
  36. data/lib/action_controller/metal/head.rb +2 -0
  37. data/lib/action_controller/metal/helpers.rb +4 -3
  38. data/lib/action_controller/metal/http_authentication.rb +8 -9
  39. data/lib/action_controller/metal/implicit_render.rb +2 -0
  40. data/lib/action_controller/metal/instrumentation.rb +4 -6
  41. data/lib/action_controller/metal/live.rb +3 -1
  42. data/lib/action_controller/metal/mime_responds.rb +3 -1
  43. data/lib/action_controller/metal/parameter_encoding.rb +2 -0
  44. data/lib/action_controller/metal/params_wrapper.rb +13 -9
  45. data/lib/action_controller/metal/redirecting.rb +21 -10
  46. data/lib/action_controller/metal/renderers.rb +4 -3
  47. data/lib/action_controller/metal/rendering.rb +2 -2
  48. data/lib/action_controller/metal/request_forgery_protection.rb +22 -6
  49. data/lib/action_controller/metal/rescue.rb +5 -3
  50. data/lib/action_controller/metal/streaming.rb +2 -0
  51. data/lib/action_controller/metal/strong_parameters.rb +19 -11
  52. data/lib/action_controller/metal/testing.rb +2 -6
  53. data/lib/action_controller/metal/url_for.rb +2 -0
  54. data/lib/action_controller/railtie.rb +16 -4
  55. data/lib/action_controller/railties/helpers.rb +2 -0
  56. data/lib/action_controller/renderer.rb +2 -0
  57. data/lib/action_controller/template_assertions.rb +2 -0
  58. data/lib/action_controller/test_case.rb +4 -1
  59. data/lib/action_dispatch.rb +3 -0
  60. data/lib/action_dispatch/http/cache.rb +15 -9
  61. data/lib/action_dispatch/http/content_security_policy.rb +233 -0
  62. data/lib/action_dispatch/http/filter_parameters.rb +4 -2
  63. data/lib/action_dispatch/http/filter_redirect.rb +2 -0
  64. data/lib/action_dispatch/http/headers.rb +2 -0
  65. data/lib/action_dispatch/http/mime_negotiation.rb +4 -13
  66. data/lib/action_dispatch/http/mime_type.rb +15 -13
  67. data/lib/action_dispatch/http/mime_types.rb +4 -2
  68. data/lib/action_dispatch/http/parameter_filter.rb +2 -0
  69. data/lib/action_dispatch/http/parameters.rb +6 -9
  70. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  71. data/lib/action_dispatch/http/request.rb +36 -16
  72. data/lib/action_dispatch/http/response.rb +11 -9
  73. data/lib/action_dispatch/http/upload.rb +2 -0
  74. data/lib/action_dispatch/http/url.rb +4 -5
  75. data/lib/action_dispatch/journey.rb +2 -0
  76. data/lib/action_dispatch/journey/formatter.rb +4 -2
  77. data/lib/action_dispatch/journey/gtg/builder.rb +2 -0
  78. data/lib/action_dispatch/journey/gtg/simulator.rb +2 -8
  79. data/lib/action_dispatch/journey/gtg/transition_table.rb +3 -2
  80. data/lib/action_dispatch/journey/nfa/builder.rb +2 -0
  81. data/lib/action_dispatch/journey/nfa/dot.rb +2 -0
  82. data/lib/action_dispatch/journey/nfa/simulator.rb +2 -0
  83. data/lib/action_dispatch/journey/nfa/transition_table.rb +2 -0
  84. data/lib/action_dispatch/journey/nodes/node.rb +2 -0
  85. data/lib/action_dispatch/journey/parser_extras.rb +2 -0
  86. data/lib/action_dispatch/journey/path/pattern.rb +2 -0
  87. data/lib/action_dispatch/journey/route.rb +15 -6
  88. data/lib/action_dispatch/journey/router.rb +3 -1
  89. data/lib/action_dispatch/journey/router/utils.rb +14 -7
  90. data/lib/action_dispatch/journey/routes.rb +2 -1
  91. data/lib/action_dispatch/journey/scanner.rb +1 -0
  92. data/lib/action_dispatch/journey/visitors.rb +5 -3
  93. data/lib/action_dispatch/middleware/callbacks.rb +2 -0
  94. data/lib/action_dispatch/middleware/cookies.rb +141 -91
  95. data/lib/action_dispatch/middleware/debug_exceptions.rb +4 -2
  96. data/lib/action_dispatch/middleware/debug_locks.rb +9 -7
  97. data/lib/action_dispatch/middleware/exception_wrapper.rb +4 -6
  98. data/lib/action_dispatch/middleware/executor.rb +2 -0
  99. data/lib/action_dispatch/middleware/flash.rb +3 -1
  100. data/lib/action_dispatch/middleware/public_exceptions.rb +6 -4
  101. data/lib/action_dispatch/middleware/reloader.rb +2 -0
  102. data/lib/action_dispatch/middleware/remote_ip.rb +7 -5
  103. data/lib/action_dispatch/middleware/request_id.rb +2 -0
  104. data/lib/action_dispatch/middleware/session/abstract_store.rb +3 -1
  105. data/lib/action_dispatch/middleware/session/cache_store.rb +2 -0
  106. data/lib/action_dispatch/middleware/session/cookie_store.rb +13 -25
  107. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +2 -0
  108. data/lib/action_dispatch/middleware/show_exceptions.rb +3 -1
  109. data/lib/action_dispatch/middleware/ssl.rb +42 -37
  110. data/lib/action_dispatch/middleware/stack.rb +2 -0
  111. data/lib/action_dispatch/middleware/static.rb +10 -8
  112. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +1 -0
  113. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +6 -2
  114. data/lib/action_dispatch/railtie.rb +7 -0
  115. data/lib/action_dispatch/request/session.rb +8 -4
  116. data/lib/action_dispatch/request/utils.rb +4 -4
  117. data/lib/action_dispatch/routing.rb +3 -1
  118. data/lib/action_dispatch/routing/endpoint.rb +8 -4
  119. data/lib/action_dispatch/routing/inspector.rb +5 -3
  120. data/lib/action_dispatch/routing/mapper.rb +62 -51
  121. data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -0
  122. data/lib/action_dispatch/routing/redirection.rb +7 -5
  123. data/lib/action_dispatch/routing/route_set.rb +26 -33
  124. data/lib/action_dispatch/routing/routes_proxy.rb +5 -2
  125. data/lib/action_dispatch/routing/url_for.rb +6 -4
  126. data/lib/action_dispatch/system_test_case.rb +14 -6
  127. data/lib/action_dispatch/system_testing/driver.rb +20 -2
  128. data/lib/action_dispatch/system_testing/server.rb +2 -16
  129. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +6 -4
  130. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +2 -0
  131. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  132. data/lib/action_dispatch/testing/assertion_response.rb +2 -0
  133. data/lib/action_dispatch/testing/assertions.rb +2 -0
  134. data/lib/action_dispatch/testing/assertions/response.rb +4 -2
  135. data/lib/action_dispatch/testing/assertions/routing.rb +5 -5
  136. data/lib/action_dispatch/testing/integration.rb +24 -21
  137. data/lib/action_dispatch/testing/request_encoder.rb +2 -0
  138. data/lib/action_dispatch/testing/test_process.rb +2 -0
  139. data/lib/action_dispatch/testing/test_request.rb +3 -1
  140. data/lib/action_dispatch/testing/test_response.rb +23 -3
  141. data/lib/action_pack.rb +2 -0
  142. data/lib/action_pack/gem_version.rb +5 -3
  143. data/lib/action_pack/version.rb +2 -0
  144. metadata +17 -13
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "action_dispatch/journey/router"
2
4
  require "action_dispatch/journey/gtg/builder"
3
5
  require "action_dispatch/journey/gtg/simulator"
@@ -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 # need for variable scope
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 "action_dispatch/journey/gtg/transition_table"
2
4
 
3
5
  module ActionDispatch
@@ -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 File.dirname(__FILE__), "..", "visualizer"
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
  require "action_dispatch/journey/nfa/transition_table"
2
4
  require "action_dispatch/journey/gtg/transition_table"
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionDispatch
2
4
  module Journey # :nodoc:
3
5
  module NFA # :nodoc:
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "strscan"
2
4
 
3
5
  module ActionDispatch
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "action_dispatch/journey/nfa/dot"
2
4
 
3
5
  module ActionDispatch
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "action_dispatch/journey/visitors"
2
4
 
3
5
  module ActionDispatch
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "action_dispatch/journey/scanner"
2
4
  require "action_dispatch/journey/nodes/node"
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionDispatch
2
4
  module Journey # :nodoc:
3
5
  module Path # :nodoc:
@@ -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
- class #{v}
15
- def self.verb; name.split("::").last; end
16
- def self.call(req); req.#{v.downcase}?; end
17
- end
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
- return [404, { "X-Cascade" => "pass" }, ["Not Found"]]
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 url encoded string to uppercase.
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
- # http://tools.ietf.org/html/rfc3986
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) # :doc:
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) # :doc:
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,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "strscan"
3
4
 
4
5
  module ActionDispatch
@@ -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 << "|".freeze unless last_child == c
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 << "(".freeze) << ")".freeze
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
  module ActionDispatch
2
4
  # Provides callbacks to be executed before and after dispatching the request.
3
5
  class Callbacks
@@ -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.from_now }
103
+ # cookies[:login] = { value: "XJ-122", expires: 1.hour }
104
+ #
105
+ # # Sets a cookie that expires at a specific time.
106
+ # cookies[:login] = { value: "XJ-122", expires: Time.utc(2020, 10, 15, 5) }
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.signed[:login] = "XJ-122"
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.from_now,
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 1.
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 +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
213
+ # If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
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 +secrets.secret_key_base+.
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 +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
231
+ # If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
208
232
  # legacy cookies signed with the old key generator will be transparently upgraded.
209
233
  #
210
- # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+.
234
+ # If +config.action_dispatch.encrypted_cookie_salt+ and +config.action_dispatch.encrypted_signed_cookie_salt+
235
+ # are both set, legacy cookies encrypted with HMAC AES-256-CBC will be transparently upgraded.
236
+ #
237
+ # This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+.
211
238
  #
212
239
  # Example:
213
240
  #
214
241
  # cookies.encrypted[:discount] = 45
215
- # # => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/
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
- # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream
246
- # to the Message{Encryptor,Verifier} allows us to handle the
247
- # (de)serialization step within the cookie jar, which gives us the
248
- # opportunity to detect and migrate legacy cookies.
249
- module VerifyAndUpgradeLegacySignedMessage # :nodoc:
250
- def initialize(*args)
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
- def verify_and_upgrade_legacy_signed_message(name, signed_message)
256
- deserialize(name, @legacy_verifier.verify(signed_message)).tap do |value|
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
- private
264
- def parse(name, signed_message)
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) #:nodoc:
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
- # if there is a provided tld length then we use it otherwise default domain regexp
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
- # if host is not ip and matches domain regexp
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
- # if host matches one of the supplied domains without a dot in front of it
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, value)
531
+ def deserialize(name)
532
+ rotate = false
533
+ value = yield -> { rotate = true }
534
+
507
535
  if value
508
- if needs_migration?(value)
509
- Marshal.load(value).tap do |v|
510
- self[name] = { value: v }
511
- end
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 SignedCookieJar < AbstractCookieJar # :nodoc:
564
+ class SignedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
540
565
  include SerializedCookieJars
541
566
 
542
567
  def initialize(parent_jar)
543
568
  super
544
- secret = key_generator.generate_key(request.signed_cookie_salt)
545
- @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
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 name, @verifier.verified(signed_message)
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
- # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if
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 ActiveSupport::LegacyKeyGenerator === key_generator
575
- raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " \
576
- "Read the upgrade documentation to learn more about this new config option."
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
- secret = key_generator.generate_key(request.encrypted_cookie_salt || "")[0, ActiveSupport::MessageEncryptor.key_len]
580
- sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || "")
581
- @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
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 name, @encryptor.decrypt_and_verify(encrypted_message)
587
- rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
588
- nil
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
- # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore
599
- # instead of EncryptedCookieJar if secrets.secret_token and secrets.secret_key_base
600
- # are both set. It reads legacy cookies signed with the old dummy key generator and
601
- # encrypts and re-saves them using the new key generator to provide a smooth upgrade path.
602
- class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc:
603
- include VerifyAndUpgradeLegacySignedMessage
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)