actionpack 5.1.7 → 5.2.4.3

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 (148) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +282 -362
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +5 -5
  5. data/lib/abstract_controller.rb +3 -0
  6. data/lib/abstract_controller/asset_paths.rb +2 -0
  7. data/lib/abstract_controller/base.rb +10 -2
  8. data/lib/abstract_controller/caching.rb +3 -2
  9. data/lib/abstract_controller/caching/fragments.rb +30 -7
  10. data/lib/abstract_controller/callbacks.rb +25 -3
  11. data/lib/abstract_controller/collector.rb +2 -0
  12. data/lib/abstract_controller/error.rb +2 -0
  13. data/lib/abstract_controller/helpers.rb +4 -5
  14. data/lib/abstract_controller/logger.rb +2 -0
  15. data/lib/abstract_controller/railties/routes_helpers.rb +2 -0
  16. data/lib/abstract_controller/rendering.rb +9 -16
  17. data/lib/abstract_controller/translation.rb +2 -0
  18. data/lib/abstract_controller/url_for.rb +2 -0
  19. data/lib/action_controller.rb +3 -0
  20. data/lib/action_controller/api.rb +2 -0
  21. data/lib/action_controller/api/api_rendering.rb +2 -0
  22. data/lib/action_controller/base.rb +3 -0
  23. data/lib/action_controller/caching.rb +2 -0
  24. data/lib/action_controller/form_builder.rb +2 -0
  25. data/lib/action_controller/log_subscriber.rb +5 -3
  26. data/lib/action_controller/metal.rb +13 -14
  27. data/lib/action_controller/metal/basic_implicit_render.rb +2 -0
  28. data/lib/action_controller/metal/conditional_get.rb +4 -3
  29. data/lib/action_controller/metal/content_security_policy.rb +52 -0
  30. data/lib/action_controller/metal/cookies.rb +2 -0
  31. data/lib/action_controller/metal/data_streaming.rb +7 -5
  32. data/lib/action_controller/metal/etag_with_flash.rb +2 -0
  33. data/lib/action_controller/metal/etag_with_template_digest.rb +3 -2
  34. data/lib/action_controller/metal/exceptions.rb +2 -3
  35. data/lib/action_controller/metal/flash.rb +3 -2
  36. data/lib/action_controller/metal/force_ssl.rb +4 -2
  37. data/lib/action_controller/metal/head.rb +2 -0
  38. data/lib/action_controller/metal/helpers.rb +4 -3
  39. data/lib/action_controller/metal/http_authentication.rb +8 -9
  40. data/lib/action_controller/metal/implicit_render.rb +2 -0
  41. data/lib/action_controller/metal/instrumentation.rb +4 -6
  42. data/lib/action_controller/metal/live.rb +3 -1
  43. data/lib/action_controller/metal/mime_responds.rb +3 -1
  44. data/lib/action_controller/metal/parameter_encoding.rb +2 -0
  45. data/lib/action_controller/metal/params_wrapper.rb +14 -10
  46. data/lib/action_controller/metal/redirecting.rb +22 -11
  47. data/lib/action_controller/metal/renderers.rb +4 -3
  48. data/lib/action_controller/metal/rendering.rb +2 -2
  49. data/lib/action_controller/metal/request_forgery_protection.rb +62 -10
  50. data/lib/action_controller/metal/rescue.rb +5 -3
  51. data/lib/action_controller/metal/streaming.rb +3 -1
  52. data/lib/action_controller/metal/strong_parameters.rb +36 -25
  53. data/lib/action_controller/metal/testing.rb +2 -6
  54. data/lib/action_controller/metal/url_for.rb +2 -0
  55. data/lib/action_controller/railtie.rb +16 -4
  56. data/lib/action_controller/railties/helpers.rb +2 -0
  57. data/lib/action_controller/renderer.rb +2 -0
  58. data/lib/action_controller/template_assertions.rb +2 -0
  59. data/lib/action_controller/test_case.rb +16 -10
  60. data/lib/action_dispatch.rb +9 -5
  61. data/lib/action_dispatch/http/cache.rb +22 -14
  62. data/lib/action_dispatch/http/content_security_policy.rb +272 -0
  63. data/lib/action_dispatch/http/filter_parameters.rb +4 -2
  64. data/lib/action_dispatch/http/filter_redirect.rb +2 -0
  65. data/lib/action_dispatch/http/headers.rb +2 -0
  66. data/lib/action_dispatch/http/mime_negotiation.rb +4 -8
  67. data/lib/action_dispatch/http/mime_type.rb +15 -13
  68. data/lib/action_dispatch/http/mime_types.rb +17 -2
  69. data/lib/action_dispatch/http/parameter_filter.rb +2 -0
  70. data/lib/action_dispatch/http/parameters.rb +6 -9
  71. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  72. data/lib/action_dispatch/http/request.rb +36 -16
  73. data/lib/action_dispatch/http/response.rb +11 -9
  74. data/lib/action_dispatch/http/upload.rb +2 -0
  75. data/lib/action_dispatch/http/url.rb +5 -6
  76. data/lib/action_dispatch/journey.rb +2 -0
  77. data/lib/action_dispatch/journey/formatter.rb +4 -2
  78. data/lib/action_dispatch/journey/gtg/builder.rb +2 -0
  79. data/lib/action_dispatch/journey/gtg/simulator.rb +2 -8
  80. data/lib/action_dispatch/journey/gtg/transition_table.rb +3 -2
  81. data/lib/action_dispatch/journey/nfa/builder.rb +2 -0
  82. data/lib/action_dispatch/journey/nfa/dot.rb +12 -10
  83. data/lib/action_dispatch/journey/nfa/simulator.rb +2 -0
  84. data/lib/action_dispatch/journey/nfa/transition_table.rb +2 -0
  85. data/lib/action_dispatch/journey/nodes/node.rb +2 -0
  86. data/lib/action_dispatch/journey/parser_extras.rb +2 -0
  87. data/lib/action_dispatch/journey/path/pattern.rb +4 -1
  88. data/lib/action_dispatch/journey/route.rb +15 -6
  89. data/lib/action_dispatch/journey/router.rb +3 -1
  90. data/lib/action_dispatch/journey/router/utils.rb +14 -7
  91. data/lib/action_dispatch/journey/routes.rb +3 -1
  92. data/lib/action_dispatch/journey/scanner.rb +1 -0
  93. data/lib/action_dispatch/journey/visitors.rb +5 -3
  94. data/lib/action_dispatch/middleware/callbacks.rb +2 -0
  95. data/lib/action_dispatch/middleware/cookies.rb +148 -91
  96. data/lib/action_dispatch/middleware/debug_exceptions.rb +4 -2
  97. data/lib/action_dispatch/middleware/debug_locks.rb +9 -7
  98. data/lib/action_dispatch/middleware/exception_wrapper.rb +5 -6
  99. data/lib/action_dispatch/middleware/executor.rb +2 -0
  100. data/lib/action_dispatch/middleware/flash.rb +4 -2
  101. data/lib/action_dispatch/middleware/public_exceptions.rb +6 -4
  102. data/lib/action_dispatch/middleware/reloader.rb +2 -0
  103. data/lib/action_dispatch/middleware/remote_ip.rb +7 -5
  104. data/lib/action_dispatch/middleware/request_id.rb +3 -1
  105. data/lib/action_dispatch/middleware/session/abstract_store.rb +17 -1
  106. data/lib/action_dispatch/middleware/session/cache_store.rb +13 -6
  107. data/lib/action_dispatch/middleware/session/cookie_store.rb +31 -32
  108. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +2 -0
  109. data/lib/action_dispatch/middleware/show_exceptions.rb +3 -1
  110. data/lib/action_dispatch/middleware/ssl.rb +44 -38
  111. data/lib/action_dispatch/middleware/stack.rb +4 -2
  112. data/lib/action_dispatch/middleware/static.rb +14 -12
  113. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +21 -0
  114. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +13 -0
  115. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +1 -0
  116. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +6 -2
  117. data/lib/action_dispatch/railtie.rb +11 -1
  118. data/lib/action_dispatch/request/session.rb +16 -5
  119. data/lib/action_dispatch/request/utils.rb +6 -4
  120. data/lib/action_dispatch/routing.rb +3 -1
  121. data/lib/action_dispatch/routing/endpoint.rb +9 -2
  122. data/lib/action_dispatch/routing/inspector.rb +6 -4
  123. data/lib/action_dispatch/routing/mapper.rb +64 -52
  124. data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -0
  125. data/lib/action_dispatch/routing/redirection.rb +7 -5
  126. data/lib/action_dispatch/routing/route_set.rb +29 -24
  127. data/lib/action_dispatch/routing/routes_proxy.rb +5 -2
  128. data/lib/action_dispatch/routing/url_for.rb +25 -5
  129. data/lib/action_dispatch/system_test_case.rb +22 -6
  130. data/lib/action_dispatch/system_testing/browser.rb +49 -0
  131. data/lib/action_dispatch/system_testing/driver.rb +9 -3
  132. data/lib/action_dispatch/system_testing/server.rb +2 -16
  133. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +12 -14
  134. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +8 -2
  135. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  136. data/lib/action_dispatch/testing/assertion_response.rb +2 -0
  137. data/lib/action_dispatch/testing/assertions.rb +2 -0
  138. data/lib/action_dispatch/testing/assertions/response.rb +4 -2
  139. data/lib/action_dispatch/testing/assertions/routing.rb +5 -5
  140. data/lib/action_dispatch/testing/integration.rb +24 -21
  141. data/lib/action_dispatch/testing/request_encoder.rb +3 -1
  142. data/lib/action_dispatch/testing/test_process.rb +2 -0
  143. data/lib/action_dispatch/testing/test_request.rb +3 -1
  144. data/lib/action_dispatch/testing/test_response.rb +23 -3
  145. data/lib/action_pack.rb +3 -1
  146. data/lib/action_pack/gem_version.rb +5 -3
  147. data/lib/action_pack/version.rb +2 -0
  148. metadata +23 -11
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/core_ext/module/attribute_accessors"
2
4
 
