omg-actionpack 8.0.0.alpha1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +129 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +57 -0
- data/lib/abstract_controller/asset_paths.rb +14 -0
- data/lib/abstract_controller/base.rb +299 -0
- data/lib/abstract_controller/caching/fragments.rb +149 -0
- data/lib/abstract_controller/caching.rb +68 -0
- data/lib/abstract_controller/callbacks.rb +265 -0
- data/lib/abstract_controller/collector.rb +44 -0
- data/lib/abstract_controller/deprecator.rb +9 -0
- data/lib/abstract_controller/error.rb +8 -0
- data/lib/abstract_controller/helpers.rb +243 -0
- data/lib/abstract_controller/logger.rb +16 -0
- data/lib/abstract_controller/railties/routes_helpers.rb +25 -0
- data/lib/abstract_controller/rendering.rb +126 -0
- data/lib/abstract_controller/translation.rb +42 -0
- data/lib/abstract_controller/url_for.rb +37 -0
- data/lib/abstract_controller.rb +36 -0
- data/lib/action_controller/api/api_rendering.rb +18 -0
- data/lib/action_controller/api.rb +155 -0
- data/lib/action_controller/base.rb +332 -0
- data/lib/action_controller/caching.rb +49 -0
- data/lib/action_controller/deprecator.rb +9 -0
- data/lib/action_controller/form_builder.rb +55 -0
- data/lib/action_controller/log_subscriber.rb +96 -0
- data/lib/action_controller/metal/allow_browser.rb +123 -0
- data/lib/action_controller/metal/basic_implicit_render.rb +17 -0
- data/lib/action_controller/metal/conditional_get.rb +341 -0
- data/lib/action_controller/metal/content_security_policy.rb +86 -0
- data/lib/action_controller/metal/cookies.rb +20 -0
- data/lib/action_controller/metal/data_streaming.rb +154 -0
- data/lib/action_controller/metal/default_headers.rb +21 -0
- data/lib/action_controller/metal/etag_with_flash.rb +22 -0
- data/lib/action_controller/metal/etag_with_template_digest.rb +59 -0
- data/lib/action_controller/metal/exceptions.rb +106 -0
- data/lib/action_controller/metal/flash.rb +67 -0
- data/lib/action_controller/metal/head.rb +67 -0
- data/lib/action_controller/metal/helpers.rb +129 -0
- data/lib/action_controller/metal/http_authentication.rb +565 -0
- data/lib/action_controller/metal/implicit_render.rb +67 -0
- data/lib/action_controller/metal/instrumentation.rb +120 -0
- data/lib/action_controller/metal/live.rb +398 -0
- data/lib/action_controller/metal/logging.rb +22 -0
- data/lib/action_controller/metal/mime_responds.rb +337 -0
- data/lib/action_controller/metal/parameter_encoding.rb +84 -0
- data/lib/action_controller/metal/params_wrapper.rb +312 -0
- data/lib/action_controller/metal/permissions_policy.rb +38 -0
- data/lib/action_controller/metal/rate_limiting.rb +62 -0
- data/lib/action_controller/metal/redirecting.rb +251 -0
- data/lib/action_controller/metal/renderers.rb +181 -0
- data/lib/action_controller/metal/rendering.rb +260 -0
- data/lib/action_controller/metal/request_forgery_protection.rb +667 -0
- data/lib/action_controller/metal/rescue.rb +33 -0
- data/lib/action_controller/metal/streaming.rb +183 -0
- data/lib/action_controller/metal/strong_parameters.rb +1546 -0
- data/lib/action_controller/metal/testing.rb +25 -0
- data/lib/action_controller/metal/url_for.rb +65 -0
- data/lib/action_controller/metal.rb +339 -0
- data/lib/action_controller/railtie.rb +149 -0
- data/lib/action_controller/railties/helpers.rb +26 -0
- data/lib/action_controller/renderer.rb +161 -0
- data/lib/action_controller/template_assertions.rb +13 -0
- data/lib/action_controller/test_case.rb +691 -0
- data/lib/action_controller.rb +80 -0
- data/lib/action_dispatch/constants.rb +34 -0
- data/lib/action_dispatch/deprecator.rb +9 -0
- data/lib/action_dispatch/http/cache.rb +249 -0
- data/lib/action_dispatch/http/content_disposition.rb +47 -0
- data/lib/action_dispatch/http/content_security_policy.rb +365 -0
- data/lib/action_dispatch/http/filter_parameters.rb +80 -0
- data/lib/action_dispatch/http/filter_redirect.rb +50 -0
- data/lib/action_dispatch/http/headers.rb +134 -0
- data/lib/action_dispatch/http/mime_negotiation.rb +187 -0
- data/lib/action_dispatch/http/mime_type.rb +389 -0
- data/lib/action_dispatch/http/mime_types.rb +54 -0
- data/lib/action_dispatch/http/parameters.rb +119 -0
- data/lib/action_dispatch/http/permissions_policy.rb +189 -0
- data/lib/action_dispatch/http/rack_cache.rb +67 -0
- data/lib/action_dispatch/http/request.rb +498 -0
- data/lib/action_dispatch/http/response.rb +556 -0
- data/lib/action_dispatch/http/upload.rb +107 -0
- data/lib/action_dispatch/http/url.rb +344 -0
- data/lib/action_dispatch/journey/formatter.rb +226 -0
- data/lib/action_dispatch/journey/gtg/builder.rb +149 -0
- data/lib/action_dispatch/journey/gtg/simulator.rb +50 -0
- data/lib/action_dispatch/journey/gtg/transition_table.rb +217 -0
- data/lib/action_dispatch/journey/nfa/dot.rb +27 -0
- data/lib/action_dispatch/journey/nodes/node.rb +208 -0
- data/lib/action_dispatch/journey/parser.rb +103 -0
- data/lib/action_dispatch/journey/path/pattern.rb +209 -0
- data/lib/action_dispatch/journey/route.rb +189 -0
- data/lib/action_dispatch/journey/router/utils.rb +105 -0
- data/lib/action_dispatch/journey/router.rb +151 -0
- data/lib/action_dispatch/journey/routes.rb +82 -0
- data/lib/action_dispatch/journey/scanner.rb +70 -0
- data/lib/action_dispatch/journey/visitors.rb +267 -0
- data/lib/action_dispatch/journey/visualizer/fsm.css +30 -0
- data/lib/action_dispatch/journey/visualizer/fsm.js +159 -0
- data/lib/action_dispatch/journey/visualizer/index.html.erb +52 -0
- data/lib/action_dispatch/journey.rb +7 -0
- data/lib/action_dispatch/log_subscriber.rb +25 -0
- data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
- data/lib/action_dispatch/middleware/assume_ssl.rb +27 -0
- data/lib/action_dispatch/middleware/callbacks.rb +38 -0
- data/lib/action_dispatch/middleware/cookies.rb +719 -0
- data/lib/action_dispatch/middleware/debug_exceptions.rb +206 -0
- data/lib/action_dispatch/middleware/debug_locks.rb +129 -0
- data/lib/action_dispatch/middleware/debug_view.rb +73 -0
- data/lib/action_dispatch/middleware/exception_wrapper.rb +350 -0
- data/lib/action_dispatch/middleware/executor.rb +32 -0
- data/lib/action_dispatch/middleware/flash.rb +318 -0
- data/lib/action_dispatch/middleware/host_authorization.rb +171 -0
- data/lib/action_dispatch/middleware/public_exceptions.rb +64 -0
- data/lib/action_dispatch/middleware/reloader.rb +16 -0
- data/lib/action_dispatch/middleware/remote_ip.rb +199 -0
- data/lib/action_dispatch/middleware/request_id.rb +50 -0
- data/lib/action_dispatch/middleware/server_timing.rb +78 -0
- data/lib/action_dispatch/middleware/session/abstract_store.rb +112 -0
- data/lib/action_dispatch/middleware/session/cache_store.rb +66 -0
- data/lib/action_dispatch/middleware/session/cookie_store.rb +129 -0
- data/lib/action_dispatch/middleware/session/mem_cache_store.rb +34 -0
- data/lib/action_dispatch/middleware/show_exceptions.rb +88 -0
- data/lib/action_dispatch/middleware/ssl.rb +180 -0
- data/lib/action_dispatch/middleware/stack.rb +194 -0
- data/lib/action_dispatch/middleware/static.rb +192 -0
- 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 +17 -0
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +23 -0
- data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +36 -0
- data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
- data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +62 -0
- data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +9 -0
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +12 -0
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +9 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +35 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +16 -0
- data/lib/action_dispatch/middleware/templates/rescues/layout.erb +284 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +23 -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 +11 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +32 -0
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb +11 -0
- data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +20 -0
- data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +7 -0
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +6 -0
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +19 -0
- data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +232 -0
- data/lib/action_dispatch/railtie.rb +77 -0
- data/lib/action_dispatch/request/session.rb +283 -0
- data/lib/action_dispatch/request/utils.rb +109 -0
- data/lib/action_dispatch/routing/endpoint.rb +19 -0
- data/lib/action_dispatch/routing/inspector.rb +323 -0
- data/lib/action_dispatch/routing/mapper.rb +2372 -0
- data/lib/action_dispatch/routing/polymorphic_routes.rb +363 -0
- data/lib/action_dispatch/routing/redirection.rb +218 -0
- data/lib/action_dispatch/routing/route_set.rb +958 -0
- data/lib/action_dispatch/routing/routes_proxy.rb +66 -0
- data/lib/action_dispatch/routing/url_for.rb +244 -0
- data/lib/action_dispatch/routing.rb +262 -0
- data/lib/action_dispatch/system_test_case.rb +206 -0
- data/lib/action_dispatch/system_testing/browser.rb +75 -0
- data/lib/action_dispatch/system_testing/driver.rb +85 -0
- data/lib/action_dispatch/system_testing/server.rb +33 -0
- data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +164 -0
- data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +23 -0
- data/lib/action_dispatch/testing/assertion_response.rb +48 -0
- data/lib/action_dispatch/testing/assertions/response.rb +114 -0
- data/lib/action_dispatch/testing/assertions/routing.rb +343 -0
- data/lib/action_dispatch/testing/assertions.rb +25 -0
- data/lib/action_dispatch/testing/integration.rb +694 -0
- data/lib/action_dispatch/testing/request_encoder.rb +60 -0
- data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
- data/lib/action_dispatch/testing/test_process.rb +57 -0
- data/lib/action_dispatch/testing/test_request.rb +73 -0
- data/lib/action_dispatch/testing/test_response.rb +58 -0
- data/lib/action_dispatch.rb +147 -0
- data/lib/action_pack/gem_version.rb +19 -0
- data/lib/action_pack/version.rb +12 -0
- data/lib/action_pack.rb +27 -0
- metadata +375 -0
@@ -0,0 +1,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionDispatch
|
6
|
+
# # Action Dispatch HostAuthorization
|
7
|
+
#
|
8
|
+
# This middleware guards from DNS rebinding attacks by explicitly permitting the
|
9
|
+
# hosts a request can be sent to, and is passed the options set in
|
10
|
+
# `config.host_authorization`.
|
11
|
+
#
|
12
|
+
# Requests can opt-out of Host Authorization with `exclude`:
|
13
|
+
#
|
14
|
+
# config.host_authorization = { exclude: ->(request) { request.path =~ /healthcheck/ } }
|
15
|
+
#
|
16
|
+
# When a request comes to an unauthorized host, the `response_app` application
|
17
|
+
# will be executed and rendered. If no `response_app` is given, a default one
|
18
|
+
# will run. The default response app logs blocked host info with level 'error'
|
19
|
+
# and responds with `403 Forbidden`. The body of the response contains debug
|
20
|
+
# info if `config.consider_all_requests_local` is set to true, otherwise the
|
21
|
+
# body is empty.
|
22
|
+
class HostAuthorization
|
23
|
+
ALLOWED_HOSTS_IN_DEVELOPMENT = [".localhost", ".test", IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0")]
|
24
|
+
PORT_REGEX = /(?::\d+)/ # :nodoc:
|
25
|
+
SUBDOMAIN_REGEX = /(?:[a-z0-9-]+\.)/i # :nodoc:
|
26
|
+
IPV4_HOSTNAME = /(?<host>\d+\.\d+\.\d+\.\d+)#{PORT_REGEX}?/ # :nodoc:
|
27
|
+
IPV6_HOSTNAME = /(?<host>[a-f0-9]*:[a-f0-9.:]+)/i # :nodoc:
|
28
|
+
IPV6_HOSTNAME_WITH_PORT = /\[#{IPV6_HOSTNAME}\]#{PORT_REGEX}/i # :nodoc:
|
29
|
+
VALID_IP_HOSTNAME = Regexp.union( # :nodoc:
|
30
|
+
/\A#{IPV4_HOSTNAME}\z/,
|
31
|
+
/\A#{IPV6_HOSTNAME}\z/,
|
32
|
+
/\A#{IPV6_HOSTNAME_WITH_PORT}\z/,
|
33
|
+
)
|
34
|
+
|
35
|
+
class Permissions # :nodoc:
|
36
|
+
def initialize(hosts)
|
37
|
+
@hosts = sanitize_hosts(hosts)
|
38
|
+
end
|
39
|
+
|
40
|
+
def empty?
|
41
|
+
@hosts.empty?
|
42
|
+
end
|
43
|
+
|
44
|
+
def allows?(host)
|
45
|
+
@hosts.any? do |allowed|
|
46
|
+
if allowed.is_a?(IPAddr)
|
47
|
+
begin
|
48
|
+
allowed === extract_hostname(host)
|
49
|
+
rescue
|
50
|
+
# IPAddr#=== raises an error if you give it a hostname instead of IP. Treat
|
51
|
+
# similar errors as blocked access.
|
52
|
+
false
|
53
|
+
end
|
54
|
+
else
|
55
|
+
allowed === host
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
def sanitize_hosts(hosts)
|
62
|
+
Array(hosts).map do |host|
|
63
|
+
case host
|
64
|
+
when Regexp then sanitize_regexp(host)
|
65
|
+
when String then sanitize_string(host)
|
66
|
+
else host
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def sanitize_regexp(host)
|
72
|
+
/\A#{host}#{PORT_REGEX}?\z/
|
73
|
+
end
|
74
|
+
|
75
|
+
def sanitize_string(host)
|
76
|
+
if host.start_with?(".")
|
77
|
+
/\A#{SUBDOMAIN_REGEX}?#{Regexp.escape(host[1..-1])}#{PORT_REGEX}?\z/i
|
78
|
+
else
|
79
|
+
/\A#{Regexp.escape host}#{PORT_REGEX}?\z/i
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def extract_hostname(host)
|
84
|
+
host.slice(VALID_IP_HOSTNAME, "host") || host
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class DefaultResponseApp # :nodoc:
|
89
|
+
RESPONSE_STATUS = 403
|
90
|
+
|
91
|
+
def call(env)
|
92
|
+
request = Request.new(env)
|
93
|
+
format = request.xhr? ? "text/plain" : "text/html"
|
94
|
+
|
95
|
+
log_error(request)
|
96
|
+
response(format, response_body(request))
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
def response_body(request)
|
101
|
+
return "" unless request.get_header("action_dispatch.show_detailed_exceptions")
|
102
|
+
|
103
|
+
template = DebugView.new(hosts: request.env["action_dispatch.blocked_hosts"])
|
104
|
+
template.render(template: "rescues/blocked_host", layout: "rescues/layout")
|
105
|
+
end
|
106
|
+
|
107
|
+
def response(format, body)
|
108
|
+
[RESPONSE_STATUS,
|
109
|
+
{ Rack::CONTENT_TYPE => "#{format}; charset=#{Response.default_charset}",
|
110
|
+
Rack::CONTENT_LENGTH => body.bytesize.to_s },
|
111
|
+
[body]]
|
112
|
+
end
|
113
|
+
|
114
|
+
def log_error(request)
|
115
|
+
logger = available_logger(request)
|
116
|
+
|
117
|
+
return unless logger
|
118
|
+
|
119
|
+
logger.error("[#{self.class.name}] Blocked hosts: #{request.env["action_dispatch.blocked_hosts"].join(", ")}")
|
120
|
+
end
|
121
|
+
|
122
|
+
def available_logger(request)
|
123
|
+
request.logger || ActionView::Base.logger
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def initialize(app, hosts, exclude: nil, response_app: nil)
|
128
|
+
@app = app
|
129
|
+
@permissions = Permissions.new(hosts)
|
130
|
+
@exclude = exclude
|
131
|
+
|
132
|
+
@response_app = response_app || DefaultResponseApp.new
|
133
|
+
end
|
134
|
+
|
135
|
+
def call(env)
|
136
|
+
return @app.call(env) if @permissions.empty?
|
137
|
+
|
138
|
+
request = Request.new(env)
|
139
|
+
hosts = blocked_hosts(request)
|
140
|
+
|
141
|
+
if hosts.empty? || excluded?(request)
|
142
|
+
mark_as_authorized(request)
|
143
|
+
@app.call(env)
|
144
|
+
else
|
145
|
+
env["action_dispatch.blocked_hosts"] = hosts
|
146
|
+
@response_app.call(env)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
def blocked_hosts(request)
|
152
|
+
hosts = []
|
153
|
+
|
154
|
+
origin_host = request.get_header("HTTP_HOST")
|
155
|
+
hosts << origin_host unless @permissions.allows?(origin_host)
|
156
|
+
|
157
|
+
forwarded_host = request.x_forwarded_host&.split(/,\s?/)&.last
|
158
|
+
hosts << forwarded_host unless forwarded_host.blank? || @permissions.allows?(forwarded_host)
|
159
|
+
|
160
|
+
hosts
|
161
|
+
end
|
162
|
+
|
163
|
+
def excluded?(request)
|
164
|
+
@exclude && @exclude.call(request)
|
165
|
+
end
|
166
|
+
|
167
|
+
def mark_as_authorized(request)
|
168
|
+
request.set_header("action_dispatch.authorized_host", request.host)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionDispatch
|
6
|
+
# # Action Dispatch PublicExceptions
|
7
|
+
#
|
8
|
+
# When called, this middleware renders an error page. By default if an HTML
|
9
|
+
# response is expected it will render static error pages from the `/public`
|
10
|
+
# directory. For example when this middleware receives a 500 response it will
|
11
|
+
# render the template found in `/public/500.html`. If an internationalized
|
12
|
+
# locale is set, this middleware will attempt to render the template in
|
13
|
+
# `/public/500.<locale>.html`. If an internationalized template is not found it
|
14
|
+
# will fall back on `/public/500.html`.
|
15
|
+
#
|
16
|
+
# When a request with a content type other than HTML is made, this middleware
|
17
|
+
# will attempt to convert error information into the appropriate response type.
|
18
|
+
class PublicExceptions
|
19
|
+
attr_accessor :public_path
|
20
|
+
|
21
|
+
def initialize(public_path)
|
22
|
+
@public_path = public_path
|
23
|
+
end
|
24
|
+
|
25
|
+
def call(env)
|
26
|
+
request = ActionDispatch::Request.new(env)
|
27
|
+
status = request.path_info[1..-1].to_i
|
28
|
+
begin
|
29
|
+
content_type = request.formats.first
|
30
|
+
rescue ActionDispatch::Http::MimeNegotiation::InvalidType
|
31
|
+
content_type = Mime[:text]
|
32
|
+
end
|
33
|
+
body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
|
34
|
+
|
35
|
+
render(status, content_type, body)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def render(status, content_type, body)
|
40
|
+
format = "to_#{content_type.to_sym}" if content_type
|
41
|
+
if format && body.respond_to?(format)
|
42
|
+
render_format(status, content_type, body.public_send(format))
|
43
|
+
else
|
44
|
+
render_html(status)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def render_format(status, content_type, body)
|
49
|
+
[status, { Rack::CONTENT_TYPE => "#{content_type}; charset=#{ActionDispatch::Response.default_charset}",
|
50
|
+
Rack::CONTENT_LENGTH => body.bytesize.to_s }, [body]]
|
51
|
+
end
|
52
|
+
|
53
|
+
def render_html(status)
|
54
|
+
path = "#{public_path}/#{status}.#{I18n.locale}.html"
|
55
|
+
path = "#{public_path}/#{status}.html" unless (found = File.exist?(path))
|
56
|
+
|
57
|
+
if found || File.exist?(path)
|
58
|
+
render_format(status, "text/html", File.read(path))
|
59
|
+
else
|
60
|
+
[404, { Constants::X_CASCADE => "pass" }, []]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionDispatch
|
6
|
+
# # Action Dispatch Reloader
|
7
|
+
#
|
8
|
+
# ActionDispatch::Reloader wraps the request with callbacks provided by
|
9
|
+
# ActiveSupport::Reloader, intended to assist with code reloading during
|
10
|
+
# development.
|
11
|
+
#
|
12
|
+
# ActionDispatch::Reloader is included in the middleware stack only if reloading
|
13
|
+
# is enabled, which it is by the default in `development` mode.
|
14
|
+
class Reloader < Executor
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
require "ipaddr"
|
6
|
+
|
7
|
+
module ActionDispatch
|
8
|
+
# # Action Dispatch RemoteIp
|
9
|
+
#
|
10
|
+
# This middleware calculates the IP address of the remote client that is making
|
11
|
+
# the request. It does this by checking various headers that could contain the
|
12
|
+
# address, and then picking the last-set address that is not on the list of
|
13
|
+
# trusted IPs. This follows the precedent set by e.g. [the Tomcat
|
14
|
+
# server](https://issues.apache.org/bugzilla/show_bug.cgi?id=50453). A more
|
15
|
+
# detailed explanation of the algorithm is given at GetIp#calculate_ip.
|
16
|
+
#
|
17
|
+
# Some Rack servers concatenate repeated headers, like [HTTP RFC
|
18
|
+
# 2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2) requires.
|
19
|
+
# Some Rack servers simply drop preceding headers, and only report the value
|
20
|
+
# that was [given in the last
|
21
|
+
# header](https://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-server
|
22
|
+
# s). If you are behind multiple proxy servers (like NGINX to HAProxy to
|
23
|
+
# Unicorn) then you should test your Rack server to make sure your data is good.
|
24
|
+
#
|
25
|
+
# IF YOU DON'T USE A PROXY, THIS MAKES YOU VULNERABLE TO IP SPOOFING. This
|
26
|
+
# middleware assumes that there is at least one proxy sitting around and setting
|
27
|
+
# headers with the client's remote IP address. If you don't use a proxy, because
|
28
|
+
# you are hosted on e.g. Heroku without SSL, any client can claim to have any IP
|
29
|
+
# address by setting the `X-Forwarded-For` header. If you care about that, then
|
30
|
+
# you need to explicitly drop or ignore those headers sometime before this
|
31
|
+
# middleware runs. Alternatively, remove this middleware to avoid inadvertently
|
32
|
+
# relying on it.
|
33
|
+
class RemoteIp
|
34
|
+
class IpSpoofAttackError < StandardError; end
|
35
|
+
|
36
|
+
# The default trusted IPs list simply includes IP addresses that are guaranteed
|
37
|
+
# by the IP specification to be private addresses. Those will not be the
|
38
|
+
# ultimate client IP in production, and so are discarded. See
|
39
|
+
# https://en.wikipedia.org/wiki/Private_network for details.
|
40
|
+
TRUSTED_PROXIES = [
|
41
|
+
"127.0.0.0/8", # localhost IPv4 range, per RFC-3330
|
42
|
+
"::1", # localhost IPv6
|
43
|
+
"fc00::/7", # private IPv6 range fc00::/7
|
44
|
+
"10.0.0.0/8", # private IPv4 range 10.x.x.x
|
45
|
+
"172.16.0.0/12", # private IPv4 range 172.16.0.0 .. 172.31.255.255
|
46
|
+
"192.168.0.0/16", # private IPv4 range 192.168.x.x
|
47
|
+
].map { |proxy| IPAddr.new(proxy) }
|
48
|
+
|
49
|
+
attr_reader :check_ip, :proxies
|
50
|
+
|
51
|
+
# Create a new `RemoteIp` middleware instance.
|
52
|
+
#
|
53
|
+
# The `ip_spoofing_check` option is on by default. When on, an exception is
|
54
|
+
# raised if it looks like the client is trying to lie about its own IP address.
|
55
|
+
# It makes sense to turn off this check on sites aimed at non-IP clients (like
|
56
|
+
# WAP devices), or behind proxies that set headers in an incorrect or confusing
|
57
|
+
# way (like AWS ELB).
|
58
|
+
#
|
59
|
+
# The `custom_proxies` argument can take an enumerable which will be used
|
60
|
+
# instead of `TRUSTED_PROXIES`. Any proxy setup will put the value you want in
|
61
|
+
# the middle (or at the beginning) of the `X-Forwarded-For` list, with your
|
62
|
+
# proxy servers after it. If your proxies aren't removed, pass them in via the
|
63
|
+
# `custom_proxies` parameter. That way, the middleware will ignore those IP
|
64
|
+
# addresses, and return the one that you want.
|
65
|
+
def initialize(app, ip_spoofing_check = true, custom_proxies = nil)
|
66
|
+
@app = app
|
67
|
+
@check_ip = ip_spoofing_check
|
68
|
+
@proxies = if custom_proxies.blank?
|
69
|
+
TRUSTED_PROXIES
|
70
|
+
elsif custom_proxies.respond_to?(:any?)
|
71
|
+
custom_proxies
|
72
|
+
else
|
73
|
+
raise(ArgumentError, <<~EOM)
|
74
|
+
Setting config.action_dispatch.trusted_proxies to a single value isn't
|
75
|
+
supported. Please set this to an enumerable instead. For
|
76
|
+
example, instead of:
|
77
|
+
|
78
|
+
config.action_dispatch.trusted_proxies = IPAddr.new("10.0.0.0/8")
|
79
|
+
|
80
|
+
Wrap the value in an Array:
|
81
|
+
|
82
|
+
config.action_dispatch.trusted_proxies = [IPAddr.new("10.0.0.0/8")]
|
83
|
+
|
84
|
+
Note that passing an enumerable will *replace* the default set of trusted proxies.
|
85
|
+
EOM
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Since the IP address may not be needed, we store the object here without
|
90
|
+
# calculating the IP to keep from slowing down the majority of requests. For
|
91
|
+
# those requests that do need to know the IP, the GetIp#calculate_ip method will
|
92
|
+
# calculate the memoized client IP address.
|
93
|
+
def call(env)
|
94
|
+
req = ActionDispatch::Request.new env
|
95
|
+
req.remote_ip = GetIp.new(req, check_ip, proxies)
|
96
|
+
@app.call(req.env)
|
97
|
+
end
|
98
|
+
|
99
|
+
# The GetIp class exists as a way to defer processing of the request data into
|
100
|
+
# an actual IP address. If the ActionDispatch::Request#remote_ip method is
|
101
|
+
# called, this class will calculate the value and then memoize it.
|
102
|
+
class GetIp
|
103
|
+
def initialize(req, check_ip, proxies)
|
104
|
+
@req = req
|
105
|
+
@check_ip = check_ip
|
106
|
+
@proxies = proxies
|
107
|
+
end
|
108
|
+
|
109
|
+
# Sort through the various IP address headers, looking for the IP most likely to
|
110
|
+
# be the address of the actual remote client making this request.
|
111
|
+
#
|
112
|
+
# REMOTE_ADDR will be correct if the request is made directly against the Ruby
|
113
|
+
# process, on e.g. Heroku. When the request is proxied by another server like
|
114
|
+
# HAProxy or NGINX, the IP address that made the original request will be put in
|
115
|
+
# an `X-Forwarded-For` header. If there are multiple proxies, that header may
|
116
|
+
# contain a list of IPs. Other proxy services set the `Client-Ip` header
|
117
|
+
# instead, so we check that too.
|
118
|
+
#
|
119
|
+
# As discussed in [this post about Rails IP
|
120
|
+
# Spoofing](https://web.archive.org/web/20170626095448/https://blog.gingerlime.c
|
121
|
+
# om/2012/rails-ip-spoofing-vulnerabilities-and-protection/), while the first IP
|
122
|
+
# in the list is likely to be the "originating" IP, it could also have been set
|
123
|
+
# by the client maliciously.
|
124
|
+
#
|
125
|
+
# In order to find the first address that is (probably) accurate, we take the
|
126
|
+
# list of IPs, remove known and trusted proxies, and then take the last address
|
127
|
+
# left, which was presumably set by one of those proxies.
|
128
|
+
def calculate_ip
|
129
|
+
# Set by the Rack web server, this is a single value.
|
130
|
+
remote_addr = ips_from(@req.remote_addr).last
|
131
|
+
|
132
|
+
# Could be a CSV list and/or repeated headers that were concatenated.
|
133
|
+
client_ips = ips_from(@req.client_ip).reverse!
|
134
|
+
forwarded_ips = ips_from(@req.x_forwarded_for).reverse!
|
135
|
+
|
136
|
+
# `Client-Ip` and `X-Forwarded-For` should not, generally, both be set. If they
|
137
|
+
# are both set, it means that either:
|
138
|
+
#
|
139
|
+
# 1) This request passed through two proxies with incompatible IP header
|
140
|
+
# conventions.
|
141
|
+
#
|
142
|
+
# 2) The client passed one of `Client-Ip` or `X-Forwarded-For`
|
143
|
+
# (whichever the proxy servers weren't using) themselves.
|
144
|
+
#
|
145
|
+
# Either way, there is no way for us to determine which header is the right one
|
146
|
+
# after the fact. Since we have no idea, if we are concerned about IP spoofing
|
147
|
+
# we need to give up and explode. (If you're not concerned about IP spoofing you
|
148
|
+
# can turn the `ip_spoofing_check` option off.)
|
149
|
+
should_check_ip = @check_ip && client_ips.last && forwarded_ips.last
|
150
|
+
if should_check_ip && !forwarded_ips.include?(client_ips.last)
|
151
|
+
# We don't know which came from the proxy, and which from the user
|
152
|
+
raise IpSpoofAttackError, "IP spoofing attack?! " \
|
153
|
+
"HTTP_CLIENT_IP=#{@req.client_ip.inspect} " \
|
154
|
+
"HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}"
|
155
|
+
end
|
156
|
+
|
157
|
+
# We assume these things about the IP headers:
|
158
|
+
#
|
159
|
+
# - X-Forwarded-For will be a list of IPs, one per proxy, or blank
|
160
|
+
# - Client-Ip is propagated from the outermost proxy, or is blank
|
161
|
+
# - REMOTE_ADDR will be the IP that made the request to Rack
|
162
|
+
ips = forwarded_ips + client_ips
|
163
|
+
ips.compact!
|
164
|
+
|
165
|
+
# If every single IP option is in the trusted list, return the IP that's
|
166
|
+
# furthest away
|
167
|
+
filter_proxies(ips + [remote_addr]).first || ips.last || remote_addr
|
168
|
+
end
|
169
|
+
|
170
|
+
# Memoizes the value returned by #calculate_ip and returns it for
|
171
|
+
# ActionDispatch::Request to use.
|
172
|
+
def to_s
|
173
|
+
@ip ||= calculate_ip
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
def ips_from(header) # :doc:
|
178
|
+
return [] unless header
|
179
|
+
# Split the comma-separated list into an array of strings.
|
180
|
+
ips = header.strip.split(/[,\s]+/)
|
181
|
+
ips.select! do |ip|
|
182
|
+
# Only return IPs that are valid according to the IPAddr#new method.
|
183
|
+
range = IPAddr.new(ip).to_range
|
184
|
+
# We want to make sure nobody is sneaking a netmask in.
|
185
|
+
range.begin == range.end
|
186
|
+
rescue ArgumentError
|
187
|
+
nil
|
188
|
+
end
|
189
|
+
ips
|
190
|
+
end
|
191
|
+
|
192
|
+
def filter_proxies(ips) # :doc:
|
193
|
+
ips.reject do |ip|
|
194
|
+
@proxies.any? { |proxy| proxy === ip }
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
require "securerandom"
|
6
|
+
require "active_support/core_ext/string/access"
|
7
|
+
|
8
|
+
module ActionDispatch
|
9
|
+
# # Action Dispatch RequestId
|
10
|
+
#
|
11
|
+
# Makes a unique request id available to the `action_dispatch.request_id` env
|
12
|
+
# variable (which is then accessible through ActionDispatch::Request#request_id
|
13
|
+
# or the alias ActionDispatch::Request#uuid) and sends the same id to the client
|
14
|
+
# via the `X-Request-Id` header.
|
15
|
+
#
|
16
|
+
# The unique request id is either based on the `X-Request-Id` header in the
|
17
|
+
# request, which would typically be generated by a firewall, load balancer, or
|
18
|
+
# the web server, or, if this header is not available, a random uuid. If the
|
19
|
+
# header is accepted from the outside world, we sanitize it to a max of 255
|
20
|
+
# chars and alphanumeric and dashes only.
|
21
|
+
#
|
22
|
+
# The unique request id can be used to trace a request end-to-end and would
|
23
|
+
# typically end up being part of log files from multiple pieces of the stack.
|
24
|
+
class RequestId
|
25
|
+
def initialize(app, header:)
|
26
|
+
@app = app
|
27
|
+
@header = header
|
28
|
+
@env_header = "HTTP_#{header.upcase.tr("-", "_")}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def call(env)
|
32
|
+
req = ActionDispatch::Request.new env
|
33
|
+
req.request_id = make_request_id(req.get_header(@env_header))
|
34
|
+
@app.call(env).tap { |_status, headers, _body| headers[@header] = req.request_id }
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
def make_request_id(request_id)
|
39
|
+
if request_id.presence
|
40
|
+
request_id.gsub(/[^\w\-@]/, "").first(255)
|
41
|
+
else
|
42
|
+
internal_request_id
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def internal_request_id
|
47
|
+
SecureRandom.uuid
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
require "active_support/notifications"
|
6
|
+
|
7
|
+
module ActionDispatch
|
8
|
+
class ServerTiming
|
9
|
+
class Subscriber # :nodoc:
|
10
|
+
include Singleton
|
11
|
+
KEY = :action_dispatch_server_timing_events
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@mutex = Mutex.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(event)
|
18
|
+
if events = ActiveSupport::IsolatedExecutionState[KEY]
|
19
|
+
events << event
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def collect_events
|
24
|
+
events = []
|
25
|
+
ActiveSupport::IsolatedExecutionState[KEY] = events
|
26
|
+
yield
|
27
|
+
events
|
28
|
+
ensure
|
29
|
+
ActiveSupport::IsolatedExecutionState.delete(KEY)
|
30
|
+
end
|
31
|
+
|
32
|
+
def ensure_subscribed
|
33
|
+
@mutex.synchronize do
|
34
|
+
# Subscribe to all events, except those beginning with "!" Ideally we would be
|
35
|
+
# more selective of what is being measured
|
36
|
+
@subscriber ||= ActiveSupport::Notifications.subscribe(/\A[^!]/, self)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def unsubscribe
|
41
|
+
@mutex.synchronize do
|
42
|
+
ActiveSupport::Notifications.unsubscribe @subscriber
|
43
|
+
@subscriber = nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.unsubscribe # :nodoc:
|
49
|
+
Subscriber.instance.unsubscribe
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize(app)
|
53
|
+
@app = app
|
54
|
+
@subscriber = Subscriber.instance
|
55
|
+
@subscriber.ensure_subscribed
|
56
|
+
end
|
57
|
+
|
58
|
+
def call(env)
|
59
|
+
response = nil
|
60
|
+
events = @subscriber.collect_events do
|
61
|
+
response = @app.call(env)
|
62
|
+
end
|
63
|
+
|
64
|
+
headers = response[1]
|
65
|
+
|
66
|
+
header_info = events.group_by(&:name).map do |event_name, events_collection|
|
67
|
+
"%s;dur=%.2f" % [event_name, events_collection.sum(&:duration)]
|
68
|
+
end
|
69
|
+
|
70
|
+
if headers[ActionDispatch::Constants::SERVER_TIMING].present?
|
71
|
+
header_info.prepend(headers[ActionDispatch::Constants::SERVER_TIMING])
|
72
|
+
end
|
73
|
+
headers[ActionDispatch::Constants::SERVER_TIMING] = header_info.join(", ")
|
74
|
+
|
75
|
+
response
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
require "rack/utils"
|
6
|
+
require "rack/request"
|
7
|
+
require "rack/session/abstract/id"
|
8
|
+
require "action_dispatch/middleware/cookies"
|
9
|
+
require "action_dispatch/request/session"
|
10
|
+
|
11
|
+
module ActionDispatch
|
12
|
+
module Session
|
13
|
+
class SessionRestoreError < StandardError # :nodoc:
|
14
|
+
def initialize
|
15
|
+
super("Session contains objects whose class definition isn't available.\n" \
|
16
|
+
"Remember to require the classes for all objects kept in the session.\n" \
|
17
|
+
"(Original exception: #{$!.message} [#{$!.class}])\n")
|
18
|
+
set_backtrace $!.backtrace
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module Compatibility
|
23
|
+
def initialize(app, options = {})
|
24
|
+
options[:key] ||= "_session_id"
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
def generate_sid
|
29
|
+
sid = SecureRandom.hex(16)
|
30
|
+
sid.encode!(Encoding::UTF_8)
|
31
|
+
sid
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def initialize_sid # :doc:
|
36
|
+
@default_options.delete(:sidbits)
|
37
|
+
@default_options.delete(:secure_random)
|
38
|
+
end
|
39
|
+
|
40
|
+
def make_request(env)
|
41
|
+
ActionDispatch::Request.new env
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module StaleSessionCheck
|
46
|
+
def load_session(env)
|
47
|
+
stale_session_check! { super }
|
48
|
+
end
|
49
|
+
|
50
|
+
def extract_session_id(env)
|
51
|
+
stale_session_check! { super }
|
52
|
+
end
|
53
|
+
|
54
|
+
def stale_session_check!
|
55
|
+
yield
|
56
|
+
rescue ArgumentError => argument_error
|
57
|
+
if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
|
58
|
+
begin
|
59
|
+
# Note that the regexp does not allow $1 to end with a ':'.
|
60
|
+
$1.constantize
|
61
|
+
rescue LoadError, NameError
|
62
|
+
raise ActionDispatch::Session::SessionRestoreError
|
63
|
+
end
|
64
|
+
retry
|
65
|
+
else
|
66
|
+
raise
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
module SessionObject # :nodoc:
|
72
|
+
def commit_session(req, res)
|
73
|
+
req.commit_csrf_token
|
74
|
+
super(req, res)
|
75
|
+
end
|
76
|
+
|
77
|
+
def prepare_session(req)
|
78
|
+
Request::Session.create(self, req, @default_options)
|
79
|
+
end
|
80
|
+
|
81
|
+
def loaded_session?(session)
|
82
|
+
!session.is_a?(Request::Session) || session.loaded?
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class AbstractStore < Rack::Session::Abstract::Persisted
|
87
|
+
include Compatibility
|
88
|
+
include StaleSessionCheck
|
89
|
+
include SessionObject
|
90
|
+
|
91
|
+
private
|
92
|
+
def set_cookie(request, response, cookie)
|
93
|
+
request.cookie_jar[key] = cookie
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class AbstractSecureStore < Rack::Session::Abstract::PersistedSecure
|
98
|
+
include Compatibility
|
99
|
+
include StaleSessionCheck
|
100
|
+
include SessionObject
|
101
|
+
|
102
|
+
def generate_sid
|
103
|
+
Rack::Session::SessionId.new(super)
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
def set_cookie(request, response, cookie)
|
108
|
+
request.cookie_jar[key] = cookie
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|