From f8861ca16bd475e8519e7dbf5a2b55e81b329874 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 28 Jun 2024 12:15:41 -0600 Subject: [PATCH 01/15] reverseproxy: Wire up TLS options for H3 transport --- modules/caddyhttp/reverseproxy/httptransport.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index 80a498066..d42453684 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -363,6 +363,13 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e // site owners control the backends), so it must be exclusive if len(h.Versions) == 1 && h.Versions[0] == "3" { h.h3Transport = new(http3.RoundTripper) + if h.TLS != nil { + var err error + h.h3Transport.TLSClientConfig, err = h.TLS.MakeTLSClientConfig(caddyCtx) + if err != nil { + return nil, fmt.Errorf("making TLS client config for HTTP/3 transport: %v", err) + } + } } else if len(h.Versions) > 1 && sliceContains(h.Versions, "3") { return nil, fmt.Errorf("if HTTP/3 is enabled to the upstream, no other HTTP versions are supported") } From 0287009ee5fbe171e7a84f7d5b965992bb5488a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 3 Jul 2024 16:43:13 +0200 Subject: [PATCH 02/15] intercept: fix http.intercept.header.* placeholder (#6429) --- caddytest/integration/intercept_test.go | 8 +++++++- modules/caddyhttp/intercept/intercept.go | 3 +-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/caddytest/integration/intercept_test.go b/caddytest/integration/intercept_test.go index 81db6a7d6..6f8ffc929 100644 --- a/caddytest/integration/intercept_test.go +++ b/caddytest/integration/intercept_test.go @@ -18,17 +18,23 @@ func TestIntercept(t *testing.T) { localhost:9080 { respond /intercept "I'm a teapot" 408 + header /intercept To-Intercept ok respond /no-intercept "I'm not a teapot" intercept { @teapot status 408 handle_response @teapot { + header /intercept intercepted {resp.header.To-Intercept} respond /intercept "I'm a combined coffee/tea pot that is temporarily out of coffee" 503 } } } `, "caddyfile") - tester.AssertGetResponse("http://localhost:9080/intercept", 503, "I'm a combined coffee/tea pot that is temporarily out of coffee") + r, _ := tester.AssertGetResponse("http://localhost:9080/intercept", 503, "I'm a combined coffee/tea pot that is temporarily out of coffee") + if r.Header.Get("intercepted") != "ok" { + t.Fatalf(`header "intercepted" value is not "ok": %s`, r.Header.Get("intercepted")) + } + tester.AssertGetResponse("http://localhost:9080/no-intercept", 200, "I'm not a teapot") } diff --git a/modules/caddyhttp/intercept/intercept.go b/modules/caddyhttp/intercept/intercept.go index 47d7511f7..720a09333 100644 --- a/modules/caddyhttp/intercept/intercept.go +++ b/modules/caddyhttp/intercept/intercept.go @@ -50,7 +50,6 @@ type Intercept struct { // // Three new placeholders are available in this handler chain: // - `{http.intercept.status_code}` The status code from the response - // - `{http.intercept.status_text}` The status text from the response // - `{http.intercept.header.*}` The headers from the response HandleResponse []caddyhttp.ResponseHandler `json:"handle_response,omitempty"` @@ -161,7 +160,7 @@ func (ir Intercept) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy // set up the replacer so that parts of the original response can be // used for routing decisions - for field, value := range r.Header { + for field, value := range rec.Header() { repl.Set("http.intercept.header."+field, strings.Join(value, ",")) } repl.Set("http.intercept.status_code", rec.Status()) From f350e001b6319dd8833fbdb31ffb0ccadb2aa2e0 Mon Sep 17 00:00:00 2001 From: klaxa Date: Wed, 3 Jul 2024 21:05:52 +0200 Subject: [PATCH 03/15] reverseproxy: Only log host is up status on change (fixes #6415) (#6419) --- modules/caddyhttp/reverseproxy/healthchecks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go index 90db9b340..888dadb79 100644 --- a/modules/caddyhttp/reverseproxy/healthchecks.go +++ b/modules/caddyhttp/reverseproxy/healthchecks.go @@ -426,6 +426,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre } if upstream.Host.activeHealthPasses() >= h.HealthChecks.Active.Passes { if upstream.setHealthy(true) { + h.HealthChecks.Active.logger.Info("host is up", zap.String("host", hostAddr)) h.events.Emit(h.ctx, "healthy", map[string]any{"host": hostAddr}) upstream.Host.resetHealth() } @@ -492,7 +493,6 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre } // passed health check parameters, so mark as healthy - h.HealthChecks.Active.logger.Info("host is up", zap.String("host", hostAddr)) markHealthy() return nil From 15d986e1c9decae4d753d7cbec41275264697b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Thu, 4 Jul 2024 22:57:13 +0200 Subject: [PATCH 04/15] encode: Don't compress already-compressed fonts (#6432) * fix: don't compress already compressed fonts * fix: remove WOFF --- modules/caddyhttp/encode/encode.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go index 908e37b35..cf3d17b69 100644 --- a/modules/caddyhttp/encode/encode.go +++ b/modules/caddyhttp/encode/encode.go @@ -112,7 +112,8 @@ func (enc *Encode) Provision(ctx caddy.Context) error { "application/x-ttf*", "application/xhtml+xml*", "application/xml*", - "font/*", + "font/ttf*", + "font/otf*", "image/svg+xml*", "image/vnd.microsoft.icon*", "image/x-icon*", From c3fb5f4d3fb3eed9136f766cb88f2d8ac54de685 Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Fri, 5 Jul 2024 10:46:20 -0600 Subject: [PATCH 05/15] caddyhttp: Reject 0-RTT early data in IP matchers and set Early-Data header when proxying (#6427) * caddyhttp: Reject 0-RTT early data in IP matchers and set Early-Data header when proxying See RFC 8470: https://httpwg.org/specs/rfc8470.html Thanks to Michael Wedl (@MWedl) at the University of Applied Sciences St. Poelten for reporting this. * Don't return value for {remote} placeholder in early data * Add Caddyfile support --- listeners.go | 6 -- modules/caddyhttp/ip_matchers.go | 6 ++ modules/caddyhttp/matchers.go | 64 +++++++++++++++++++ modules/caddyhttp/replacer.go | 8 +++ .../caddyhttp/reverseproxy/reverseproxy.go | 12 ++++ 5 files changed, 90 insertions(+), 6 deletions(-) diff --git a/listeners.go b/listeners.go index bb0e9b69c..fa5ac1f56 100644 --- a/listeners.go +++ b/listeners.go @@ -60,8 +60,6 @@ type NetworkAddress struct { // ListenAll calls Listen() for all addresses represented by this struct, i.e. all ports in the range. // (If the address doesn't use ports or has 1 port only, then only 1 listener will be created.) // It returns an error if any listener failed to bind, and closes any listeners opened up to that point. -// -// TODO: Experimental API: subject to change or removal. func (na NetworkAddress) ListenAll(ctx context.Context, config net.ListenConfig) ([]any, error) { var listeners []any var err error @@ -130,8 +128,6 @@ func (na NetworkAddress) ListenAll(ctx context.Context, config net.ListenConfig) // Unix sockets will be unlinked before being created, to ensure we can bind to // it even if the previous program using it exited uncleanly; it will also be // unlinked upon a graceful exit (or when a new config does not use that socket). -// -// TODO: Experimental API: subject to change or removal. func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) { if na.IsUnixNetwork() { unixSocketsMu.Lock() @@ -221,8 +217,6 @@ func (na NetworkAddress) JoinHostPort(offset uint) string { } // Expand returns one NetworkAddress for each port in the port range. -// -// This is EXPERIMENTAL and subject to change or removal. func (na NetworkAddress) Expand() []NetworkAddress { size := na.PortRangeSize() addrs := make([]NetworkAddress, size) diff --git a/modules/caddyhttp/ip_matchers.go b/modules/caddyhttp/ip_matchers.go index baa7c51ce..9101a0357 100644 --- a/modules/caddyhttp/ip_matchers.go +++ b/modules/caddyhttp/ip_matchers.go @@ -143,6 +143,9 @@ func (m *MatchRemoteIP) Provision(ctx caddy.Context) error { // Match returns true if r matches m. func (m MatchRemoteIP) Match(r *http.Request) bool { + if r.TLS != nil && !r.TLS.HandshakeComplete { + return false // if handshake is not finished, we infer 0-RTT that has not verified remote IP; could be spoofed + } address := r.RemoteAddr clientIP, zoneID, err := parseIPZoneFromString(address) if err != nil { @@ -228,6 +231,9 @@ func (m *MatchClientIP) Provision(ctx caddy.Context) error { // Match returns true if r matches m. func (m MatchClientIP) Match(r *http.Request) bool { + if r.TLS != nil && !r.TLS.HandshakeComplete { + return false // if handshake is not finished, we infer 0-RTT that has not verified remote IP; could be spoofed + } address := GetVar(r.Context(), ClientIPVarKey).(string) clientIP, zoneID, err := parseIPZoneFromString(address) if err != nil { diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index 392312b6c..b7952ab69 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -178,6 +178,22 @@ type ( // "http/2", "http/3", or minimum versions: "http/2+", etc. MatchProtocol string + // MatchTLS matches HTTP requests based on the underlying + // TLS connection state. If this matcher is specified but + // the request did not come over TLS, it will never match. + // If this matcher is specified but is empty and the request + // did come in over TLS, it will always match. + MatchTLS struct { + // Matches if the TLS handshake has completed. QUIC 0-RTT early + // data may arrive before the handshake completes. Generally, it + // is unsafe to replay these requests if they are not idempotent; + // additionally, the remote IP of early data packets can more + // easily be spoofed. It is conventional to respond with HTTP 425 + // Too Early if the request cannot risk being processed in this + // state. + HandshakeComplete *bool `json:"handshake_complete,omitempty"` + } + // MatchNot matches requests by negating the results of its matcher // sets. A single "not" matcher takes one or more matcher sets. Each // matcher set is OR'ed; in other words, if any matcher set returns @@ -213,6 +229,7 @@ func init() { caddy.RegisterModule(MatchHeader{}) caddy.RegisterModule(MatchHeaderRE{}) caddy.RegisterModule(new(MatchProtocol)) + caddy.RegisterModule(MatchTLS{}) caddy.RegisterModule(MatchNot{}) } @@ -1236,6 +1253,53 @@ func (MatchProtocol) CELLibrary(_ caddy.Context) (cel.Library, error) { ) } +// CaddyModule returns the Caddy module information. +func (MatchTLS) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "http.matchers.tls", + New: func() caddy.Module { return new(MatchTLS) }, + } +} + +// Match returns true if r matches m. +func (m MatchTLS) Match(r *http.Request) bool { + if r.TLS == nil { + return false + } + if m.HandshakeComplete != nil { + if (!*m.HandshakeComplete && r.TLS.HandshakeComplete) || + (*m.HandshakeComplete && !r.TLS.HandshakeComplete) { + return false + } + } + return true +} + +// UnmarshalCaddyfile parses Caddyfile tokens for this matcher. Syntax: +// +// ... tls [early_data] +// +// EXPERIMENTAL SYNTAX: Subject to change. +func (m *MatchTLS) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + // iterate to merge multiple matchers into one + for d.Next() { + if d.NextArg() { + switch d.Val() { + case "early_data": + var false bool + m.HandshakeComplete = &false + } + } + if d.NextArg() { + return d.ArgErr() + } + if d.NextBlock(0) { + return d.Err("malformed tls matcher: blocks are not supported yet") + } + } + return nil +} + // CaddyModule returns the Caddy module information. func (MatchNot) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go index 1cf3ec474..2c0f32357 100644 --- a/modules/caddyhttp/replacer.go +++ b/modules/caddyhttp/replacer.go @@ -142,8 +142,16 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo } return port, true case "http.request.remote": + if req.TLS != nil && !req.TLS.HandshakeComplete { + // without a complete handshake (QUIC "early data") we can't trust the remote IP address to not be spoofed + return nil, true + } return req.RemoteAddr, true case "http.request.remote.host": + if req.TLS != nil && !req.TLS.HandshakeComplete { + // without a complete handshake (QUIC "early data") we can't trust the remote IP address to not be spoofed + return nil, true + } host, _, err := net.SplitHostPort(req.RemoteAddr) if err != nil { // req.RemoteAddr is host:port for tcp and udp sockets and /unix/socket.path diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 1a559e5dd..4f97edead 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -605,6 +605,18 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http. req.Header.Set("User-Agent", "") } + // Indicate if request has been conveyed in early data. + // RFC 8470: "An intermediary that forwards a request prior to the + // completion of the TLS handshake with its client MUST send it with + // the Early-Data header field set to “1” (i.e., it adds it if not + // present in the request). An intermediary MUST use the Early-Data + // header field if the request might have been subject to a replay and + // might already have been forwarded by it or another instance + // (see Section 6.2)." + if req.TLS != nil && !req.TLS.HandshakeComplete { + req.Header.Set("Early-Data", "1") + } + reqUpType := upgradeType(req.Header) removeConnectionHeaders(req.Header) From 7142d7c1e43ba2dad8e0118aa29d77dc74b44dda Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Sat, 6 Jul 2024 12:43:19 -0400 Subject: [PATCH 06/15] reverseproxy: Add placeholder for host in active health check headers (#6440) --- modules/caddyhttp/reverseproxy/healthchecks.go | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go index 888dadb79..ac92604ca 100644 --- a/modules/caddyhttp/reverseproxy/healthchecks.go +++ b/modules/caddyhttp/reverseproxy/healthchecks.go @@ -386,6 +386,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre // set headers, using a replacer with only globals (env vars, system info, etc.) repl := caddy.NewReplacer() + repl.Set("http.reverse_proxy.active.target_host", hostAddr) for key, vals := range h.HealthChecks.Active.Headers { key = repl.ReplaceAll(key, "") if key == "Host" { From 4ef360745dab1023a7d4c04aebca3d05499dd5e1 Mon Sep 17 00:00:00 2001 From: Steffen Busch <37350514+steffenbusch@users.noreply.github.com> Date: Sat, 6 Jul 2024 18:46:08 +0200 Subject: [PATCH 07/15] browse: add Content-Security-Policy w/ nonce (#6425) * browse: add Content-Security-Policy w/ nonce * Add backward-compat values to script-src * Remove dummy "#" href from layout anchors --- modules/caddyhttp/fileserver/browse.html | 73 ++++++++++++++++-------- 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/modules/caddyhttp/fileserver/browse.html b/modules/caddyhttp/fileserver/browse.html index 7b0df1e5f..43d5f4514 100644 --- a/modules/caddyhttp/fileserver/browse.html +++ b/modules/caddyhttp/fileserver/browse.html @@ -1,10 +1,17 @@ +{{ $nonce := uuidv4 -}} +{{ $nonceAttribute := print "nonce=" (quote $nonce) -}} +{{ $csp := printf "default-src 'none'; img-src 'self'; object-src 'none'; base-uri 'none'; script-src 'strict-dynamic' 'nonce-%s' 'unsafe-inline' https: http:; style-src 'strict-dynamic' 'nonce-%s'; frame-ancestors 'self'; form-action 'self'; block-all-mixed-content;" $nonce $nonce -}} +{{/* To disable the Content-Security-Policy, set this to false */}}{{ $enableCsp := true -}} +{{ if $enableCsp -}} + {{- .RespHeader.Set "Content-Security-Policy" $csp -}} +{{ end -}} {{- define "icon"}} {{- if .IsDir}} {{- if .IsSymlink}} - + {{- else}} @@ -303,7 +310,7 @@ - {{- if eq .Layout "grid"}} - + {{- end}} - +
@@ -799,7 +810,7 @@ footer { {{- end}}
- + @@ -807,7 +818,7 @@ footer { List - + @@ -886,7 +897,7 @@ footer { - + @@ -1000,70 +1011,70 @@ footer { -