3
5
  module ActionDispatch
@@ -7,8 +9,7 @@ module ActionDispatch
7
9
  HOST_REGEXP = /(^[^:]+:\/\/)?(\[[^\]]+\]|[^:]+)(?::(\d+$))?/
8
10
  PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/
9
11
 
10
- mattr_accessor :tld_length
11
- self.tld_length = 1
12
+ mattr_accessor :tld_length, default: 1
12
13
 
13
14
  class << self
14
15
  # Returns the domain part of a host given the domain level.
@@ -101,10 +102,8 @@ module ActionDispatch
101
102
  end
102
103
 
103
104
  def add_trailing_slash(path)
104
- # includes querysting
105
105
  if path.include?("?")
106
106
  path.sub!(/\?/, '/\&')
107
- # does not have a .format
108
107
  elsif !path.include?(".")
109
108
  path.sub!(/[^\/]\z|\A\z/, '\&/')
110
109
  end
@@ -158,7 +157,7 @@ module ActionDispatch
158
157
  subdomain = options.fetch :subdomain, true
159
158
  domain = options[:domain]
160
159
 
161
- host = ""
160
+ host = "".dup
162
161
  if subdomain == true
163
162
  return _host if domain.nil?
164
163
 
@@ -275,7 +274,7 @@ module ActionDispatch
275
274
  def standard_port
