Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions lib/falcon/middleware/proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ class Proxy < Protocol::HTTP::Middleware
FORWARDED = "forwarded"
X_FORWARDED_FOR = "x-forwarded-for"
X_FORWARDED_PROTO = "x-forwarded-proto"
X_FORWARDED_HOST = "x-forwarded-host"
X_FORWARDED_PORT = "x-forwarded-port"
X_FORWARDED_SCHEME = "x-forwarded-scheme"
X_FORWARDED_SSL = "x-forwarded-ssl"
X_REAL_IP = "x-real-ip"
CLIENT_IP = "client-ip"

VIA = "via"
CONNECTION = "connection"
Expand All @@ -48,6 +54,19 @@ class Proxy < Protocol::HTTP::Middleware
"upgrade",
]

# Headers which disclose client/proxy provenance across a trust boundary.
FORWARDED_HEADERS = [
FORWARDED,
X_FORWARDED_FOR,
X_FORWARDED_PROTO,
X_FORWARDED_HOST,
X_FORWARDED_PORT,
X_FORWARDED_SCHEME,
X_FORWARDED_SSL,
X_REAL_IP,
CLIENT_IP,
]

# Initialize the proxy middleware.
# @parameter app [Protocol::HTTP::Middleware] The middleware to use if a request can't be proxied.
# @parameter hosts [Hash(String, Service::Proxy)] The host applications to proxy to.
Expand Down Expand Up @@ -99,6 +118,12 @@ def prepare_headers(headers)
headers.extract(HOP_HEADERS)
end

# Prepare forwarded headers before adding the proxy's own provenance.
# Override this method to preserve or transform trusted upstream headers.
def prepare_forwarded_headers(headers)
headers.extract(FORWARDED_HEADERS)
end

# Prepare the request to be proxied to the specified host.
# In particular, we set appropriate {VIA}, {FORWARDED}, {X_FORWARDED_FOR} and {X_FORWARDED_PROTO} headers.
def prepare_request(request, host)
Expand All @@ -114,6 +139,8 @@ def prepare_request(request, host)
# The authority of the request must match the authority of the endpoint we are proxying to, otherwise SNI and other things won't work correctly.
request.authority = host.authority

self.prepare_forwarded_headers(request.headers)

if address = request.remote_address
request.headers.add(X_FORWARDED_FOR, address.ip_address)
forwarded << "for=#{address.ip_address}"
Expand Down
53 changes: 53 additions & 0 deletions test/falcon/middleware/proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,57 @@ def proxy_for(**options)
message: be(:include?, "Request authority: www.google.com"),
)
end

it "strips forwarded headers before preparing requests" do
headers = Protocol::HTTP::Headers[
"forwarded" => "for=203.0.113.1;proto=http",
"x-forwarded-for" => "203.0.113.1",
"x-forwarded-proto" => "http",
"x-forwarded-host" => "example.com",
"x-forwarded-port" => "80",
"x-forwarded-scheme" => "http",
"x-forwarded-ssl" => "off",
"x-real-ip" => "203.0.113.1",
"client-ip" => "203.0.113.1",
]
request = Protocol::HTTP::Request.new("https", "www.google.com", "GET", "/", "HTTP/1.1", headers, nil)
host = proxy_for(authority: "www.google.com", endpoint: Async::HTTP::Endpoint.parse("https://www.google.com"))

expect(request).to receive(:remote_address).and_return(Addrinfo.ip("127.0.0.1"))

proxy.prepare_request(request, host)

expect(request.headers["forwarded"]).to be == ["for=127.0.0.1;proto=https"]
expect(request.headers["x-forwarded-for"]).to be == ["127.0.0.1"]
expect(request.headers["x-forwarded-proto"]).to be == ["https"]
expect(request.headers["x-forwarded-host"]).to be_nil
expect(request.headers["x-forwarded-port"]).to be_nil
expect(request.headers["x-forwarded-scheme"]).to be_nil
expect(request.headers["x-forwarded-ssl"]).to be_nil
expect(request.headers["x-real-ip"]).to be_nil
expect(request.headers["client-ip"]).to be_nil
end

it "allows forwarded header preparation to be overridden" do
klass = Class.new(subject) do
def prepare_forwarded_headers(headers)
end
end
proxy = klass.new(Falcon::Middleware::BadRequest, {})
headers = Protocol::HTTP::Headers[
"forwarded" => "for=203.0.113.1;proto=http",
"x-forwarded-for" => "203.0.113.1",
"x-forwarded-proto" => "http",
]
request = Protocol::HTTP::Request.new("https", "www.google.com", "GET", "/", "HTTP/1.1", headers, nil)
host = proxy_for(authority: "www.google.com", endpoint: Async::HTTP::Endpoint.parse("https://www.google.com"))

expect(request).to receive(:remote_address).and_return(Addrinfo.ip("127.0.0.1"))

proxy.prepare_request(request, host)

expect(request.headers["forwarded"]).to be == ["for=203.0.113.1;proto=http", "for=127.0.0.1;proto=https"]
expect(request.headers["x-forwarded-for"]).to be == ["203.0.113.1", "127.0.0.1"]
expect(request.headers["x-forwarded-proto"]).to be == ["http", "https"]
end
end
Loading