actionpack 5.2.1 → 7.0.2.4
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 +264 -220
- data/MIT-LICENSE +1 -1
- data/README.rdoc +6 -6
- data/lib/abstract_controller/asset_paths.rb +1 -1
- data/lib/abstract_controller/base.rb +24 -4
- data/lib/abstract_controller/caching/fragments.rb +8 -24
- data/lib/abstract_controller/caching.rb +2 -2
- data/lib/abstract_controller/callbacks.rb +34 -8
- data/lib/abstract_controller/collector.rb +5 -4
- data/lib/abstract_controller/error.rb +1 -1
- data/lib/abstract_controller/helpers.rb +107 -90
- data/lib/abstract_controller/logger.rb +1 -1
- data/lib/abstract_controller/railties/routes_helpers.rb +19 -1
- data/lib/abstract_controller/rendering.rb +9 -9
- data/lib/abstract_controller/translation.rb +12 -5
- data/lib/abstract_controller/url_for.rb +4 -6
- data/lib/abstract_controller.rb +2 -0
- data/lib/action_controller/api.rb +5 -4
- data/lib/action_controller/base.rb +6 -9
- data/lib/action_controller/caching.rb +1 -3
- data/lib/action_controller/log_subscriber.rb +13 -9
- data/lib/action_controller/metal/basic_implicit_render.rb +1 -1
- data/lib/action_controller/metal/conditional_get.rb +57 -6
- data/lib/action_controller/metal/content_security_policy.rb +2 -3
- data/lib/action_controller/metal/cookies.rb +4 -2
- data/lib/action_controller/metal/data_streaming.rb +9 -18
- data/lib/action_controller/metal/default_headers.rb +17 -0
- data/lib/action_controller/metal/etag_with_template_digest.rb +4 -6
- data/lib/action_controller/metal/exceptions.rb +55 -12
- data/lib/action_controller/metal/flash.rb +10 -6
- data/lib/action_controller/metal/head.rb +7 -4
- data/lib/action_controller/metal/helpers.rb +15 -6
- data/lib/action_controller/metal/http_authentication.rb +41 -39
- data/lib/action_controller/metal/implicit_render.rb +5 -15
- data/lib/action_controller/metal/instrumentation.rb +59 -55
- data/lib/action_controller/metal/live.rb +80 -33
- data/lib/action_controller/metal/logging.rb +20 -0
- data/lib/action_controller/metal/mime_responds.rb +22 -7
- data/lib/action_controller/metal/parameter_encoding.rb +35 -4
- data/lib/action_controller/metal/params_wrapper.rb +50 -31
- data/lib/action_controller/metal/permissions_policy.rb +46 -0
- data/lib/action_controller/metal/redirecting.rb +93 -23
- data/lib/action_controller/metal/renderers.rb +4 -4
- data/lib/action_controller/metal/rendering.rb +14 -9
- data/lib/action_controller/metal/request_forgery_protection.rb +160 -58
- data/lib/action_controller/metal/rescue.rb +2 -2
- data/lib/action_controller/metal/streaming.rb +1 -4
- data/lib/action_controller/metal/strong_parameters.rb +236 -88
- data/lib/action_controller/metal/testing.rb +9 -2
- data/lib/action_controller/metal/url_for.rb +1 -1
- data/lib/action_controller/metal.rb +16 -17
- data/lib/action_controller/railtie.rb +49 -6
- data/lib/action_controller/railties/helpers.rb +1 -1
- data/lib/action_controller/renderer.rb +37 -13
- data/lib/action_controller/template_assertions.rb +1 -1
- data/lib/action_controller/test_case.rb +98 -68
- data/lib/action_controller.rb +4 -5
- data/lib/action_dispatch/http/cache.rb +45 -32
- data/lib/action_dispatch/http/content_disposition.rb +45 -0
- data/lib/action_dispatch/http/content_security_policy.rb +69 -56
- data/lib/action_dispatch/http/filter_parameters.rb +14 -8
- data/lib/action_dispatch/http/filter_redirect.rb +2 -3
- data/lib/action_dispatch/http/headers.rb +4 -4
- data/lib/action_dispatch/http/mime_negotiation.rb +44 -16
- data/lib/action_dispatch/http/mime_type.rb +47 -30
- data/lib/action_dispatch/http/parameters.rb +18 -27
- data/lib/action_dispatch/http/permissions_policy.rb +173 -0
- data/lib/action_dispatch/http/request.rb +49 -35
- data/lib/action_dispatch/http/response.rb +34 -26
- data/lib/action_dispatch/http/upload.rb +9 -1
- data/lib/action_dispatch/http/url.rb +86 -94
- data/lib/action_dispatch/journey/formatter.rb +55 -31
- data/lib/action_dispatch/journey/gtg/builder.rb +30 -46
- data/lib/action_dispatch/journey/gtg/simulator.rb +15 -8
- data/lib/action_dispatch/journey/gtg/transition_table.rb +78 -21
- data/lib/action_dispatch/journey/nfa/dot.rb +0 -11
- data/lib/action_dispatch/journey/nodes/node.rb +83 -16
- data/lib/action_dispatch/journey/parser.rb +13 -13
- data/lib/action_dispatch/journey/parser.y +1 -1
- data/lib/action_dispatch/journey/path/pattern.rb +42 -34
- data/lib/action_dispatch/journey/route.rb +14 -31
- data/lib/action_dispatch/journey/router/utils.rb +16 -14
- data/lib/action_dispatch/journey/router.rb +27 -35
- data/lib/action_dispatch/journey/routes.rb +3 -5
- data/lib/action_dispatch/journey/scanner.rb +10 -4
- data/lib/action_dispatch/journey/visitors.rb +1 -4
- data/lib/action_dispatch/journey/visualizer/fsm.js +49 -24
- data/lib/action_dispatch/journey/visualizer/index.html.erb +1 -1
- data/lib/action_dispatch/journey.rb +0 -2
- data/lib/action_dispatch/middleware/actionable_exceptions.rb +45 -0
- data/lib/action_dispatch/middleware/callbacks.rb +2 -4
- data/lib/action_dispatch/middleware/cookies.rb +136 -113
- data/lib/action_dispatch/middleware/debug_exceptions.rb +47 -68
- data/lib/action_dispatch/middleware/debug_locks.rb +8 -8
- data/lib/action_dispatch/middleware/debug_view.rb +66 -0
- data/lib/action_dispatch/middleware/exception_wrapper.rb +79 -30
- data/lib/action_dispatch/middleware/executor.rb +4 -1
- data/lib/action_dispatch/middleware/flash.rb +10 -12
- data/lib/action_dispatch/middleware/host_authorization.rb +159 -0
- data/lib/action_dispatch/middleware/public_exceptions.rb +6 -3
- data/lib/action_dispatch/middleware/remote_ip.rb +30 -20
- data/lib/action_dispatch/middleware/request_id.rb +5 -6
- data/lib/action_dispatch/middleware/server_timing.rb +33 -0
- data/lib/action_dispatch/middleware/session/abstract_store.rb +16 -3
- data/lib/action_dispatch/middleware/session/cache_store.rb +11 -6
- data/lib/action_dispatch/middleware/session/cookie_store.rb +24 -19
- data/lib/action_dispatch/middleware/show_exceptions.rb +20 -11
- data/lib/action_dispatch/middleware/ssl.rb +20 -15
- data/lib/action_dispatch/middleware/stack.rb +79 -7
- data/lib/action_dispatch/middleware/static.rb +150 -94
- 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/_message_and_suggestions.html.erb +22 -0
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +6 -11
- 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 +46 -36
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +8 -0
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +7 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +25 -6
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +1 -1
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +9 -6
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +4 -1
- data/lib/action_dispatch/middleware/templates/rescues/layout.erb +121 -15
- 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 +5 -5
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +4 -4
- data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +5 -5
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +4 -4
- data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +16 -2
- data/lib/action_dispatch/railtie.rb +16 -4
- data/lib/action_dispatch/request/session.rb +59 -22
- data/lib/action_dispatch/request/utils.rb +28 -2
- data/lib/action_dispatch/routing/inspector.rb +102 -54
- data/lib/action_dispatch/routing/mapper.rb +184 -156
- data/lib/action_dispatch/routing/polymorphic_routes.rb +21 -19
- data/lib/action_dispatch/routing/redirection.rb +4 -6
- data/lib/action_dispatch/routing/route_set.rb +83 -73
- data/lib/action_dispatch/routing/routes_proxy.rb +1 -1
- data/lib/action_dispatch/routing/url_for.rb +2 -3
- data/lib/action_dispatch/routing.rb +23 -22
- data/lib/action_dispatch/system_test_case.rb +65 -16
- data/lib/action_dispatch/system_testing/browser.rb +43 -16
- data/lib/action_dispatch/system_testing/driver.rb +42 -10
- data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +58 -12
- data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +3 -10
- data/lib/action_dispatch/testing/assertion_response.rb +0 -1
- data/lib/action_dispatch/testing/assertions/response.rb +4 -7
- data/lib/action_dispatch/testing/assertions/routing.rb +20 -8
- data/lib/action_dispatch/testing/assertions.rb +3 -6
- data/lib/action_dispatch/testing/integration.rb +61 -30
- data/lib/action_dispatch/testing/request_encoder.rb +2 -2
- data/lib/action_dispatch/testing/test_process.rb +8 -6
- data/lib/action_dispatch/testing/test_request.rb +3 -3
- data/lib/action_dispatch/testing/test_response.rb +4 -32
- data/lib/action_dispatch.rb +15 -7
- data/lib/action_pack/gem_version.rb +4 -4
- data/lib/action_pack.rb +1 -1
- metadata +44 -25
- data/lib/action_controller/metal/force_ssl.rb +0 -99
- data/lib/action_dispatch/http/parameter_filter.rb +0 -86
- data/lib/action_dispatch/journey/nfa/builder.rb +0 -78
- data/lib/action_dispatch/journey/nfa/simulator.rb +0 -49
- data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -120
- data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +0 -26
@@ -13,7 +13,7 @@ module ActionDispatch
|
|
13
13
|
#
|
14
14
|
# Requests can opt-out of redirection with +exclude+:
|
15
15
|
#
|
16
|
-
# config.ssl_options = { redirect: { exclude: -> request { request.path
|
16
|
+
# config.ssl_options = { redirect: { exclude: -> request { /healthcheck/.match?(request.path) } } }
|
17
17
|
#
|
18
18
|
# Cookies will not be flagged as secure for excluded requests.
|
19
19
|
#
|
@@ -29,7 +29,7 @@ module ActionDispatch
|
|
29
29
|
#
|
30
30
|
# * +expires+: How long, in seconds, these settings will stick. The minimum
|
31
31
|
# required to qualify for browser preload lists is 1 year. Defaults to
|
32
|
-
#
|
32
|
+
# 2 years (recommended).
|
33
33
|
#
|
34
34
|
# * +subdomains+: Set to +true+ to tell the browser to apply these settings
|
35
35
|
# to all subdomains. This protects your cookies from interception by a
|
@@ -49,14 +49,16 @@ module ActionDispatch
|
|
49
49
|
class SSL
|
50
50
|
# :stopdoc:
|
51
51
|
|
52
|
-
# Default to
|
53
|
-
HSTS_EXPIRES_IN =
|
52
|
+
# Default to 2 years as recommended on hstspreload.org.
|
53
|
+
HSTS_EXPIRES_IN = 63072000
|
54
|
+
|
55
|
+
PERMANENT_REDIRECT_REQUEST_METHODS = %w[GET HEAD] # :nodoc:
|
54
56
|
|
55
57
|
def self.default_hsts_options
|
56
58
|
{ expires: HSTS_EXPIRES_IN, subdomains: true, preload: false }
|
57
59
|
end
|
58
60
|
|
59
|
-
def initialize(app, redirect: {}, hsts: {}, secure_cookies: true)
|
61
|
+
def initialize(app, redirect: {}, hsts: {}, secure_cookies: true, ssl_default_redirect_status: nil)
|
60
62
|
@app = app
|
61
63
|
|
62
64
|
@redirect = redirect
|
@@ -65,6 +67,7 @@ module ActionDispatch
|
|
65
67
|
@secure_cookies = secure_cookies
|
66
68
|
|
67
69
|
@hsts_header = build_hsts_header(normalize_hsts_options(hsts))
|
70
|
+
@ssl_default_redirect_status = ssl_default_redirect_status
|
68
71
|
end
|
69
72
|
|
70
73
|
def call(env)
|
@@ -83,7 +86,7 @@ module ActionDispatch
|
|
83
86
|
|
84
87
|
private
|
85
88
|
def set_hsts_header!(headers)
|
86
|
-
headers["Strict-Transport-Security"
|
89
|
+
headers["Strict-Transport-Security"] ||= @hsts_header
|
87
90
|
end
|
88
91
|
|
89
92
|
def normalize_hsts_options(options)
|
@@ -102,23 +105,23 @@ module ActionDispatch
|
|
102
105
|
|
103
106
|
# https://tools.ietf.org/html/rfc6797#section-6.1
|
104
107
|
def build_hsts_header(hsts)
|
105
|
-
value = "max-age=#{hsts[:expires].to_i}"
|
108
|
+
value = +"max-age=#{hsts[:expires].to_i}"
|
106
109
|
value << "; includeSubDomains" if hsts[:subdomains]
|
107
110
|
value << "; preload" if hsts[:preload]
|
108
111
|
value
|
109
112
|
end
|
110
113
|
|
111
114
|
def flag_cookies_as_secure!(headers)
|
112
|
-
if cookies = headers["Set-Cookie"
|
113
|
-
cookies = cookies.split("\n"
|
115
|
+
if cookies = headers["Set-Cookie"]
|
116
|
+
cookies = cookies.split("\n")
|
114
117
|
|
115
|
-
headers["Set-Cookie"
|
116
|
-
if
|
118
|
+
headers["Set-Cookie"] = cookies.map { |cookie|
|
119
|
+
if !/;\s*secure\s*(;|$)/i.match?(cookie)
|
117
120
|
"#{cookie}; secure"
|
118
121
|
else
|
119
122
|
cookie
|
120
123
|
end
|
121
|
-
}.join("\n"
|
124
|
+
}.join("\n")
|
122
125
|
end
|
123
126
|
end
|
124
127
|
|
@@ -126,12 +129,14 @@ module ActionDispatch
|
|
126
129
|
[ @redirect.fetch(:status, redirection_status(request)),
|
127
130
|
{ "Content-Type" => "text/html",
|
128
131
|
"Location" => https_location_for(request) },
|
129
|
-
@redirect
|
132
|
+
(@redirect[:body] || []) ]
|
130
133
|
end
|
131
134
|
|
132
135
|
def redirection_status(request)
|
133
|
-
if
|
136
|
+
if PERMANENT_REDIRECT_REQUEST_METHODS.include?(request.raw_request_method)
|
134
137
|
301 # Issue a permanent redirect via a GET request.
|
138
|
+
elsif @ssl_default_redirect_status
|
139
|
+
@ssl_default_redirect_status
|
135
140
|
else
|
136
141
|
307 # Issue a fresh request redirect to preserve the HTTP method.
|
137
142
|
end
|
@@ -141,7 +146,7 @@ module ActionDispatch
|
|
141
146
|
host = @redirect[:host] || request.host
|
142
147
|
port = @redirect[:port] || request.port
|
143
148
|
|
144
|
-
location = "https://#{host}"
|
149
|
+
location = +"https://#{host}"
|
145
150
|
location << ":#{port}" if port != 80 && port != 443
|
146
151
|
location << request.fullpath
|
147
152
|
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
|
@@ -47,8 +72,8 @@ module ActionDispatch
|
|
47
72
|
yield(self) if block_given?
|
48
73
|
end
|
49
74
|
|
50
|
-
def each
|
51
|
-
@middlewares.each
|
75
|
+
def each(&block)
|
76
|
+
@middlewares.each(&block)
|
52
77
|
end
|
53
78
|
|
54
79
|
def size
|
@@ -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)
|
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)
|
78
105
|
|
79
106
|
alias_method :insert_before, :insert
|
80
107
|
|
@@ -82,29 +109,68 @@ module ActionDispatch
|
|
82
109
|
index = assert_index(index, :after)
|
83
110
|
insert(index + 1, *args, &block)
|
84
111
|
end
|
112
|
+
ruby2_keywords(:insert_after)
|
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)
|
91
120
|
|
121
|
+
# Deletes a middleware from the middleware stack.
|
122
|
+
#
|
123
|
+
# Returns the array of middlewares not including the deleted item, or
|
124
|
+
# returns nil if the target is not found.
|
92
125
|
def delete(target)
|
93
|
-
middlewares.
|
126
|
+
middlewares.reject! { |m| m.name == target.name }
|
127
|
+
end
|
128
|
+
|
129
|
+
# Deletes a middleware from the middleware stack.
|
130
|
+
#
|
131
|
+
# Returns the array of middlewares not including the deleted item, or
|
132
|
+
# raises +RuntimeError+ if the target is not found.
|
133
|
+
def delete!(target)
|
134
|
+
delete(target) || (raise "No such middleware to remove: #{target.inspect}")
|
135
|
+
end
|
136
|
+
|
137
|
+
def move(target, source)
|
138
|
+
source_index = assert_index(source, :before)
|
139
|
+
source_middleware = middlewares.delete_at(source_index)
|
140
|
+
|
141
|
+
target_index = assert_index(target, :before)
|
142
|
+
middlewares.insert(target_index, source_middleware)
|
143
|
+
end
|
144
|
+
|
145
|
+
alias_method :move_before, :move
|
146
|
+
|
147
|
+
def move_after(target, source)
|
148
|
+
source_index = assert_index(source, :after)
|
149
|
+
source_middleware = middlewares.delete_at(source_index)
|
150
|
+
|
151
|
+
target_index = assert_index(target, :after)
|
152
|
+
middlewares.insert(target_index + 1, source_middleware)
|
94
153
|
end
|
95
154
|
|
96
155
|
def use(klass, *args, &block)
|
97
156
|
middlewares.push(build_middleware(klass, args, block))
|
98
157
|
end
|
158
|
+
ruby2_keywords(:use)
|
99
159
|
|
100
|
-
def build(app =
|
101
|
-
|
160
|
+
def build(app = nil, &block)
|
161
|
+
instrumenting = ActiveSupport::Notifications.notifier.listening?(InstrumentationProxy::EVENT_NAME)
|
162
|
+
middlewares.freeze.reverse.inject(app || block) do |a, e|
|
163
|
+
if instrumenting
|
164
|
+
e.build_instrumented(a)
|
165
|
+
else
|
166
|
+
e.build(a)
|
167
|
+
end
|
168
|
+
end
|
102
169
|
end
|
103
170
|
|
104
171
|
private
|
105
|
-
|
106
172
|
def assert_index(index, where)
|
107
|
-
i = index.is_a?(Integer) ? index :
|
173
|
+
i = index.is_a?(Integer) ? index : index_of(index)
|
108
174
|
raise "No such middleware to insert #{where}: #{index.inspect}" unless i
|
109
175
|
i
|
110
176
|
end
|
@@ -112,5 +178,11 @@ module ActionDispatch
|
|
112
178
|
def build_middleware(klass, args, block)
|
113
179
|
Middleware.new(klass, args, block)
|
114
180
|
end
|
181
|
+
|
182
|
+
def index_of(klass)
|
183
|
+
middlewares.index do |m|
|
184
|
+
m.name == klass.name
|
185
|
+
end
|
186
|
+
end
|
115
187
|
end
|
116
188
|
end
|
@@ -1,130 +1,186 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "rack/utils"
|
4
|
-
require "active_support/core_ext/uri"
|
5
4
|
|
6
5
|
module ActionDispatch
|
7
|
-
# This middleware
|
8
|
-
#
|
9
|
-
# when a response containing a file's contents is delivered.
|
6
|
+
# This middleware serves static files from disk, if available.
|
7
|
+
# If no file is found, it hands off to the main app.
|
10
8
|
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
@
|
21
|
-
@
|
9
|
+
# In Rails apps, this middleware is configured to serve assets from
|
10
|
+
# the +public/+ directory.
|
11
|
+
#
|
12
|
+
# Only GET and HEAD requests are served. POST and other HTTP methods
|
13
|
+
# are handed off to the main app.
|
14
|
+
#
|
15
|
+
# Only files in the root directory are served; path traversal is denied.
|
16
|
+
class Static
|
17
|
+
def initialize(app, path, index: "index", headers: {})
|
18
|
+
@app = app
|
19
|
+
@file_handler = FileHandler.new(path, index: index, headers: headers)
|
22
20
|
end
|
23
21
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
# Used by the +Static+ class to check the existence of a valid file
|
29
|
-
# in the server's +public/+ directory (see Static#call).
|
30
|
-
def match?(path)
|
31
|
-
path = ::Rack::Utils.unescape_path path
|
32
|
-
return false unless ::Rack::Utils.valid_path? path
|
33
|
-
path = ::Rack::Utils.clean_path_info path
|
34
|
-
|
35
|
-
paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"]
|
36
|
-
|
37
|
-
if match = paths.detect { |p|
|
38
|
-
path = File.join(@root, p.b)
|
39
|
-
begin
|
40
|
-
File.file?(path) && File.readable?(path)
|
41
|
-
rescue SystemCallError
|
42
|
-
false
|
43
|
-
end
|
22
|
+
def call(env)
|
23
|
+
@file_handler.attempt(env) || @app.call(env)
|
24
|
+
end
|
25
|
+
end
|
44
26
|
|
45
|
-
|
46
|
-
|
47
|
-
|
27
|
+
# This endpoint serves static files from disk using Rack::File.
|
28
|
+
#
|
29
|
+
# URL paths are matched with static files according to expected
|
30
|
+
# conventions: +path+, +path+.html, +path+/index.html.
|
31
|
+
#
|
32
|
+
# Precompressed versions of these files are checked first. Brotli (.br)
|
33
|
+
# and gzip (.gz) files are supported. If +path+.br exists, this
|
34
|
+
# endpoint returns that file with a <tt>Content-Encoding: br</tt> header.
|
35
|
+
#
|
36
|
+
# If no matching file is found, this endpoint responds 404 Not Found.
|
37
|
+
#
|
38
|
+
# Pass the +root+ directory to search for matching files, an optional
|
39
|
+
# <tt>index: "index"</tt> to change the default +path+/index.html, and optional
|
40
|
+
# additional response headers.
|
41
|
+
class FileHandler
|
42
|
+
# Accept-Encoding value -> file extension
|
43
|
+
PRECOMPRESSED = {
|
44
|
+
"br" => ".br",
|
45
|
+
"gzip" => ".gz",
|
46
|
+
"identity" => nil
|
47
|
+
}
|
48
|
+
|
49
|
+
def initialize(root, index: "index", headers: {}, precompressed: %i[ br gzip ], compressible_content_types: /\A(?:text\/|application\/javascript)/)
|
50
|
+
@root = root.chomp("/").b
|
51
|
+
@index = index
|
52
|
+
|
53
|
+
@precompressed = Array(precompressed).map(&:to_s) | %w[ identity ]
|
54
|
+
@compressible_content_types = compressible_content_types
|
55
|
+
|
56
|
+
@file_server = ::Rack::File.new(@root, headers)
|
48
57
|
end
|
49
58
|
|
50
59
|
def call(env)
|
51
|
-
|
60
|
+
attempt(env) || @file_server.call(env)
|
52
61
|
end
|
53
62
|
|
54
|
-
def
|
55
|
-
|
56
|
-
gzip_path = gzip_file_path(path)
|
63
|
+
def attempt(env)
|
64
|
+
request = Rack::Request.new env
|
57
65
|
|
58
|
-
if
|
59
|
-
request.path_info
|
60
|
-
|
61
|
-
if status == 304
|
62
|
-
return [status, headers, body]
|
66
|
+
if request.get? || request.head?
|
67
|
+
if found = find_file(request.path_info, accept_encoding: request.accept_encoding)
|
68
|
+
serve request, *found
|
63
69
|
end
|
64
|
-
headers["Content-Encoding"] = "gzip"
|
65
|
-
headers["Content-Type"] = content_type(path)
|
66
|
-
else
|
67
|
-
status, headers, body = @file_server.call(request.env)
|
68
70
|
end
|
69
|
-
|
70
|
-
headers["Vary"] = "Accept-Encoding" if gzip_path
|
71
|
-
|
72
|
-
return [status, headers, body]
|
73
|
-
ensure
|
74
|
-
request.path_info = path
|
75
71
|
end
|
76
72
|
|
77
73
|
private
|
78
|
-
def
|
79
|
-
|
74
|
+
def serve(request, filepath, content_headers)
|
75
|
+
original, request.path_info =
|
76
|
+
request.path_info, ::Rack::Utils.escape_path(filepath).b
|
77
|
+
|
78
|
+
@file_server.call(request.env).tap do |status, headers, body|
|
79
|
+
# Omit Content-Encoding/Type/etc headers for 304 Not Modified
|
80
|
+
if status != 304
|
81
|
+
headers.update(content_headers)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
ensure
|
85
|
+
request.path_info = original
|
80
86
|
end
|
81
87
|
|
82
|
-
|
83
|
-
|
88
|
+
# Match a URI path to a static file to be served.
|
89
|
+
#
|
90
|
+
# Used by the +Static+ class to negotiate a servable file in the
|
91
|
+
# +public/+ directory (see Static#call).
|
92
|
+
#
|
93
|
+
# Checks for +path+, +path+.html, and +path+/index.html files,
|
94
|
+
# in that order, including .br and .gzip compressed extensions.
|
95
|
+
#
|
96
|
+
# If a matching file is found, the path and necessary response headers
|
97
|
+
# (Content-Type, Content-Encoding) are returned.
|
98
|
+
def find_file(path_info, accept_encoding:)
|
99
|
+
each_candidate_filepath(path_info) do |filepath, content_type|
|
100
|
+
if response = try_files(filepath, content_type, accept_encoding: accept_encoding)
|
101
|
+
return response
|
102
|
+
end
|
103
|
+
end
|
84
104
|
end
|
85
105
|
|
86
|
-
def
|
87
|
-
|
106
|
+
def try_files(filepath, content_type, accept_encoding:)
|
107
|
+
headers = { "Content-Type" => content_type }
|
108
|
+
|
109
|
+
if compressible? content_type
|
110
|
+
try_precompressed_files filepath, headers, accept_encoding: accept_encoding
|
111
|
+
elsif file_readable? filepath
|
112
|
+
[ filepath, headers ]
|
113
|
+
end
|
88
114
|
end
|
89
115
|
|
90
|
-
def
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
116
|
+
def try_precompressed_files(filepath, headers, accept_encoding:)
|
117
|
+
each_precompressed_filepath(filepath) do |content_encoding, precompressed_filepath|
|
118
|
+
if file_readable? precompressed_filepath
|
119
|
+
# Identity encoding is default, so we skip Accept-Encoding
|
120
|
+
# negotiation and needn't set Content-Encoding.
|
121
|
+
#
|
122
|
+
# Vary header is expected when we've found other available
|
123
|
+
# encodings that Accept-Encoding ruled out.
|
124
|
+
if content_encoding == "identity"
|
125
|
+
return precompressed_filepath, headers
|
126
|
+
else
|
127
|
+
headers["Vary"] = "Accept-Encoding"
|
128
|
+
|
129
|
+
if accept_encoding.any? { |enc, _| /\b#{content_encoding}\b/i.match?(enc) }
|
130
|
+
headers["Content-Encoding"] = content_encoding
|
131
|
+
return precompressed_filepath, headers
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
97
135
|
end
|
98
136
|
end
|
99
|
-
end
|
100
137
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
#
|
106
|
-
# This middleware verifies the path to ensure that only files
|
107
|
-
# living in the root directory can be rendered. A request cannot
|
108
|
-
# produce a directory traversal using this middleware. Only 'GET' and 'HEAD'
|
109
|
-
# requests will result in a file being returned.
|
110
|
-
class Static
|
111
|
-
def initialize(app, path, index: "index", headers: {})
|
112
|
-
@app = app
|
113
|
-
@file_handler = FileHandler.new(path, index: index, headers: headers)
|
114
|
-
end
|
138
|
+
def file_readable?(path)
|
139
|
+
file_path = File.join(@root, path.b)
|
140
|
+
File.file?(file_path) && File.readable?(file_path)
|
141
|
+
end
|
115
142
|
|
116
|
-
|
117
|
-
|
143
|
+
def compressible?(content_type)
|
144
|
+
@compressible_content_types.match?(content_type)
|
145
|
+
end
|
118
146
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
return @file_handler.serve(req)
|
147
|
+
def each_precompressed_filepath(filepath)
|
148
|
+
@precompressed.each do |content_encoding|
|
149
|
+
precompressed_ext = PRECOMPRESSED.fetch(content_encoding)
|
150
|
+
yield content_encoding, "#{filepath}#{precompressed_ext}"
|
124
151
|
end
|
152
|
+
|
153
|
+
nil
|
125
154
|
end
|
126
155
|
|
127
|
-
|
128
|
-
|
156
|
+
def each_candidate_filepath(path_info)
|
157
|
+
return unless path = clean_path(path_info)
|
158
|
+
|
159
|
+
ext = ::File.extname(path)
|
160
|
+
content_type = ::Rack::Mime.mime_type(ext, nil)
|
161
|
+
yield path, content_type || "text/plain"
|
162
|
+
|
163
|
+
# Tack on .html and /index.html only for paths that don't have
|
164
|
+
# an explicit, resolvable file extension. No need to check
|
165
|
+
# for foo.js.html and foo.js/index.html.
|
166
|
+
unless content_type
|
167
|
+
default_ext = ::ActionController::Base.default_static_extension
|
168
|
+
if ext != default_ext
|
169
|
+
default_content_type = ::Rack::Mime.mime_type(default_ext, "text/plain")
|
170
|
+
|
171
|
+
yield "#{path}#{default_ext}", default_content_type
|
172
|
+
yield "#{path}/#{@index}#{default_ext}", default_content_type
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
nil
|
177
|
+
end
|
178
|
+
|
179
|
+
def clean_path(path_info)
|
180
|
+
path = ::Rack::Utils.unescape_path path_info.chomp("/")
|
181
|
+
if ::Rack::Utils.valid_path? path
|
182
|
+
::Rack::Utils.clean_path_info path
|
183
|
+
end
|
184
|
+
end
|
129
185
|
end
|
130
186
|
end
|
@@ -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
|
@@ -0,0 +1,22 @@
|
|
1
|
+
<% if exception.respond_to?(:original_message) && exception.respond_to?(:corrections) %>
|
2
|
+
<div class="exception-message">
|
3
|
+
<%= simple_format h(exception.original_message), { class: "message" }, wrapper_tag: "div" %>
|
4
|
+
</div>
|
5
|
+
<%
|
6
|
+
# The 'did_you_mean' gem can raise exceptions when calling #corrections on
|
7
|
+
# the exception. If it does there are no corrections to show.
|
8
|
+
corrections = exception.corrections rescue []
|
9
|
+
%>
|
10
|
+
<% if corrections.any? %>
|
11
|
+
<b>Did you mean?</b>
|
12
|
+
<ul>
|
13
|
+
<% corrections.each do |correction| %>
|
14
|
+
<li class="correction"><%= h correction %></li>
|
15
|
+
<% end %>
|
16
|
+
</ul>
|
17
|
+
<% end %>
|
18
|
+
<% else %>
|
19
|
+
<div class="exception-message">
|
20
|
+
<%= simple_format h(exception.message), { class: "message" }, wrapper_tag: "div" %>
|
21
|
+
</div>
|
22
|
+
<% end %>
|
@@ -1,22 +1,17 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
<% end %>
|
5
|
-
<pre id="blame_trace" <%='style="display:none"' if hide %>><code><%= @exception.describe_blame %></code></pre>
|
1
|
+
<h2 class="request-heading">Request</h2>
|
2
|
+
<% if params_valid? %>
|
3
|
+
<p><b>Parameters</b>:</p> <pre><%= debug_params(@request.filtered_parameters) %></pre>
|
6
4
|
<% end %>
|
7
5
|
|
8
|
-
<h2 style="margin-top: 30px">Request</h2>
|
9
|
-
<p><b>Parameters</b>:</p> <pre><%= debug_params(@request.filtered_parameters) %></pre>
|
10
|
-
|
11
6
|
<div class="details">
|
12
7
|
<div class="summary"><a href="#" onclick="return toggleSessionDump()">Toggle session dump</a></div>
|
13
|
-
<div id="session_dump"
|
8
|
+
<div id="session_dump" class="hidden"><pre><%= debug_hash @request.session %></pre></div>
|
14
9
|
</div>
|
15
10
|
|
16
11
|
<div class="details">
|
17
12
|
<div class="summary"><a href="#" onclick="return toggleEnvDump()">Toggle env dump</a></div>
|
18
|
-
<div id="env_dump"
|
13
|
+
<div id="env_dump" class="hidden"><pre><%= debug_hash @request.env.slice(*@request.class::ENV_METHODS) %></pre></div>
|
19
14
|
</div>
|
20
15
|
|
21
|
-
<h2
|
16
|
+
<h2 class="response-heading">Response</h2>
|
22
17
|
<p><b>Headers</b>:</p> <pre><%= debug_headers(defined?(@response) ? @response.headers : {}) %></pre>
|
@@ -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>
|