276
275
  case protocol
277
276
  when "https://" then 443
278
- else 80
277
+ else 80
279
278
  end
280
279
  end
281
280
 
@@ -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:
@@ -7,16 +9,16 @@ module ActionDispatch
7
9
  " #{from} -> #{to} [label=\"#{sym || 'ε'}\"];"
8
10
  }
9
11
 
10
- #memo_nodes = memos.values.flatten.map { |n|
11
- # label = n
12
- # if Journey::Route === n
13
- # label = "#{n.verb.source} #{n.path.spec}"
14
- # end
15
- # " #{n.object_id} [label=\"#{label}\", shape=box];"
16
- #}
17
- #memo_edges = memos.flat_map { |k, memos|
18
- # (memos || []).map { |v| " #{k} -> #{v.object_id};" }
19
- #}.uniq
12
+ # memo_nodes = memos.values.flatten.map { |n|
13
+ # label = n
14
+ # if Journey::Route === n
15
+ # label = "#{n.verb.source} #{n.path.spec}"
16
+ # end
17
+ # " #{n.object_id} [label=\"#{label}\", shape=box];"
18
+ # }
19
+ # memo_edges = memos.flat_map { |k, memos|
20
+ # (memos || []).map { |v| " #{k} -> #{v.object_id};" }
21
+ # }.uniq
20
22
 
21
23
  <<-eodot
