actionpack 5.2.8.1 → 6.0.6
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 +270 -347
- data/MIT-LICENSE +1 -1
- data/README.rdoc +4 -3
- data/lib/abstract_controller/base.rb +4 -3
- data/lib/abstract_controller/caching/fragments.rb +6 -22
- data/lib/abstract_controller/caching.rb +1 -1
- data/lib/abstract_controller/callbacks.rb +12 -0
- data/lib/abstract_controller/collector.rb +1 -2
- data/lib/abstract_controller/helpers.rb +7 -6
- data/lib/abstract_controller/railties/routes_helpers.rb +1 -1
- data/lib/abstract_controller/translation.rb +4 -4
- data/lib/action_controller/api.rb +2 -1
- data/lib/action_controller/base.rb +2 -7
- data/lib/action_controller/caching.rb +1 -2
- data/lib/action_controller/log_subscriber.rb +8 -5
- data/lib/action_controller/metal/basic_implicit_render.rb +1 -1
- data/lib/action_controller/metal/conditional_get.rb +9 -3
- data/lib/action_controller/metal/content_security_policy.rb +0 -1
- data/lib/action_controller/metal/data_streaming.rb +5 -6
- data/lib/action_controller/metal/default_headers.rb +17 -0
- data/lib/action_controller/metal/etag_with_template_digest.rb +1 -1
- data/lib/action_controller/metal/exceptions.rb +23 -2
- data/lib/action_controller/metal/flash.rb +5 -5
- data/lib/action_controller/metal/force_ssl.rb +15 -56
- data/lib/action_controller/metal/head.rb +1 -1
- data/lib/action_controller/metal/helpers.rb +3 -4
- data/lib/action_controller/metal/http_authentication.rb +20 -21
- data/lib/action_controller/metal/implicit_render.rb +4 -14
- data/lib/action_controller/metal/instrumentation.rb +3 -6
- data/lib/action_controller/metal/live.rb +29 -31
- data/lib/action_controller/metal/mime_responds.rb +13 -2
- data/lib/action_controller/metal/params_wrapper.rb +18 -14
- data/lib/action_controller/metal/redirecting.rb +5 -5
- data/lib/action_controller/metal/renderers.rb +4 -4
- data/lib/action_controller/metal/rendering.rb +2 -3
- data/lib/action_controller/metal/request_forgery_protection.rb +25 -48
- data/lib/action_controller/metal/streaming.rb +0 -1
- data/lib/action_controller/metal/strong_parameters.rb +65 -44
- data/lib/action_controller/metal/url_for.rb +1 -1
- data/lib/action_controller/metal.rb +8 -6
- data/lib/action_controller/railties/helpers.rb +1 -1
- data/lib/action_controller/renderer.rb +17 -3
- data/lib/action_controller/template_assertions.rb +1 -1
- data/lib/action_controller/test_case.rb +7 -8
- data/lib/action_controller.rb +5 -1
- data/lib/action_dispatch/http/cache.rb +14 -11
- data/lib/action_dispatch/http/content_disposition.rb +45 -0
- data/lib/action_dispatch/http/content_security_policy.rb +28 -17
- data/lib/action_dispatch/http/filter_parameters.rb +8 -7
- data/lib/action_dispatch/http/filter_redirect.rb +1 -2
- data/lib/action_dispatch/http/headers.rb +1 -2
- data/lib/action_dispatch/http/mime_negotiation.rb +13 -6
- data/lib/action_dispatch/http/mime_type.rb +14 -8
- data/lib/action_dispatch/http/parameter_filter.rb +5 -79
- data/lib/action_dispatch/http/parameters.rb +15 -6
- data/lib/action_dispatch/http/request.rb +21 -14
- data/lib/action_dispatch/http/response.rb +40 -21
- data/lib/action_dispatch/http/upload.rb +9 -1
- data/lib/action_dispatch/http/url.rb +81 -82
- data/lib/action_dispatch/journey/formatter.rb +2 -3
- data/lib/action_dispatch/journey/gtg/builder.rb +0 -1
- data/lib/action_dispatch/journey/gtg/transition_table.rb +0 -1
- data/lib/action_dispatch/journey/nfa/simulator.rb +0 -2
- data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -1
- data/lib/action_dispatch/journey/nodes/node.rb +9 -8
- data/lib/action_dispatch/journey/path/pattern.rb +6 -3
- data/lib/action_dispatch/journey/route.rb +5 -4
- data/lib/action_dispatch/journey/router/utils.rb +10 -10
- data/lib/action_dispatch/journey/router.rb +0 -4
- data/lib/action_dispatch/journey/routes.rb +0 -2
- data/lib/action_dispatch/journey/scanner.rb +10 -4
- data/lib/action_dispatch/journey/visitors.rb +1 -4
- data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
- data/lib/action_dispatch/middleware/callbacks.rb +2 -4
- data/lib/action_dispatch/middleware/cookies.rb +62 -78
- data/lib/action_dispatch/middleware/debug_exceptions.rb +45 -61
- data/lib/action_dispatch/middleware/debug_locks.rb +5 -5
- data/lib/action_dispatch/middleware/debug_view.rb +66 -0
- data/lib/action_dispatch/middleware/exception_wrapper.rb +49 -16
- data/lib/action_dispatch/middleware/flash.rb +1 -1
- data/lib/action_dispatch/middleware/host_authorization.rb +121 -0
- data/lib/action_dispatch/middleware/public_exceptions.rb +6 -3
- data/lib/action_dispatch/middleware/remote_ip.rb +9 -12
- data/lib/action_dispatch/middleware/request_id.rb +2 -2
- data/lib/action_dispatch/middleware/session/abstract_store.rb +0 -1
- data/lib/action_dispatch/middleware/session/cookie_store.rb +1 -7
- data/lib/action_dispatch/middleware/show_exceptions.rb +1 -2
- data/lib/action_dispatch/middleware/ssl.rb +8 -8
- data/lib/action_dispatch/middleware/stack.rb +38 -2
- data/lib/action_dispatch/middleware/static.rb +6 -7
- data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
- data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +3 -1
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
- data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +4 -2
- data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +45 -35
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +7 -0
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +5 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +26 -4
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +1 -1
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +7 -4
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +5 -2
- data/lib/action_dispatch/middleware/templates/rescues/layout.erb +4 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +2 -2
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -1
- data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +2 -2
- data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +3 -0
- data/lib/action_dispatch/railtie.rb +7 -2
- data/lib/action_dispatch/request/session.rb +9 -2
- data/lib/action_dispatch/routing/inspector.rb +97 -50
- data/lib/action_dispatch/routing/mapper.rb +63 -42
- data/lib/action_dispatch/routing/polymorphic_routes.rb +3 -6
- data/lib/action_dispatch/routing/route_set.rb +25 -31
- data/lib/action_dispatch/routing/url_for.rb +2 -2
- data/lib/action_dispatch/routing.rb +21 -20
- data/lib/action_dispatch/system_test_case.rb +44 -6
- data/lib/action_dispatch/system_testing/browser.rb +38 -7
- data/lib/action_dispatch/system_testing/driver.rb +11 -2
- data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +6 -5
- data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +7 -6
- data/lib/action_dispatch/testing/assertion_response.rb +0 -1
- data/lib/action_dispatch/testing/assertions/response.rb +2 -3
- data/lib/action_dispatch/testing/assertions/routing.rb +15 -3
- data/lib/action_dispatch/testing/assertions.rb +1 -1
- data/lib/action_dispatch/testing/integration.rb +33 -12
- data/lib/action_dispatch/testing/request_encoder.rb +2 -2
- data/lib/action_dispatch/testing/test_process.rb +2 -2
- data/lib/action_dispatch/testing/test_response.rb +4 -32
- data/lib/action_dispatch.rb +7 -2
- data/lib/action_pack/gem_version.rb +4 -4
- data/lib/action_pack.rb +1 -1
- metadata +29 -15
- data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +0 -26
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_dispatch/http/request"
|
4
|
+
|
5
|
+
module ActionDispatch
|
6
|
+
# This middleware guards from DNS rebinding attacks by explicitly permitting
|
7
|
+
# the hosts a request can be sent to.
|
8
|
+
#
|
9
|
+
# When a request comes to an unauthorized host, the +response_app+
|
10
|
+
# application will be executed and rendered. If no +response_app+ is given, a
|
11
|
+
# default one will run, which responds with +403 Forbidden+.
|
12
|
+
class HostAuthorization
|
13
|
+
ALLOWED_HOSTS_IN_DEVELOPMENT = [".localhost", IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0")]
|
14
|
+
PORT_REGEX = /(?::\d+)/ # :nodoc:
|
15
|
+
IPV4_HOSTNAME = /(?<host>\d+\.\d+\.\d+\.\d+)#{PORT_REGEX}?/ # :nodoc:
|
16
|
+
IPV6_HOSTNAME = /(?<host>[a-f0-9]*:[a-f0-9.:]+)/i # :nodoc:
|
17
|
+
IPV6_HOSTNAME_WITH_PORT = /\[#{IPV6_HOSTNAME}\]#{PORT_REGEX}/i # :nodoc:
|
18
|
+
VALID_IP_HOSTNAME = Regexp.union( # :nodoc:
|
19
|
+
/\A#{IPV4_HOSTNAME}\z/,
|
20
|
+
/\A#{IPV6_HOSTNAME}\z/,
|
21
|
+
/\A#{IPV6_HOSTNAME_WITH_PORT}\z/,
|
22
|
+
)
|
23
|
+
|
24
|
+
class Permissions # :nodoc:
|
25
|
+
def initialize(hosts)
|
26
|
+
@hosts = sanitize_hosts(hosts)
|
27
|
+
end
|
28
|
+
|
29
|
+
def empty?
|
30
|
+
@hosts.empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
def allows?(host)
|
34
|
+
@hosts.any? do |allowed|
|
35
|
+
if allowed.is_a?(IPAddr)
|
36
|
+
begin
|
37
|
+
allowed === extract_hostname(host)
|
38
|
+
rescue
|
39
|
+
# IPAddr#=== raises an error if you give it a hostname instead of
|
40
|
+
# IP. Treat similar errors as blocked access.
|
41
|
+
false
|
42
|
+
end
|
43
|
+
else
|
44
|
+
allowed === host
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
def sanitize_hosts(hosts)
|
51
|
+
Array(hosts).map do |host|
|
52
|
+
case host
|
53
|
+
when Regexp then sanitize_regexp(host)
|
54
|
+
when String then sanitize_string(host)
|
55
|
+
else host
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def sanitize_regexp(host)
|
61
|
+
/\A#{host}#{PORT_REGEX}?\z/
|
62
|
+
end
|
63
|
+
|
64
|
+
def sanitize_string(host)
|
65
|
+
if host.start_with?(".")
|
66
|
+
/\A([a-z0-9-]+\.)?#{Regexp.escape(host[1..-1])}#{PORT_REGEX}?\z/i
|
67
|
+
else
|
68
|
+
/\A#{Regexp.escape host}#{PORT_REGEX}?\z/i
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def extract_hostname(host)
|
73
|
+
host.slice(VALID_IP_HOSTNAME, "host") || host
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
DEFAULT_RESPONSE_APP = -> env do
|
78
|
+
request = Request.new(env)
|
79
|
+
|
80
|
+
format = request.xhr? ? "text/plain" : "text/html"
|
81
|
+
template = DebugView.new(host: request.host)
|
82
|
+
body = template.render(template: "rescues/blocked_host", layout: "rescues/layout")
|
83
|
+
|
84
|
+
[403, {
|
85
|
+
"Content-Type" => "#{format}; charset=#{Response.default_charset}",
|
86
|
+
"Content-Length" => body.bytesize.to_s,
|
87
|
+
}, [body]]
|
88
|
+
end
|
89
|
+
|
90
|
+
def initialize(app, hosts, response_app = nil)
|
91
|
+
@app = app
|
92
|
+
@permissions = Permissions.new(hosts)
|
93
|
+
@response_app = response_app || DEFAULT_RESPONSE_APP
|
94
|
+
end
|
95
|
+
|
96
|
+
def call(env)
|
97
|
+
return @app.call(env) if @permissions.empty?
|
98
|
+
|
99
|
+
request = Request.new(env)
|
100
|
+
|
101
|
+
if authorized?(request)
|
102
|
+
mark_as_authorized(request)
|
103
|
+
@app.call(env)
|
104
|
+
else
|
105
|
+
@response_app.call(env)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
def authorized?(request)
|
111
|
+
origin_host = request.get_header("HTTP_HOST")
|
112
|
+
forwarded_host = request.x_forwarded_host&.split(/,\s?/)&.last
|
113
|
+
|
114
|
+
@permissions.allows?(origin_host) && (forwarded_host.blank? || @permissions.allows?(forwarded_host))
|
115
|
+
end
|
116
|
+
|
117
|
+
def mark_as_authorized(request)
|
118
|
+
request.set_header("action_dispatch.authorized_host", request.host)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -21,14 +21,17 @@ module ActionDispatch
|
|
21
21
|
def call(env)
|
22
22
|
request = ActionDispatch::Request.new(env)
|
23
23
|
status = request.path_info[1..-1].to_i
|
24
|
-
|
25
|
-
|
24
|
+
begin
|
25
|
+
content_type = request.formats.first
|
26
|
+
rescue ActionDispatch::Http::MimeNegotiation::InvalidType
|
27
|
+
content_type = Mime[:text]
|
28
|
+
end
|
29
|
+
body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
|
26
30
|
|
27
31
|
render(status, content_type, body)
|
28
32
|
end
|
29
33
|
|
30
34
|
private
|
31
|
-
|
32
35
|
def render(status, content_type, body)
|
33
36
|
format = "to_#{content_type.to_sym}" if content_type
|
34
37
|
if format && body.respond_to?(format)
|
@@ -8,13 +8,13 @@ module ActionDispatch
|
|
8
8
|
# contain the address, and then picking the last-set address that is not
|
9
9
|
# on the list of trusted IPs. This follows the precedent set by e.g.
|
10
10
|
# {the Tomcat server}[https://issues.apache.org/bugzilla/show_bug.cgi?id=50453],
|
11
|
-
# with {reasoning explained at length}[
|
11
|
+
# with {reasoning explained at length}[https://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection]
|
12
12
|
# by @gingerlime. A more detailed explanation of the algorithm is given
|
13
13
|
# at GetIp#calculate_ip.
|
14
14
|
#
|
15
15
|
# Some Rack servers concatenate repeated headers, like {HTTP RFC 2616}[https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2]
|
16
16
|
# requires. Some Rack servers simply drop preceding headers, and only report
|
17
|
-
# the value that was {given in the last header}[
|
17
|
+
# the value that was {given in the last header}[https://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-servers].
|
18
18
|
# If you are behind multiple proxy servers (like NGINX to HAProxy to Unicorn)
|
19
19
|
# then you should test your Rack server to make sure your data is good.
|
20
20
|
#
|
@@ -102,7 +102,7 @@ module ActionDispatch
|
|
102
102
|
# proxies, that header may contain a list of IPs. Other proxy services
|
103
103
|
# set the Client-Ip header instead, so we check that too.
|
104
104
|
#
|
105
|
-
# As discussed in {this post about Rails IP Spoofing}[
|
105
|
+
# As discussed in {this post about Rails IP Spoofing}[https://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/],
|
106
106
|
# while the first IP in the list is likely to be the "originating" IP,
|
107
107
|
# it could also have been set by the client maliciously.
|
108
108
|
#
|
@@ -156,20 +156,17 @@ module ActionDispatch
|
|
156
156
|
end
|
157
157
|
|
158
158
|
private
|
159
|
-
|
160
159
|
def ips_from(header) # :doc:
|
161
160
|
return [] unless header
|
162
161
|
# Split the comma-separated list into an array of strings.
|
163
162
|
ips = header.strip.split(/[,\s]+/)
|
164
163
|
ips.select do |ip|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
nil
|
172
|
-
end
|
164
|
+
# Only return IPs that are valid according to the IPAddr#new method.
|
165
|
+
range = IPAddr.new(ip).to_range
|
166
|
+
# We want to make sure nobody is sneaking a netmask in.
|
167
|
+
range.begin == range.end
|
168
|
+
rescue ArgumentError
|
169
|
+
nil
|
173
170
|
end
|
174
171
|
end
|
175
172
|
|
@@ -15,7 +15,7 @@ module ActionDispatch
|
|
15
15
|
# The unique request id can be used to trace a request end-to-end and would typically end up being part of log files
|
16
16
|
# from multiple pieces of the stack.
|
17
17
|
class RequestId
|
18
|
-
X_REQUEST_ID = "X-Request-Id"
|
18
|
+
X_REQUEST_ID = "X-Request-Id" #:nodoc:
|
19
19
|
|
20
20
|
def initialize(app)
|
21
21
|
@app = app
|
@@ -30,7 +30,7 @@ module ActionDispatch
|
|
30
30
|
private
|
31
31
|
def make_request_id(request_id)
|
32
32
|
if request_id.presence
|
33
|
-
request_id.gsub(/[^\w\-@]/, ""
|
33
|
+
request_id.gsub(/[^\w\-@]/, "").first(255)
|
34
34
|
else
|
35
35
|
internal_request_id
|
36
36
|
end
|
@@ -16,16 +16,11 @@ module ActionDispatch
|
|
16
16
|
# The cookie jar used for storage is automatically configured to be the
|
17
17
|
# best possible option given your application's configuration.
|
18
18
|
#
|
19
|
-
# If you only have secret_token set, your cookies will be signed, but
|
20
|
-
# not encrypted. This means a user cannot alter their +user_id+ without
|
21
|
-
# knowing your app's secret key, but can easily read their +user_id+. This
|
22
|
-
# was the default for Rails 3 apps.
|
23
|
-
#
|
24
19
|
# Your cookies will be encrypted using your apps secret_key_base. This
|
25
20
|
# goes a step further than signed cookies in that encrypted cookies cannot
|
26
21
|
# be altered or read by users. This is the default starting in Rails 4.
|
27
22
|
#
|
28
|
-
# Configure your session store in
|
23
|
+
# Configure your session store in an initializer:
|
29
24
|
#
|
30
25
|
# Rails.application.config.session_store :cookie_store, key: '_your_app_session'
|
31
26
|
#
|
@@ -81,7 +76,6 @@ module ActionDispatch
|
|
81
76
|
end
|
82
77
|
|
83
78
|
private
|
84
|
-
|
85
79
|
def extract_session_id(req)
|
86
80
|
stale_session_check! do
|
87
81
|
sid = unpacked_cookie_data(req)["session_id"]
|
@@ -40,12 +40,11 @@ module ActionDispatch
|
|
40
40
|
end
|
41
41
|
|
42
42
|
private
|
43
|
-
|
44
43
|
def render_exception(request, exception)
|
45
44
|
backtrace_cleaner = request.get_header "action_dispatch.backtrace_cleaner"
|
46
45
|
wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
|
47
46
|
status = wrapper.status_code
|
48
|
-
request.set_header "action_dispatch.exception", wrapper.
|
47
|
+
request.set_header "action_dispatch.exception", wrapper.unwrapped_exception
|
49
48
|
request.set_header "action_dispatch.original_path", request.path_info
|
50
49
|
request.path_info = "/#{status}"
|
51
50
|
response = @exceptions_app.call(request.env)
|
@@ -83,7 +83,7 @@ module ActionDispatch
|
|
83
83
|
|
84
84
|
private
|
85
85
|
def set_hsts_header!(headers)
|
86
|
-
headers["Strict-Transport-Security"
|
86
|
+
headers["Strict-Transport-Security"] ||= @hsts_header
|
87
87
|
end
|
88
88
|
|
89
89
|
def normalize_hsts_options(options)
|
@@ -102,23 +102,23 @@ module ActionDispatch
|
|
102
102
|
|
103
103
|
# https://tools.ietf.org/html/rfc6797#section-6.1
|
104
104
|
def build_hsts_header(hsts)
|
105
|
-
value = "max-age=#{hsts[:expires].to_i}"
|
105
|
+
value = +"max-age=#{hsts[:expires].to_i}"
|
106
106
|
value << "; includeSubDomains" if hsts[:subdomains]
|
107
107
|
value << "; preload" if hsts[:preload]
|
108
108
|
value
|
109
109
|
end
|
110
110
|
|
111
111
|
def flag_cookies_as_secure!(headers)
|
112
|
-
if cookies = headers["Set-Cookie"
|
113
|
-
cookies = cookies.split("\n"
|
112
|
+
if cookies = headers["Set-Cookie"]
|
113
|
+
cookies = cookies.split("\n")
|
114
114
|
|
115
|
-
headers["Set-Cookie"
|
116
|
-
if
|
115
|
+
headers["Set-Cookie"] = cookies.map { |cookie|
|
116
|
+
if !/;\s*secure\s*(;|$)/i.match?(cookie)
|
117
117
|
"#{cookie}; secure"
|
118
118
|
else
|
119
119
|
cookie
|
120
120
|
end
|
121
|
-
}.join("\n"
|
121
|
+
}.join("\n")
|
122
122
|
end
|
123
123
|
end
|
124
124
|
|
@@ -141,7 +141,7 @@ module ActionDispatch
|
|
141
141
|
host = @redirect[:host] || request.host
|
142
142
|
port = @redirect[:port] || request.port
|
143
143
|
|
144
|
-
location = "https://#{host}"
|
144
|
+
location = +"https://#{host}"
|
145
145
|
location << ":#{port}" if port != 80 && port != 443
|
146
146
|
location << request.fullpath
|
147
147
|
location
|
@@ -36,6 +36,31 @@ module ActionDispatch
|
|
36
36
|
def build(app)
|
37
37
|
klass.new(app, *args, &block)
|
38
38
|
end
|
39
|
+
|
40
|
+
def build_instrumented(app)
|
41
|
+
InstrumentationProxy.new(build(app), inspect)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# This class is used to instrument the execution of a single middleware.
|
46
|
+
# It proxies the `call` method transparently and instruments the method
|
47
|
+
# call.
|
48
|
+
class InstrumentationProxy
|
49
|
+
EVENT_NAME = "process_middleware.action_dispatch"
|
50
|
+
|
51
|
+
def initialize(middleware, class_name)
|
52
|
+
@middleware = middleware
|
53
|
+
|
54
|
+
@payload = {
|
55
|
+
middleware: class_name,
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def call(env)
|
60
|
+
ActiveSupport::Notifications.instrument(EVENT_NAME, @payload) do
|
61
|
+
@middleware.call(env)
|
62
|
+
end
|
63
|
+
end
|
39
64
|
end
|
40
65
|
|
41
66
|
include Enumerable
|
@@ -66,6 +91,7 @@ module ActionDispatch
|
|
66
91
|
def unshift(klass, *args, &block)
|
67
92
|
middlewares.unshift(build_middleware(klass, args, block))
|
68
93
|
end
|
94
|
+
ruby2_keywords(:unshift) if respond_to?(:ruby2_keywords, true)
|
69
95
|
|
70
96
|
def initialize_copy(other)
|
71
97
|
self.middlewares = other.middlewares.dup
|
@@ -75,6 +101,7 @@ module ActionDispatch
|
|
75
101
|
index = assert_index(index, :before)
|
76
102
|
middlewares.insert(index, build_middleware(klass, args, block))
|
77
103
|
end
|
104
|
+
ruby2_keywords(:insert) if respond_to?(:ruby2_keywords, true)
|
78
105
|
|
79
106
|
alias_method :insert_before, :insert
|
80
107
|
|
@@ -82,12 +109,14 @@ module ActionDispatch
|
|
82
109
|
index = assert_index(index, :after)
|
83
110
|
insert(index + 1, *args, &block)
|
84
111
|
end
|
112
|
+
ruby2_keywords(:insert_after) if respond_to?(:ruby2_keywords, true)
|
85
113
|
|
86
114
|
def swap(target, *args, &block)
|
87
115
|
index = assert_index(target, :before)
|
88
116
|
insert(index, *args, &block)
|
89
117
|
middlewares.delete_at(index + 1)
|
90
118
|
end
|
119
|
+
ruby2_keywords(:swap) if respond_to?(:ruby2_keywords, true)
|
91
120
|
|
92
121
|
def delete(target)
|
93
122
|
middlewares.delete_if { |m| m.klass == target }
|
@@ -96,13 +125,20 @@ module ActionDispatch
|
|
96
125
|
def use(klass, *args, &block)
|
97
126
|
middlewares.push(build_middleware(klass, args, block))
|
98
127
|
end
|
128
|
+
ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
|
99
129
|
|
100
130
|
def build(app = nil, &block)
|
101
|
-
|
131
|
+
instrumenting = ActiveSupport::Notifications.notifier.listening?(InstrumentationProxy::EVENT_NAME)
|
132
|
+
middlewares.freeze.reverse.inject(app || block) do |a, e|
|
133
|
+
if instrumenting
|
134
|
+
e.build_instrumented(a)
|
135
|
+
else
|
136
|
+
e.build(a)
|
137
|
+
end
|
138
|
+
end
|
102
139
|
end
|
103
140
|
|
104
141
|
private
|
105
|
-
|
106
142
|
def assert_index(index, where)
|
107
143
|
i = index.is_a?(Integer) ? index : middlewares.index { |m| m.klass == index }
|
108
144
|
raise "No such middleware to insert #{where}: #{index.inspect}" unless i
|
@@ -41,9 +41,8 @@ module ActionDispatch
|
|
41
41
|
rescue SystemCallError
|
42
42
|
false
|
43
43
|
end
|
44
|
-
|
45
44
|
}
|
46
|
-
|
45
|
+
::Rack::Utils.escape_path(match).b
|
47
46
|
end
|
48
47
|
end
|
49
48
|
|
@@ -69,7 +68,7 @@ module ActionDispatch
|
|
69
68
|
|
70
69
|
headers["Vary"] = "Accept-Encoding" if gzip_path
|
71
70
|
|
72
|
-
|
71
|
+
[status, headers, body]
|
73
72
|
ensure
|
74
73
|
request.path_info = path
|
75
74
|
end
|
@@ -80,7 +79,7 @@ module ActionDispatch
|
|
80
79
|
end
|
81
80
|
|
82
81
|
def content_type(path)
|
83
|
-
::Rack::Mime.mime_type(::File.extname(path), "text/plain"
|
82
|
+
::Rack::Mime.mime_type(::File.extname(path), "text/plain")
|
84
83
|
end
|
85
84
|
|
86
85
|
def gzip_encoding_accepted?(request)
|
@@ -90,8 +89,8 @@ module ActionDispatch
|
|
90
89
|
def gzip_file_path(path)
|
91
90
|
can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/
|
92
91
|
gzip_path = "#{path}.gz"
|
93
|
-
if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path)
|
94
|
-
gzip_path
|
92
|
+
if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path)))
|
93
|
+
gzip_path
|
95
94
|
else
|
96
95
|
false
|
97
96
|
end
|
@@ -117,7 +116,7 @@ module ActionDispatch
|
|
117
116
|
req = Rack::Request.new env
|
118
117
|
|
119
118
|
if req.get? || req.head?
|
120
|
-
path = req.path_info.chomp("/"
|
119
|
+
path = req.path_info.chomp("/")
|
121
120
|
if match = @file_handler.match?(path)
|
122
121
|
req.path_info = match
|
123
122
|
return @file_handler.serve(req)
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<% actions = ActiveSupport::ActionableError.actions(exception) %>
|
2
|
+
|
3
|
+
<% if actions.any? %>
|
4
|
+
<div class="actions">
|
5
|
+
<% actions.each do |action, _| %>
|
6
|
+
<%= button_to action, ActionDispatch::ActionableExceptions.endpoint, params: {
|
7
|
+
error: exception.class.name,
|
8
|
+
action: action,
|
9
|
+
location: request.path
|
10
|
+
} %>
|
11
|
+
<% end %>
|
12
|
+
</div>
|
13
|
+
<% end %>
|
File without changes
|
@@ -6,7 +6,9 @@
|
|
6
6
|
<% end %>
|
7
7
|
|
8
8
|
<h2 style="margin-top: 30px">Request</h2>
|
9
|
-
|
9
|
+
<% if params_valid? %>
|
10
|
+
<p><b>Parameters</b>:</p> <pre><%= debug_params(@request.filtered_parameters) %></pre>
|
11
|
+
<% end %>
|
10
12
|
|
11
13
|
<div class="details">
|
12
14
|
<div class="summary"><a href="#" onclick="return toggleSessionDump()">Toggle session dump</a></div>
|
@@ -1,6 +1,8 @@
|
|
1
|
-
<%
|
1
|
+
<% error_index = local_assigns[:error_index] || 0 %>
|
2
|
+
|
3
|
+
<% source_extracts.each_with_index do |source_extract, index| %>
|
2
4
|
<% if source_extract[:code] %>
|
3
|
-
<div class="source <%="hidden" if
|
5
|
+
<div class="source <%= "hidden" if show_source_idx != index %>" id="frame-source-<%= error_index %>-<%= index %>">
|
4
6
|
<div class="info">
|
5
7
|
Extracted source (around line <strong>#<%= source_extract[:line_number] %></strong>):
|
6
8
|
</div>
|
@@ -1,52 +1,62 @@
|
|
1
|
-
<% names =
|
1
|
+
<% names = traces.keys %>
|
2
|
+
<% error_index = local_assigns[:error_index] || 0 %>
|
2
3
|
|
3
4
|
<p><code>Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %></code></p>
|
4
5
|
|
5
|
-
<div id="traces">
|
6
|
+
<div id="traces-<%= error_index %>">
|
6
7
|
<% names.each do |name| %>
|
7
8
|
<%
|
8
|
-
show = "show('#{name.gsub(/\s/, '-')}');"
|
9
|
-
hide = (names - [name]).collect {|hide_name| "hide('#{hide_name.gsub(/\s/, '-')}');"}
|
9
|
+
show = "show('#{name.gsub(/\s/, '-')}-#{error_index}');"
|
10
|
+
hide = (names - [name]).collect {|hide_name| "hide('#{hide_name.gsub(/\s/, '-')}-#{error_index}');"}
|
10
11
|
%>
|
11
12
|
<a href="#" onclick="<%= hide.join %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %>
|
12
13
|
<% end %>
|
13
14
|
|
14
|
-
<%
|
15
|
-
<div id="<%= name.gsub(/\s/, '-') %>" style="display: <%= (name ==
|
16
|
-
<
|
15
|
+
<% traces.each do |name, trace| %>
|
16
|
+
<div id="<%= "#{name.gsub(/\s/, '-')}-#{error_index}" %>" style="display: <%= (name == trace_to_show) ? 'block' : 'none' %>;">
|
17
|
+
<code style="font-size: 11px;">
|
18
|
+
<% trace.each do |frame| %>
|
19
|
+
<a class="trace-frames trace-frames-<%= error_index %>" data-exception-object-id="<%= frame[:exception_object_id] %>" data-frame-id="<%= frame[:id] %>" href="#">
|
20
|
+
<%= frame[:trace] %>
|
21
|
+
</a>
|
22
|
+
<br>
|
23
|
+
<% end %>
|
24
|
+
</code>
|
17
25
|
</div>
|
18
26
|
<% end %>
|
19
27
|
|
20
28
|
<script type="text/javascript">
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
traceFrames
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
currentSource
|
46
|
-
|
47
|
-
|
29
|
+
(function() {
|
30
|
+
var traceFrames = document.getElementsByClassName('trace-frames-<%= error_index %>');
|
31
|
+
var selectedFrame, currentSource = document.getElementById('frame-source-<%= error_index %>-0');
|
32
|
+
|
33
|
+
// Add click listeners for all stack frames
|
34
|
+
for (var i = 0; i < traceFrames.length; i++) {
|
35
|
+
traceFrames[i].addEventListener('click', function(e) {
|
36
|
+
e.preventDefault();
|
37
|
+
var target = e.target;
|
38
|
+
var frame_id = target.dataset.frameId;
|
39
|
+
|
40
|
+
if (selectedFrame) {
|
41
|
+
selectedFrame.className = selectedFrame.className.replace("selected", "");
|
42
|
+
}
|
43
|
+
|
44
|
+
target.className += " selected";
|
45
|
+
selectedFrame = target;
|
46
|
+
|
47
|
+
// Change the extracted source code
|
48
|
+
changeSourceExtract(frame_id);
|
49
|
+
});
|
50
|
+
|
51
|
+
function changeSourceExtract(frame_id) {
|
52
|
+
var el = document.getElementById('frame-source-<%= error_index %>-' + frame_id);
|
53
|
+
if (currentSource && el) {
|
54
|
+
currentSource.className += " hidden";
|
55
|
+
el.className = el.className.replace(" hidden", "");
|
56
|
+
currentSource = el;
|
57
|
+
}
|
48
58
|
}
|
49
59
|
}
|
50
|
-
}
|
60
|
+
})();
|
51
61
|
</script>
|
52
62
|
</div>
|
@@ -0,0 +1,7 @@
|
|
1
|
+
<header>
|
2
|
+
<h1>Blocked host: <%= @host %></h1>
|
3
|
+
</header>
|
4
|
+
<div id="container">
|
5
|
+
<h2>To allow requests to <%= @host %> make sure it is a valid hostname (containing only numbers, letters, dashes and dots), then add the following to your environment configuration:</h2>
|
6
|
+
<pre>config.hosts << "<%= @host %>"</pre>
|
7
|
+
</div>
|
@@ -1,16 +1,38 @@
|
|
1
1
|
<header>
|
2
2
|
<h1>
|
3
3
|
<%= @exception.class.to_s %>
|
4
|
-
<% if @request.parameters['controller'] %>
|
4
|
+
<% if params_valid? && @request.parameters['controller'] %>
|
5
5
|
in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %>
|
6
6
|
<% end %>
|
7
7
|
</h1>
|
8
8
|
</header>
|
9
9
|
|
10
10
|
<div id="container">
|
11
|
-
<h2
|
11
|
+
<h2>
|
12
|
+
<%= h @exception.message %>
|
13
|
+
|
14
|
+
<%= render "rescues/actions", exception: @exception, request: @request %>
|
15
|
+
</h2>
|
16
|
+
|
17
|
+
<%= render "rescues/source", source_extracts: @source_extracts, show_source_idx: @show_source_idx, error_index: 0 %>
|
18
|
+
<%= render "rescues/trace", traces: @traces, trace_to_show: @trace_to_show, error_index: 0 %>
|
19
|
+
|
20
|
+
<% if @exception.cause %>
|
21
|
+
<h2>Exception Causes</h2>
|
22
|
+
<% end %>
|
23
|
+
|
24
|
+
<% @exception_wrapper.wrapped_causes.each.with_index(1) do |wrapper, index| %>
|
25
|
+
<div class="details">
|
26
|
+
<a class="summary" href="#" style="color: #F0F0F0; text-decoration: none; background: #C52F24; border-bottom: none;" onclick="return toggle(<%= wrapper.exception.object_id %>)">
|
27
|
+
<%= wrapper.exception.class.name %>: <%= h wrapper.exception.message %>
|
28
|
+
</a>
|
29
|
+
</div>
|
30
|
+
|
31
|
+
<div id="<%= wrapper.exception.object_id %>" style="display: none;">
|
32
|
+
<%= render "rescues/source", source_extracts: wrapper.source_extracts, show_source_idx: wrapper.source_to_show_id, error_index: index %>
|
33
|
+
<%= render "rescues/trace", traces: wrapper.traces, trace_to_show: wrapper.trace_to_show, error_index: index %>
|
34
|
+
</div>
|
35
|
+
<% end %>
|
12
36
|
|
13
|
-
<%= render template: "rescues/_source" %>
|
14
|
-
<%= render template: "rescues/_trace" %>
|
15
37
|
<%= render template: "rescues/_request_and_response" %>
|
16
38
|
</div>
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<%= @exception.class.to_s %><%
|
2
|
-
if @request.parameters['controller']
|
2
|
+
if params_valid? && @request.parameters['controller']
|
3
3
|
%> in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %>
|
4
4
|
<% end %>
|
5
5
|
|