Repository navigation
Strip te, trailer and proxy-connection hop-by-hop headers in Falcon::Middleware::Proxy. - #370
Open
samuel-williams-shopify wants to merge 6 commits into
Open
samuel-williams-shopify wants to merge 6 commits into
samuel-williams-shopify wants to merge 6 commits into
Conversation
…alcon::Middleware::Proxy`.
…:Middleware::Proxy`.
…tes the request authority.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Falcon::Middleware::Proxynow stripste,trailerandproxy-connectionfrom requests before forwarding them upstream, while preservingte: trailers, and removes the stalehostheader when it rewrites the request authority.Problem
HOP_HEADERSlistedconnection,keep-alive,public,proxy-authenticate,proxy-authorization,transfer-encodingandupgrade, but omitted three connection-specific fields that intermediaries must remove:tedescribes transfer codings the immediate peer will accept. Forwarding it to an HTTP/2 upstream (the default forfalcon virtual, sinceEnvironment::Application#protocolis HTTP/2) produces a malformed request per RFC 9113 §8.2.2 unless the value is exactlytrailers.async-httptolerates this today, but a stricter upstream would reset the stream.traileris meaningless oncetransfer-encodinghas been stripped and the body is de-chunked for the upstream connection (RFC 9112 §7.1.3).proxy-connectionis named in RFC 9110 §7.6.1 and RFC 9112 Appendix C.2.2 as a field intermediaries must remove; it is widely emitted by clients.Separately, HTTP/2 ingress may carry a
hostfield alongside:authority.prepare_requestrewroterequest.authoritybut lefthostuntouched, so an HTTP/2 upstream would see:authorityandhostdisagree (RFC 9113 §8.3.1), and an HTTP/1.1 upstream could receive twohostlines (one written fromrequest.authority, one forwarded).Solution
Add the three field names to
HOP_HEADERS(keeping the list alphabetical) and correct the comment's RFC attribution: RFC 9110 §7.6.1 for the connection-specific fields, RFC 9112 §7.1.3 fortrailer, the legacy RFC 2616 §13.5.1 list for the proxy-authentication fields, and RFC 2068 §14.35 forpublic(a response-only relic, left in place).prepare_headerscapturestebefore any extraction, strips everything inHOP_HEADERS, then re-addste: trailersif the client advertisedtrailers. RFC 9113 §8.2.2 permits exactly this value over HTTP/2, and gRPC's PROTOCOL-HTTP2 makes it mandatory ("used to detect incompatible proxies"); Envoy normalises tote: trailersand nginxgrpc_passinjects it, and curl reverted an unconditional strip for the same reason (curl/curl#17122). This is truthful because the proxy forwards request trailers end-to-end: the ingressHeadersobject is shared with the upstream request, and the async-http client callstrailer!on it. The value is captured before extracting fields named byconnection, because a compliant HTTP/1.1 client sendste: trailerswithconnection: te; reading it afterwards would drop exactly the well-formed case.One caveat: over HTTP/1.1, RFC 9110 §10.1.4 says a sender of
teMUST also list it inconnection. The proxy cannot know the upstream protocol inprepare_headers, and the async-http client writes its ownconnectionheader, so the forwardedte: trailersis not accompanied byconnection: te. Envoy and nginx accept the same trade-off, and in practice it only matters for HTTP/1.1 upstreams.prepare_requestnow deleteshostafter rewriting the authority. HTTP/1.1 ingress already stripshost(protocol-http1 removes it when reading the request line), so this only affects HTTP/2 ingress, and both upstream protocols derive their own authority fromrequest.authority.Tests in
test/falcon/middleware/proxy.rbassert thattrailer,proxy-connectionandupgradeare removed whileacceptsurvives; thatte: gzipis removed,te: trailersis kept,te: gzip, trailersis forwarded astrailers, andte: trailerssurvives whenteis also listed inconnection; that fields named by a mixed-caseConnection: TE, Keep-Aliveare extracted case-insensitively; and thathostis removed when the authority is rewritten. The existingconnection: x-custom-hoptest passed onmainand pins that behaviour rather than covering a new path.Not addressed here
callreturns the upstream response verbatim, so an HTTP/1.1 upstream'sconnection,keep-aliveorproxy-connectionreach an HTTP/2 downstream as malformed headers (RFC 9113 §8.2.2).Protocol::Rack::Response::HOP_HEADERSis the sibling list for that direction. This is worth a follow-up PR.expectwas deliberately not added: it is end-to-end, and the upstream must see it to send100 Continue.http2-settingswas deliberately not added: it must be listed inconnection(RFC 7540 §3.2.1), so it is already removed by theconnection-driven extraction when sent correctly.