New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support RFC 7239: HTTP Forwarded header #1017
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,9 +41,10 @@ def call(env) | |
def log(env, status, header, began_at) | ||
now = Time.now | ||
length = extract_content_length(header) | ||
forwarded_for = Utils::forwarded_values(env['HTTP_FORWARDED'])[:for].last | ||
|
||
msg = FORMAT % [ | ||
env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-", | ||
forwarded_for || env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems this is kinda-implementing |
||
env["REMOTE_USER"] || "-", | ||
now.strftime("%d/%b/%Y:%H:%M:%S %z"), | ||
env[REQUEST_METHOD], | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -134,6 +134,7 @@ module Helpers | |
# to include the port in a generated URI. | ||
DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 } | ||
|
||
HTTP_FORWARDED = 'HTTP_FORWARDED'.freeze | ||
HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME'.freeze | ||
HTTP_X_FORWARDED_PROTO = 'HTTP_X_FORWARDED_PROTO'.freeze | ||
HTTP_X_FORWARDED_HOST = 'HTTP_X_FORWARDED_HOST'.freeze | ||
|
@@ -203,6 +204,10 @@ def unlink?; request_method == UNLINK end | |
def scheme | ||
if get_header(HTTPS) == 'on' | ||
'https' | ||
elsif get_header(HTTP_FORWARDED) | ||
# Use first proto field set as it's most likely to be represent the | ||
# original client: https://tools.ietf.org/html/rfc7239#section-4 | ||
get_http_forwarder(:proto)[0] | ||
elsif get_header(HTTP_X_FORWARDED_SSL) == 'on' | ||
'https' | ||
elsif get_header(HTTP_X_FORWARDED_SCHEME) | ||
|
@@ -240,7 +245,9 @@ def xhr? | |
end | ||
|
||
def host_with_port | ||
if forwarded = get_header(HTTP_X_FORWARDED_HOST) | ||
if forwarded = get_http_forwarder(:host).last | ||
forwarded | ||
elsif forwarded = get_header(HTTP_X_FORWARDED_HOST) | ||
forwarded.split(/,\s?/).last | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Curious that we use the first forward for protocol, the last forward for host, and the first trusted forward for IP. |
||
else | ||
get_header(HTTP_HOST) || "#{get_header(SERVER_NAME) || get_header(SERVER_ADDR)}:#{get_header(SERVER_PORT)}" | ||
|
@@ -257,6 +264,8 @@ def port | |
port.to_i | ||
elsif port = get_header(HTTP_X_FORWARDED_PORT) | ||
port.to_i | ||
elsif get_http_forwarder(:proto) | ||
DEFAULT_PORTS[scheme] | ||
elsif has_header?(HTTP_X_FORWARDED_HOST) | ||
DEFAULT_PORTS[scheme] | ||
elsif has_header?(HTTP_X_FORWARDED_PROTO) | ||
|
@@ -276,7 +285,9 @@ def ip | |
|
||
return remote_addrs.first if remote_addrs.any? | ||
|
||
forwarded_ips = split_ip_addresses(get_header('HTTP_X_FORWARDED_FOR')) | ||
rfc7239_forwarded_ips = get_http_forwarder(:for) | ||
x_forwarded_ips = split_ip_addresses(get_header('HTTP_X_FORWARDED_FOR')) | ||
forwarded_ips = x_forwarded_ips.concat(rfc7239_forwarded_ips) | ||
|
||
return reject_trusted_ip_addresses(forwarded_ips).last || get_header("REMOTE_ADDR") | ||
end | ||
|
@@ -452,6 +463,11 @@ def parse_http_accept_header(header) | |
end | ||
end | ||
|
||
# Get an array of values set in the RFC 7239 `Forwarded` request header. | ||
def get_http_forwarder(token) | ||
Utils::forwarded_values(get_header(HTTP_FORWARDED))[token] | ||
end | ||
|
||
def query_parser | ||
Utils.default_query_parser | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -120,6 +120,23 @@ class RackRequestTest < Minitest::Spec | |
Rack::MockRequest.env_for("/", "SERVER_NAME" => "example.org", "SERVER_PORT" => "9292") | ||
req.host.must_equal "example.org" | ||
|
||
req = make_request \ | ||
Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_FORWARDED" => "host=example.org:9292") | ||
req.host.must_equal "example.org" | ||
|
||
# Test obfuscated identifier: https://tools.ietf.org/html/rfc7239#section-6.3 | ||
req = make_request \ | ||
Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_FORWARDED" => "host=ObFuScaTeD") | ||
req.host.must_equal "ObFuScaTeD" | ||
|
||
req = make_request \ | ||
Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_FORWARDED" => "host=example.com; host=example.org:9292") | ||
req.host.must_equal "example.org" | ||
|
||
req = make_request \ | ||
Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "example.org:9292", "HTTP_FORWARDED" => "host=example.com") | ||
req.host.must_equal "example.com" | ||
|
||
req = make_request \ | ||
Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "example.org:9292") | ||
req.host.must_equal "example.org" | ||
|
@@ -539,6 +556,18 @@ def initialize(*) | |
request.scheme.must_equal "http" | ||
request.wont_be :ssl? | ||
|
||
request = make_request(Rack::MockRequest.env_for("/", 'HTTP_FORWARDED' => 'proto=https')) | ||
request.scheme.must_equal "https" | ||
request.must_be :ssl? | ||
|
||
request = make_request(Rack::MockRequest.env_for("/", 'HTTP_FORWARDED' => 'proto=https, proto=http')) | ||
request.scheme.must_equal "https" | ||
request.must_be :ssl? | ||
|
||
request = make_request(Rack::MockRequest.env_for("/", 'HTTP_FORWARDED' => 'proto=http, proto=https')) | ||
request.scheme.must_equal "http" | ||
request.wont_be :ssl? | ||
|
||
request = make_request(Rack::MockRequest.env_for("/", 'HTTPS' => 'on')) | ||
request.scheme.must_equal "https" | ||
request.must_be :ssl? | ||
|
@@ -1186,6 +1215,21 @@ def ip_app | |
it 'deals with proxies' do | ||
mock = Rack::MockRequest.new(Rack::Lint.new(ip_app)) | ||
|
||
res = mock.get '/', | ||
'REMOTE_ADDR' => '1.2.3.4', | ||
'HTTP_FORWARDED' => 'for=3.4.5.6' | ||
res.body.must_equal '1.2.3.4' | ||
|
||
res = mock.get '/', | ||
'HTTP_X_FORWARDED_FOR' => '3.4.5.6', | ||
'HTTP_FORWARDED' => 'for=5.6.7.8' | ||
res.body.must_equal '5.6.7.8' | ||
|
||
res = mock.get '/', | ||
'HTTP_X_FORWARDED_FOR' => '3.4.5.6', | ||
'HTTP_FORWARDED' => 'for=5.6.7.8, for=7.8.9.0' | ||
res.body.must_equal '7.8.9.0' | ||
|
||
res = mock.get '/', | ||
'REMOTE_ADDR' => '1.2.3.4', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test coverage demonstrating that trusted proxy detection behaves with Forwarded, too? |
||
'HTTP_X_FORWARDED_FOR' => '3.4.5.6' | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -354,6 +354,43 @@ def initialize(*) | |
] | ||
end | ||
|
||
it "parses RFC 7239 Forwarded header" do | ||
Rack::Utils.forwarded_values('for=3.4.5.6').must_equal({ | ||
:for => [ '3.4.5.6' ], | ||
}) | ||
|
||
Rack::Utils.forwarded_values(';;;for=3.4.5.6,,').must_equal({ | ||
:for => [ '3.4.5.6' ], | ||
}) | ||
|
||
Rack::Utils.forwarded_values('for=3.4.5.6').must_equal({ | ||
:for => [ '3.4.5.6' ], | ||
}) | ||
|
||
Rack::Utils.forwarded_values('for = 3.4.5.6').must_equal({ | ||
:for => [ '3.4.5.6' ], | ||
}) | ||
|
||
Rack::Utils.forwarded_values('for="3.4.5.6"').must_equal({ | ||
:for => [ '3.4.5.6' ], | ||
}) | ||
|
||
Rack::Utils.forwarded_values('for=3.4.5.6;proto=https').must_equal({ | ||
:for => [ '3.4.5.6' ], | ||
:proto => [ 'https' ] | ||
}) | ||
|
||
Rack::Utils.forwarded_values('for=3.4.5.6; proto=http, proto=https').must_equal({ | ||
:for => [ '3.4.5.6' ], | ||
:proto => [ 'http', 'https' ] | ||
}) | ||
|
||
Rack::Utils.forwarded_values('for=3.4.5.6; proto=http, proto=https; for=1.2.3.4').must_equal({ | ||
:for => [ '3.4.5.6', '1.2.3.4' ], | ||
:proto => [ 'http', 'https' ] | ||
}) | ||
end | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like we're covering https://tools.ietf.org/html/rfc7239#section-7.1 👍 Can't do much about multiple header testing since that falls on the Rack handler to coalesce. |
||
it "select best quality match" do | ||
Rack::Utils.best_q_match("text/html", %w[text/html]).must_equal "text/html" | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This codebase uses
.
for method calls and::
for constant scope resolution.