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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +282 -362
- data/MIT-LICENSE +1 -1
- data/README.rdoc +5 -5
- data/lib/abstract_controller.rb +3 -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 +13 -14
- 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 +52 -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 +4 -2
- 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 +14 -10
- data/lib/action_controller/metal/redirecting.rb +22 -11
- 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 +62 -10
- data/lib/action_controller/metal/rescue.rb +5 -3
- data/lib/action_controller/metal/streaming.rb +3 -1
- data/lib/action_controller/metal/strong_parameters.rb +36 -25
- 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 +16 -10
- data/lib/action_dispatch.rb +9 -5
- data/lib/action_dispatch/http/cache.rb +22 -14
- data/lib/action_dispatch/http/content_security_policy.rb +272 -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 -8
- data/lib/action_dispatch/http/mime_type.rb +15 -13
- data/lib/action_dispatch/http/mime_types.rb +17 -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 +5 -6
- 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 +12 -10
- 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 +4 -1
- 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 +3 -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 +148 -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 +5 -6
- data/lib/action_dispatch/middleware/executor.rb +2 -0
- data/lib/action_dispatch/middleware/flash.rb +4 -2
- 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 +3 -1
- data/lib/action_dispatch/middleware/session/abstract_store.rb +17 -1
- data/lib/action_dispatch/middleware/session/cache_store.rb +13 -6
- data/lib/action_dispatch/middleware/session/cookie_store.rb +31 -32
- 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 +44 -38
- data/lib/action_dispatch/middleware/stack.rb +4 -2
- data/lib/action_dispatch/middleware/static.rb +14 -12
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +21 -0
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +13 -0
- 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 +11 -1
- data/lib/action_dispatch/request/session.rb +16 -5
- data/lib/action_dispatch/request/utils.rb +6 -4
- data/lib/action_dispatch/routing.rb +3 -1
- data/lib/action_dispatch/routing/endpoint.rb +9 -2
- data/lib/action_dispatch/routing/inspector.rb +6 -4
- data/lib/action_dispatch/routing/mapper.rb +64 -52
- 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 +29 -24
- data/lib/action_dispatch/routing/routes_proxy.rb +5 -2
- data/lib/action_dispatch/routing/url_for.rb +25 -5
- data/lib/action_dispatch/system_test_case.rb +22 -6
- data/lib/action_dispatch/system_testing/browser.rb +49 -0
- data/lib/action_dispatch/system_testing/driver.rb +9 -3
- data/lib/action_dispatch/system_testing/server.rb +2 -16
- data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +12 -14
- data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +8 -2
- 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 +3 -1
- 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 +3 -1
- data/lib/action_pack/gem_version.rb +5 -3
- data/lib/action_pack/version.rb +2 -0
- 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
|
-
|
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_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
|
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
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#}
|
17
|
-
#memo_edges = memos.flat_map { |k, memos|
|
18
|
-
#
|
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
|
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
|
-
|
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
|
-
|
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
|
@@ -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)
|
54
|
+
Nodes::Or.new(asts)
|
53
55
|
end
|
54
56
|
end
|
55
57
|
|
@@ -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
|
|
@@ -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)
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
538
|
+
def deserialize(name)
|
539
|
+
rotate = false
|
540
|
+
value = yield -> { rotate = true }
|
541
|
+
|
507
542
|
if value
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
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
|
571
|
+
class SignedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
|
540
572
|
include SerializedCookieJars
|
541
573
|
|
542
574
|
def initialize(parent_jar)
|
543
575
|
super
|
544
|
-
|
545
|
-
|
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
|
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
|
-
|
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
|
575
|
-
|
576
|
-
|
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
|
-
|
580
|
-
|
581
|
-
|
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
|
587
|
-
|
588
|
-
|
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
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
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)
|