Skip to content
Merged
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
9 changes: 9 additions & 0 deletions docs/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,15 @@ are sent can instead appear as a connection error. Declared trailers are
forwarded only after the body completes successfully. This does not add HTTP
Upgrade support or change CONNECT tunnels, and is not included in v1.0.1.

Source builds preserve declared safe trailers on streamed HTTP forward-proxy
uploads, including integrity metadata finalized only at body EOF. Uploads are
not buffered; trailer values are copied before the outbound transport writes
them. Forbidden trailer fields, proxy credentials and initially
connection-nominated hop fields are removed. A nomination received only at EOF
removes its value but cannot retract a declaration already streamed upstream.
Ordinary origin `Authorization` headers retain their existing behavior. This
is not included in v1.0.1.

The relay's separate `--destination-write-timeout` defaults to `5m`; `0`
also selects `5m`, rather than disabling the limit. It bounds the completion
of each TCP destination write, split into chunks of at most 32 KiB. Each
Expand Down
1 change: 1 addition & 0 deletions internal/proxy/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ func (s *HTTPServer) serveForward(w http.ResponseWriter, r *http.Request) {
}

out := r.Clone(r.Context())
prepareHTTPForwardRequestTrailers(r, out)
out.RequestURI = ""
// For absolute-form requests the URI authority is authoritative. A proxy
// must not let a conflicting inbound Host field select a different virtual
Expand Down
115 changes: 115 additions & 0 deletions internal/proxy/http_request_trailer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package proxy

import (
"io"
"net/http"
"strings"
"sync"

"golang.org/x/net/http/httpguts"
)

// Clone owns Trailer but shares Body. The server body fills the original map
// only at EOF; publish its safe declared values before the transport sees EOF.
func prepareHTTPForwardRequestTrailers(in, out *http.Request) {
blocked := httpForwardTrailerNominations(in.Header, in.Trailer)
destination := make(http.Header, len(out.Trailer))
declared := make(map[string]string, len(out.Trailer))
for field, values := range out.Trailer {
if !httpForwardTrailerAllowed(field, blocked) {
continue
}
key := http.CanonicalHeaderKey(field)
declared[strings.ToLower(field)] = key
destination[key] = append(destination[key], values...)
}
out.Trailer = destination
if len(declared) == 0 || out.Body == nil || out.Body == http.NoBody {
return
}
out.Body = &httpForwardTrailerBody{
body: out.Body, source: in, destination: destination,
declared: declared, blocked: blocked,
}
// Known-length embedder requests can have trailers too. Keep the incoming
// body's length validation but use H1 framing which actually emits trailers.
out.ContentLength = -1
out.TransferEncoding = []string{"chunked"}
for field := range out.Header {
if strings.EqualFold(field, "Content-Length") {
delete(out.Header, field)
}
}
// A replay getter would bypass the EOF bridge. Do not modify the caller's.
out.GetBody = nil
}

func httpForwardTrailerNominations(headers ...http.Header) map[string]bool {
blocked := make(map[string]bool)
for _, header := range headers {
for field, values := range header {
if !strings.EqualFold(field, "Connection") {
continue
}
for _, value := range values {
for _, token := range strings.Split(value, ",") {
token = strings.TrimSpace(token)
if httpguts.ValidHeaderFieldName(token) {
blocked[strings.ToLower(token)] = true
}
}
}
}
}
return blocked
}

func httpForwardTrailerAllowed(field string, blocked map[string]bool) bool {
return httpguts.ValidHeaderFieldName(field) && httpguts.ValidTrailerHeader(field) &&
!isHopByHopHeader(field) && !strings.EqualFold(field, "Proxy-Authentication-Info") &&
!blocked[strings.ToLower(field)]
}

// One body reader may run concurrently with Close, as net/http requires.
// Close delegates without taking an I/O lock so it can unblock that reader.
// Only successful EOF publishes metadata; neither errors nor Close do so.
type httpForwardTrailerBody struct {
body io.ReadCloser
source *http.Request
destination http.Header
declared map[string]string
blocked map[string]bool
once sync.Once
}

func (b *httpForwardTrailerBody) Read(p []byte) (int, error) {
n, err := b.body.Read(p)
if err == io.EOF {
b.once.Do(func() {
current := b.source.Trailer // Custom bodies may replace the map at EOF.
for field := range httpForwardTrailerNominations(current) {
b.blocked[field] = true
}
// Keep the map identity already captured by the transport.
clear(b.destination)
for _, key := range b.declared {
if httpForwardTrailerAllowed(key, b.blocked) {
b.destination[key] = nil
}
}
// One ASCII alias contributes values once to its logical declaration.
// Copy slices so the outbound metadata never aliases caller storage.
for field, values := range current {
if !httpForwardTrailerAllowed(field, b.blocked) {
continue
}
if key, ok := b.declared[strings.ToLower(field)]; ok {
b.destination[key] = append(b.destination[key], values...)
}
}
})
}
return n, err
}

func (b *httpForwardTrailerBody) Close() error { return b.body.Close() }
Loading
Loading