22
24
  digraph nfa {
@@ -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:
@@ -117,7 +119,8 @@ module ActionDispatch
117
119
 
118
120
  class UnanchoredRegexp < AnchoredRegexp # :nodoc:
119
121
  def accept(node)
120
- %r{\A#{visit node}}
122
+ path = visit node
123
+ path == "/" ? %r{\A/} : %r{\A#{path}(?:\b|\Z|/)}
121
124
  end
122
125
  end
123
126
 
@@ -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
@@ -49,7 +51,7 @@ module ActionDispatch
49
51
  def ast
50
52
  @ast ||= begin
51
53
  asts = anchored_routes.map(&:ast)
52
- Nodes::Or.new(asts) unless asts.empty?
54
+ Nodes::Or.new(asts)
53
55
  end
54
56
  end
55
57
 
@@ -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
 
@@ -325,6 +338,9 @@ module ActionDispatch
325
338
  end
326
339
  alias :has_key? :key?
327
340
 
341
+ # Returns the cookies as Hash.
342
+ alias :to_hash :to_h
343
+
328
344
  def update(other_hash)
329
345
  @cookies.update other_hash.stringify_keys
330
346
  self
@@ -341,20 +357,24 @@ module ActionDispatch
341
357
  @cookies.map { |k, v| "#{escape(k)}=#{escape(v)}" }.join "; "
342
358
  end
343
359
 
344
- def handle_options(options) #:nodoc:
360
+ def handle_options(options) # :nodoc:
361
+ if options[:expires].respond_to?(:from_now)
362
+ options[:expires] = options[:expires].from_now
363
+ end
364
+
345
365
  options[:path] ||= "/"
346
366
 
347
367
  if options[:domain] == :all || options[:domain] == "all"
348
- # if there is a provided tld length then we use it otherwise default domain regexp
368
+ # If there is a provided tld length then we use it otherwise default domain regexp.
349
369
  domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
350
370
 
351
- # if host is not ip and matches domain regexp
371
+ # If host is not ip and matches domain regexp.
352
372
  # (ip confirms to domain regexp so we explicitly check for ip)
353
373
  options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
354
374
  ".#{$&}"
355
375
  end
356
376
  elsif options[:domain].is_a? Array
357
- # if host matches one of the supplied domains without a dot in front of it
377
+ # If host matches one of the supplied domains without a dot in front of it.
358
378
  options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
359
379
  end
360
380
  end
@@ -404,7 +424,7 @@ module ActionDispatch
404
424
  @delete_cookies[name.to_s] == options
405
425
  end
406
426
 
407
- # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie
427
+ # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie.
408
428
  def clear(options = {})
409
429
  @cookies.each_key { |k| delete(k, options) }
410
430
  end
@@ -415,8 +435,7 @@ module ActionDispatch
415
435
  end
416
436
  end
417
437
 
418
- mattr_accessor :always_write_cookie
419
- self.always_write_cookie = false
438
+ mattr_accessor :always_write_cookie, default: false
420
439
 
421
440
  private
422
441
 
@@ -470,6 +489,18 @@ module ActionDispatch
470
489
  def request; @parent_jar.request; end
471
490
 
472
491
  private
492
+ def expiry_options(options)
493
+ if request.use_authenticated_cookie_encryption
494
+ if options[:expires].respond_to?(:from_now)
495
+ { expires_in: options[:expires] }
496
+ else
497
+ { expires_at: options[:expires] }
498
+ end
499
+ else
500
+ {}
501
+ end
502
+ end
503
+
473
504
  def parse(name, data); data; end
474
505
  def commit(options); end
475
506
  end
@@ -493,6 +524,7 @@ module ActionDispatch
493
524
 
494
525
  module SerializedCookieJars # :nodoc:
495
526
  MARSHAL_SIGNATURE = "\x04\x08".freeze
527
+ SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer
496
528
 
497
529
  protected
498
530
  def needs_migration?(value)
@@ -503,12 +535,16 @@ module ActionDispatch
503
535
  serializer.dump(value)
504
536
  end
505
537
 
506
- def deserialize(name, value)
538
+ def deserialize(name)
539
+ rotate = false
540
+ value = yield -> { rotate = true }
541
+
507
542
  if value
508
- if needs_migration?(value)
509
- Marshal.load(value).tap do |v|
510
- self[name] = { value: v }
511
- end
543
+ case
544
+ when needs_migration?(value)
545
+ self[name] = Marshal.load(value)
546
+ when rotate
547
+ self[name] = serializer.load(value)
512
548
  else
513
549
  serializer.load(value)
514
550
  end
@@ -530,77 +566,98 @@ module ActionDispatch
530
566
  def digest
531
567
  request.cookies_digest || "SHA1"
532
568
  end
533
-
534
- def key_generator
535
- request.key_generator
536
- end
537
569
  end
538
570
 
539
- class SignedCookieJar < AbstractCookieJar # :nodoc:
571
+ class SignedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
540
572
  include SerializedCookieJars
541
573
 
542
574
  def initialize(parent_jar)
543
575
  super
544
- secret = key_generator.generate_key(request.signed_cookie_salt)
545
- @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
576
+
577
+ secret = request.key_generator.generate_key(request.signed_cookie_salt)
578
+ @verifier = ActiveSupport::MessageVerifier.new(secret, digest: signed_cookie_digest, serializer: SERIALIZER)
579
+
580
+ request.cookies_rotations.signed.each do |*secrets, **options|
581
+ @verifier.rotate(*secrets, serializer: SERIALIZER, **options)
582
+ end
583
+
584
+ if upgrade_legacy_signed_cookies?
585
+ @verifier.rotate request.secret_token, serializer: SERIALIZER
586
+ end
546
587
  end
547
588
 
548
589
  private
549
590
  def parse(name, signed_message)
550
- deserialize name, @verifier.verified(signed_message)
591
+ deserialize(name) do |rotate|
592
+ @verifier.verified(signed_message, on_rotation: rotate)
593
+ end
551
594
  end
552
595
 
553
596
  def commit(options)
554
- options[:value] = @verifier.generate(serialize(options[:value]))
597
+ options[:value] = @verifier.generate(serialize(options[:value]), expiry_options(options))
555
598
 
556
599
  raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
557
600
  end
558
601
  end
559
602
 
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:
603
+ class EncryptedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
569
604
  include SerializedCookieJars
570
605
 
571
606
  def initialize(parent_jar)
572
607
  super
573
608
 
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."
609
+ if request.use_authenticated_cookie_encryption
610
+ key_len = ActiveSupport::MessageEncryptor.key_len(encrypted_cookie_cipher)
611
+ secret = request.key_generator.generate_key(request.authenticated_encrypted_cookie_salt, key_len)
612
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: encrypted_cookie_cipher, serializer: SERIALIZER)
613
+ else
614
+ key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-cbc")
615
+ secret = request.key_generator.generate_key(request.encrypted_cookie_salt, key_len)
616
+ sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
617
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: SERIALIZER)
618
+ end
619
+
620
+ request.cookies_rotations.encrypted.each do |*secrets, **options|
621
+ @encryptor.rotate(*secrets, serializer: SERIALIZER, **options)
622
+ end
623
+
624
+ if upgrade_legacy_hmac_aes_cbc_cookies?
625
+ legacy_cipher = "aes-256-cbc"
626
+ secret = request.key_generator.generate_key(request.encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len(legacy_cipher))
627
+ sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
628
+
629
+ @encryptor.rotate(secret, sign_secret, cipher: legacy_cipher, digest: digest, serializer: SERIALIZER)
577
630
  end
578
631
 
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)
632
+ if upgrade_legacy_signed_cookies?
633
+ @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, digest: digest, serializer: SERIALIZER)
634
+ end
582
635
  end
583
636
 
584
637
  private
585
638
  def parse(name, encrypted_message)
586
- deserialize name, @encryptor.decrypt_and_verify(encrypted_message)
587
- rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
588
- nil
639
+ deserialize(name) do |rotate|
640
+ @encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate)
641
+ end
642
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature
643
+ parse_legacy_signed_message(name, encrypted_message)
589
644
  end
590
645
 
591
646
  def commit(options)
592
- options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]))
647
+ options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), expiry_options(options))
593
648
 
594
649
  raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
595
650
  end
596
- end
597
651
 
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
652
+ def parse_legacy_signed_message(name, legacy_signed_message)
653
+ if defined?(@legacy_verifier)
654
+ deserialize(name) do |rotate|
655
+ rotate.call
656
+
657
+ @legacy_verifier.verified(legacy_signed_message)
658
+ end
659
+ end
660
+ end
604
661
  end
605
662
 
606
663
  def initialize(app)