From f8861ca16bd475e8519e7dbf5a2b55e81b329874 Mon Sep 17 00:00:00 2001
From: Matthew Holt <mholt@users.noreply.github.com>
Date: Fri, 28 Jun 2024 12:15:41 -0600
Subject: [PATCH 01/62] 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?= <kevin@dunglas.fr>
Date: Wed, 3 Jul 2024 16:43:13 +0200
Subject: [PATCH 02/62] 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 <klaxa1337@googlemail.com>
Date: Wed, 3 Jul 2024 21:05:52 +0200
Subject: [PATCH 03/62] 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?= <kevin@dunglas.fr>
Date: Thu, 4 Jul 2024 22:57:13 +0200
Subject: [PATCH 04/62] 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 <mholt@users.noreply.github.com>
Date: Fri, 5 Jul 2024 10:46:20 -0600
Subject: [PATCH 05/62] 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 <lavofr@gmail.com>
Date: Sat, 6 Jul 2024 12:43:19 -0400
Subject: [PATCH 06/62] 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/62] 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}}
 		<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-folder-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
 			<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
 			<path d="M9 3a1 1 0 0 1 .608 .206l.1 .087l2.706 2.707h6.586a3 3 0 0 1 2.995 2.824l.005 .176v8a3 3 0 0 1 -2.824 2.995l-.176 .005h-14a3 3 0 0 1 -2.995 -2.824l-.005 -.176v-11a3 3 0 0 1 2.824 -2.995l.176 -.005h4z" stroke-width="0" fill="currentColor"/>
-			<path fill="#000" d="M2.795 17.306c0-2.374 1.792-4.314 4.078-4.538v-1.104a.38.38 0 0 1 .651-.272l2.45 2.492a.132.132 0 0 1 0 .188l-2.45 2.492a.381.381 0 0 1-.651-.272V15.24c-1.889.297-3.436 1.39-3.817 3.26a2.809 2.809 0 0 1-.261-1.193Z" style="stroke-width:.127478"/>
+			<path fill="#000" d="M2.795 17.306c0-2.374 1.792-4.314 4.078-4.538v-1.104a.38.38 0 0 1 .651-.272l2.45 2.492a.132.132 0 0 1 0 .188l-2.45 2.492a.381.381 0 0 1-.651-.272V15.24c-1.889.297-3.436 1.39-3.817 3.26a2.809 2.809 0 0 1-.261-1.193Z" stroke-width=".127478"/>
 		</svg>
 		{{- else}}
 		<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-folder-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
@@ -303,7 +310,7 @@
 		<meta charset="utf-8">
 		<meta name="color-scheme" content="light dark">
 		<meta name="viewport" content="width=device-width, initial-scale=1.0">
-<style>
+<style {{ $nonceAttribute }}>
 * { padding: 0; margin: 0; box-sizing: border-box; }
 
 body {
@@ -342,6 +349,10 @@ svg,
 	text-decoration: none;
 }
 
+#layout-list, #layout-grid {
+	cursor: pointer;
+}
+
 .wrapper {
 	max-width: 1200px;
 	margin-left: auto;
@@ -768,10 +779,10 @@ footer {
 
 </style>
 {{- if eq .Layout "grid"}}
-<style>.wrapper { max-width: none; } main { margin-top: 1px; }</style>
+<style {{ $nonceAttribute }}>.wrapper { max-width: none; } main { margin-top: 1px; }</style>
 {{- end}}
 </head>
-<body onload="initPage()">
+<body>
 	<header>
 		<div class="wrapper">
 			<div class="breadcrumbs">Folder Path</div>
@@ -799,7 +810,7 @@ footer {
 						</span>
 						{{- end}}
 					</div>
-					<a href="javascript:queryParam('layout', '')" id="layout-list" class='layout{{if eq $.Layout "list" ""}}current{{end}}'>
+					<a id="layout-list" class='layout{{if eq $.Layout "list" ""}}current{{end}}'>
 						<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-layout-list" width="16" height="16" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
 							<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
 							<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"/>
@@ -807,7 +818,7 @@ footer {
 						</svg>
 						List
 					</a>
-					<a href="javascript:queryParam('layout', 'grid')" id="layout-grid" class='layout{{if eq $.Layout "grid"}}current{{end}}'>
+					<a id="layout-grid" class='layout{{if eq $.Layout "grid"}}current{{end}}'>
 						<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-layout-grid" width="16" height="16" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
 							<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
 							<path d="M4 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/>
@@ -886,7 +897,7 @@ footer {
 									<path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0"/>
 									<path d="M21 21l-6 -6"/>
 								</svg>
-								<input type="search" placeholder="Search" id="filter" onkeyup='filter()'>
+								<input type="search" placeholder="Search" id="filter">
 							</div>
 						</th>
 						<th>
@@ -1000,70 +1011,70 @@ footer {
 		<footer>
 			Served with
 			<a rel="noopener noreferrer" href="https://caddyserver.com">
-				<svg class="caddy-logo" viewBox="0 0 379 114" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;">
+				<svg class="caddy-logo" viewBox="0 0 379 114" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
 					<g transform="matrix(1,0,0,1,-1982.99,-530.985)">
 						<g transform="matrix(1.16548,0,0,1.10195,1823.12,393.466)">
 							<g transform="matrix(1,0,0,1,0.233052,1.17986)">
 								<g id="Icon" transform="matrix(0.858013,0,0,0.907485,-3224.99,-1435.83)">
 									<g>
 										<g transform="matrix(-0.191794,-0.715786,0.715786,-0.191794,4329.14,4673.64)">
-											<path d="M3901.56,610.734C3893.53,610.261 3886.06,608.1 3879.2,604.877C3872.24,601.608 3866.04,597.093 3860.8,591.633C3858.71,589.457 3856.76,587.149 3854.97,584.709C3853.2,582.281 3851.57,579.733 3850.13,577.066C3845.89,569.224 3843.21,560.381 3842.89,550.868C3842.57,543.321 3843.64,536.055 3845.94,529.307C3848.37,522.203 3852.08,515.696 3856.83,510.049L3855.79,509.095C3850.39,514.54 3846.02,520.981 3842.9,528.125C3839.84,535.125 3838.03,542.781 3837.68,550.868C3837.34,561.391 3839.51,571.425 3843.79,580.306C3845.27,583.38 3847.03,586.304 3849.01,589.049C3851.01,591.806 3853.24,594.39 3855.69,596.742C3861.75,602.568 3869,607.19 3877.03,610.1C3884.66,612.867 3892.96,614.059 3901.56,613.552L3901.56,610.734Z" style="fill:rgb(0,144,221);"/>
+											<path d="M3901.56,610.734C3893.53,610.261 3886.06,608.1 3879.2,604.877C3872.24,601.608 3866.04,597.093 3860.8,591.633C3858.71,589.457 3856.76,587.149 3854.97,584.709C3853.2,582.281 3851.57,579.733 3850.13,577.066C3845.89,569.224 3843.21,560.381 3842.89,550.868C3842.57,543.321 3843.64,536.055 3845.94,529.307C3848.37,522.203 3852.08,515.696 3856.83,510.049L3855.79,509.095C3850.39,514.54 3846.02,520.981 3842.9,528.125C3839.84,535.125 3838.03,542.781 3837.68,550.868C3837.34,561.391 3839.51,571.425 3843.79,580.306C3845.27,583.38 3847.03,586.304 3849.01,589.049C3851.01,591.806 3853.24,594.39 3855.69,596.742C3861.75,602.568 3869,607.19 3877.03,610.1C3884.66,612.867 3892.96,614.059 3901.56,613.552L3901.56,610.734Z" fill="rgb(0,144,221)"/>
 										</g>
 										<g transform="matrix(-0.191794,-0.715786,0.715786,-0.191794,4329.14,4673.64)">
-											<path d="M3875.69,496.573C3879.62,494.538 3883.8,492.897 3888.2,491.786C3892.49,490.704 3896.96,490.124 3901.56,490.032C3903.82,490.13 3906.03,490.332 3908.21,490.688C3917.13,492.147 3925.19,495.814 3932.31,500.683C3936.13,503.294 3939.59,506.335 3942.81,509.619C3947.09,513.98 3950.89,518.816 3953.85,524.232C3958.2,532.197 3960.96,541.186 3961.32,550.868C3961.61,558.748 3960.46,566.345 3957.88,573.322C3956.09,578.169 3953.7,582.753 3950.66,586.838C3947.22,591.461 3942.96,595.427 3938.27,598.769C3933.66,602.055 3928.53,604.619 3923.09,606.478C3922.37,606.721 3921.6,606.805 3920.93,607.167C3920.42,607.448 3920.14,607.854 3919.69,608.224L3920.37,610.389C3920.98,610.432 3921.47,610.573 3922.07,610.474C3922.86,610.344 3923.55,609.883 3924.28,609.566C3931.99,606.216 3938.82,601.355 3944.57,595.428C3947.02,592.903 3949.25,590.174 3951.31,587.319C3953.59,584.168 3955.66,580.853 3957.43,577.348C3961.47,569.34 3964.01,560.422 3964.36,550.868C3964.74,540.511 3962.66,530.628 3958.48,521.868C3955.57,515.775 3951.72,510.163 3946.95,505.478C3943.37,501.962 3939.26,498.99 3934.84,496.562C3926.88,492.192 3917.87,489.76 3908.37,489.229C3906.12,489.104 3903.86,489.054 3901.56,489.154C3896.87,489.06 3892.3,489.519 3887.89,490.397C3883.3,491.309 3878.89,492.683 3874.71,494.525L3875.69,496.573Z" style="fill:rgb(0,144,221);"/>
+											<path d="M3875.69,496.573C3879.62,494.538 3883.8,492.897 3888.2,491.786C3892.49,490.704 3896.96,490.124 3901.56,490.032C3903.82,490.13 3906.03,490.332 3908.21,490.688C3917.13,492.147 3925.19,495.814 3932.31,500.683C3936.13,503.294 3939.59,506.335 3942.81,509.619C3947.09,513.98 3950.89,518.816 3953.85,524.232C3958.2,532.197 3960.96,541.186 3961.32,550.868C3961.61,558.748 3960.46,566.345 3957.88,573.322C3956.09,578.169 3953.7,582.753 3950.66,586.838C3947.22,591.461 3942.96,595.427 3938.27,598.769C3933.66,602.055 3928.53,604.619 3923.09,606.478C3922.37,606.721 3921.6,606.805 3920.93,607.167C3920.42,607.448 3920.14,607.854 3919.69,608.224L3920.37,610.389C3920.98,610.432 3921.47,610.573 3922.07,610.474C3922.86,610.344 3923.55,609.883 3924.28,609.566C3931.99,606.216 3938.82,601.355 3944.57,595.428C3947.02,592.903 3949.25,590.174 3951.31,587.319C3953.59,584.168 3955.66,580.853 3957.43,577.348C3961.47,569.34 3964.01,560.422 3964.36,550.868C3964.74,540.511 3962.66,530.628 3958.48,521.868C3955.57,515.775 3951.72,510.163 3946.95,505.478C3943.37,501.962 3939.26,498.99 3934.84,496.562C3926.88,492.192 3917.87,489.76 3908.37,489.229C3906.12,489.104 3903.86,489.054 3901.56,489.154C3896.87,489.06 3892.3,489.519 3887.89,490.397C3883.3,491.309 3878.89,492.683 3874.71,494.525L3875.69,496.573Z" fill="rgb(0,144,221)"/>
 										</g>
 									</g>
 									<g>
 										<g transform="matrix(-3.37109,-0.514565,0.514565,-3.37109,4078.07,1806.88)">
-											<path d="M22,12C22,10.903 21.097,10 20,10C19.421,10 18.897,10.251 18.53,10.649C18.202,11.006 18,11.481 18,12C18,13.097 18.903,14 20,14C21.097,14 22,13.097 22,12Z" style="fill:none;fill-rule:nonzero;stroke:rgb(0,144,221);stroke-width:1.05px;"/>
+											<path d="M22,12C22,10.903 21.097,10 20,10C19.421,10 18.897,10.251 18.53,10.649C18.202,11.006 18,11.481 18,12C18,13.097 18.903,14 20,14C21.097,14 22,13.097 22,12Z" fill="none" fill-rule="nonzero" stroke="rgb(0,144,221)" stroke-width="1.05px"/>
 										</g>
 										<g transform="matrix(-5.33921,-5.26159,-3.12106,-6.96393,4073.87,1861.55)">
-											<path d="M10.315,5.333C10.315,5.333 9.748,5.921 9.03,6.673C7.768,7.995 6.054,9.805 6.054,9.805L6.237,9.86C6.237,9.86 8.045,8.077 9.36,6.771C10.107,6.028 10.689,5.444 10.689,5.444L10.315,5.333Z" style="fill:rgb(0,144,221);"/>
+											<path d="M10.315,5.333C10.315,5.333 9.748,5.921 9.03,6.673C7.768,7.995 6.054,9.805 6.054,9.805L6.237,9.86C6.237,9.86 8.045,8.077 9.36,6.771C10.107,6.028 10.689,5.444 10.689,5.444L10.315,5.333Z" fill="rgb(0,144,221)"/>
 										</g>
 									</g>
 									<g id="Padlock" transform="matrix(3.11426,0,0,3.11426,3938.31,1737.25)">
 										<g>
-											<path d="M9.876,21L18.162,21C18.625,21 19,20.625 19,20.162L19,11.838C19,11.375 18.625,11 18.162,11L5.838,11C5.375,11 5,11.375 5,11.838L5,16.758" style="fill:none;stroke:rgb(34,182,56);stroke-width:1.89px;stroke-linecap:butt;stroke-linejoin:miter;"/>
-											<path d="M8,11L8,7C8,4.806 9.806,3 12,3C14.194,3 16,4.806 16,7L16,11" style="fill:none;fill-rule:nonzero;stroke:rgb(34,182,56);stroke-width:1.89px;"/>
+											<path d="M9.876,21L18.162,21C18.625,21 19,20.625 19,20.162L19,11.838C19,11.375 18.625,11 18.162,11L5.838,11C5.375,11 5,11.375 5,11.838L5,16.758" fill="none" stroke="rgb(34,182,56)" stroke-width="1.89px" stroke-linecap="butt" stroke-linejoin="miter"/>
+											<path d="M8,11L8,7C8,4.806 9.806,3 12,3C14.194,3 16,4.806 16,7L16,11" fill="none" fill-rule="nonzero" stroke="rgb(34,182,56)" stroke-width="1.89px"/>
 										</g>
 									</g>
 									<g>
 										<g transform="matrix(5.30977,0.697415,-0.697415,5.30977,3852.72,1727.97)">
-											<path d="M22,12C22,11.659 21.913,11.337 21.76,11.055C21.421,10.429 20.756,10 20,10C18.903,10 18,10.903 18,12C18,13.097 18.903,14 20,14C21.097,14 22,13.097 22,12Z" style="fill:none;fill-rule:nonzero;stroke:rgb(0,144,221);stroke-width:0.98px;"/>
+											<path d="M22,12C22,11.659 21.913,11.337 21.76,11.055C21.421,10.429 20.756,10 20,10C18.903,10 18,10.903 18,12C18,13.097 18.903,14 20,14C21.097,14 22,13.097 22,12Z" fill="none" fill-rule="nonzero" stroke="rgb(0,144,221)" stroke-width="0.98px"/>
 										</g>
 										<g transform="matrix(4.93114,2.49604,1.11018,5.44847,3921.41,1726.72)">
-											<path d="M8.902,6.77C8.902,6.77 7.235,8.253 6.027,9.366C5.343,9.996 4.819,10.502 4.819,10.502L5.52,11.164C5.52,11.164 6.021,10.637 6.646,9.951C7.749,8.739 9.219,7.068 9.219,7.068L8.902,6.77Z" style="fill:rgb(0,144,221);"/>
+											<path d="M8.902,6.77C8.902,6.77 7.235,8.253 6.027,9.366C5.343,9.996 4.819,10.502 4.819,10.502L5.52,11.164C5.52,11.164 6.021,10.637 6.646,9.951C7.749,8.739 9.219,7.068 9.219,7.068L8.902,6.77Z" fill="rgb(0,144,221)"/>
 										</g>
 									</g>
 								</g>
 								<g id="Text">
 									<g id="Wordmark" transform="matrix(1.32271,0,0,2.60848,-899.259,-791.691)">
 										<g id="y" transform="matrix(0.50291,0,0,0.281607,905.533,304.987)">
-											<path d="M192.152,286.875L202.629,268.64C187.804,270.106 183.397,265.779 180.143,263.391C176.888,261.004 174.362,257.99 172.563,254.347C170.765,250.705 169.866,246.691 169.866,242.305L169.866,208.107L183.21,208.107L183.21,242.213C183.21,245.188 183.896,247.822 185.268,250.116C186.64,252.41 188.465,254.197 190.743,255.475C193.022,256.754 195.501,257.393 198.182,257.393C200.894,257.393 203.393,256.75 205.68,255.463C207.966,254.177 209.799,252.391 211.178,250.105C212.558,247.818 213.248,245.188 213.248,242.213L213.248,208.107L226.545,208.107L226.545,242.305C226.545,246.707 225.378,258.46 218.079,268.64C215.735,271.909 207.835,286.875 207.835,286.875L192.152,286.875Z" style="fill:rgb(47,47,47);fill-rule:nonzero;"/>
+											<path d="M192.152,286.875L202.629,268.64C187.804,270.106 183.397,265.779 180.143,263.391C176.888,261.004 174.362,257.99 172.563,254.347C170.765,250.705 169.866,246.691 169.866,242.305L169.866,208.107L183.21,208.107L183.21,242.213C183.21,245.188 183.896,247.822 185.268,250.116C186.64,252.41 188.465,254.197 190.743,255.475C193.022,256.754 195.501,257.393 198.182,257.393C200.894,257.393 203.393,256.75 205.68,255.463C207.966,254.177 209.799,252.391 211.178,250.105C212.558,247.818 213.248,245.188 213.248,242.213L213.248,208.107L226.545,208.107L226.545,242.305C226.545,246.707 225.378,258.46 218.079,268.64C215.735,271.909 207.835,286.875 207.835,286.875L192.152,286.875Z" fill="rgb(47,47,47)" fill-rule="nonzero"/>
 										</g>
 										<g id="add" transform="matrix(0.525075,0,0,0.281607,801.871,304.987)">
 											<g transform="matrix(116.242,0,0,116.242,161.846,267.39)">
-												<path d="M0.276,0.012C0.227,0.012 0.186,0 0.15,-0.024C0.115,-0.048 0.088,-0.08 0.069,-0.12C0.05,-0.161 0.04,-0.205 0.04,-0.254C0.04,-0.305 0.051,-0.35 0.072,-0.39C0.094,-0.431 0.125,-0.463 0.165,-0.487C0.205,-0.51 0.254,-0.522 0.31,-0.522C0.366,-0.522 0.413,-0.51 0.452,-0.486C0.491,-0.463 0.521,-0.431 0.542,-0.39C0.562,-0.35 0.573,-0.305 0.573,-0.256L0.573,-0L0.458,-0L0.458,-0.095L0.456,-0.095C0.446,-0.076 0.433,-0.058 0.417,-0.042C0.401,-0.026 0.381,-0.013 0.358,-0.003C0.335,0.007 0.307,0.012 0.276,0.012ZM0.307,-0.086C0.337,-0.086 0.363,-0.093 0.386,-0.108C0.408,-0.123 0.426,-0.144 0.438,-0.17C0.45,-0.195 0.456,-0.224 0.456,-0.256C0.456,-0.288 0.45,-0.317 0.438,-0.342C0.426,-0.367 0.409,-0.387 0.387,-0.402C0.365,-0.417 0.338,-0.424 0.308,-0.424C0.276,-0.424 0.249,-0.417 0.226,-0.402C0.204,-0.387 0.186,-0.366 0.174,-0.341C0.162,-0.315 0.156,-0.287 0.156,-0.255C0.156,-0.224 0.162,-0.195 0.174,-0.169C0.186,-0.144 0.203,-0.123 0.226,-0.108C0.248,-0.093 0.275,-0.086 0.307,-0.086Z" style="fill:rgb(47,47,47);fill-rule:nonzero;"/>
+												<path d="M0.276,0.012C0.227,0.012 0.186,0 0.15,-0.024C0.115,-0.048 0.088,-0.08 0.069,-0.12C0.05,-0.161 0.04,-0.205 0.04,-0.254C0.04,-0.305 0.051,-0.35 0.072,-0.39C0.094,-0.431 0.125,-0.463 0.165,-0.487C0.205,-0.51 0.254,-0.522 0.31,-0.522C0.366,-0.522 0.413,-0.51 0.452,-0.486C0.491,-0.463 0.521,-0.431 0.542,-0.39C0.562,-0.35 0.573,-0.305 0.573,-0.256L0.573,-0L0.458,-0L0.458,-0.095L0.456,-0.095C0.446,-0.076 0.433,-0.058 0.417,-0.042C0.401,-0.026 0.381,-0.013 0.358,-0.003C0.335,0.007 0.307,0.012 0.276,0.012ZM0.307,-0.086C0.337,-0.086 0.363,-0.093 0.386,-0.108C0.408,-0.123 0.426,-0.144 0.438,-0.17C0.45,-0.195 0.456,-0.224 0.456,-0.256C0.456,-0.288 0.45,-0.317 0.438,-0.342C0.426,-0.367 0.409,-0.387 0.387,-0.402C0.365,-0.417 0.338,-0.424 0.308,-0.424C0.276,-0.424 0.249,-0.417 0.226,-0.402C0.204,-0.387 0.186,-0.366 0.174,-0.341C0.162,-0.315 0.156,-0.287 0.156,-0.255C0.156,-0.224 0.162,-0.195 0.174,-0.169C0.186,-0.144 0.203,-0.123 0.226,-0.108C0.248,-0.093 0.275,-0.086 0.307,-0.086Z" fill="rgb(47,47,47)" fill-rule="nonzero"/>
 											</g>
 											<g transform="matrix(116.242,0,0,116.242,226.592,267.39)">
-												<path d="M0.306,0.012C0.265,0.012 0.229,0.006 0.196,-0.008C0.163,-0.021 0.135,-0.039 0.112,-0.064C0.089,-0.088 0.071,-0.117 0.059,-0.151C0.046,-0.185 0.04,-0.222 0.04,-0.263C0.04,-0.315 0.051,-0.36 0.072,-0.399C0.093,-0.437 0.122,-0.468 0.159,-0.489C0.196,-0.511 0.239,-0.522 0.287,-0.522C0.311,-0.522 0.333,-0.518 0.355,-0.511C0.377,-0.504 0.396,-0.493 0.413,-0.48C0.431,-0.466 0.445,-0.451 0.455,-0.433L0.456,-0.433L0.456,-0.73L0.571,-0.73L0.571,-0.261C0.571,-0.205 0.56,-0.156 0.537,-0.115C0.515,-0.074 0.484,-0.043 0.444,-0.021C0.405,0.001 0.358,0.012 0.306,0.012ZM0.306,-0.086C0.335,-0.086 0.361,-0.093 0.384,-0.107C0.406,-0.122 0.423,-0.141 0.436,-0.167C0.448,-0.192 0.455,-0.221 0.455,-0.255C0.455,-0.288 0.448,-0.317 0.436,-0.343C0.423,-0.368 0.406,-0.388 0.383,-0.402C0.361,-0.417 0.335,-0.424 0.305,-0.424C0.276,-0.424 0.251,-0.417 0.228,-0.402C0.206,-0.387 0.188,-0.368 0.175,-0.342C0.163,-0.317 0.156,-0.288 0.156,-0.255C0.156,-0.222 0.163,-0.193 0.175,-0.167C0.188,-0.142 0.206,-0.122 0.229,-0.108C0.251,-0.093 0.277,-0.086 0.306,-0.086Z" style="fill:rgb(47,47,47);fill-rule:nonzero;"/>
+												<path d="M0.306,0.012C0.265,0.012 0.229,0.006 0.196,-0.008C0.163,-0.021 0.135,-0.039 0.112,-0.064C0.089,-0.088 0.071,-0.117 0.059,-0.151C0.046,-0.185 0.04,-0.222 0.04,-0.263C0.04,-0.315 0.051,-0.36 0.072,-0.399C0.093,-0.437 0.122,-0.468 0.159,-0.489C0.196,-0.511 0.239,-0.522 0.287,-0.522C0.311,-0.522 0.333,-0.518 0.355,-0.511C0.377,-0.504 0.396,-0.493 0.413,-0.48C0.431,-0.466 0.445,-0.451 0.455,-0.433L0.456,-0.433L0.456,-0.73L0.571,-0.73L0.571,-0.261C0.571,-0.205 0.56,-0.156 0.537,-0.115C0.515,-0.074 0.484,-0.043 0.444,-0.021C0.405,0.001 0.358,0.012 0.306,0.012ZM0.306,-0.086C0.335,-0.086 0.361,-0.093 0.384,-0.107C0.406,-0.122 0.423,-0.141 0.436,-0.167C0.448,-0.192 0.455,-0.221 0.455,-0.255C0.455,-0.288 0.448,-0.317 0.436,-0.343C0.423,-0.368 0.406,-0.388 0.383,-0.402C0.361,-0.417 0.335,-0.424 0.305,-0.424C0.276,-0.424 0.251,-0.417 0.228,-0.402C0.206,-0.387 0.188,-0.368 0.175,-0.342C0.163,-0.317 0.156,-0.288 0.156,-0.255C0.156,-0.222 0.163,-0.193 0.175,-0.167C0.188,-0.142 0.206,-0.122 0.229,-0.108C0.251,-0.093 0.277,-0.086 0.306,-0.086Z" fill="rgb(47,47,47)" fill-rule="nonzero"/>
 											</g>
 											<g transform="matrix(116.242,0,0,116.242,290.293,267.39)">
-												<path d="M0.306,0.012C0.265,0.012 0.229,0.006 0.196,-0.008C0.163,-0.021 0.135,-0.039 0.112,-0.064C0.089,-0.088 0.071,-0.117 0.059,-0.151C0.046,-0.185 0.04,-0.222 0.04,-0.263C0.04,-0.315 0.051,-0.36 0.072,-0.399C0.093,-0.437 0.122,-0.468 0.159,-0.489C0.196,-0.511 0.239,-0.522 0.287,-0.522C0.311,-0.522 0.333,-0.518 0.355,-0.511C0.377,-0.504 0.396,-0.493 0.413,-0.48C0.431,-0.466 0.445,-0.451 0.455,-0.433L0.456,-0.433L0.456,-0.73L0.571,-0.73L0.571,-0.261C0.571,-0.205 0.56,-0.156 0.537,-0.115C0.515,-0.074 0.484,-0.043 0.444,-0.021C0.405,0.001 0.358,0.012 0.306,0.012ZM0.306,-0.086C0.335,-0.086 0.361,-0.093 0.384,-0.107C0.406,-0.122 0.423,-0.141 0.436,-0.167C0.448,-0.192 0.455,-0.221 0.455,-0.255C0.455,-0.288 0.448,-0.317 0.436,-0.343C0.423,-0.368 0.406,-0.388 0.383,-0.402C0.361,-0.417 0.335,-0.424 0.305,-0.424C0.276,-0.424 0.251,-0.417 0.228,-0.402C0.206,-0.387 0.188,-0.368 0.175,-0.342C0.163,-0.317 0.156,-0.288 0.156,-0.255C0.156,-0.222 0.163,-0.193 0.175,-0.167C0.188,-0.142 0.206,-0.122 0.229,-0.108C0.251,-0.093 0.277,-0.086 0.306,-0.086Z" style="fill:rgb(47,47,47);fill-rule:nonzero;"/>
+												<path d="M0.306,0.012C0.265,0.012 0.229,0.006 0.196,-0.008C0.163,-0.021 0.135,-0.039 0.112,-0.064C0.089,-0.088 0.071,-0.117 0.059,-0.151C0.046,-0.185 0.04,-0.222 0.04,-0.263C0.04,-0.315 0.051,-0.36 0.072,-0.399C0.093,-0.437 0.122,-0.468 0.159,-0.489C0.196,-0.511 0.239,-0.522 0.287,-0.522C0.311,-0.522 0.333,-0.518 0.355,-0.511C0.377,-0.504 0.396,-0.493 0.413,-0.48C0.431,-0.466 0.445,-0.451 0.455,-0.433L0.456,-0.433L0.456,-0.73L0.571,-0.73L0.571,-0.261C0.571,-0.205 0.56,-0.156 0.537,-0.115C0.515,-0.074 0.484,-0.043 0.444,-0.021C0.405,0.001 0.358,0.012 0.306,0.012ZM0.306,-0.086C0.335,-0.086 0.361,-0.093 0.384,-0.107C0.406,-0.122 0.423,-0.141 0.436,-0.167C0.448,-0.192 0.455,-0.221 0.455,-0.255C0.455,-0.288 0.448,-0.317 0.436,-0.343C0.423,-0.368 0.406,-0.388 0.383,-0.402C0.361,-0.417 0.335,-0.424 0.305,-0.424C0.276,-0.424 0.251,-0.417 0.228,-0.402C0.206,-0.387 0.188,-0.368 0.175,-0.342C0.163,-0.317 0.156,-0.288 0.156,-0.255C0.156,-0.222 0.163,-0.193 0.175,-0.167C0.188,-0.142 0.206,-0.122 0.229,-0.108C0.251,-0.093 0.277,-0.086 0.306,-0.086Z" fill="rgb(47,47,47)" fill-rule="nonzero"/>
 											</g>
 										</g>
 										<g id="c" transform="matrix(-0.0716462,0.31304,-0.583685,-0.0384251,1489.76,-444.051)">
-											<path d="M2668.11,700.4C2666.79,703.699 2666.12,707.216 2666.12,710.766C2666.12,726.268 2678.71,738.854 2694.21,738.854C2709.71,738.854 2722.3,726.268 2722.3,710.766C2722.3,704.111 2719.93,697.672 2715.63,692.597L2707.63,699.378C2710.33,702.559 2711.57,706.602 2711.81,710.766C2712.2,717.38 2706.61,724.52 2697.27,726.637C2683.9,728.581 2676.61,720.482 2676.61,710.766C2676.61,708.541 2677.03,706.336 2677.85,704.269L2668.11,700.4Z" style="fill:rgb(46,46,46);"/>
+											<path d="M2668.11,700.4C2666.79,703.699 2666.12,707.216 2666.12,710.766C2666.12,726.268 2678.71,738.854 2694.21,738.854C2709.71,738.854 2722.3,726.268 2722.3,710.766C2722.3,704.111 2719.93,697.672 2715.63,692.597L2707.63,699.378C2710.33,702.559 2711.57,706.602 2711.81,710.766C2712.2,717.38 2706.61,724.52 2697.27,726.637C2683.9,728.581 2676.61,720.482 2676.61,710.766C2676.61,708.541 2677.03,706.336 2677.85,704.269L2668.11,700.4Z" fill="rgb(46,46,46)"/>
 										</g>
 									</g>
 									<g id="R" transform="matrix(0.426446,0,0,0.451034,-1192.44,-722.167)">
 										<g transform="matrix(1,0,0,1,-0.10786,0.450801)">
 											<g transform="matrix(12.1247,0,0,12.1247,3862.61,1929.9)">
-												<path d="M0.073,-0L0.073,-0.7L0.383,-0.7C0.428,-0.7 0.469,-0.69 0.506,-0.67C0.543,-0.651 0.572,-0.623 0.594,-0.588C0.616,-0.553 0.627,-0.512 0.627,-0.465C0.627,-0.418 0.615,-0.377 0.592,-0.342C0.569,-0.306 0.539,-0.279 0.501,-0.259L0.57,-0.128C0.574,-0.12 0.579,-0.115 0.584,-0.111C0.59,-0.107 0.596,-0.106 0.605,-0.106L0.664,-0.106L0.664,-0L0.587,-0C0.56,-0 0.535,-0.007 0.514,-0.02C0.493,-0.034 0.476,-0.052 0.463,-0.075L0.381,-0.232C0.375,-0.231 0.368,-0.231 0.361,-0.231C0.354,-0.231 0.347,-0.231 0.34,-0.231L0.192,-0.231L0.192,-0L0.073,-0ZM0.192,-0.336L0.368,-0.336C0.394,-0.336 0.417,-0.341 0.438,-0.351C0.459,-0.361 0.476,-0.376 0.489,-0.396C0.501,-0.415 0.507,-0.438 0.507,-0.465C0.507,-0.492 0.501,-0.516 0.488,-0.535C0.475,-0.554 0.459,-0.569 0.438,-0.579C0.417,-0.59 0.394,-0.595 0.369,-0.595L0.192,-0.595L0.192,-0.336Z" style="fill:rgb(46,46,46);fill-rule:nonzero;"/>
+												<path d="M0.073,-0L0.073,-0.7L0.383,-0.7C0.428,-0.7 0.469,-0.69 0.506,-0.67C0.543,-0.651 0.572,-0.623 0.594,-0.588C0.616,-0.553 0.627,-0.512 0.627,-0.465C0.627,-0.418 0.615,-0.377 0.592,-0.342C0.569,-0.306 0.539,-0.279 0.501,-0.259L0.57,-0.128C0.574,-0.12 0.579,-0.115 0.584,-0.111C0.59,-0.107 0.596,-0.106 0.605,-0.106L0.664,-0.106L0.664,-0L0.587,-0C0.56,-0 0.535,-0.007 0.514,-0.02C0.493,-0.034 0.476,-0.052 0.463,-0.075L0.381,-0.232C0.375,-0.231 0.368,-0.231 0.361,-0.231C0.354,-0.231 0.347,-0.231 0.34,-0.231L0.192,-0.231L0.192,-0L0.073,-0ZM0.192,-0.336L0.368,-0.336C0.394,-0.336 0.417,-0.341 0.438,-0.351C0.459,-0.361 0.476,-0.376 0.489,-0.396C0.501,-0.415 0.507,-0.438 0.507,-0.465C0.507,-0.492 0.501,-0.516 0.488,-0.535C0.475,-0.554 0.459,-0.569 0.438,-0.579C0.417,-0.59 0.394,-0.595 0.369,-0.595L0.192,-0.595L0.192,-0.336Z" fill="rgb(46,46,46)" fill-rule="nonzero"/>
 											</g>
 										</g>
 										<g transform="matrix(1,0,0,1,0.278569,0.101881)">
-											<circle cx="3866.43" cy="1926.14" r="8.923" style="fill:none;stroke:rgb(46,46,46);stroke-width:2px;stroke-linecap:butt;stroke-linejoin:miter;"/>
+											<circle cx="3866.43" cy="1926.14" r="8.923" fill="none" stroke="rgb(46,46,46)" stroke-width="2px" stroke-linecap="butt" stroke-linejoin="miter"/>
 										</g>
 									</g>
 								</g>
@@ -1074,7 +1085,7 @@ footer {
 			</a>
 		</footer>
 
-		<script>
+		<script {{ $nonceAttribute }}>
 			const filterEl = document.getElementById('filter');
 			filterEl?.focus({ preventScroll: true });
 
@@ -1120,6 +1131,20 @@ footer {
 				});
 			}
 
+			const filterElem = document.getElementById("filter");
+			if (filterElem) {
+				filterElem.addEventListener("keyup", filter);
+			}
+
+			document.getElementById("layout-list").addEventListener("click", function() {
+				queryParam('layout', '');
+			});
+			document.getElementById("layout-grid").addEventListener("click", function() {
+				queryParam('layout', 'grid');
+			});
+
+			window.addEventListener("load", initPage);
+
 			function queryParam(k, v) {
 				const qs = new URLSearchParams(window.location.search);
 				if (!v) {

From 88c7e53da55e23c113d4d1f2b67ddf8b978e8a45 Mon Sep 17 00:00:00 2001
From: Steffen Busch <37350514+steffenbusch@users.noreply.github.com>
Date: Sun, 7 Jul 2024 14:56:47 +0200
Subject: [PATCH 08/62] browse: fix Content-Security-Policy warnings in Firefox
 (#6443)

* Remove 'strict-dynamic' + block-all-mixed-content

* CSP: remove 'unsafe-inline' from script-src
---
 modules/caddyhttp/fileserver/browse.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/caddyhttp/fileserver/browse.html b/modules/caddyhttp/fileserver/browse.html
index 43d5f4514..1e21a9d11 100644
--- a/modules/caddyhttp/fileserver/browse.html
+++ b/modules/caddyhttp/fileserver/browse.html
@@ -1,6 +1,6 @@
 {{ $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 -}}
+{{ $csp := printf "default-src 'none'; img-src 'self'; object-src 'none'; base-uri 'none'; script-src 'nonce-%s'; style-src 'nonce-%s'; frame-ancestors 'self'; form-action 'self';" $nonce $nonce -}}
 {{/* To disable the Content-Security-Policy, set this to false */}}{{ $enableCsp := true -}}
 {{ if $enableCsp -}}
   {{- .RespHeader.Set "Content-Security-Policy" $csp -}}

From 9338741ca79a74247ced86bc26e4994138470852 Mon Sep 17 00:00:00 2001
From: Francis Lavoie <lavofr@gmail.com>
Date: Sun, 7 Jul 2024 09:01:07 -0400
Subject: [PATCH 09/62] browse: Exclude symlink target size from total, show
 arrow on size (#6412)

* fileserver: Exclude symlink target size from total, show arrow on size

* Keep both totals

* Linter doesn't like my spelling :(

* Stop parallelizing tests for now

* Update modules/caddyhttp/fileserver/browse.html

* Minor renamings

---------

Co-authored-by: Matthew Holt <mholt@users.noreply.github.com>
---
 .github/workflows/ci.yml                      |  2 +-
 modules/caddyhttp/fileserver/browse.html      |  2 +-
 .../caddyhttp/fileserver/browsetplcontext.go  | 24 +++++++++++++++++--
 3 files changed, 24 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e6fe6d755..cae0b9840 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -157,7 +157,7 @@ jobs:
 
           # The environment is fresh, so there's no point in keeping accepting and adding the key.
           rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . "$CI_USER"@ci-s390x.caddyserver.com:/var/tmp/"$short_sha"
-          ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t "$CI_USER"@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -tags nobadger -v ./..."
+          ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t "$CI_USER"@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -p 1 -tags nobadger -v ./..."
           test_result=$?
 
           # There's no need leaving the files around
diff --git a/modules/caddyhttp/fileserver/browse.html b/modules/caddyhttp/fileserver/browse.html
index 1e21a9d11..d2d698197 100644
--- a/modules/caddyhttp/fileserver/browse.html
+++ b/modules/caddyhttp/fileserver/browse.html
@@ -991,7 +991,7 @@ footer {
 							<div class="sizebar">
 								<div class="sizebar-bar"></div>
 								<div class="sizebar-text">
-									{{.HumanSize}}
+									{{if .IsSymlink}}↱&nbsp;{{end}}{{.HumanSize}}
 								</div>
 							</div>
 						</td>
diff --git a/modules/caddyhttp/fileserver/browsetplcontext.go b/modules/caddyhttp/fileserver/browsetplcontext.go
index 74c14608f..81e260c71 100644
--- a/modules/caddyhttp/fileserver/browsetplcontext.go
+++ b/modules/caddyhttp/fileserver/browsetplcontext.go
@@ -80,6 +80,13 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS,
 		}
 
 		size := info.Size()
+
+		if !isDir {
+			// increase the total by the symlink's size, not the target's size,
+			// by incrementing before we follow the symlink
+			tplCtx.TotalFileSize += size
+		}
+
 		fileIsSymlink := isSymlink(info)
 		symlinkPath := ""
 		if fileIsSymlink {
@@ -103,7 +110,8 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS,
 		}
 
 		if !isDir {
-			tplCtx.TotalFileSize += size
+			// increase the total including the symlink target's size
+			tplCtx.TotalFileSizeFollowingSymlinks += size
 		}
 
 		u := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name
@@ -150,9 +158,15 @@ type browseTemplateContext struct {
 	// The number of files (items that aren't directories) in the listing.
 	NumFiles int `json:"num_files"`
 
-	// The total size of all files in the listing.
+	// The total size of all files in the listing. Only includes the
+	// size of the files themselves, not the size of symlink targets
+	// (i.e. the calculation of this value does not follow symlinks).
 	TotalFileSize int64 `json:"total_file_size"`
 
+	// The total size of all files in the listing, including the
+	// size of the files targeted by symlinks.
+	TotalFileSizeFollowingSymlinks int64 `json:"total_file_size_following_symlinks"`
+
 	// Sort column used
 	Sort string `json:"sort,omitempty"`
 
@@ -288,6 +302,12 @@ func (btc browseTemplateContext) HumanTotalFileSize() string {
 	return humanize.IBytes(uint64(btc.TotalFileSize))
 }
 
+// HumanTotalFileSizeFollowingSymlinks is the same as HumanTotalFileSize
+// except the returned value reflects the size of symlink targets.
+func (btc browseTemplateContext) HumanTotalFileSizeFollowingSymlinks() string {
+	return humanize.IBytes(uint64(btc.TotalFileSizeFollowingSymlinks))
+}
+
 // HumanModTime returns the modified time of the file
 // as a human-readable string given by format.
 func (fi fileInfo) HumanModTime(format string) string {

From 630c62b3137abf688aa1a698a614fa28c08e43dd Mon Sep 17 00:00:00 2001
From: Aziz Rmadi <46684200+armadi1809@users.noreply.github.com>
Date: Tue, 9 Jul 2024 02:06:30 -0500
Subject: [PATCH 10/62] fixed bug in resolving ip version in dynamic upstreams
 (#6448)

---
 modules/caddyhttp/reverseproxy/upstreams.go   | 26 +++++----
 .../caddyhttp/reverseproxy/upstreams_test.go  | 56 +++++++++++++++++++
 2 files changed, 70 insertions(+), 12 deletions(-)
 create mode 100644 modules/caddyhttp/reverseproxy/upstreams_test.go

diff --git a/modules/caddyhttp/reverseproxy/upstreams.go b/modules/caddyhttp/reverseproxy/upstreams.go
index 46e45c646..c8ba930d2 100644
--- a/modules/caddyhttp/reverseproxy/upstreams.go
+++ b/modules/caddyhttp/reverseproxy/upstreams.go
@@ -231,6 +231,19 @@ type IPVersions struct {
 	IPv6 *bool `json:"ipv6,omitempty"`
 }
 
+func resolveIpVersion(versions *IPVersions) string {
+	resolveIpv4 := versions == nil || (versions.IPv4 == nil && versions.IPv6 == nil) || (versions.IPv4 != nil && *versions.IPv4)
+	resolveIpv6 := versions == nil || (versions.IPv6 == nil && versions.IPv4 == nil) || (versions.IPv6 != nil && *versions.IPv6)
+	switch {
+	case resolveIpv4 && !resolveIpv6:
+		return "ip4"
+	case !resolveIpv4 && resolveIpv6:
+		return "ip6"
+	default:
+		return "ip"
+	}
+}
+
 // AUpstreams provides upstreams from A/AAAA lookups.
 // Results are cached and refreshed at the configured
 // refresh interval.
@@ -313,9 +326,6 @@ func (au *AUpstreams) Provision(ctx caddy.Context) error {
 func (au AUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
 	repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
 
-	resolveIpv4 := au.Versions == nil || au.Versions.IPv4 == nil || *au.Versions.IPv4
-	resolveIpv6 := au.Versions == nil || au.Versions.IPv6 == nil || *au.Versions.IPv6
-
 	// Map ipVersion early, so we can use it as part of the cache-key.
 	// This should be fairly inexpensive and comes and the upside of
 	// allowing the same dynamic upstream (name + port combination)
@@ -324,15 +334,7 @@ func (au AUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
 	// It also forced a cache-miss if a previously cached dynamic
 	// upstream changes its ip version, e.g. after a config reload,
 	// while keeping the cache-invalidation as simple as it currently is.
-	var ipVersion string
-	switch {
-	case resolveIpv4 && !resolveIpv6:
-		ipVersion = "ip4"
-	case !resolveIpv4 && resolveIpv6:
-		ipVersion = "ip6"
-	default:
-		ipVersion = "ip"
-	}
+	ipVersion := resolveIpVersion(au.Versions)
 
 	auStr := repl.ReplaceAll(au.String()+ipVersion, "")
 
diff --git a/modules/caddyhttp/reverseproxy/upstreams_test.go b/modules/caddyhttp/reverseproxy/upstreams_test.go
new file mode 100644
index 000000000..48e2d2a63
--- /dev/null
+++ b/modules/caddyhttp/reverseproxy/upstreams_test.go
@@ -0,0 +1,56 @@
+package reverseproxy
+
+import "testing"
+
+func TestResolveIpVersion(t *testing.T) {
+	falseBool := false
+	trueBool := true
+	tests := []struct {
+		Versions          *IPVersions
+		expectedIpVersion string
+	}{
+		{
+			Versions:          &IPVersions{IPv4: &trueBool},
+			expectedIpVersion: "ip4",
+		},
+		{
+			Versions:          &IPVersions{IPv4: &falseBool},
+			expectedIpVersion: "ip",
+		},
+		{
+			Versions:          &IPVersions{IPv4: &trueBool, IPv6: &falseBool},
+			expectedIpVersion: "ip4",
+		},
+		{
+			Versions:          &IPVersions{IPv6: &trueBool},
+			expectedIpVersion: "ip6",
+		},
+		{
+			Versions:          &IPVersions{IPv6: &falseBool},
+			expectedIpVersion: "ip",
+		},
+		{
+			Versions:          &IPVersions{IPv6: &trueBool, IPv4: &falseBool},
+			expectedIpVersion: "ip6",
+		},
+		{
+			Versions:          &IPVersions{},
+			expectedIpVersion: "ip",
+		},
+		{
+			Versions:          &IPVersions{IPv4: &trueBool, IPv6: &trueBool},
+			expectedIpVersion: "ip",
+		},
+		{
+			Versions:          &IPVersions{IPv4: &falseBool, IPv6: &falseBool},
+			expectedIpVersion: "ip",
+		},
+	}
+	for _, test := range tests {
+		ipVersion := resolveIpVersion(test.Versions)
+		if ipVersion != test.expectedIpVersion {
+			t.Errorf("resolveIpVersion(): Expected %s got %s", test.expectedIpVersion, ipVersion)
+		}
+	}
+
+}

From 4943a4fc527f41e32c1770164b00980b4226f7c7 Mon Sep 17 00:00:00 2001
From: schultzie <9121234+dylanschultzie@users.noreply.github.com>
Date: Tue, 9 Jul 2024 11:08:25 -0700
Subject: [PATCH 11/62] reverseproxy: Add placeholder for networkAddr in active
 health check headers (#6450)

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
---
 modules/caddyhttp/reverseproxy/healthchecks.go | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go
index ac92604ca..30ed04185 100644
--- a/modules/caddyhttp/reverseproxy/healthchecks.go
+++ b/modules/caddyhttp/reverseproxy/healthchecks.go
@@ -312,7 +312,7 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
 				// so use a fake Host value instead; unix sockets are usually local
 				hostAddr = "localhost"
 			}
-			err = h.doActiveHealthCheck(DialInfo{Network: addr.Network, Address: dialAddr}, hostAddr, upstream)
+			err = h.doActiveHealthCheck(DialInfo{Network: addr.Network, Address: dialAddr}, hostAddr, networkAddr, upstream)
 			if err != nil {
 				h.HealthChecks.Active.logger.Error("active health check failed",
 					zap.String("address", hostAddr),
@@ -330,7 +330,7 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
 // according to whether it passes the health check. An error is
 // returned only if the health check fails to occur or if marking
 // the host's health status fails.
-func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstream *Upstream) error {
+func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networkAddr string, upstream *Upstream) error {
 	// create the URL for the request that acts as a health check
 	u := &url.URL{
 		Scheme: "http",
@@ -386,7 +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)
+	repl.Set("http.reverse_proxy.active.target_upstream", networkAddr)
 	for key, vals := range h.HealthChecks.Active.Headers {
 		key = repl.ReplaceAll(key, "")
 		if key == "Host" {

From dc2a5d5c52b3baeac182311fe7473870e671126a Mon Sep 17 00:00:00 2001
From: Jesper Brix Rosenkilde <jbr@humppa.dk>
Date: Thu, 11 Jul 2024 15:24:13 +0200
Subject: [PATCH 12/62] reverseproxy: Configurable method for active health
 checks (#6453)

* Add option to set which HTTP method to use for active health checks

* Default Method to GET if not set
---
 modules/caddyhttp/reverseproxy/healthchecks.go | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go
index 30ed04185..adb1bf5e8 100644
--- a/modules/caddyhttp/reverseproxy/healthchecks.go
+++ b/modules/caddyhttp/reverseproxy/healthchecks.go
@@ -82,6 +82,9 @@ type ActiveHealthChecks struct {
 	// HTTP headers to set on health check requests.
 	Headers http.Header `json:"headers,omitempty"`
 
+	// The HTTP method to use for health checks (default "GET").
+	Method string `json:"method,omitempty"`
+
 	// Whether to follow HTTP redirects in response to active health checks (default off).
 	FollowRedirects bool `json:"follow_redirects,omitempty"`
 
@@ -133,6 +136,11 @@ func (a *ActiveHealthChecks) Provision(ctx caddy.Context, h *Handler) error {
 	}
 	a.Headers = cleaned
 
+	// If Method is not set, default to GET
+	if a.Method == "" {
+		a.Method = http.MethodGet
+	}
+
 	h.HealthChecks.Active.logger = h.logger.Named("health_checker.active")
 
 	timeout := time.Duration(a.Timeout)
@@ -377,7 +385,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ
 	ctx = context.WithValue(ctx, caddyhttp.VarsCtxKey, map[string]any{
 		dialInfoVarKey: dialInfo,
 	})
-	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
+	req, err := http.NewRequestWithContext(ctx, h.HealthChecks.Active.Method, u.String(), nil)
 	if err != nil {
 		return fmt.Errorf("making request: %v", err)
 	}

From 07c863637dff831c99d91cce129899ac8aa7b25c Mon Sep 17 00:00:00 2001
From: Jesper Brix Rosenkilde <jbr@humppa.dk>
Date: Fri, 12 Jul 2024 23:01:58 +0200
Subject: [PATCH 13/62] reverseproxy: Caddyfile support for health_method
 (#6454)

* Add Caddyfile support of setting active health check request method

* Add integration test for active health check request method
---
 .../reverse_proxy_health_method.caddyfiletest | 40 +++++++++++++++++++
 modules/caddyhttp/reverseproxy/caddyfile.go   | 13 ++++++
 2 files changed, 53 insertions(+)
 create mode 100644 caddytest/integration/caddyfile_adapt/reverse_proxy_health_method.caddyfiletest

diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_health_method.caddyfiletest b/caddytest/integration/caddyfile_adapt/reverse_proxy_health_method.caddyfiletest
new file mode 100644
index 000000000..920702c10
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_health_method.caddyfiletest
@@ -0,0 +1,40 @@
+:8884
+
+reverse_proxy 127.0.0.1:65535 {
+	health_uri /health
+	health_method HEAD
+}
+----------
+{
+	"apps": {
+		"http": {
+			"servers": {
+				"srv0": {
+					"listen": [
+						":8884"
+					],
+					"routes": [
+						{
+							"handle": [
+								{
+									"handler": "reverse_proxy",
+									"health_checks": {
+										"active": {
+											"method": "HEAD",
+											"uri": "/health"
+										}
+									},
+									"upstreams": [
+										{
+											"dial": "127.0.0.1:65535"
+										}
+									]
+								}
+							]
+						}
+					]
+				}
+			}
+		}
+	}
+}
diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go
index 9de7aedd9..ab180b3d8 100644
--- a/modules/caddyhttp/reverseproxy/caddyfile.go
+++ b/modules/caddyhttp/reverseproxy/caddyfile.go
@@ -79,6 +79,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
 //	    health_headers {
 //	        <field> [<values...>]
 //	    }
+//	    health_method   <value>
 //
 //	    # passive health checking
 //	    fail_duration     <duration>
@@ -387,6 +388,18 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 			}
 			h.HealthChecks.Active.Headers = healthHeaders
 
+		case "health_method":
+			if !d.NextArg() {
+				return d.ArgErr()
+			}
+			if h.HealthChecks == nil {
+				h.HealthChecks = new(HealthChecks)
+			}
+			if h.HealthChecks.Active == nil {
+				h.HealthChecks.Active = new(ActiveHealthChecks)
+			}
+			h.HealthChecks.Active.Method = d.Val()
+
 		case "health_interval":
 			if !d.NextArg() {
 				return d.ArgErr()

From b2492f8567069c223eab2156d18904c03f9dc4f3 Mon Sep 17 00:00:00 2001
From: schultzie <9121234+dylanschultzie@users.noreply.github.com>
Date: Mon, 15 Jul 2024 10:00:12 -0700
Subject: [PATCH 14/62] reverseproxy: add health_upstream subdirective (#6451)

* Add health_upstream

Signed-off-by: Dylan Schultz <9121234+dylanschultzie@users.noreply.github.com>

* Add health_upstream to caddyfile parsing

* Add Active Upstream case for health checks

* Update ignore health port comment

Signed-off-by: Dylan Schultz <9121234+dylanschultzie@users.noreply.github.com>

* Update Upstream json doc

Signed-off-by: Dylan Schultz <9121234+dylanschultzie@users.noreply.github.com>

* Update modules/caddyhttp/reverseproxy/healthchecks.go

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

* Use error rather than log for health_port override

Signed-off-by: Dylan Schultz <9121234+dylanschultzie@users.noreply.github.com>

* Add comment about port being ignore if using upstream

Signed-off-by: Dylan Schultz <9121234+dylanschultzie@users.noreply.github.com>

---------

Signed-off-by: Dylan Schultz <9121234+dylanschultzie@users.noreply.github.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
---
 modules/caddyhttp/reverseproxy/caddyfile.go   | 24 ++++++++++++++++
 .../caddyhttp/reverseproxy/healthchecks.go    | 28 +++++++++++++++----
 modules/caddyhttp/reverseproxy/hosts.go       |  9 +++---
 3 files changed, 52 insertions(+), 9 deletions(-)

diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go
index ab180b3d8..1c3b49447 100644
--- a/modules/caddyhttp/reverseproxy/caddyfile.go
+++ b/modules/caddyhttp/reverseproxy/caddyfile.go
@@ -16,6 +16,7 @@ package reverseproxy
 
 import (
 	"fmt"
+	"net"
 	"net/http"
 	"reflect"
 	"strconv"
@@ -354,6 +355,26 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 			h.HealthChecks.Active.Path = d.Val()
 			caddy.Log().Named("config.adapter.caddyfile").Warn("the 'health_path' subdirective is deprecated, please use 'health_uri' instead!")
 
+		case "health_upstream":
+			if !d.NextArg() {
+				return d.ArgErr()
+			}
+			if h.HealthChecks == nil {
+				h.HealthChecks = new(HealthChecks)
+			}
+			if h.HealthChecks.Active == nil {
+				h.HealthChecks.Active = new(ActiveHealthChecks)
+			}
+			_, port, err := net.SplitHostPort(d.Val())
+			if err != nil {
+				return d.Errf("health_upstream is malformed '%s': %v", d.Val(), err)
+			}
+			_, err = strconv.Atoi(port)
+			if err != nil {
+				return d.Errf("bad port number '%s': %v", d.Val(), err)
+			}
+			h.HealthChecks.Active.Upstream = d.Val()
+
 		case "health_port":
 			if !d.NextArg() {
 				return d.ArgErr()
@@ -364,6 +385,9 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 			if h.HealthChecks.Active == nil {
 				h.HealthChecks.Active = new(ActiveHealthChecks)
 			}
+			if h.HealthChecks.Active.Upstream != "" {
+				return d.Errf("the 'health_port' subdirective is ignored if 'health_upstream' is used!")
+			}
 			portNum, err := strconv.Atoi(d.Val())
 			if err != nil {
 				return d.Errf("bad port number '%s': %v", d.Val(), err)
diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go
index adb1bf5e8..3b5a6a3af 100644
--- a/modules/caddyhttp/reverseproxy/healthchecks.go
+++ b/modules/caddyhttp/reverseproxy/healthchecks.go
@@ -75,8 +75,16 @@ type ActiveHealthChecks struct {
 	// The URI (path and query) to use for health checks
 	URI string `json:"uri,omitempty"`
 
+	// The host:port to use (if different from the upstream's dial address)
+	// for health checks. This should be used in tandem with `health_header` and
+	// `{http.reverse_proxy.active.target_upstream}`. This can be helpful when
+	// creating an intermediate service to do a more thorough health check.
+	// If upstream is set, the active health check port is ignored.
+	Upstream string `json:"upstream,omitempty"`
+
 	// The port to use (if different from the upstream's dial
-	// address) for health checks.
+	// address) for health checks. If active upstream is set,
+	// this value is ignored.
 	Port int `json:"port,omitempty"`
 
 	// HTTP headers to set on health check requests.
@@ -173,9 +181,14 @@ func (a *ActiveHealthChecks) Provision(ctx caddy.Context, h *Handler) error {
 	}
 
 	for _, upstream := range h.Upstreams {
-		// if there's an alternative port for health-check provided in the config,
-		// then use it, otherwise use the port of upstream.
-		if a.Port != 0 {
+		// if there's an alternative upstream for health-check provided in the config,
+		// then use it, otherwise use the upstream's dial address. if upstream is used,
+		// then the port is ignored.
+		if a.Upstream != "" {
+			upstream.activeHealthCheckUpstream = a.Upstream
+		} else if a.Port != 0 {
+			// if there's an alternative port for health-check provided in the config,
+			// then use it, otherwise use the port of upstream.
 			upstream.activeHealthCheckPort = a.Port
 		}
 	}
@@ -350,7 +363,12 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ
 	if err != nil {
 		host = hostAddr
 	}
-	if h.HealthChecks.Active.Port != 0 {
+
+	// ignore active health check port if active upstream is provided as the
+	// active upstream already contains the replacement port
+	if h.HealthChecks.Active.Upstream != "" {
+		u.Host = h.HealthChecks.Active.Upstream
+	} else if h.HealthChecks.Active.Port != 0 {
 		port := strconv.Itoa(h.HealthChecks.Active.Port)
 		u.Host = net.JoinHostPort(host, port)
 	}
diff --git a/modules/caddyhttp/reverseproxy/hosts.go b/modules/caddyhttp/reverseproxy/hosts.go
index be1146ac9..0a676e431 100644
--- a/modules/caddyhttp/reverseproxy/hosts.go
+++ b/modules/caddyhttp/reverseproxy/hosts.go
@@ -57,10 +57,11 @@ type Upstream struct {
 	// HeaderAffinity string
 	// IPAffinity     string
 
-	activeHealthCheckPort int
-	healthCheckPolicy     *PassiveHealthChecks
-	cb                    CircuitBreaker
-	unhealthy             int32 // accessed atomically; status from active health checker
+	activeHealthCheckPort     int
+	activeHealthCheckUpstream string
+	healthCheckPolicy         *PassiveHealthChecks
+	cb                        CircuitBreaker
+	unhealthy                 int32 // accessed atomically; status from active health checker
 }
 
 // (pointer receiver necessary to avoid a race condition, since

From 806f5b111781a3b2734223c8e14780d86ac4edd5 Mon Sep 17 00:00:00 2001
From: Matthew Holt <mholt@users.noreply.github.com>
Date: Thu, 18 Jul 2024 21:31:07 -0600
Subject: [PATCH 15/62] reverseproxy: Fix panic when using header-related flags
 (fix #6464)

---
 modules/caddyhttp/reverseproxy/command.go | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/modules/caddyhttp/reverseproxy/command.go b/modules/caddyhttp/reverseproxy/command.go
index 59fa67df0..f9304efa2 100644
--- a/modules/caddyhttp/reverseproxy/command.go
+++ b/modules/caddyhttp/reverseproxy/command.go
@@ -229,11 +229,13 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
 
 	if changeHost {
 		if handler.Headers == nil {
-			handler.Headers = &headers.Handler{
-				Request: &headers.HeaderOps{
-					Set: http.Header{},
-				},
-			}
+			handler.Headers = new(headers.Handler)
+		}
+		if handler.Headers.Request == nil {
+			handler.Headers.Request = new(headers.HeaderOps)
+		}
+		if handler.Headers.Request.Set == nil {
+			handler.Headers.Request.Set = http.Header{}
 		}
 		handler.Headers.Request.Set.Set("Host", "{http.reverse_proxy.upstream.hostport}")
 	}

From 3afa02ba4e5bee5628ee9334a64a0002a102a60b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?June=20=F0=9F=8D=93=F0=9F=A6=B4?= <june@girlboss.ceo>
Date: Mon, 22 Jul 2024 19:37:44 -0400
Subject: [PATCH 16/62] ci: correct `-tags nobadger` on binary build (#6470)

---
 .github/workflows/ci.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index cae0b9840..a006d26b3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -99,7 +99,7 @@ jobs:
       env:
         CGO_ENABLED: 0
       run: |
-        go build -tags nobdger -trimpath -ldflags="-w -s" -v
+        go build -tags nobadger -trimpath -ldflags="-w -s" -v
 
     - name: Smoke test Caddy
       working-directory: ./cmd/caddy

From 61fe152c603e6efb6ca3d3522923beebe6d6701f Mon Sep 17 00:00:00 2001
From: vnxme <46669194+vnxme@users.noreply.github.com>
Date: Wed, 24 Jul 2024 18:26:09 +0300
Subject: [PATCH 17/62] caddytls: Caddyfile support for TLS handshake matchers
 (#6461)

* Caddyfile support for TLS handshake matchers:

- caddytls.MatchLocalIP
- caddytls.MatchRemoteIP
- caddytls.MatchServerName

* Caddyfile support for TLS handshake matchers:

- fix imports order

Co-authored-by: Francis Lavoie <lavofr@gmail.com>

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
---
 modules/caddytls/matchers.go | 103 +++++++++++++++++++++++++++++++++++
 1 file changed, 103 insertions(+)

diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go
index b01fb8334..a23a19317 100644
--- a/modules/caddytls/matchers.go
+++ b/modules/caddytls/matchers.go
@@ -25,6 +25,7 @@ import (
 	"go.uber.org/zap"
 
 	"github.com/caddyserver/caddy/v2"
+	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
 )
 
 func init() {
@@ -56,6 +57,29 @@ func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool {
 	return false
 }
 
+// UnmarshalCaddyfile sets up the MatchServerName from Caddyfile tokens. Syntax:
+//
+//	sni <domains...>
+func (m *MatchServerName) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
+	for d.Next() {
+		wrapper := d.Val()
+
+		// At least one same-line option must be provided
+		if d.CountRemainingArgs() == 0 {
+			return d.ArgErr()
+		}
+
+		*m = append(*m, d.RemainingArgs()...)
+
+		// No blocks are supported
+		if d.NextBlock(d.Nesting()) {
+			return d.Errf("malformed TLS handshake matcher '%s': blocks are not supported", wrapper)
+		}
+	}
+
+	return nil
+}
+
 // MatchRemoteIP matches based on the remote IP of the
 // connection. Specific IPs or CIDR ranges can be specified.
 //
@@ -145,6 +169,50 @@ func (MatchRemoteIP) matches(ip netip.Addr, ranges []netip.Prefix) bool {
 	return false
 }
 
+// UnmarshalCaddyfile sets up the MatchRemoteIP from Caddyfile tokens. Syntax:
+//
+//	remote_ip <ranges...>
+//
+// Note: IPs and CIDRs prefixed with ! symbol are treated as not_ranges
+func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
+	for d.Next() {
+		wrapper := d.Val()
+
+		// At least one same-line option must be provided
+		if d.CountRemainingArgs() == 0 {
+			return d.ArgErr()
+		}
+
+		for d.NextArg() {
+			val := d.Val()
+			if len(val) > 1 && val[0] == '!' {
+				prefixes, err := m.parseIPRange(val[1:])
+				if err != nil {
+					return err
+				}
+				for _, prefix := range prefixes {
+					m.NotRanges = append(m.NotRanges, prefix.String())
+				}
+			} else {
+				prefixes, err := m.parseIPRange(val)
+				if err != nil {
+					return err
+				}
+				for _, prefix := range prefixes {
+					m.Ranges = append(m.Ranges, prefix.String())
+				}
+			}
+		}
+
+		// No blocks are supported
+		if d.NextBlock(d.Nesting()) {
+			return d.Errf("malformed TLS handshake matcher '%s': blocks are not supported", wrapper)
+		}
+	}
+
+	return nil
+}
+
 // MatchLocalIP matches based on the IP address of the interface
 // receiving the connection. Specific IPs or CIDR ranges can be specified.
 type MatchLocalIP struct {
@@ -219,6 +287,37 @@ func (MatchLocalIP) matches(ip netip.Addr, ranges []netip.Prefix) bool {
 	return false
 }
 
+// UnmarshalCaddyfile sets up the MatchLocalIP from Caddyfile tokens. Syntax:
+//
+//	local_ip <ranges...>
+func (m *MatchLocalIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
+	for d.Next() {
+		wrapper := d.Val()
+
+		// At least one same-line option must be provided
+		if d.CountRemainingArgs() == 0 {
+			return d.ArgErr()
+		}
+
+		for d.NextArg() {
+			prefixes, err := m.parseIPRange(d.Val())
+			if err != nil {
+				return err
+			}
+			for _, prefix := range prefixes {
+				m.Ranges = append(m.Ranges, prefix.String())
+			}
+		}
+
+		// No blocks are supported
+		if d.NextBlock(d.Nesting()) {
+			return d.Errf("malformed TLS handshake matcher '%s': blocks are not supported", wrapper)
+		}
+	}
+
+	return nil
+}
+
 // Interface guards
 var (
 	_ ConnectionMatcher = (*MatchServerName)(nil)
@@ -226,4 +325,8 @@ var (
 
 	_ caddy.Provisioner = (*MatchLocalIP)(nil)
 	_ ConnectionMatcher = (*MatchLocalIP)(nil)
+
+	_ caddyfile.Unmarshaler = (*MatchLocalIP)(nil)
+	_ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
+	_ caddyfile.Unmarshaler = (*MatchServerName)(nil)
 )

From 3579815a6c099bfbcff0143f7a251a92b4fa4198 Mon Sep 17 00:00:00 2001
From: vnxme <46669194+vnxme@users.noreply.github.com>
Date: Wed, 24 Jul 2024 20:01:06 +0300
Subject: [PATCH 18/62] caddytls: Caddyfile support for TLS conn and cert sel
 policies (#6462)

* Caddyfile support for TLS custom certificate selection policy

* Caddyfile support for TLS connection policy
---
 modules/caddytls/certselection.go |  78 ++++++++++++++
 modules/caddytls/connpolicy.go    | 174 +++++++++++++++++++++++++++++-
 2 files changed, 251 insertions(+), 1 deletion(-)

diff --git a/modules/caddytls/certselection.go b/modules/caddytls/certselection.go
index 1bef890aa..84ca2e118 100644
--- a/modules/caddytls/certselection.go
+++ b/modules/caddytls/certselection.go
@@ -22,6 +22,8 @@ import (
 	"math/big"
 
 	"github.com/caddyserver/certmagic"
+
+	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
 )
 
 // CustomCertSelectionPolicy represents a policy for selecting the certificate
@@ -122,6 +124,79 @@ nextChoice:
 	return certmagic.DefaultCertificateSelector(hello, viable)
 }
 
+// UnmarshalCaddyfile sets up the CustomCertSelectionPolicy from Caddyfile tokens. Syntax:
+//
+//	cert_selection {
+//		all_tags             <values...>
+//		any_tag              <values...>
+//		public_key_algorithm <dsa|ecdsa|rsa>
+//		serial_number        <big_integers...>
+//		subject_organization <values...>
+//	}
+func (p *CustomCertSelectionPolicy) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
+	_, wrapper := d.Next(), d.Val() // consume wrapper name
+
+	// No same-line options are supported
+	if d.CountRemainingArgs() > 0 {
+		return d.ArgErr()
+	}
+
+	var hasPublicKeyAlgorithm bool
+	for nesting := d.Nesting(); d.NextBlock(nesting); {
+		optionName := d.Val()
+		switch optionName {
+		case "all_tags":
+			if d.CountRemainingArgs() == 0 {
+				return d.ArgErr()
+			}
+			p.AllTags = append(p.AllTags, d.RemainingArgs()...)
+		case "any_tag":
+			if d.CountRemainingArgs() == 0 {
+				return d.ArgErr()
+			}
+			p.AnyTag = append(p.AnyTag, d.RemainingArgs()...)
+		case "public_key_algorithm":
+			if hasPublicKeyAlgorithm {
+				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
+			}
+			if d.CountRemainingArgs() != 1 {
+				return d.ArgErr()
+			}
+			d.NextArg()
+			if err := p.PublicKeyAlgorithm.UnmarshalJSON([]byte(d.Val())); err != nil {
+				return d.Errf("parsing %s option '%s': %v", wrapper, optionName, err)
+			}
+			hasPublicKeyAlgorithm = true
+		case "serial_number":
+			if d.CountRemainingArgs() == 0 {
+				return d.ArgErr()
+			}
+			for d.NextArg() {
+				val, bi := d.Val(), bigInt{}
+				_, ok := bi.SetString(val, 10)
+				if !ok {
+					return d.Errf("parsing %s option '%s': invalid big.int value %s", wrapper, optionName, val)
+				}
+				p.SerialNumber = append(p.SerialNumber, bi)
+			}
+		case "subject_organization":
+			if d.CountRemainingArgs() == 0 {
+				return d.ArgErr()
+			}
+			p.SubjectOrganization = append(p.SubjectOrganization, d.RemainingArgs()...)
+		default:
+			return d.ArgErr()
+		}
+
+		// No nested blocks are supported
+		if d.NextBlock(nesting + 1) {
+			return d.Errf("malformed %s option '%s': blocks are not supported", wrapper, optionName)
+		}
+	}
+
+	return nil
+}
+
 // bigInt is a big.Int type that interops with JSON encodings as a string.
 type bigInt struct{ big.Int }
 
@@ -144,3 +219,6 @@ func (bi *bigInt) UnmarshalJSON(p []byte) error {
 	}
 	return nil
 }
+
+// Interface guard
+var _ caddyfile.Unmarshaler = (*CustomCertSelectionPolicy)(nil)
diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go
index c9705ffdc..4ec0e673a 100644
--- a/modules/caddytls/connpolicy.go
+++ b/modules/caddytls/connpolicy.go
@@ -363,6 +363,136 @@ func (p ConnectionPolicy) SettingsEmpty() bool {
 		p.InsecureSecretsLog == ""
 }
 
+// UnmarshalCaddyfile sets up the ConnectionPolicy from Caddyfile tokens. Syntax:
+//
+//	connection_policy {
+//		alpn                  <values...>
+//		cert_selection {
+//			...
+//		}
+//		ciphers               <cipher_suites...>
+//		client_auth {
+//			...
+//		}
+//		curves                <curves...>
+//		default_sni           <server_name>
+//		match {
+//			...
+//		}
+//		protocols             <min> [<max>]
+//		# EXPERIMENTAL:
+//		drop
+//		fallback_sni          <server_name>
+//		insecure_secrets_log  <log_file>
+//	}
+func (cp *ConnectionPolicy) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
+	_, wrapper := d.Next(), d.Val()
+
+	// No same-line options are supported
+	if d.CountRemainingArgs() > 0 {
+		return d.ArgErr()
+	}
+
+	var hasCertSelection, hasClientAuth, hasDefaultSNI, hasDrop,
+		hasFallbackSNI, hasInsecureSecretsLog, hasMatch, hasProtocols bool
+	for nesting := d.Nesting(); d.NextBlock(nesting); {
+		optionName := d.Val()
+		switch optionName {
+		case "alpn":
+			if d.CountRemainingArgs() == 0 {
+				return d.ArgErr()
+			}
+			cp.ALPN = append(cp.ALPN, d.RemainingArgs()...)
+		case "cert_selection":
+			if hasCertSelection {
+				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
+			}
+			p := &CustomCertSelectionPolicy{}
+			if err := p.UnmarshalCaddyfile(d.NewFromNextSegment()); err != nil {
+				return err
+			}
+			cp.CertSelection, hasCertSelection = p, true
+		case "client_auth":
+			if hasClientAuth {
+				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
+			}
+			ca := &ClientAuthentication{}
+			if err := ca.UnmarshalCaddyfile(d.NewFromNextSegment()); err != nil {
+				return err
+			}
+			cp.ClientAuthentication, hasClientAuth = ca, true
+		case "ciphers":
+			if d.CountRemainingArgs() == 0 {
+				return d.ArgErr()
+			}
+			cp.CipherSuites = append(cp.CipherSuites, d.RemainingArgs()...)
+		case "curves":
+			if d.CountRemainingArgs() == 0 {
+				return d.ArgErr()
+			}
+			cp.Curves = append(cp.Curves, d.RemainingArgs()...)
+		case "default_sni":
+			if hasDefaultSNI {
+				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
+			}
+			if d.CountRemainingArgs() != 1 {
+				return d.ArgErr()
+			}
+			_, cp.DefaultSNI, hasDefaultSNI = d.NextArg(), d.Val(), true
+		case "drop": // EXPERIMENTAL
+			if hasDrop {
+				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
+			}
+			cp.Drop, hasDrop = true, true
+		case "fallback_sni": // EXPERIMENTAL
+			if hasFallbackSNI {
+				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
+			}
+			if d.CountRemainingArgs() != 1 {
+				return d.ArgErr()
+			}
+			_, cp.FallbackSNI, hasFallbackSNI = d.NextArg(), d.Val(), true
+		case "insecure_secrets_log": // EXPERIMENTAL
+			if hasInsecureSecretsLog {
+				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
+			}
+			if d.CountRemainingArgs() != 1 {
+				return d.ArgErr()
+			}
+			_, cp.InsecureSecretsLog, hasInsecureSecretsLog = d.NextArg(), d.Val(), true
+		case "match":
+			if hasMatch {
+				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
+			}
+			matcherSet, err := ParseCaddyfileNestedMatcherSet(d)
+			if err != nil {
+				return err
+			}
+			cp.MatchersRaw, hasMatch = matcherSet, true
+		case "protocols":
+			if hasProtocols {
+				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
+			}
+			if d.CountRemainingArgs() == 0 || d.CountRemainingArgs() > 2 {
+				return d.ArgErr()
+			}
+			_, cp.ProtocolMin, hasProtocols = d.NextArg(), d.Val(), true
+			if d.NextArg() {
+				cp.ProtocolMax = d.Val()
+			}
+		default:
+			return d.ArgErr()
+		}
+
+		// No nested blocks are supported
+		if d.NextBlock(nesting + 1) {
+			return d.Errf("malformed %s option '%s': blocks are not supported", wrapper, optionName)
+		}
+	}
+
+	return nil
+}
+
 // ClientAuthentication configures TLS client auth.
 type ClientAuthentication struct {
 	// Certificate authority module which provides the certificate pool of trusted certificates
@@ -819,4 +949,46 @@ func (d destructableWriter) Destruct() error { return d.Close() }
 
 var secretsLogPool = caddy.NewUsagePool()
 
-var _ caddyfile.Unmarshaler = (*ClientAuthentication)(nil)
+// Interface guards
+var (
+	_ caddyfile.Unmarshaler = (*ClientAuthentication)(nil)
+	_ caddyfile.Unmarshaler = (*ConnectionPolicy)(nil)
+)
+
+// ParseCaddyfileNestedMatcherSet parses the Caddyfile tokens for a nested
+// matcher set, and returns its raw module map value.
+func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, error) {
+	matcherMap := make(map[string]ConnectionMatcher)
+
+	tokensByMatcherName := make(map[string][]caddyfile.Token)
+	for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
+		matcherName := d.Val()
+		tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...)
+	}
+
+	for matcherName, tokens := range tokensByMatcherName {
+		dd := caddyfile.NewDispenser(tokens)
+		dd.Next() // consume wrapper name
+
+		unm, err := caddyfile.UnmarshalModule(dd, "tls.handshake_match."+matcherName)
+		if err != nil {
+			return nil, err
+		}
+		cm, ok := unm.(ConnectionMatcher)
+		if !ok {
+			return nil, fmt.Errorf("matcher module '%s' is not a connection matcher", matcherName)
+		}
+		matcherMap[matcherName] = cm
+	}
+
+	matcherSet := make(caddy.ModuleMap)
+	for name, matcher := range matcherMap {
+		jsonBytes, err := json.Marshal(matcher)
+		if err != nil {
+			return nil, fmt.Errorf("marshaling %T matcher: %v", matcher, err)
+		}
+		matcherSet[name] = jsonBytes
+	}
+
+	return matcherSet, nil
+}

From 9cc26ee7bf323f5fdffdfe7349794aa320a6ce6c Mon Sep 17 00:00:00 2001
From: Mohammed Al Sahaf <msaa1990@gmail.com>
Date: Sun, 28 Jul 2024 02:20:09 +0300
Subject: [PATCH 19/62] chore: update golangci config (#6479)

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
---
 .golangci.yml | 14 ++++++++++----
 1 file changed, 10 insertions(+), 4 deletions(-)

diff --git a/.golangci.yml b/.golangci.yml
index d144395db..74e3503c4 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,7 +1,9 @@
 linters-settings:
   errcheck:
-    ignore: fmt:.*,go.uber.org/zap/zapcore:^Add.*
-    ignoretests: true
+    exclude-functions:
+      - fmt.*
+      - (go.uber.org/zap/zapcore.ObjectEncoder).AddObject
+      - (go.uber.org/zap/zapcore.ObjectEncoder).AddArray
   gci:
     sections:
       - standard # Standard section: captures all standard packages.
@@ -130,13 +132,14 @@ linters:
 run:
   # default concurrency is a available CPU number.
   # concurrency: 4 # explicitly omit this value to fully utilize available resources.
-  deadline: 5m
+  timeout: 5m
   issues-exit-code: 1
   tests: false
 
 # output configuration options
 output:
-  format: 'colored-line-number'
+  formats:
+    - format: 'colored-line-number'
   print-issued-lines: true
   print-linter-name: true
 
@@ -166,3 +169,6 @@ issues:
     - path: modules/logging/filters.go
       linters:
         - dupl
+    - path: _test\.go
+      linters:
+        - errcheck

From 976469ca0dbbbd948dbfd6a4caedb1e7c914ef3d Mon Sep 17 00:00:00 2001
From: WeidiDeng <weidi_deng@icloud.com>
Date: Sun, 28 Jul 2024 07:46:56 +0800
Subject: [PATCH 20/62] encode: flush already compressed data from the encoder
 (#6471)

---
 modules/caddyhttp/encode/encode.go | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go
index cf3d17b69..00e507277 100644
--- a/modules/caddyhttp/encode/encode.go
+++ b/modules/caddyhttp/encode/encode.go
@@ -266,6 +266,14 @@ func (rw *responseWriter) FlushError() error {
 		// to rw.Write (see bug in #4314)
 		return nil
 	}
+	// also flushes the encoder, if any
+	// see: https://github.com/jjiang-stripe/caddy-slow-gzip
+	if rw.w != nil {
+		err := rw.w.Flush()
+		if err != nil {
+			return err
+		}
+	}
 	//nolint:bodyclose
 	return http.NewResponseController(rw.ResponseWriter).Flush()
 }
@@ -475,6 +483,7 @@ type encodingPreference struct {
 type Encoder interface {
 	io.WriteCloser
 	Reset(io.Writer)
+	Flush() error // encoder by default buffers data to maximize compressing rate
 }
 
 // Encoding is a type which can create encoders of its kind

From 840094ac65c2c27dbf0be63478d36969a57ce7e0 Mon Sep 17 00:00:00 2001
From: Prakhar Awasthi <53934224+pi-prakhar@users.noreply.github.com>
Date: Sat, 3 Aug 2024 22:21:50 +0530
Subject: [PATCH 21/62] proxyprotocol: Update WrapListener to use
 ConnPolicyFunc for PROXY protocol (#6485)

* proxyprotocol : Update WrapListener to use ConnPolicyFunc for PROXY protocol support

* proxyprotocol : Updated dependency pires/go-proxyproto to pseudo latest version
---
 go.mod                                             |  2 +-
 go.sum                                             |  2 ++
 modules/caddyhttp/proxyprotocol/listenerwrapper.go | 11 ++++++-----
 3 files changed, 9 insertions(+), 6 deletions(-)

diff --git a/go.mod b/go.mod
index f5559a8d9..d2fec7db0 100644
--- a/go.mod
+++ b/go.mod
@@ -123,7 +123,7 @@ require (
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/go-ps v1.0.0 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
-	github.com/pires/go-proxyproto v0.7.0
+	github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/prometheus/client_model v0.5.0 // indirect
 	github.com/prometheus/common v0.48.0 // indirect
diff --git a/go.sum b/go.sum
index 63306efc1..0ed480b2a 100644
--- a/go.sum
+++ b/go.sum
@@ -322,6 +322,8 @@ github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsU
 github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
 github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
 github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
+github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964 h1:ct/vxNBgHpASQ4sT8NaBX9LtsEtluZqaUJydLG50U3E=
+github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
diff --git a/modules/caddyhttp/proxyprotocol/listenerwrapper.go b/modules/caddyhttp/proxyprotocol/listenerwrapper.go
index e0d9b86ce..e25fe02a6 100644
--- a/modules/caddyhttp/proxyprotocol/listenerwrapper.go
+++ b/modules/caddyhttp/proxyprotocol/listenerwrapper.go
@@ -50,7 +50,7 @@ type ListenerWrapper struct {
 	// Policy definitions are here: https://pkg.go.dev/github.com/pires/go-proxyproto@v0.7.0#Policy
 	FallbackPolicy Policy `json:"fallback_policy,omitempty"`
 
-	policy goproxy.PolicyFunc
+	policy goproxy.ConnPolicyFunc
 }
 
 // Provision sets up the listener wrapper.
@@ -69,13 +69,14 @@ func (pp *ListenerWrapper) Provision(ctx caddy.Context) error {
 		}
 		pp.deny = append(pp.deny, ipnet)
 	}
-	pp.policy = func(upstream net.Addr) (goproxy.Policy, error) {
+
+	pp.policy = func(options goproxy.ConnPolicyOptions) (goproxy.Policy, error) {
 		// trust unix sockets
-		if network := upstream.Network(); caddy.IsUnixNetwork(network) {
+		if network := options.Upstream.Network(); caddy.IsUnixNetwork(network) {
 			return goproxy.USE, nil
 		}
 		ret := pp.FallbackPolicy
-		host, _, err := net.SplitHostPort(upstream.String())
+		host, _, err := net.SplitHostPort(options.Upstream.String())
 		if err != nil {
 			return goproxy.REJECT, err
 		}
@@ -106,6 +107,6 @@ func (pp *ListenerWrapper) WrapListener(l net.Listener) net.Listener {
 		Listener:          l,
 		ReadHeaderTimeout: time.Duration(pp.Timeout),
 	}
-	pl.Policy = pp.policy
+	pl.ConnPolicy = pp.policy
 	return pl
 }

From b1986781740b2b7f546c6ce56496c5cc7145674d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?lollipopkit=F0=9F=8F=B3=EF=B8=8F=E2=80=8D=E2=9A=A7?=
 =?UTF-8?q?=EF=B8=8F?= <10864310+lollipopkit@users.noreply.github.com>
Date: Mon, 5 Aug 2024 22:27:45 +0800
Subject: [PATCH 22/62] browse: Customizable default sort options (#6468)

* fileserver: add `sort` options

* fix: test

* fileserver: check options in `Provison`

* fileserver: more obvious err alerts in sort options
---
 .../file_server_sort.caddyfiletest            | 36 +++++++++++++++++++
 modules/caddyhttp/fileserver/browse.go        | 31 +++++++++++++---
 .../caddyhttp/fileserver/browsetplcontext.go  |  3 ++
 modules/caddyhttp/fileserver/caddyfile.go     | 11 ++++++
 modules/caddyhttp/fileserver/staticfiles.go   | 26 ++++++++++++++
 5 files changed, 103 insertions(+), 4 deletions(-)
 create mode 100644 caddytest/integration/caddyfile_adapt/file_server_sort.caddyfiletest

diff --git a/caddytest/integration/caddyfile_adapt/file_server_sort.caddyfiletest b/caddytest/integration/caddyfile_adapt/file_server_sort.caddyfiletest
new file mode 100644
index 000000000..62bfd0cba
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/file_server_sort.caddyfiletest
@@ -0,0 +1,36 @@
+:80
+
+file_server browse {
+	sort size desc
+}
+----------
+{
+	"apps": {
+		"http": {
+			"servers": {
+				"srv0": {
+					"listen": [
+						":80"
+					],
+					"routes": [
+						{
+							"handle": [
+								{
+									"browse": {},
+									"handler": "file_server",
+									"hide": [
+										"./Caddyfile"
+									],
+									"sort": [
+										"size",
+										"desc"
+									]
+								}
+							]
+						}
+					]
+				}
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/modules/caddyhttp/fileserver/browse.go b/modules/caddyhttp/fileserver/browse.go
index 8b2b48e77..bd02c584f 100644
--- a/modules/caddyhttp/fileserver/browse.go
+++ b/modules/caddyhttp/fileserver/browse.go
@@ -206,11 +206,34 @@ func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, fileSystem fs
 // browseApplyQueryParams applies query parameters to the listing.
 // It mutates the listing and may set cookies.
 func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Request, listing *browseTemplateContext) {
+	var orderParam, sortParam string
+
+	// The configs in Caddyfile have lower priority than Query params,
+	// so put it at first.
+	for idx, item := range fsrv.SortOptions {
+		// Only `sort` & `order`, 2 params are allowed
+		if idx >= 2 {
+			break
+		}
+		switch item {
+		case sortByName, sortByNameDirFirst, sortBySize, sortByTime:
+			sortParam = item
+		case sortOrderAsc, sortOrderDesc:
+			orderParam = item
+		}
+	}
+
 	layoutParam := r.URL.Query().Get("layout")
-	sortParam := r.URL.Query().Get("sort")
-	orderParam := r.URL.Query().Get("order")
 	limitParam := r.URL.Query().Get("limit")
 	offsetParam := r.URL.Query().Get("offset")
+	sortParamTmp := r.URL.Query().Get("sort")
+	if sortParamTmp != "" {
+		sortParam = sortParamTmp
+	}
+	orderParamTmp := r.URL.Query().Get("order")
+	if orderParamTmp != "" {
+		orderParam = orderParamTmp
+	}
 
 	switch layoutParam {
 	case "list", "grid", "":
@@ -233,11 +256,11 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re
 	// then figure out the order
 	switch orderParam {
 	case "":
-		orderParam = "asc"
+		orderParam = sortOrderAsc
 		if orderCookie, orderErr := r.Cookie("order"); orderErr == nil {
 			orderParam = orderCookie.Value
 		}
-	case "asc", "desc":
+	case sortOrderAsc, sortOrderDesc:
 		http.SetCookie(w, &http.Cookie{Name: "order", Value: orderParam, Secure: r.TLS != nil})
 	}
 
diff --git a/modules/caddyhttp/fileserver/browsetplcontext.go b/modules/caddyhttp/fileserver/browsetplcontext.go
index 81e260c71..0251bc581 100644
--- a/modules/caddyhttp/fileserver/browsetplcontext.go
+++ b/modules/caddyhttp/fileserver/browsetplcontext.go
@@ -373,4 +373,7 @@ const (
 	sortByNameDirFirst = "namedirfirst"
 	sortBySize         = "size"
 	sortByTime         = "time"
+
+	sortOrderAsc  = "asc"
+	sortOrderDesc = "desc"
 )
diff --git a/modules/caddyhttp/fileserver/caddyfile.go b/modules/caddyhttp/fileserver/caddyfile.go
index f65695018..603a828c9 100644
--- a/modules/caddyhttp/fileserver/caddyfile.go
+++ b/modules/caddyhttp/fileserver/caddyfile.go
@@ -171,6 +171,17 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 			}
 			fsrv.EtagFileExtensions = etagFileExtensions
 
+		case "sort":
+			for d.NextArg() {
+				dVal := d.Val()
+				switch dVal {
+				case sortByName, sortBySize, sortByTime, sortOrderAsc, sortOrderDesc:
+					fsrv.SortOptions = append(fsrv.SortOptions, dVal)
+				default:
+					return d.Errf("unknown sort option '%s'", dVal)
+				}
+			}
+
 		default:
 			return d.Errf("unknown subdirective '%s'", d.Val())
 		}
diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go
index 3d7032804..48812cfec 100644
--- a/modules/caddyhttp/fileserver/staticfiles.go
+++ b/modules/caddyhttp/fileserver/staticfiles.go
@@ -153,6 +153,16 @@ type FileServer struct {
 	// a 404 error. By default, this is false (disabled).
 	PassThru bool `json:"pass_thru,omitempty"`
 
+	// Override the default sort.
+	// It includes the following options:
+	//   - sort_by: name(default), namedirfirst, size, time
+	//   - order: asc(default), desc
+	// eg.:
+	//   - `sort time desc` will sort by time in descending order
+	//   - `sort size` will sort by size in ascending order
+	// The first option must be `sort_by` and the second option must be `order` (if exists).
+	SortOptions []string `json:"sort,omitempty"`
+
 	// Selection of encoders to use to check for precompressed files.
 	PrecompressedRaw caddy.ModuleMap `json:"precompressed,omitempty" caddy:"namespace=http.precompressed"`
 
@@ -236,6 +246,22 @@ func (fsrv *FileServer) Provision(ctx caddy.Context) error {
 		fsrv.precompressors[ae] = p
 	}
 
+	// check sort options
+	for idx, sortOption := range fsrv.SortOptions {
+		switch idx {
+		case 0:
+			if sortOption != sortByName && sortOption != sortByNameDirFirst && sortOption != sortBySize && sortOption != sortByTime {
+				return fmt.Errorf("the first option must be one of the following: %s, %s, %s, %s, but got %s", sortByName, sortByNameDirFirst, sortBySize, sortByTime, sortOption)
+			}
+		case 1:
+			if sortOption != sortOrderAsc && sortOption != sortOrderDesc {
+				return fmt.Errorf("the second option must be one of the following: %s, %s, but got %s", sortOrderAsc, sortOrderDesc, sortOption)
+			}
+		default:
+			return fmt.Errorf("only max 2 sort options are allowed, but got %d", idx+1)
+		}
+	}
+
 	return nil
 }
 

From a8b0dfa8da5616837f7e028578ddb9b7ad36b64e Mon Sep 17 00:00:00 2001
From: WeidiDeng <weidi_deng@icloud.com>
Date: Wed, 7 Aug 2024 12:08:32 +0800
Subject: [PATCH 23/62] go.mod: update quic-go package (#6498)

---
 go.mod                      | 2 +-
 go.sum                      | 6 ++----
 modules/caddyhttp/server.go | 1 +
 3 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/go.mod b/go.mod
index d2fec7db0..d4153c372 100644
--- a/go.mod
+++ b/go.mod
@@ -19,7 +19,7 @@ require (
 	github.com/klauspost/cpuid/v2 v2.2.7
 	github.com/mholt/acmez/v2 v2.0.1
 	github.com/prometheus/client_golang v1.19.1
-	github.com/quic-go/quic-go v0.44.0
+	github.com/quic-go/quic-go v0.46.0
 	github.com/smallstep/certificates v0.26.1
 	github.com/smallstep/nosql v0.6.1
 	github.com/smallstep/truststore v0.13.0
diff --git a/go.sum b/go.sum
index 0ed480b2a..2a8218fd5 100644
--- a/go.sum
+++ b/go.sum
@@ -320,8 +320,6 @@ github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8P
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
 github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
-github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
-github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
 github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964 h1:ct/vxNBgHpASQ4sT8NaBX9LtsEtluZqaUJydLG50U3E=
 github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -341,8 +339,8 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k
 github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
 github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
 github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
-github.com/quic-go/quic-go v0.44.0 h1:So5wOr7jyO4vzL2sd8/pD9Kesciv91zSk8BoFngItQ0=
-github.com/quic-go/quic-go v0.44.0/go.mod h1:z4cx/9Ny9UtGITIPzmPTXh1ULfOyWh4qGQlpnPcWmek=
+github.com/quic-go/quic-go v0.46.0 h1:uuwLClEEyk1DNvchH8uCByQVjo3yKL9opKulExNDs7Y=
+github.com/quic-go/quic-go v0.46.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
 github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go
index 96a819b40..6caaabcda 100644
--- a/modules/caddyhttp/server.go
+++ b/modules/caddyhttp/server.go
@@ -602,6 +602,7 @@ func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error
 			QUICConfig: &quic.Config{
 				Versions: []quic.Version{quic.Version1, quic.Version2},
 			},
+			IdleTimeout: time.Duration(s.IdleTimeout),
 			ConnContext: func(ctx context.Context, c quic.Connection) context.Context {
 				return context.WithValue(ctx, quicConnCtxKey, c)
 			},

From 59cbb2c83a03b6fe352ae0b5d05581d9148a4d24 Mon Sep 17 00:00:00 2001
From: vnxme <46669194+vnxme@users.noreply.github.com>
Date: Wed, 7 Aug 2024 20:02:23 +0300
Subject: [PATCH 24/62] caddytls,caddyhttp: Placeholders for some TLS and HTTP
 matchers (#6480)

* Runtime placeholders for caddytls matchers (1/3):

- remove IPs validation in UnmarshalCaddyfile

* Runtime placeholders for caddytls matchers (2/3):

- add placeholder replacement for IPs in Provision

* Runtime placeholders for caddytls matchers (3/3):

- add placeholder replacement for other strings

* Runtime placeholders for caddyhttp matchers (1/1):

- add placeholder replacement for IPs in Provision

* Runtime placeholders for caddyhttp/caddytls matchers:

- move PrivateRandesCIDR under internal
---
 internal/ranges.go                          | 14 +++++
 modules/caddyhttp/ip_matchers.go            |  7 ++-
 modules/caddyhttp/ip_range.go               | 16 +-----
 modules/caddyhttp/reverseproxy/caddyfile.go |  3 +-
 modules/caddytls/matchers.go                | 58 ++++++++++++---------
 5 files changed, 57 insertions(+), 41 deletions(-)
 create mode 100644 internal/ranges.go

diff --git a/internal/ranges.go b/internal/ranges.go
new file mode 100644
index 000000000..e9429e263
--- /dev/null
+++ b/internal/ranges.go
@@ -0,0 +1,14 @@
+package internal
+
+// PrivateRangesCIDR returns a list of private CIDR range
+// strings, which can be used as a configuration shortcut.
+func PrivateRangesCIDR() []string {
+	return []string{
+		"192.168.0.0/16",
+		"172.16.0.0/12",
+		"10.0.0.0/8",
+		"127.0.0.1/8",
+		"fd00::/8",
+		"::1",
+	}
+}
diff --git a/modules/caddyhttp/ip_matchers.go b/modules/caddyhttp/ip_matchers.go
index 9101a0357..2e735cb69 100644
--- a/modules/caddyhttp/ip_matchers.go
+++ b/modules/caddyhttp/ip_matchers.go
@@ -29,6 +29,7 @@ import (
 
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+	"github.com/caddyserver/caddy/v2/internal"
 )
 
 // MatchRemoteIP matches requests by the remote IP address,
@@ -79,7 +80,7 @@ func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 				return d.Err("the 'forwarded' option is no longer supported; use the 'client_ip' matcher instead")
 			}
 			if d.Val() == "private_ranges" {
-				m.Ranges = append(m.Ranges, PrivateRangesCIDR()...)
+				m.Ranges = append(m.Ranges, internal.PrivateRangesCIDR()...)
 				continue
 			}
 			m.Ranges = append(m.Ranges, d.Val())
@@ -173,7 +174,7 @@ func (m *MatchClientIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 	for d.Next() {
 		for d.NextArg() {
 			if d.Val() == "private_ranges" {
-				m.Ranges = append(m.Ranges, PrivateRangesCIDR()...)
+				m.Ranges = append(m.Ranges, internal.PrivateRangesCIDR()...)
 				continue
 			}
 			m.Ranges = append(m.Ranges, d.Val())
@@ -250,7 +251,9 @@ func (m MatchClientIP) Match(r *http.Request) bool {
 func provisionCidrsZonesFromRanges(ranges []string) ([]*netip.Prefix, []string, error) {
 	cidrs := []*netip.Prefix{}
 	zones := []string{}
+	repl := caddy.NewReplacer()
 	for _, str := range ranges {
+		str = repl.ReplaceAll(str, "")
 		// Exclude the zone_id from the IP
 		if strings.Contains(str, "%") {
 			split := strings.Split(str, "%")
diff --git a/modules/caddyhttp/ip_range.go b/modules/caddyhttp/ip_range.go
index b1db25475..7632e460c 100644
--- a/modules/caddyhttp/ip_range.go
+++ b/modules/caddyhttp/ip_range.go
@@ -22,6 +22,7 @@ import (
 
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+	"github.com/caddyserver/caddy/v2/internal"
 )
 
 func init() {
@@ -92,7 +93,7 @@ func (m *StaticIPRange) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 	}
 	for d.NextArg() {
 		if d.Val() == "private_ranges" {
-			m.Ranges = append(m.Ranges, PrivateRangesCIDR()...)
+			m.Ranges = append(m.Ranges, internal.PrivateRangesCIDR()...)
 			continue
 		}
 		m.Ranges = append(m.Ranges, d.Val())
@@ -121,19 +122,6 @@ func CIDRExpressionToPrefix(expr string) (netip.Prefix, error) {
 	return prefix, nil
 }
 
-// PrivateRangesCIDR returns a list of private CIDR range
-// strings, which can be used as a configuration shortcut.
-func PrivateRangesCIDR() []string {
-	return []string{
-		"192.168.0.0/16",
-		"172.16.0.0/12",
-		"10.0.0.0/8",
-		"127.0.0.1/8",
-		"fd00::/8",
-		"::1",
-	}
-}
-
 // Interface guards
 var (
 	_ caddy.Provisioner     = (*StaticIPRange)(nil)
diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go
index 1c3b49447..4ca5d0e0d 100644
--- a/modules/caddyhttp/reverseproxy/caddyfile.go
+++ b/modules/caddyhttp/reverseproxy/caddyfile.go
@@ -28,6 +28,7 @@ import (
 	"github.com/caddyserver/caddy/v2/caddyconfig"
 	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
 	"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
+	"github.com/caddyserver/caddy/v2/internal"
 	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
 	"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
 	"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
@@ -688,7 +689,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 		case "trusted_proxies":
 			for d.NextArg() {
 				if d.Val() == "private_ranges" {
-					h.TrustedProxies = append(h.TrustedProxies, caddyhttp.PrivateRangesCIDR()...)
+					h.TrustedProxies = append(h.TrustedProxies, internal.PrivateRangesCIDR()...)
 					continue
 				}
 				h.TrustedProxies = append(h.TrustedProxies, d.Val())
diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go
index a23a19317..83a464713 100644
--- a/modules/caddytls/matchers.go
+++ b/modules/caddytls/matchers.go
@@ -26,6 +26,7 @@ import (
 
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+	"github.com/caddyserver/caddy/v2/internal"
 )
 
 func init() {
@@ -49,8 +50,17 @@ func (MatchServerName) CaddyModule() caddy.ModuleInfo {
 
 // Match matches hello based on SNI.
 func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool {
+	// caddytls.TestServerNameMatcher calls this function without any context
+	var repl *caddy.Replacer
+	if ctx := hello.Context(); ctx != nil {
+		repl = ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
+	} else {
+		repl = caddy.NewReplacer()
+	}
+
 	for _, name := range m {
-		if certmagic.MatchWildcard(hello.ServerName, name) {
+		rs := repl.ReplaceAll(name, "")
+		if certmagic.MatchWildcard(hello.ServerName, rs) {
 			return true
 		}
 	}
@@ -107,16 +117,19 @@ func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo {
 
 // Provision parses m's IP ranges, either from IP or CIDR expressions.
 func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
+	repl := caddy.NewReplacer()
 	m.logger = ctx.Logger()
 	for _, str := range m.Ranges {
-		cidrs, err := m.parseIPRange(str)
+		rs := repl.ReplaceAll(str, "")
+		cidrs, err := m.parseIPRange(rs)
 		if err != nil {
 			return err
 		}
 		m.cidrs = append(m.cidrs, cidrs...)
 	}
 	for _, str := range m.NotRanges {
-		cidrs, err := m.parseIPRange(str)
+		rs := repl.ReplaceAll(str, "")
+		cidrs, err := m.parseIPRange(rs)
 		if err != nil {
 			return err
 		}
@@ -185,22 +198,18 @@ func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 
 		for d.NextArg() {
 			val := d.Val()
+			var exclamation bool
 			if len(val) > 1 && val[0] == '!' {
-				prefixes, err := m.parseIPRange(val[1:])
-				if err != nil {
-					return err
-				}
-				for _, prefix := range prefixes {
-					m.NotRanges = append(m.NotRanges, prefix.String())
-				}
+				exclamation, val = true, val[1:]
+			}
+			ranges := []string{val}
+			if val == "private_ranges" {
+				ranges = internal.PrivateRangesCIDR()
+			}
+			if exclamation {
+				m.NotRanges = append(m.NotRanges, ranges...)
 			} else {
-				prefixes, err := m.parseIPRange(val)
-				if err != nil {
-					return err
-				}
-				for _, prefix := range prefixes {
-					m.Ranges = append(m.Ranges, prefix.String())
-				}
+				m.Ranges = append(m.Ranges, ranges...)
 			}
 		}
 
@@ -233,9 +242,11 @@ func (MatchLocalIP) CaddyModule() caddy.ModuleInfo {
 
 // Provision parses m's IP ranges, either from IP or CIDR expressions.
 func (m *MatchLocalIP) Provision(ctx caddy.Context) error {
+	repl := caddy.NewReplacer()
 	m.logger = ctx.Logger()
 	for _, str := range m.Ranges {
-		cidrs, err := m.parseIPRange(str)
+		rs := repl.ReplaceAll(str, "")
+		cidrs, err := m.parseIPRange(rs)
 		if err != nil {
 			return err
 		}
@@ -300,13 +311,12 @@ func (m *MatchLocalIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 		}
 
 		for d.NextArg() {
-			prefixes, err := m.parseIPRange(d.Val())
-			if err != nil {
-				return err
-			}
-			for _, prefix := range prefixes {
-				m.Ranges = append(m.Ranges, prefix.String())
+			val := d.Val()
+			if val == "private_ranges" {
+				m.Ranges = append(m.Ranges, internal.PrivateRangesCIDR()...)
+				continue
 			}
+			m.Ranges = append(m.Ranges, val)
 		}
 
 		// No blocks are supported

From b85b6c64690dc75fd3096e452dbf8881456d0da7 Mon Sep 17 00:00:00 2001
From: Steffen Busch <37350514+steffenbusch@users.noreply.github.com>
Date: Wed, 7 Aug 2024 21:39:15 +0200
Subject: [PATCH 25/62] replacer: `{file.*}` global placeholder strips trailing
 newline (#6411)

Co-authored-by: Kanashimia <chad@redpilled.dev>
---
 .../foo_with_multiple_trailing_newlines.txt       |  2 ++
 .../testdata/foo_with_trailing_newline.txt        |  1 +
 replacer.go                                       |  3 +++
 replacer_test.go                                  | 15 +++++++++++++++
 4 files changed, 21 insertions(+)
 create mode 100644 caddytest/integration/testdata/foo_with_multiple_trailing_newlines.txt
 create mode 100644 caddytest/integration/testdata/foo_with_trailing_newline.txt

diff --git a/caddytest/integration/testdata/foo_with_multiple_trailing_newlines.txt b/caddytest/integration/testdata/foo_with_multiple_trailing_newlines.txt
new file mode 100644
index 000000000..75d7bfb87
--- /dev/null
+++ b/caddytest/integration/testdata/foo_with_multiple_trailing_newlines.txt
@@ -0,0 +1,2 @@
+foo
+
diff --git a/caddytest/integration/testdata/foo_with_trailing_newline.txt b/caddytest/integration/testdata/foo_with_trailing_newline.txt
new file mode 100644
index 000000000..257cc5642
--- /dev/null
+++ b/caddytest/integration/testdata/foo_with_trailing_newline.txt
@@ -0,0 +1 @@
+foo
diff --git a/replacer.go b/replacer.go
index e5d2913e9..65815c92a 100644
--- a/replacer.go
+++ b/replacer.go
@@ -15,6 +15,7 @@
 package caddy
 
 import (
+	"bytes"
 	"fmt"
 	"io"
 	"net/http"
@@ -354,6 +355,8 @@ func (f fileReplacementProvider) replace(key string) (any, bool) {
 			zap.Error(err))
 		return nil, true
 	}
+	body = bytes.TrimSuffix(body, []byte("\n"))
+	body = bytes.TrimSuffix(body, []byte("\r"))
 	return string(body), true
 }
 
diff --git a/replacer_test.go b/replacer_test.go
index cf4d321b6..1c1a7048f 100644
--- a/replacer_test.go
+++ b/replacer_test.go
@@ -431,6 +431,14 @@ func TestReplacerNew(t *testing.T) {
 			variable: "file.caddytest/integration/testdata/foo.txt",
 			value:    "foo",
 		},
+		{
+			variable: "file.caddytest/integration/testdata/foo_with_trailing_newline.txt",
+			value:    "foo",
+		},
+		{
+			variable: "file.caddytest/integration/testdata/foo_with_multiple_trailing_newlines.txt",
+			value:    "foo" + getEOL(),
+		},
 	} {
 		if val, ok := repl.providers[1].replace(tc.variable); ok {
 			if val != tc.value {
@@ -442,6 +450,13 @@ func TestReplacerNew(t *testing.T) {
 	}
 }
 
+func getEOL() string {
+	if os.PathSeparator == '\\' {
+		return "\r\n" // Windows EOL
+	}
+	return "\n" // Unix and modern macOS EOL
+}
+
 func TestReplacerNewWithoutFile(t *testing.T) {
 	repl := NewReplacer().WithoutFile()
 

From 52bad451819ca142e66a66e8424821f19cd96358 Mon Sep 17 00:00:00 2001
From: WeidiDeng <weidi_deng@icloud.com>
Date: Thu, 8 Aug 2024 09:52:50 +0800
Subject: [PATCH 26/62] go.mod: update update golang/x/net (#6500)

---
 go.mod | 14 +++++++-------
 go.sum | 28 ++++++++++++++--------------
 2 files changed, 21 insertions(+), 21 deletions(-)

diff --git a/go.mod b/go.mod
index d4153c372..e6dd928b9 100644
--- a/go.mod
+++ b/go.mod
@@ -37,11 +37,11 @@ require (
 	go.uber.org/automaxprocs v1.5.3
 	go.uber.org/zap v1.27.0
 	go.uber.org/zap/exp v0.2.0
-	golang.org/x/crypto v0.23.0
+	golang.org/x/crypto v0.26.0
 	golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595
-	golang.org/x/net v0.25.0
-	golang.org/x/sync v0.7.0
-	golang.org/x/term v0.20.0
+	golang.org/x/net v0.28.0
+	golang.org/x/sync v0.8.0
+	golang.org/x/term v0.23.0
 	golang.org/x/time v0.5.0
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 	gopkg.in/yaml.v3 v3.0.1
@@ -147,9 +147,9 @@ require (
 	go.step.sm/linkedca v0.20.1 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	golang.org/x/mod v0.17.0 // indirect
-	golang.org/x/sys v0.20.0
-	golang.org/x/text v0.15.0 // indirect
-	golang.org/x/tools v0.21.0 // indirect
+	golang.org/x/sys v0.23.0
+	golang.org/x/text v0.17.0 // indirect
+	golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
 	google.golang.org/grpc v1.63.2 // indirect
 	google.golang.org/protobuf v1.34.1 // indirect
 	howett.net/plist v1.0.0 // indirect
diff --git a/go.sum b/go.sum
index 2a8218fd5..5bf39b424 100644
--- a/go.sum
+++ b/go.sum
@@ -510,8 +510,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
-golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
+golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
 golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw=
 golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
@@ -532,15 +532,15 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
 golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
-golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
+golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
 golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
 golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
-golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -567,8 +567,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
-golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
+golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -576,8 +576,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
-golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
-golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
+golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
+golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -588,8 +588,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
-golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
+golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
 golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
 golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -603,8 +603,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
-golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
 golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

From 21af88fefc9a8239a024f635f1c6fdd9defd7eb7 Mon Sep 17 00:00:00 2001
From: WeidiDeng <weidi_deng@icloud.com>
Date: Thu, 8 Aug 2024 20:53:30 +0800
Subject: [PATCH 27/62] reverseproxy: Disable keep alive for h2c requests
 (#6343)

---
 modules/caddyhttp/reverseproxy/httptransport.go | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go
index d42453684..9a82341d0 100644
--- a/modules/caddyhttp/reverseproxy/httptransport.go
+++ b/modules/caddyhttp/reverseproxy/httptransport.go
@@ -446,6 +446,9 @@ func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
 	// if H2C ("HTTP/2 over cleartext") is enabled and the upstream request is
 	// HTTP without TLS, use the alternate H2C-capable transport instead
 	if req.URL.Scheme == "http" && h.h2cTransport != nil {
+		// There is no dedicated DisableKeepAlives field in *http2.Transport.
+		// This is an alternative way to disable keep-alive.
+		req.Close = h.Transport.DisableKeepAlives
 		return h.h2cTransport.RoundTrip(req)
 	}
 

From 7cf8376e638948490be3e9eb5c7d58ce2a4b93b3 Mon Sep 17 00:00:00 2001
From: vnxme <46669194+vnxme@users.noreply.github.com>
Date: Mon, 12 Aug 2024 10:01:09 +0300
Subject: [PATCH 28/62] matchers: fix a regression in #6480 (#6510)

The context may have no replacer
---
 modules/caddytls/matchers.go | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go
index 83a464713..f94622374 100644
--- a/modules/caddytls/matchers.go
+++ b/modules/caddytls/matchers.go
@@ -50,12 +50,13 @@ func (MatchServerName) CaddyModule() caddy.ModuleInfo {
 
 // Match matches hello based on SNI.
 func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool {
+	repl := caddy.NewReplacer()
 	// caddytls.TestServerNameMatcher calls this function without any context
-	var repl *caddy.Replacer
 	if ctx := hello.Context(); ctx != nil {
-		repl = ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
-	} else {
-		repl = caddy.NewReplacer()
+		// In some situations the existing context may have no replacer
+		if replAny := ctx.Value(caddy.ReplacerCtxKey); replAny != nil {
+			repl = replAny.(*caddy.Replacer)
+		}
 	}
 
 	for _, name := range m {

From 3a48b0336961e43e6f6f062bde7cecaa11043941 Mon Sep 17 00:00:00 2001
From: vnxme <46669194+vnxme@users.noreply.github.com>
Date: Mon, 12 Aug 2024 12:47:05 +0300
Subject: [PATCH 29/62] Move PrivateRangesCIDR() back: add a pass-through
 function (#6514)

---
 modules/caddyhttp/ip_range.go | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/modules/caddyhttp/ip_range.go b/modules/caddyhttp/ip_range.go
index 7632e460c..bfd76c14c 100644
--- a/modules/caddyhttp/ip_range.go
+++ b/modules/caddyhttp/ip_range.go
@@ -128,3 +128,10 @@ var (
 	_ caddyfile.Unmarshaler = (*StaticIPRange)(nil)
 	_ IPRangeSource         = (*StaticIPRange)(nil)
 )
+
+// PrivateRangesCIDR returns a list of private CIDR range
+// strings, which can be used as a configuration shortcut.
+// Note: this function is used at least by mholt/caddy-l4.
+func PrivateRangesCIDR() []string {
+	return internal.PrivateRangesCIDR()
+}

From 9ddb78fadcdbec89a609127918604174121dcf42 Mon Sep 17 00:00:00 2001
From: WeidiDeng <weidi_deng@icloud.com>
Date: Tue, 13 Aug 2024 10:26:20 +0800
Subject: [PATCH 30/62] cmd: ignore exec.ErrDot when starting caddy in
 background (#6512)

Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
---
 cmd/commandfuncs.go | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go
index 746cf3da6..49d0321ef 100644
--- a/cmd/commandfuncs.go
+++ b/cmd/commandfuncs.go
@@ -74,6 +74,10 @@ func cmdStart(fl Flags) (int, error) {
 	// sure by giving it some random bytes and having it echo
 	// them back to us)
 	cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String())
+	// we should be able to run caddy in relative paths
+	if errors.Is(cmd.Err, exec.ErrDot) {
+		cmd.Err = nil
+	}
 	if configFlag != "" {
 		cmd.Args = append(cmd.Args, "--config", configFlag)
 	}

From 043fe41ab8d39615f346b63c1e51bcf16feda1d8 Mon Sep 17 00:00:00 2001
From: Mohammed Al Sahaf <msaa1990@gmail.com>
Date: Sun, 18 Aug 2024 12:54:12 +0300
Subject: [PATCH 31/62] ci: don't exit early on error in remote CI machine
 (#6519)

---
 .github/workflows/ci.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a006d26b3..0c6846fc9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -150,6 +150,7 @@ jobs:
         uses: actions/checkout@v4
       - name: Run Tests
         run: |
+          set +e
           mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
 
           # short sha is enough?

From 54a0c8f94821f393edec1a1bc9459f2363c95117 Mon Sep 17 00:00:00 2001
From: Jesper Brix Rosenkilde <jbr@humppa.dk>
Date: Mon, 19 Aug 2024 18:55:55 +0200
Subject: [PATCH 32/62] reverseproxy: Active health checks request body option
 (#6520)

* Add an option to specify the body used for active health checks

* Replacer on request body
---
 ...reverse_proxy_health_reqbody.caddyfiletest | 40 +++++++++++++++++++
 modules/caddyhttp/reverseproxy/caddyfile.go   | 31 +++++++++-----
 .../caddyhttp/reverseproxy/healthchecks.go    | 19 +++++++--
 3 files changed, 78 insertions(+), 12 deletions(-)
 create mode 100644 caddytest/integration/caddyfile_adapt/reverse_proxy_health_reqbody.caddyfiletest

diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_health_reqbody.caddyfiletest b/caddytest/integration/caddyfile_adapt/reverse_proxy_health_reqbody.caddyfiletest
new file mode 100644
index 000000000..ae5a6791e
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_health_reqbody.caddyfiletest
@@ -0,0 +1,40 @@
+:8884
+
+reverse_proxy 127.0.0.1:65535 {
+	health_uri /health
+	health_request_body "test body"
+}
+----------
+{
+	"apps": {
+		"http": {
+			"servers": {
+				"srv0": {
+					"listen": [
+						":8884"
+					],
+					"routes": [
+						{
+							"handle": [
+								{
+									"handler": "reverse_proxy",
+									"health_checks": {
+										"active": {
+											"body": "test body",
+											"uri": "/health"
+										}
+									},
+									"upstreams": [
+										{
+											"dial": "127.0.0.1:65535"
+										}
+									]
+								}
+							]
+						}
+					]
+				}
+			}
+		}
+	}
+}
diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go
index 4ca5d0e0d..cd0e5d949 100644
--- a/modules/caddyhttp/reverseproxy/caddyfile.go
+++ b/modules/caddyhttp/reverseproxy/caddyfile.go
@@ -69,19 +69,20 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
 //	    lb_retry_match <request-matcher>
 //
 //	    # active health checking
-//	    health_uri      <uri>
-//	    health_port     <port>
-//	    health_interval <interval>
-//	    health_passes   <num>
-//	    health_fails    <num>
-//	    health_timeout  <duration>
-//	    health_status   <status>
-//	    health_body     <regexp>
+//	    health_uri          <uri>
+//	    health_port         <port>
+//	    health_interval     <interval>
+//	    health_passes       <num>
+//	    health_fails        <num>
+//	    health_timeout      <duration>
+//	    health_status       <status>
+//	    health_body         <regexp>
+//	    health_method       <value>
+//	    health_request_body <value>
 //	    health_follow_redirects
 //	    health_headers {
 //	        <field> [<values...>]
 //	    }
-//	    health_method   <value>
 //
 //	    # passive health checking
 //	    fail_duration     <duration>
@@ -425,6 +426,18 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 			}
 			h.HealthChecks.Active.Method = d.Val()
 
+		case "health_request_body":
+			if !d.NextArg() {
+				return d.ArgErr()
+			}
+			if h.HealthChecks == nil {
+				h.HealthChecks = new(HealthChecks)
+			}
+			if h.HealthChecks.Active == nil {
+				h.HealthChecks.Active = new(ActiveHealthChecks)
+			}
+			h.HealthChecks.Active.Body = d.Val()
+
 		case "health_interval":
 			if !d.NextArg() {
 				return d.ArgErr()
diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go
index 3b5a6a3af..efa1dbf09 100644
--- a/modules/caddyhttp/reverseproxy/healthchecks.go
+++ b/modules/caddyhttp/reverseproxy/healthchecks.go
@@ -24,6 +24,7 @@ import (
 	"regexp"
 	"runtime/debug"
 	"strconv"
+	"strings"
 	"time"
 
 	"go.uber.org/zap"
@@ -93,6 +94,9 @@ type ActiveHealthChecks struct {
 	// The HTTP method to use for health checks (default "GET").
 	Method string `json:"method,omitempty"`
 
+	// The body to send with the health check request.
+	Body string `json:"body,omitempty"`
+
 	// Whether to follow HTTP redirects in response to active health checks (default off).
 	FollowRedirects bool `json:"follow_redirects,omitempty"`
 
@@ -396,6 +400,16 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ
 		u.Path = h.HealthChecks.Active.Path
 	}
 
+	// replacer used for both body and headers. Only globals (env vars, system info, etc.) are available
+	repl := caddy.NewReplacer()
+
+	// if body is provided, create a reader for it, otherwise nil
+	var requestBody io.Reader
+	if h.HealthChecks.Active.Body != "" {
+		// set body, using replacer
+		requestBody = strings.NewReader(repl.ReplaceAll(h.HealthChecks.Active.Body, ""))
+	}
+
 	// attach dialing information to this request, as well as context values that
 	// may be expected by handlers of this request
 	ctx := h.ctx.Context
@@ -403,15 +417,14 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ
 	ctx = context.WithValue(ctx, caddyhttp.VarsCtxKey, map[string]any{
 		dialInfoVarKey: dialInfo,
 	})
-	req, err := http.NewRequestWithContext(ctx, h.HealthChecks.Active.Method, u.String(), nil)
+	req, err := http.NewRequestWithContext(ctx, h.HealthChecks.Active.Method, u.String(), requestBody)
 	if err != nil {
 		return fmt.Errorf("making request: %v", err)
 	}
 	ctx = context.WithValue(ctx, caddyhttp.OriginalRequestCtxKey, *req)
 	req = req.WithContext(ctx)
 
-	// set headers, using a replacer with only globals (env vars, system info, etc.)
-	repl := caddy.NewReplacer()
+	// set headers, using replacer
 	repl.Set("http.reverse_proxy.active.target_upstream", networkAddr)
 	for key, vals := range h.HealthChecks.Active.Headers {
 		key = repl.ReplaceAll(key, "")

From 2bb2ecc5498c99d535f5b8f56fb8a4732e818ad3 Mon Sep 17 00:00:00 2001
From: Jens-Uwe Mager <jum@anubis.han.de>
Date: Wed, 21 Aug 2024 19:39:20 +0200
Subject: [PATCH 33/62] reverseproxy: Change errors writing the response to
 warning. (#6532)

Most of the errors that can be seen here are write errors due to clients
aborting the request from their side. Often seen ones include:

	* writing: ... write: broken pipe
	* writing: ... connection timed out
	* writing: http2: stream closed
	* writing: timeout...
	* writing: h3 error...

Most of these errors are beyond of the control of caddy on the client side,
probably nothing can be done on the server side. It still warrants
researching when these errors occur very often, so a change in level from
error to warn is better here to not polute the logs with errors in the
normal case.
---
 modules/caddyhttp/reverseproxy/reverseproxy.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go
index 4f97edead..1883ac072 100644
--- a/modules/caddyhttp/reverseproxy/reverseproxy.go
+++ b/modules/caddyhttp/reverseproxy/reverseproxy.go
@@ -979,7 +979,7 @@ func (h *Handler) finalizeResponse(
 		// we'll just log the error and abort the stream here and panic just as
 		// the standard lib's proxy to propagate the stream error.
 		// see issue https://github.com/caddyserver/caddy/issues/5951
-		logger.Error("aborting with incomplete response", zap.Error(err))
+		logger.Warn("aborting with incomplete response", zap.Error(err))
 		// no extra logging from stdlib
 		panic(http.ErrAbortHandler)
 	}

From 8ccfedf2bb0a1759aa68d4989f05d9a52ba41a34 Mon Sep 17 00:00:00 2001
From: a <a@tuxpa.in>
Date: Wed, 21 Aug 2024 22:29:42 -0500
Subject: [PATCH 34/62] cmd: Use a factory to create the caddy root command
 (#6533)

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
---
 cmd/cobra.go          |  26 ++++++-----
 cmd/commandfactory.go |  28 ++++++++++++
 cmd/commands.go       | 100 ++++++++++++++++++++++--------------------
 cmd/main.go           |   2 +-
 4 files changed, 96 insertions(+), 60 deletions(-)
 create mode 100644 cmd/commandfactory.go

diff --git a/cmd/cobra.go b/cmd/cobra.go
index 1a2509206..9ecb389e2 100644
--- a/cmd/cobra.go
+++ b/cmd/cobra.go
@@ -8,9 +8,10 @@ import (
 	"github.com/caddyserver/caddy/v2"
 )
 
-var rootCmd = &cobra.Command{
-	Use: "caddy",
-	Long: `Caddy is an extensible server platform written in Go.
+var defaultFactory = newRootCommandFactory(func() *cobra.Command {
+	return &cobra.Command{
+		Use: "caddy",
+		Long: `Caddy is an extensible server platform written in Go.
 
 At its core, Caddy merely manages configuration. Modules are plugged
 in statically at compile-time to provide useful functionality. Caddy's
@@ -91,23 +92,26 @@ package installers: https://caddyserver.com/docs/install
 Instructions for running Caddy in production are also available:
 https://caddyserver.com/docs/running
 `,
-	Example: `  $ caddy run
+		Example: `  $ caddy run
   $ caddy run --config caddy.json
   $ caddy reload --config caddy.json
   $ caddy stop`,
 
-	// kind of annoying to have all the help text printed out if
-	// caddy has an error provisioning its modules, for instance...
-	SilenceUsage: true,
-	Version:      onlyVersionText(),
-}
+		// kind of annoying to have all the help text printed out if
+		// caddy has an error provisioning its modules, for instance...
+		SilenceUsage: true,
+		Version:      onlyVersionText(),
+	}
+})
 
 const fullDocsFooter = `Full documentation is available at:
 https://caddyserver.com/docs/command-line`
 
 func init() {
-	rootCmd.SetVersionTemplate("{{.Version}}\n")
-	rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n")
+	defaultFactory.Use(func(rootCmd *cobra.Command) {
+		rootCmd.SetVersionTemplate("{{.Version}}\n")
+		rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n")
+	})
 }
 
 func onlyVersionText() string {
diff --git a/cmd/commandfactory.go b/cmd/commandfactory.go
new file mode 100644
index 000000000..ac571a21a
--- /dev/null
+++ b/cmd/commandfactory.go
@@ -0,0 +1,28 @@
+package caddycmd
+
+import (
+	"github.com/spf13/cobra"
+)
+
+type rootCommandFactory struct {
+	constructor func() *cobra.Command
+	options     []func(*cobra.Command)
+}
+
+func newRootCommandFactory(fn func() *cobra.Command) *rootCommandFactory {
+	return &rootCommandFactory{
+		constructor: fn,
+	}
+}
+
+func (f *rootCommandFactory) Use(fn func(cmd *cobra.Command)) {
+	f.options = append(f.options, fn)
+}
+
+func (f *rootCommandFactory) Build() *cobra.Command {
+	o := f.constructor()
+	for _, v := range f.options {
+		v(o)
+	}
+	return o
+}
diff --git a/cmd/commands.go b/cmd/commands.go
index e5e1265e4..0853ebf83 100644
--- a/cmd/commands.go
+++ b/cmd/commands.go
@@ -438,43 +438,44 @@ EXPERIMENTAL: May be changed or removed.
 		},
 	})
 
-	RegisterCommand(Command{
-		Name:  "manpage",
-		Usage: "--directory <path>",
-		Short: "Generates the manual pages for Caddy commands",
-		Long: `
+	defaultFactory.Use(func(rootCmd *cobra.Command) {
+		RegisterCommand(Command{
+			Name:  "manpage",
+			Usage: "--directory <path>",
+			Short: "Generates the manual pages for Caddy commands",
+			Long: `
 Generates the manual pages for Caddy commands into the designated directory
 tagged into section 8 (System Administration).
 
 The manual page files are generated into the directory specified by the
 argument of --directory. If the directory does not exist, it will be created.
 `,
-		CobraFunc: func(cmd *cobra.Command) {
-			cmd.Flags().StringP("directory", "o", "", "The output directory where the manpages are generated")
-			cmd.RunE = WrapCommandFuncForCobra(func(fl Flags) (int, error) {
-				dir := strings.TrimSpace(fl.String("directory"))
-				if dir == "" {
-					return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required")
-				}
-				if err := os.MkdirAll(dir, 0o755); err != nil {
-					return caddy.ExitCodeFailedQuit, err
-				}
-				if err := doc.GenManTree(rootCmd, &doc.GenManHeader{
-					Title:   "Caddy",
-					Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections
-				}, dir); err != nil {
-					return caddy.ExitCodeFailedQuit, err
-				}
-				return caddy.ExitCodeSuccess, nil
-			})
-		},
-	})
+			CobraFunc: func(cmd *cobra.Command) {
+				cmd.Flags().StringP("directory", "o", "", "The output directory where the manpages are generated")
+				cmd.RunE = WrapCommandFuncForCobra(func(fl Flags) (int, error) {
+					dir := strings.TrimSpace(fl.String("directory"))
+					if dir == "" {
+						return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required")
+					}
+					if err := os.MkdirAll(dir, 0o755); err != nil {
+						return caddy.ExitCodeFailedQuit, err
+					}
+					if err := doc.GenManTree(rootCmd, &doc.GenManHeader{
+						Title:   "Caddy",
+						Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections
+					}, dir); err != nil {
+						return caddy.ExitCodeFailedQuit, err
+					}
+					return caddy.ExitCodeSuccess, nil
+				})
+			},
+		})
 
-	// source: https://github.com/spf13/cobra/blob/main/shell_completions.md
-	rootCmd.AddCommand(&cobra.Command{
-		Use:   "completion [bash|zsh|fish|powershell]",
-		Short: "Generate completion script",
-		Long: fmt.Sprintf(`To load completions:
+		// source: https://github.com/spf13/cobra/blob/main/shell_completions.md
+		rootCmd.AddCommand(&cobra.Command{
+			Use:   "completion [bash|zsh|fish|powershell]",
+			Short: "Generate completion script",
+			Long: fmt.Sprintf(`To load completions:
 
 	Bash:
 
@@ -513,23 +514,24 @@ argument of --directory. If the directory does not exist, it will be created.
 	  PS> %[1]s completion powershell > %[1]s.ps1
 	  # and source this file from your PowerShell profile.
 	`, rootCmd.Root().Name()),
-		DisableFlagsInUseLine: true,
-		ValidArgs:             []string{"bash", "zsh", "fish", "powershell"},
-		Args:                  cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
-		RunE: func(cmd *cobra.Command, args []string) error {
-			switch args[0] {
-			case "bash":
-				return cmd.Root().GenBashCompletion(os.Stdout)
-			case "zsh":
-				return cmd.Root().GenZshCompletion(os.Stdout)
-			case "fish":
-				return cmd.Root().GenFishCompletion(os.Stdout, true)
-			case "powershell":
-				return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
-			default:
-				return fmt.Errorf("unrecognized shell: %s", args[0])
-			}
-		},
+			DisableFlagsInUseLine: true,
+			ValidArgs:             []string{"bash", "zsh", "fish", "powershell"},
+			Args:                  cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
+			RunE: func(cmd *cobra.Command, args []string) error {
+				switch args[0] {
+				case "bash":
+					return cmd.Root().GenBashCompletion(os.Stdout)
+				case "zsh":
+					return cmd.Root().GenZshCompletion(os.Stdout)
+				case "fish":
+					return cmd.Root().GenFishCompletion(os.Stdout, true)
+				case "powershell":
+					return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
+				default:
+					return fmt.Errorf("unrecognized shell: %s", args[0])
+				}
+			},
+		})
 	})
 }
 
@@ -563,7 +565,9 @@ func RegisterCommand(cmd Command) {
 	if !commandNameRegex.MatchString(cmd.Name) {
 		panic("invalid command name")
 	}
-	rootCmd.AddCommand(caddyCmdToCobra(cmd))
+	defaultFactory.Use(func(rootCmd *cobra.Command) {
+		rootCmd.AddCommand(caddyCmdToCobra(cmd))
+	})
 }
 
 var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`)
diff --git a/cmd/main.go b/cmd/main.go
index 3c3ae6270..655c0084b 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -72,7 +72,7 @@ func Main() {
 		caddy.Log().Warn("failed to set GOMAXPROCS", zap.Error(err))
 	}
 
-	if err := rootCmd.Execute(); err != nil {
+	if err := defaultFactory.Build().Execute(); err != nil {
 		var exitError *exitError
 		if errors.As(err, &exitError) {
 			os.Exit(exitError.ExitCode)

From 098897bdea67eb31634f440a4d9a69b5753a9ac3 Mon Sep 17 00:00:00 2001
From: Cuckoo Chickoo <sunnyagain@users.noreply.github.com>
Date: Thu, 22 Aug 2024 15:45:58 +0530
Subject: [PATCH 35/62] chore: Fix a typo (#6534)

Fixes Typo in Docs
---
 modules/caddyhttp/proxyprotocol/listenerwrapper.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/caddyhttp/proxyprotocol/listenerwrapper.go b/modules/caddyhttp/proxyprotocol/listenerwrapper.go
index e25fe02a6..440e70710 100644
--- a/modules/caddyhttp/proxyprotocol/listenerwrapper.go
+++ b/modules/caddyhttp/proxyprotocol/listenerwrapper.go
@@ -40,7 +40,7 @@ type ListenerWrapper struct {
 	Allow []string `json:"allow,omitempty"`
 	allow []netip.Prefix
 
-	// Denby is an optional list of CIDR ranges to
+	// Deny is an optional list of CIDR ranges to
 	// deny PROXY headers from.
 	Deny []string `json:"deny,omitempty"`
 	deny []netip.Prefix

From 8af646730be93f4a00b873d1822bfde6be106696 Mon Sep 17 00:00:00 2001
From: Mohammed Al Sahaf <msaa1990@gmail.com>
Date: Thu, 22 Aug 2024 20:32:44 +0300
Subject: [PATCH 36/62] caddyhttp: run `error` (msg) through replacer (#6536)

* error: run `error` (msg) through replacer

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* fix integration test

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

---------

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
---
 caddytest/caddytest_test.go      | 1 -
 modules/caddyhttp/staticerror.go | 3 +--
 2 files changed, 1 insertion(+), 3 deletions(-)

diff --git a/caddytest/caddytest_test.go b/caddytest/caddytest_test.go
index 937537faa..a9d5da936 100644
--- a/caddytest/caddytest_test.go
+++ b/caddytest/caddytest_test.go
@@ -84,7 +84,6 @@ func TestLoadUnorderedJSON(t *testing.T) {
 				"servers": {
 					"s_server": {
 						"listen": [
-							":9443",
 							":9080"
 						],
 						"routes": [
diff --git a/modules/caddyhttp/staticerror.go b/modules/caddyhttp/staticerror.go
index b6e10ff32..aeb311406 100644
--- a/modules/caddyhttp/staticerror.go
+++ b/modules/caddyhttp/staticerror.go
@@ -105,8 +105,7 @@ func (e StaticError) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler
 		}
 		statusCode = intVal
 	}
-
-	return Error(statusCode, fmt.Errorf("%s", e.Error))
+	return Error(statusCode, fmt.Errorf("%s", repl.ReplaceKnown(e.Error, "")))
 }
 
 // Interface guard

From 4ade967005929e98ae2265d9d7c89b33f1ca951b Mon Sep 17 00:00:00 2001
From: Mohammed Al Sahaf <msaa1990@gmail.com>
Date: Thu, 22 Aug 2024 22:52:05 +0300
Subject: [PATCH 37/62] reverseproxy: allow user to define source address
 (#6504)

* reverseproxy: allow user to define source address

Closes #6503

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* reverse_proxy: caddyfile support for local_address

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

---------

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
---
 .../reverse_proxy_localaddr.caddyfiletest     | 57 +++++++++++++++++++
 modules/caddyhttp/reverseproxy/caddyfile.go   |  6 +-
 .../caddyhttp/reverseproxy/httptransport.go   | 29 ++++++++++
 3 files changed, 91 insertions(+), 1 deletion(-)
 create mode 100644 caddytest/integration/caddyfile_adapt/reverse_proxy_localaddr.caddyfiletest

diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_localaddr.caddyfiletest b/caddytest/integration/caddyfile_adapt/reverse_proxy_localaddr.caddyfiletest
new file mode 100644
index 000000000..d734c9ce0
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_localaddr.caddyfiletest
@@ -0,0 +1,57 @@
+https://example.com {
+	reverse_proxy http://localhost:54321 {
+		transport http {
+			local_address 192.168.0.1
+		}
+	}
+}
+
+----------
+{
+	"apps": {
+		"http": {
+			"servers": {
+				"srv0": {
+					"listen": [
+						":443"
+					],
+					"routes": [
+						{
+							"match": [
+								{
+									"host": [
+										"example.com"
+									]
+								}
+							],
+							"handle": [
+								{
+									"handler": "subroute",
+									"routes": [
+										{
+											"handle": [
+												{
+													"handler": "reverse_proxy",
+													"transport": {
+														"local_address": "192.168.0.1",
+														"protocol": "http"
+													},
+													"upstreams": [
+														{
+															"dial": "localhost:54321"
+														}
+													]
+												}
+											]
+										}
+									]
+								}
+							],
+							"terminal": true
+						}
+					]
+				}
+			}
+		}
+	}
+}
diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go
index cd0e5d949..12e2b9b97 100644
--- a/modules/caddyhttp/reverseproxy/caddyfile.go
+++ b/modules/caddyhttp/reverseproxy/caddyfile.go
@@ -1326,7 +1326,11 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 				return d.Err("cannot specify \"tls_trust_pool\" twice in caddyfile")
 			}
 			h.TLS.CARaw = caddyconfig.JSONModuleObject(ca, "provider", modStem, nil)
-
+		case "local_address":
+			if !d.NextArg() {
+				return d.ArgErr()
+			}
+			h.LocalAddress = d.Val()
 		default:
 			return d.Errf("unrecognized subdirective %s", d.Val())
 		}
diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go
index 9a82341d0..9929ae5d1 100644
--- a/modules/caddyhttp/reverseproxy/httptransport.go
+++ b/modules/caddyhttp/reverseproxy/httptransport.go
@@ -132,6 +132,10 @@ type HTTPTransport struct {
 	// to change or removal while experimental.
 	Versions []string `json:"versions,omitempty"`
 
+	// Specify the address to bind to when connecting to an upstream. In other words,
+	// it is the address the upstream sees as the remote address.
+	LocalAddress string `json:"local_address,omitempty"`
+
 	// The pre-configured underlying HTTP transport.
 	Transport *http.Transport `json:"-"`
 
@@ -185,6 +189,31 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
 		FallbackDelay: time.Duration(h.FallbackDelay),
 	}
 
+	if h.LocalAddress != "" {
+		netaddr, err := caddy.ParseNetworkAddressWithDefaults(h.LocalAddress, "tcp", 0)
+		if err != nil {
+			return nil, err
+		}
+		if netaddr.PortRangeSize() > 1 {
+			return nil, fmt.Errorf("local_address must be a single address, not a port range")
+		}
+		switch netaddr.Network {
+		case "tcp", "tcp4", "tcp6":
+			dialer.LocalAddr, err = net.ResolveTCPAddr(netaddr.Network, netaddr.JoinHostPort(0))
+			if err != nil {
+				return nil, err
+			}
+		case "unix", "unixgram", "unixpacket":
+			dialer.LocalAddr, err = net.ResolveUnixAddr(netaddr.Network, netaddr.JoinHostPort(0))
+			if err != nil {
+				return nil, err
+			}
+		case "udp", "udp4", "udp6":
+			return nil, fmt.Errorf("local_address must be a TCP address, not a UDP address")
+		default:
+			return nil, fmt.Errorf("unsupported network")
+		}
+	}
 	if h.Resolver != nil {
 		err := h.Resolver.ParseAddresses()
 		if err != nil {

From 2028da4e74cd41f0f7f94222c6599da1a371d4b8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= <kevin@dunglas.fr>
Date: Fri, 23 Aug 2024 19:01:28 +0200
Subject: [PATCH 38/62] ci: build and test with Go 1.23 (#6526)

* chore: build and test with Go 1.23

* ci: bump golangci-lint to v1.60

* fix: make properly wrap errors

* ci: remove Go 1.21
---
 .github/workflows/ci.yml           | 8 ++++----
 .github/workflows/cross-build.yml  | 4 ++++
 .github/workflows/lint.yml         | 6 +++---
 .github/workflows/release.yml      | 6 +++---
 README.md                          | 2 +-
 caddyconfig/caddyfile/dispenser.go | 2 +-
 go.mod                             | 4 ++--
 modules/caddyhttp/celmatcher.go    | 4 ++--
 8 files changed, 20 insertions(+), 16 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0c6846fc9..2c7a67c5b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -23,18 +23,18 @@ jobs:
           - mac
           - windows
         go: 
-          - '1.21'
           - '1.22'
+          - '1.23'
 
         include:
         # Set the minimum Go patch version for the given Go minor
         # Usable via ${{ matrix.GO_SEMVER }}
-        - go: '1.21'
-          GO_SEMVER: '~1.21.0'
-
         - go: '1.22'
           GO_SEMVER: '~1.22.3'
 
+        - go: '1.23'
+          GO_SEMVER: '~1.23.0'
+
         # Set some variables per OS, usable via ${{ matrix.VAR }}
         # OS_LABEL: the VM label from GitHub Actions (see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories)
         # CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
diff --git a/.github/workflows/cross-build.yml b/.github/workflows/cross-build.yml
index c54a9a80d..e77e4e992 100644
--- a/.github/workflows/cross-build.yml
+++ b/.github/workflows/cross-build.yml
@@ -28,6 +28,7 @@ jobs:
           - 'netbsd'
         go: 
           - '1.22'
+          - '1.23'
 
         include:
         # Set the minimum Go patch version for the given Go minor
@@ -35,6 +36,9 @@ jobs:
         - go: '1.22'
           GO_SEMVER: '~1.22.3'
 
+        - go: '1.23'
+          GO_SEMVER: '~1.23.0'
+
     runs-on: ubuntu-latest
     continue-on-error: true
     steps:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 1ebdb0afe..94250f88f 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -43,13 +43,13 @@ jobs:
       - uses: actions/checkout@v4
       - uses: actions/setup-go@v5
         with:
-          go-version: '~1.22.3'
+          go-version: '~1.23'
           check-latest: true
 
       - name: golangci-lint
         uses: golangci/golangci-lint-action@v6
         with:
-          version: v1.55
+          version: v1.60
 
           # Windows times out frequently after about 5m50s if we don't set a longer timeout.
           args: --timeout 10m
@@ -63,5 +63,5 @@ jobs:
       - name: govulncheck
         uses: golang/govulncheck-action@v1
         with:
-          go-version-input: '~1.22.3'
+          go-version-input: '~1.23.0'
           check-latest: true
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index cb5d750dc..1eb59e9d0 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -13,13 +13,13 @@ jobs:
         os: 
           - ubuntu-latest
         go: 
-          - '1.22'
+          - '1.23'
 
         include:
         # Set the minimum Go patch version for the given Go minor
         # Usable via ${{ matrix.GO_SEMVER }}
-        - go: '1.22'
-          GO_SEMVER: '~1.22.3'
+        - go: '1.23'
+          GO_SEMVER: '~1.23.0'
 
     runs-on: ${{ matrix.os }}
     # https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
diff --git a/README.md b/README.md
index 57463180f..2185eccd8 100644
--- a/README.md
+++ b/README.md
@@ -87,7 +87,7 @@ See [our online documentation](https://caddyserver.com/docs/install) for other i
 
 Requirements:
 
-- [Go 1.21 or newer](https://golang.org/dl/)
+- [Go 1.22.3 or newer](https://golang.org/dl/)
 
 ### For development
 
diff --git a/caddyconfig/caddyfile/dispenser.go b/caddyconfig/caddyfile/dispenser.go
index e36275a1c..325bb54d3 100644
--- a/caddyconfig/caddyfile/dispenser.go
+++ b/caddyconfig/caddyfile/dispenser.go
@@ -415,7 +415,7 @@ func (d *Dispenser) EOFErr() error {
 
 // Err generates a custom parse-time error with a message of msg.
 func (d *Dispenser) Err(msg string) error {
-	return d.Errf(msg)
+	return d.WrapErr(errors.New(msg))
 }
 
 // Errf is like Err, but for formatted error messages
diff --git a/go.mod b/go.mod
index e6dd928b9..6ae4a4a8e 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,8 @@
 module github.com/caddyserver/caddy/v2
 
-go 1.21.0
+go 1.22.3
 
-toolchain go1.22.2
+toolchain go1.23.0
 
 require (
 	github.com/BurntSushi/toml v1.3.2
diff --git a/modules/caddyhttp/celmatcher.go b/modules/caddyhttp/celmatcher.go
index d4016478e..a5565eb98 100644
--- a/modules/caddyhttp/celmatcher.go
+++ b/modules/caddyhttp/celmatcher.go
@@ -340,7 +340,7 @@ func (celTypeAdapter) NativeToValue(value any) ref.Val {
 	case time.Time:
 		return types.Timestamp{Time: v}
 	case error:
-		types.NewErr(v.Error())
+		return types.WrapErr(v)
 	}
 	return types.DefaultTypeAdapter.NativeToValue(value)
 }
@@ -499,7 +499,7 @@ func CELMatcherRuntimeFunction(funcName string, fac CELMatcherFactory) functions
 	return func(celReq, matcherData ref.Val) ref.Val {
 		matcher, err := fac(matcherData)
 		if err != nil {
-			return types.NewErr(err.Error())
+			return types.WrapErr(err)
 		}
 		httpReq := celReq.Value().(celHTTPRequest)
 		return types.Bool(matcher.Match(httpReq.Request))

From dcbf38d0b370cc0f412157b11961dd0b0e007251 Mon Sep 17 00:00:00 2001
From: Bas Westerbaan <bas@westerbaan.name>
Date: Wed, 28 Aug 2024 01:08:16 +0200
Subject: [PATCH 39/62] tls: use Go default kex for the moment that include PQC
 (#6542)

By default Go 1.23 enables X25519Kyber768, a post-quantum key agreement
method that is enabled by default on Chrome. Go 1.23 does not expose
the CurveID, so we cannot add it by specifying it in CurvePreferences.
The reason is that X25519Kyber768 is a preliminary key agreement that
will be supplanted by X25519MLKEM768. For the moment there is value
in enabling it.

A consequence of this is that by default Caddy will enable support
for P-384 and P-521.

This PR also removes the special code to add support for X25519Kyber768
via the Cloudflare Go branch.

Cf #6540
---
 cmd/caddy/main.go              |  5 +++++
 modules/caddytls/cf.go         | 24 ------------------------
 modules/caddytls/connpolicy.go | 10 +++++++++-
 modules/caddytls/values.go     |  5 +++++
 4 files changed, 19 insertions(+), 25 deletions(-)
 delete mode 100644 modules/caddytls/cf.go

diff --git a/cmd/caddy/main.go b/cmd/caddy/main.go
index 48fa149aa..f1aeda0a4 100644
--- a/cmd/caddy/main.go
+++ b/cmd/caddy/main.go
@@ -1,3 +1,8 @@
+// The below line is required to enable post-quantum key agreement in Go 1.23
+// by default without insisting on setting a minimum version of 1.23 in go.mod.
+// See https://github.com/caddyserver/caddy/issues/6540#issuecomment-2313094905
+//go:debug tlskyber=1
+
 // Copyright 2015 Matthew Holt and The Caddy Authors
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/modules/caddytls/cf.go b/modules/caddytls/cf.go
deleted file mode 100644
index e61a59c09..000000000
--- a/modules/caddytls/cf.go
+++ /dev/null
@@ -1,24 +0,0 @@
-//go:build cfgo
-
-package caddytls
-
-// This file adds support for X25519Kyber768Draft00, a post-quantum
-// key agreement that is currently being rolled out by Chrome [1]
-// and Cloudflare [2,3]. For more context, see the PR [4].
-//
-// [1] https://blog.chromium.org/2023/08/protecting-chrome-traffic-with-hybrid.html
-// [2] https://blog.cloudflare.com/post-quantum-for-all/
-// [3] https://blog.cloudflare.com/post-quantum-to-origins/
-// [4] https://github.com/caddyserver/caddy/pull/5852
-
-import (
-	"crypto/tls"
-)
-
-func init() {
-	SupportedCurves["X25519Kyber768Draft00"] = tls.X25519Kyber768Draft00
-	defaultCurves = append(
-		[]tls.CurveID{tls.X25519Kyber768Draft00},
-		defaultCurves...,
-	)
-}
diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go
index 4ec0e673a..e2890c848 100644
--- a/modules/caddytls/connpolicy.go
+++ b/modules/caddytls/connpolicy.go
@@ -841,7 +841,15 @@ func setDefaultTLSParams(cfg *tls.Config) {
 	cfg.CipherSuites = append([]uint16{tls.TLS_FALLBACK_SCSV}, cfg.CipherSuites...)
 
 	if len(cfg.CurvePreferences) == 0 {
-		cfg.CurvePreferences = defaultCurves
+		// We would want to write
+		//
+		//	cfg.CurvePreferences = defaultCurves
+		//
+		// but that would disable the post-quantum key agreement X25519Kyber768
+		// supported in Go 1.23, for which the CurveID is not exported.
+		// Instead, we'll set CurvePreferences to nil, which will enable PQC.
+		// See https://github.com/caddyserver/caddy/issues/6540
+		cfg.CurvePreferences = nil
 	}
 
 	if cfg.MinVersion == 0 {
diff --git a/modules/caddytls/values.go b/modules/caddytls/values.go
index 4e8c1adc2..20fe45ff8 100644
--- a/modules/caddytls/values.go
+++ b/modules/caddytls/values.go
@@ -108,6 +108,11 @@ var supportedCertKeyTypes = map[string]certmagic.KeyType{
 // implementation exists (e.g. P256). The latter ones can be
 // found here:
 // https://github.com/golang/go/tree/master/src/crypto/elliptic
+//
+// Temporily we ignore these default, to take advantage of X25519Kyber768
+// in Go's defaults (X25519Kyber768, X25519, P-256, P-384, P-521), which
+// isn't exported. See https://github.com/caddyserver/caddy/issues/6540
+// nolint:unused
 var defaultCurves = []tls.CurveID{
 	tls.X25519,
 	tls.CurveP256,

From 141c785420aa1cc8fd35e5fd2defa7b8af230a2f Mon Sep 17 00:00:00 2001
From: WeidiDeng <weidi_deng@icloud.com>
Date: Fri, 30 Aug 2024 04:11:25 +0800
Subject: [PATCH 40/62] ci: prepare syso files for windows embedding in release
 (#6406)

* prepare syso files for windows embedding

* don't specify main so version info will be embedded correctly

---------

Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>
---
 .goreleaser.yml | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/.goreleaser.yml b/.goreleaser.yml
index 22f96b588..c7d01571e 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -12,6 +12,10 @@ before:
     - mkdir -p caddy-build
     - cp cmd/caddy/main.go caddy-build/main.go
     - /bin/sh -c 'cd ./caddy-build && go mod init caddy'
+    # prepare syso files for windows embedding
+    - go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
+    - /bin/sh -c 'for a in amd64 arm arm64; do XCADDY_SKIP_BUILD=1 GOOS=windows GOARCH=$a $GOPATH/bin/xcaddy build {{.Env.TAG}}; done'
+    - /bin/sh -c 'mv /tmp/buildenv_*/*.syso caddy-build'
     # GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env
     # so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate
     - go mod edit -require=github.com/caddyserver/caddy/v2@{{.Env.TAG}} ./caddy-build/go.mod
@@ -31,7 +35,6 @@ builds:
 - env:
   - CGO_ENABLED=0
   - GO111MODULE=on
-  main: main.go
   dir: ./caddy-build
   binary: caddy
   goos:

From ffd28be90ab85206474739b1f479ef49a6f0d7c3 Mon Sep 17 00:00:00 2001
From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com>
Date: Fri, 30 Aug 2024 16:46:51 +0200
Subject: [PATCH 41/62] rewrite: Only serialize request if necessary (#6541)

* Prevents serializing the caddy request if log level is not debug.

* Extracts message to const.
---
 modules/caddyhttp/rewrite/rewrite.go | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/modules/caddyhttp/rewrite/rewrite.go b/modules/caddyhttp/rewrite/rewrite.go
index 9a4995186..c45ad98a2 100644
--- a/modules/caddyhttp/rewrite/rewrite.go
+++ b/modules/caddyhttp/rewrite/rewrite.go
@@ -131,6 +131,12 @@ func (rewr *Rewrite) Provision(ctx caddy.Context) error {
 
 func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
 	repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
+	const message = "rewrote request"
+
+	if rewr.logger.Check(zap.DebugLevel, message) == nil {
+		rewr.Rewrite(r, repl)
+		return next.ServeHTTP(w, r)
+	}
 
 	logger := rewr.logger.With(
 		zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: r}),
@@ -139,7 +145,7 @@ func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
 	changed := rewr.Rewrite(r, repl)
 
 	if changed {
-		logger.Debug("rewrote request",
+		logger.Debug(message,
 			zap.String("method", r.Method),
 			zap.String("uri", r.RequestURI),
 		)

From 5c47c2f147e5bef44fc8cb48a655d31f5a2a817c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?lollipopkit=F0=9F=8F=B3=EF=B8=8F=E2=80=8D=E2=9A=A7?=
 =?UTF-8?q?=EF=B8=8F?= <10864310+lollipopkit@users.noreply.github.com>
Date: Fri, 30 Aug 2024 23:01:37 +0800
Subject: [PATCH 42/62] fileserver: browse: Configurable default sort (#6502)

* fileserver: add `sort` options

* fix: test

* fileserver: check options in `Provison`

* fileserver: more obvious err alerts in sort options

* fileserver: move `sort` to `browse`

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
---
 .../file_server_sort.caddyfiletest            | 17 +++++----
 modules/caddyhttp/fileserver/browse.go        | 13 ++++++-
 modules/caddyhttp/fileserver/caddyfile.go     | 21 ++++++-----
 modules/caddyhttp/fileserver/staticfiles.go   | 36 ++++++++-----------
 4 files changed, 46 insertions(+), 41 deletions(-)

diff --git a/caddytest/integration/caddyfile_adapt/file_server_sort.caddyfiletest b/caddytest/integration/caddyfile_adapt/file_server_sort.caddyfiletest
index 62bfd0cba..7f07cba80 100644
--- a/caddytest/integration/caddyfile_adapt/file_server_sort.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/file_server_sort.caddyfiletest
@@ -1,7 +1,9 @@
 :80
 
-file_server browse {
-	sort size desc
+file_server {
+	browse {
+		sort size desc
+	}
 }
 ----------
 {
@@ -16,14 +18,15 @@ file_server browse {
 						{
 							"handle": [
 								{
-									"browse": {},
+									"browse": {
+										"sort": [
+											"size",
+											"desc"
+										]
+									},
 									"handler": "file_server",
 									"hide": [
 										"./Caddyfile"
-									],
-									"sort": [
-										"size",
-										"desc"
 									]
 								}
 							]
diff --git a/modules/caddyhttp/fileserver/browse.go b/modules/caddyhttp/fileserver/browse.go
index bd02c584f..f5cc16b42 100644
--- a/modules/caddyhttp/fileserver/browse.go
+++ b/modules/caddyhttp/fileserver/browse.go
@@ -52,8 +52,19 @@ var BrowseTemplate string
 type Browse struct {
 	// Filename of the template to use instead of the embedded browse template.
 	TemplateFile string `json:"template_file,omitempty"`
+
 	// Determines whether or not targets of symlinks should be revealed.
 	RevealSymlinks bool `json:"reveal_symlinks,omitempty"`
+
+	// Override the default sort.
+	// It includes the following options:
+	//   - sort_by: name(default), namedirfirst, size, time
+	//   - order: asc(default), desc
+	// eg.:
+	//   - `sort time desc` will sort by time in descending order
+	//   - `sort size` will sort by size in ascending order
+	// The first option must be `sort_by` and the second option must be `order` (if exists).
+	SortOptions []string `json:"sort,omitempty"`
 }
 
 func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
@@ -210,7 +221,7 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re
 
 	// The configs in Caddyfile have lower priority than Query params,
 	// so put it at first.
-	for idx, item := range fsrv.SortOptions {
+	for idx, item := range fsrv.Browse.SortOptions {
 		// Only `sort` & `order`, 2 params are allowed
 		if idx >= 2 {
 			break
diff --git a/modules/caddyhttp/fileserver/caddyfile.go b/modules/caddyhttp/fileserver/caddyfile.go
index 603a828c9..71b7638e4 100644
--- a/modules/caddyhttp/fileserver/caddyfile.go
+++ b/modules/caddyhttp/fileserver/caddyfile.go
@@ -119,6 +119,16 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 						return d.Err("Symlinks path reveal is already enabled")
 					}
 					fsrv.Browse.RevealSymlinks = true
+				case "sort":
+					for d.NextArg() {
+						dVal := d.Val()
+						switch dVal {
+						case sortByName, sortByNameDirFirst, sortBySize, sortByTime, sortOrderAsc, sortOrderDesc:
+							fsrv.Browse.SortOptions = append(fsrv.Browse.SortOptions, dVal)
+						default:
+							return d.Errf("unknown sort option '%s'", dVal)
+						}
+					}
 				default:
 					return d.Errf("unknown subdirective '%s'", d.Val())
 				}
@@ -171,17 +181,6 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 			}
 			fsrv.EtagFileExtensions = etagFileExtensions
 
-		case "sort":
-			for d.NextArg() {
-				dVal := d.Val()
-				switch dVal {
-				case sortByName, sortBySize, sortByTime, sortOrderAsc, sortOrderDesc:
-					fsrv.SortOptions = append(fsrv.SortOptions, dVal)
-				default:
-					return d.Errf("unknown sort option '%s'", dVal)
-				}
-			}
-
 		default:
 			return d.Errf("unknown subdirective '%s'", d.Val())
 		}
diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go
index 48812cfec..2dd237ae7 100644
--- a/modules/caddyhttp/fileserver/staticfiles.go
+++ b/modules/caddyhttp/fileserver/staticfiles.go
@@ -153,16 +153,6 @@ type FileServer struct {
 	// a 404 error. By default, this is false (disabled).
 	PassThru bool `json:"pass_thru,omitempty"`
 
-	// Override the default sort.
-	// It includes the following options:
-	//   - sort_by: name(default), namedirfirst, size, time
-	//   - order: asc(default), desc
-	// eg.:
-	//   - `sort time desc` will sort by time in descending order
-	//   - `sort size` will sort by size in ascending order
-	// The first option must be `sort_by` and the second option must be `order` (if exists).
-	SortOptions []string `json:"sort,omitempty"`
-
 	// Selection of encoders to use to check for precompressed files.
 	PrecompressedRaw caddy.ModuleMap `json:"precompressed,omitempty" caddy:"namespace=http.precompressed"`
 
@@ -246,19 +236,21 @@ func (fsrv *FileServer) Provision(ctx caddy.Context) error {
 		fsrv.precompressors[ae] = p
 	}
 
-	// check sort options
-	for idx, sortOption := range fsrv.SortOptions {
-		switch idx {
-		case 0:
-			if sortOption != sortByName && sortOption != sortByNameDirFirst && sortOption != sortBySize && sortOption != sortByTime {
-				return fmt.Errorf("the first option must be one of the following: %s, %s, %s, %s, but got %s", sortByName, sortByNameDirFirst, sortBySize, sortByTime, sortOption)
+	if fsrv.Browse != nil {
+		// check sort options
+		for idx, sortOption := range fsrv.Browse.SortOptions {
+			switch idx {
+			case 0:
+				if sortOption != sortByName && sortOption != sortByNameDirFirst && sortOption != sortBySize && sortOption != sortByTime {
+					return fmt.Errorf("the first option must be one of the following: %s, %s, %s, %s, but got %s", sortByName, sortByNameDirFirst, sortBySize, sortByTime, sortOption)
+				}
+			case 1:
+				if sortOption != sortOrderAsc && sortOption != sortOrderDesc {
+					return fmt.Errorf("the second option must be one of the following: %s, %s, but got %s", sortOrderAsc, sortOrderDesc, sortOption)
+				}
+			default:
+				return fmt.Errorf("only max 2 sort options are allowed, but got %d", idx+1)
 			}
-		case 1:
-			if sortOption != sortOrderAsc && sortOption != sortOrderDesc {
-				return fmt.Errorf("the second option must be one of the following: %s, %s, but got %s", sortOrderAsc, sortOrderDesc, sortOption)
-			}
-		default:
-			return fmt.Errorf("only max 2 sort options are allowed, but got %d", idx+1)
 		}
 	}
 

From c050a37e1c3228708a6716c8971361134243e941 Mon Sep 17 00:00:00 2001
From: Steffen Busch <37350514+steffenbusch@users.noreply.github.com>
Date: Fri, 30 Aug 2024 19:53:56 +0200
Subject: [PATCH 43/62] reverse_proxy: add placeholder
 http.reverse_proxy.retries (#6553)

* Add placeholder http.reverse_proxy.lb.retries

* Renamed placeholder to http.reverse_proxy.retries
---
 modules/caddyhttp/reverseproxy/reverseproxy.go | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go
index 1883ac072..44cc2f9d9 100644
--- a/modules/caddyhttp/reverseproxy/reverseproxy.go
+++ b/modules/caddyhttp/reverseproxy/reverseproxy.go
@@ -67,6 +67,7 @@ func init() {
 // `{http.reverse_proxy.upstream.duration_ms}` | Same as 'upstream.duration', but in milliseconds.
 // `{http.reverse_proxy.duration}` | Total time spent proxying, including selecting an upstream, retries, and writing response.
 // `{http.reverse_proxy.duration_ms}` | Same as 'duration', but in milliseconds.
+// `{http.reverse_proxy.retries}` | The number of retries actually performed to communicate with an upstream.
 type Handler struct {
 	// Configures the method of transport for the proxy. A transport
 	// is what performs the actual "round trip" to the backend.
@@ -443,6 +444,9 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
 		retries++
 	}
 
+	// number of retries actually performed
+	repl.Set("http.reverse_proxy.retries", retries)
+
 	if proxyErr != nil {
 		return statusError(proxyErr)
 	}

From 91e62db666b799ba4bb6577d8548fbe779d91c28 Mon Sep 17 00:00:00 2001
From: Jesper Brix Rosenkilde <jbr@humppa.dk>
Date: Tue, 3 Sep 2024 19:57:55 +0200
Subject: [PATCH 44/62] caddyhttp: Make route provisioning idempotent (#6558)

ref: https://github.com/caddyserver/caddy/issues/6551
---
 modules/caddyhttp/routes.go | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go
index 6f2371495..54a3f38e6 100644
--- a/modules/caddyhttp/routes.go
+++ b/modules/caddyhttp/routes.go
@@ -159,6 +159,9 @@ func (r *Route) ProvisionHandlers(ctx caddy.Context, metrics *Metrics) error {
 		r.Handlers = append(r.Handlers, handler.(MiddlewareHandler))
 	}
 
+	// Make ProvisionHandlers idempotent by clearing the middleware field
+	r.middleware = []Middleware{}
+
 	// pre-compile the middleware handler chain
 	for _, midhandler := range r.Handlers {
 		r.middleware = append(r.middleware, wrapMiddleware(ctx, midhandler, metrics))

From 2d12fb7ac6c7dfcbb8abeafbfb64af5ad1175bb3 Mon Sep 17 00:00:00 2001
From: vnxme <46669194+vnxme@users.noreply.github.com>
Date: Thu, 12 Sep 2024 05:51:59 +0300
Subject: [PATCH 45/62] caddytls: Add sni_regexp matcher (#6569)

---
 modules/caddytls/matchers.go      | 155 +++++++++++++++++++++++++++++-
 modules/caddytls/matchers_test.go |  46 +++++++++
 2 files changed, 199 insertions(+), 2 deletions(-)

diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go
index f94622374..65bbfa311 100644
--- a/modules/caddytls/matchers.go
+++ b/modules/caddytls/matchers.go
@@ -19,6 +19,8 @@ import (
 	"fmt"
 	"net"
 	"net/netip"
+	"regexp"
+	"strconv"
 	"strings"
 
 	"github.com/caddyserver/certmagic"
@@ -31,6 +33,7 @@ import (
 
 func init() {
 	caddy.RegisterModule(MatchServerName{})
+	caddy.RegisterModule(MatchServerNameRE{})
 	caddy.RegisterModule(MatchRemoteIP{})
 	caddy.RegisterModule(MatchLocalIP{})
 }
@@ -91,6 +94,146 @@ func (m *MatchServerName) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 	return nil
 }
 
+// MatchRegexp is an embeddable type for matching
+// using regular expressions. It adds placeholders
+// to the request's replacer. In fact, it is a copy of
+// caddyhttp.MatchRegexp with a local replacer prefix
+// and placeholders support in a regular expression pattern.
+type MatchRegexp struct {
+	// A unique name for this regular expression. Optional,
+	// but useful to prevent overwriting captures from other
+	// regexp matchers.
+	Name string `json:"name,omitempty"`
+
+	// The regular expression to evaluate, in RE2 syntax,
+	// which is the same general syntax used by Go, Perl,
+	// and Python. For details, see
+	// [Go's regexp package](https://golang.org/pkg/regexp/).
+	// Captures are accessible via placeholders. Unnamed
+	// capture groups are exposed as their numeric, 1-based
+	// index, while named capture groups are available by
+	// the capture group name.
+	Pattern string `json:"pattern"`
+
+	compiled *regexp.Regexp
+}
+
+// Provision compiles the regular expression which may include placeholders.
+func (mre *MatchRegexp) Provision(caddy.Context) error {
+	repl := caddy.NewReplacer()
+	re, err := regexp.Compile(repl.ReplaceAll(mre.Pattern, ""))
+	if err != nil {
+		return fmt.Errorf("compiling matcher regexp %s: %v", mre.Pattern, err)
+	}
+	mre.compiled = re
+	return nil
+}
+
+// Validate ensures mre is set up correctly.
+func (mre *MatchRegexp) Validate() error {
+	if mre.Name != "" && !wordRE.MatchString(mre.Name) {
+		return fmt.Errorf("invalid regexp name (must contain only word characters): %s", mre.Name)
+	}
+	return nil
+}
+
+// Match returns true if input matches the compiled regular
+// expression in m. It sets values on the replacer repl
+// associated with capture groups, using the given scope
+// (namespace).
+func (mre *MatchRegexp) Match(input string, repl *caddy.Replacer) bool {
+	matches := mre.compiled.FindStringSubmatch(input)
+	if matches == nil {
+		return false
+	}
+
+	// save all capture groups, first by index
+	for i, match := range matches {
+		keySuffix := "." + strconv.Itoa(i)
+		if mre.Name != "" {
+			repl.Set(regexpPlaceholderPrefix+"."+mre.Name+keySuffix, match)
+		}
+		repl.Set(regexpPlaceholderPrefix+keySuffix, match)
+	}
+
+	// then by name
+	for i, name := range mre.compiled.SubexpNames() {
+		// skip the first element (the full match), and empty names
+		if i == 0 || name == "" {
+			continue
+		}
+
+		keySuffix := "." + name
+		if mre.Name != "" {
+			repl.Set(regexpPlaceholderPrefix+"."+mre.Name+keySuffix, matches[i])
+		}
+		repl.Set(regexpPlaceholderPrefix+keySuffix, matches[i])
+	}
+
+	return true
+}
+
+// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
+func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
+	// iterate to merge multiple matchers into one
+	for d.Next() {
+		// If this is the second iteration of the loop
+		// then there's more than one *_regexp matcher,
+		// and we would end up overwriting the old one
+		if mre.Pattern != "" {
+			return d.Err("regular expression can only be used once per named matcher")
+		}
+
+		args := d.RemainingArgs()
+		switch len(args) {
+		case 1:
+			mre.Pattern = args[0]
+		case 2:
+			mre.Name = args[0]
+			mre.Pattern = args[1]
+		default:
+			return d.ArgErr()
+		}
+
+		// Default to the named matcher's name, if no regexp name is provided.
+		// Note: it requires d.SetContext(caddyfile.MatcherNameCtxKey, value)
+		// called before this unmarshalling, otherwise it wouldn't work.
+		if mre.Name == "" {
+			mre.Name = d.GetContextString(caddyfile.MatcherNameCtxKey)
+		}
+
+		if d.NextBlock(0) {
+			return d.Err("malformed regexp matcher: blocks are not supported")
+		}
+	}
+	return nil
+}
+
+// MatchServerNameRE matches based on SNI using a regular expression.
+type MatchServerNameRE struct{ MatchRegexp }
+
+// CaddyModule returns the Caddy module information.
+func (MatchServerNameRE) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID:  "tls.handshake_match.sni_regexp",
+		New: func() caddy.Module { return new(MatchServerNameRE) },
+	}
+}
+
+// Match matches hello based on SNI using a regular expression.
+func (m MatchServerNameRE) Match(hello *tls.ClientHelloInfo) bool {
+	repl := caddy.NewReplacer()
+	// caddytls.TestServerNameMatcher calls this function without any context
+	if ctx := hello.Context(); ctx != nil {
+		// In some situations the existing context may have no replacer
+		if replAny := ctx.Value(caddy.ReplacerCtxKey); replAny != nil {
+			repl = replAny.(*caddy.Replacer)
+		}
+	}
+
+	return m.MatchRegexp.Match(hello.ServerName, repl)
+}
+
 // MatchRemoteIP matches based on the remote IP of the
 // connection. Specific IPs or CIDR ranges can be specified.
 //
@@ -331,13 +474,21 @@ func (m *MatchLocalIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 
 // Interface guards
 var (
-	_ ConnectionMatcher = (*MatchServerName)(nil)
+	_ ConnectionMatcher = (*MatchLocalIP)(nil)
 	_ ConnectionMatcher = (*MatchRemoteIP)(nil)
+	_ ConnectionMatcher = (*MatchServerName)(nil)
+	_ ConnectionMatcher = (*MatchServerNameRE)(nil)
 
 	_ caddy.Provisioner = (*MatchLocalIP)(nil)
-	_ ConnectionMatcher = (*MatchLocalIP)(nil)
+	_ caddy.Provisioner = (*MatchRemoteIP)(nil)
+	_ caddy.Provisioner = (*MatchServerNameRE)(nil)
 
 	_ caddyfile.Unmarshaler = (*MatchLocalIP)(nil)
 	_ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
 	_ caddyfile.Unmarshaler = (*MatchServerName)(nil)
+	_ caddyfile.Unmarshaler = (*MatchServerNameRE)(nil)
 )
+
+var wordRE = regexp.MustCompile(`\w+`)
+
+const regexpPlaceholderPrefix = "tls.regexp"
diff --git a/modules/caddytls/matchers_test.go b/modules/caddytls/matchers_test.go
index 54dfdb9c4..824f72070 100644
--- a/modules/caddytls/matchers_test.go
+++ b/modules/caddytls/matchers_test.go
@@ -89,6 +89,52 @@ func TestServerNameMatcher(t *testing.T) {
 	}
 }
 
+func TestServerNameREMatcher(t *testing.T) {
+	for i, tc := range []struct {
+		pattern string
+		input   string
+		expect  bool
+	}{
+		{
+			pattern: "^example\\.(com|net)$",
+			input:   "example.com",
+			expect:  true,
+		},
+		{
+			pattern: "^example\\.(com|net)$",
+			input:   "foo.com",
+			expect:  false,
+		},
+		{
+			pattern: "^example\\.(com|net)$",
+			input:   "",
+			expect:  false,
+		},
+		{
+			pattern: "",
+			input:   "",
+			expect:  true,
+		},
+		{
+			pattern: "^example\\.(com|net)$",
+			input:   "foo.example.com",
+			expect:  false,
+		},
+	} {
+		chi := &tls.ClientHelloInfo{ServerName: tc.input}
+		mre := MatchServerNameRE{MatchRegexp{Pattern: tc.pattern}}
+		ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
+		if mre.Provision(ctx) != nil {
+			t.Errorf("Test %d: Failed to provision a regexp matcher (pattern=%v)", i, tc.pattern)
+		}
+		actual := mre.Match(chi)
+		if actual != tc.expect {
+			t.Errorf("Test %d: Expected %t but got %t (input=%s match=%v)",
+				i, tc.expect, actual, tc.input, tc.pattern)
+		}
+	}
+}
+
 func TestRemoteIPMatcher(t *testing.T) {
 	ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
 	defer cancel()

From 21f9c20a04ec5c2ac430daa8e4ba8fbdef67f773 Mon Sep 17 00:00:00 2001
From: mister-turtle <50653342+mister-turtle@users.noreply.github.com>
Date: Fri, 13 Sep 2024 08:22:03 +0100
Subject: [PATCH 46/62] rewrite: Avoid panic on bad arg count for `uri` (#6571)

---
 modules/caddyhttp/rewrite/caddyfile.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/modules/caddyhttp/rewrite/caddyfile.go b/modules/caddyhttp/rewrite/caddyfile.go
index 0ce5c41d2..89f44c79b 100644
--- a/modules/caddyhttp/rewrite/caddyfile.go
+++ b/modules/caddyhttp/rewrite/caddyfile.go
@@ -106,7 +106,7 @@ func parseCaddyfileURI(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, err
 
 	switch args[0] {
 	case "strip_prefix":
-		if len(args) > 2 {
+		if len(args) != 2 {
 			return nil, h.ArgErr()
 		}
 		rewr.StripPathPrefix = args[1]
@@ -115,7 +115,7 @@ func parseCaddyfileURI(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, err
 		}
 
 	case "strip_suffix":
-		if len(args) > 2 {
+		if len(args) != 2 {
 			return nil, h.ArgErr()
 		}
 		rewr.StripPathSuffix = args[1]

From f4bf4e0097853438eb69c573bbaa0581e9b9c02d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= <kevin@dunglas.fr>
Date: Fri, 13 Sep 2024 19:16:37 +0200
Subject: [PATCH 47/62] perf: use zap's Check() to prevent useless allocs
 (#6560)

* perf: use zap's Check() to prevent useless allocs

* fix

* fix

* fix

* fix

* restore previous replacer behavior

* fix linter
---
 modules/caddyhttp/caddyauth/caddyauth.go      |   7 +-
 modules/caddyhttp/fileserver/browse.go        |  11 +-
 .../caddyhttp/fileserver/browsetplcontext.go  |   7 +-
 modules/caddyhttp/fileserver/matcher.go       |  14 +-
 modules/caddyhttp/fileserver/staticfiles.go   |  91 +++++++----
 modules/caddyhttp/intercept/intercept.go      |   5 +-
 modules/caddyhttp/ip_matchers.go              |  10 +-
 modules/caddyhttp/logging.go                  |  12 +-
 modules/caddyhttp/push/handler.go             |  24 ++-
 modules/caddyhttp/requestbody/requestbody.go  |   9 +-
 .../caddyhttp/reverseproxy/fastcgi/client.go  |  10 +-
 .../caddyhttp/reverseproxy/fastcgi/fastcgi.go |  11 +-
 .../caddyhttp/reverseproxy/healthchecks.go    | 154 +++++++++++-------
 .../caddyhttp/reverseproxy/httptransport.go   |   9 +-
 modules/caddyhttp/reverseproxy/metrics.go     |  10 +-
 .../caddyhttp/reverseproxy/reverseproxy.go    |  54 ++++--
 modules/caddyhttp/reverseproxy/streaming.go   |  84 +++++++---
 modules/caddyhttp/reverseproxy/upstreams.go   |  62 ++++---
 modules/caddyhttp/rewrite/rewrite.go          |  10 +-
 modules/caddyhttp/server.go                   | 141 ++++++++++------
 modules/caddyhttp/server_test.go              |  23 +++
 modules/caddyhttp/tracing/tracerprovider.go   |   5 +-
 modules/caddypki/acmeserver/acmeserver.go     |  13 +-
 modules/caddytls/acmeissuer.go                |   5 +-
 modules/caddytls/automation.go                |  28 +++-
 modules/caddytls/certmanagers.go              |   5 +-
 modules/caddytls/connpolicy.go                |   6 +-
 modules/caddytls/matchers.go                  |   9 +-
 modules/caddytls/ondemand.go                  |  25 ++-
 modules/caddytls/tls.go                       |  27 ++-
 30 files changed, 599 insertions(+), 282 deletions(-)

diff --git a/modules/caddyhttp/caddyauth/caddyauth.go b/modules/caddyhttp/caddyauth/caddyauth.go
index c60de880b..f799d7a0c 100644
--- a/modules/caddyhttp/caddyauth/caddyauth.go
+++ b/modules/caddyhttp/caddyauth/caddyauth.go
@@ -19,6 +19,7 @@ import (
 	"net/http"
 
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
@@ -76,9 +77,9 @@ func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
 	for provName, prov := range a.Providers {
 		user, authed, err = prov.Authenticate(w, r)
 		if err != nil {
-			a.logger.Error("auth provider returned error",
-				zap.String("provider", provName),
-				zap.Error(err))
+			if c := a.logger.Check(zapcore.ErrorLevel, "auth provider returned error"); c != nil {
+				c.Write(zap.String("provider", provName), zap.Error(err))
+			}
 			continue
 		}
 		if authed {
diff --git a/modules/caddyhttp/fileserver/browse.go b/modules/caddyhttp/fileserver/browse.go
index f5cc16b42..a19b4e17a 100644
--- a/modules/caddyhttp/fileserver/browse.go
+++ b/modules/caddyhttp/fileserver/browse.go
@@ -33,6 +33,7 @@ import (
 	"time"
 
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
@@ -68,9 +69,9 @@ type Browse struct {
 }
 
 func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
-	fsrv.logger.Debug("browse enabled; listing directory contents",
-		zap.String("path", dirPath),
-		zap.String("root", root))
+	if c := fsrv.logger.Check(zapcore.DebugLevel, "browse enabled; listing directory contents"); c != nil {
+		c.Write(zap.String("path", dirPath), zap.String("root", root))
+	}
 
 	// Navigation on the client-side gets messed up if the
 	// URL doesn't end in a trailing slash because hrefs to
@@ -92,7 +93,9 @@ func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w ht
 	origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
 	if r.URL.Path == "" || path.Base(origReq.URL.Path) == path.Base(r.URL.Path) {
 		if !strings.HasSuffix(origReq.URL.Path, "/") {
-			fsrv.logger.Debug("redirecting to trailing slash to preserve hrefs", zap.String("request_path", r.URL.Path))
+			if c := fsrv.logger.Check(zapcore.DebugLevel, "redirecting to trailing slash to preserve hrefs"); c != nil {
+				c.Write(zap.String("request_path", r.URL.Path))
+			}
 			return redirect(w, r, origReq.URL.Path+"/")
 		}
 	}
diff --git a/modules/caddyhttp/fileserver/browsetplcontext.go b/modules/caddyhttp/fileserver/browsetplcontext.go
index 0251bc581..37e1cc3dd 100644
--- a/modules/caddyhttp/fileserver/browsetplcontext.go
+++ b/modules/caddyhttp/fileserver/browsetplcontext.go
@@ -28,6 +28,7 @@ import (
 
 	"github.com/dustin/go-humanize"
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
@@ -57,9 +58,9 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS,
 
 		info, err := entry.Info()
 		if err != nil {
-			fsrv.logger.Error("could not get info about directory entry",
-				zap.String("name", entry.Name()),
-				zap.String("root", root))
+			if c := fsrv.logger.Check(zapcore.ErrorLevel, "could not get info about directory entry"); c != nil {
+				c.Write(zap.String("name", entry.Name()), zap.String("root", root))
+			}
 			continue
 		}
 
diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go
index c315b8e38..6ab2180ad 100644
--- a/modules/caddyhttp/fileserver/matcher.go
+++ b/modules/caddyhttp/fileserver/matcher.go
@@ -33,6 +33,7 @@ import (
 	"github.com/google/cel-go/common/types/ref"
 	"github.com/google/cel-go/parser"
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -326,7 +327,9 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
 
 	fileSystem, ok := m.fsmap.Get(fsName)
 	if !ok {
-		m.logger.Error("use of unregistered filesystem", zap.String("fs", fsName))
+		if c := m.logger.Check(zapcore.ErrorLevel, "use of unregistered filesystem"); c != nil {
+			c.Write(zap.String("fs", fsName))
+		}
 		return false
 	}
 	type matchCandidate struct {
@@ -356,7 +359,10 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
 			return val, nil
 		})
 		if err != nil {
-			m.logger.Error("evaluating placeholders", zap.Error(err))
+			if c := m.logger.Check(zapcore.ErrorLevel, "evaluating placeholders"); c != nil {
+				c.Write(zap.Error(err))
+			}
+
 			expandedFile = file // "oh well," I guess?
 		}
 
@@ -379,7 +385,9 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
 		} else {
 			globResults, err = fs.Glob(fileSystem, fullPattern)
 			if err != nil {
-				m.logger.Error("expanding glob", zap.Error(err))
+				if c := m.logger.Check(zapcore.ErrorLevel, "expanding glob"); c != nil {
+					c.Write(zap.Error(err))
+				}
 			}
 		}
 
diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go
index 2dd237ae7..4ae69b647 100644
--- a/modules/caddyhttp/fileserver/staticfiles.go
+++ b/modules/caddyhttp/fileserver/staticfiles.go
@@ -31,6 +31,7 @@ import (
 	"strings"
 
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
@@ -286,11 +287,14 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
 	// remove any trailing `/` as it breaks fs.ValidPath() in the stdlib
 	filename := strings.TrimSuffix(caddyhttp.SanitizedPathJoin(root, r.URL.Path), "/")
 
-	fsrv.logger.Debug("sanitized path join",
-		zap.String("site_root", root),
-		zap.String("fs", fsName),
-		zap.String("request_path", r.URL.Path),
-		zap.String("result", filename))
+	if c := fsrv.logger.Check(zapcore.DebugLevel, "sanitized path join"); c != nil {
+		c.Write(
+			zap.String("site_root", root),
+			zap.String("fs", fsName),
+			zap.String("request_path", r.URL.Path),
+			zap.String("result", filename),
+		)
+	}
 
 	// get information about the file
 	info, err := fs.Stat(fileSystem, filename)
@@ -313,9 +317,12 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
 			indexPath := caddyhttp.SanitizedPathJoin(filename, indexPage)
 			if fileHidden(indexPath, filesToHide) {
 				// pretend this file doesn't exist
-				fsrv.logger.Debug("hiding index file",
-					zap.String("filename", indexPath),
-					zap.Strings("files_to_hide", filesToHide))
+				if c := fsrv.logger.Check(zapcore.DebugLevel, "hiding index file"); c != nil {
+					c.Write(
+						zap.String("filename", indexPath),
+						zap.Strings("files_to_hide", filesToHide),
+					)
+				}
 				continue
 			}
 
@@ -335,7 +342,9 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
 			info = indexInfo
 			filename = indexPath
 			implicitIndexFile = true
-			fsrv.logger.Debug("located index file", zap.String("filename", filename))
+			if c := fsrv.logger.Check(zapcore.DebugLevel, "located index file"); c != nil {
+				c.Write(zap.String("filename", filename))
+			}
 			break
 		}
 	}
@@ -343,9 +352,12 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
 	// if still referencing a directory, delegate
 	// to browse or return an error
 	if info.IsDir() {
-		fsrv.logger.Debug("no index file in directory",
-			zap.String("path", filename),
-			zap.Strings("index_filenames", fsrv.IndexNames))
+		if c := fsrv.logger.Check(zapcore.DebugLevel, "no index file in directory"); c != nil {
+			c.Write(
+				zap.String("path", filename),
+				zap.Strings("index_filenames", fsrv.IndexNames),
+			)
+		}
 		if fsrv.Browse != nil && !fileHidden(filename, filesToHide) {
 			return fsrv.serveBrowse(fileSystem, root, filename, w, r, next)
 		}
@@ -355,9 +367,12 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
 	// one last check to ensure the file isn't hidden (we might
 	// have changed the filename from when we last checked)
 	if fileHidden(filename, filesToHide) {
-		fsrv.logger.Debug("hiding file",
-			zap.String("filename", filename),
-			zap.Strings("files_to_hide", filesToHide))
+		if c := fsrv.logger.Check(zapcore.DebugLevel, "hiding file"); c != nil {
+			c.Write(
+				zap.String("filename", filename),
+				zap.Strings("files_to_hide", filesToHide),
+			)
+		}
 		return fsrv.notFound(w, r, next)
 	}
 
@@ -375,15 +390,21 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
 		if path.Base(origReq.URL.Path) == path.Base(r.URL.Path) {
 			if implicitIndexFile && !strings.HasSuffix(origReq.URL.Path, "/") {
 				to := origReq.URL.Path + "/"
-				fsrv.logger.Debug("redirecting to canonical URI (adding trailing slash for directory)",
-					zap.String("from_path", origReq.URL.Path),
-					zap.String("to_path", to))
+				if c := fsrv.logger.Check(zapcore.DebugLevel, "redirecting to canonical URI (adding trailing slash for directory"); c != nil {
+					c.Write(
+						zap.String("from_path", origReq.URL.Path),
+						zap.String("to_path", to),
+					)
+				}
 				return redirect(w, r, to)
 			} else if !implicitIndexFile && strings.HasSuffix(origReq.URL.Path, "/") {
 				to := origReq.URL.Path[:len(origReq.URL.Path)-1]
-				fsrv.logger.Debug("redirecting to canonical URI (removing trailing slash for file)",
-					zap.String("from_path", origReq.URL.Path),
-					zap.String("to_path", to))
+				if c := fsrv.logger.Check(zapcore.DebugLevel, "redirecting to canonical URI (removing trailing slash for file"); c != nil {
+					c.Write(
+						zap.String("from_path", origReq.URL.Path),
+						zap.String("to_path", to),
+					)
+				}
 				return redirect(w, r, to)
 			}
 		}
@@ -411,13 +432,19 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
 		compressedFilename := filename + precompress.Suffix()
 		compressedInfo, err := fs.Stat(fileSystem, compressedFilename)
 		if err != nil || compressedInfo.IsDir() {
-			fsrv.logger.Debug("precompressed file not accessible", zap.String("filename", compressedFilename), zap.Error(err))
+			if c := fsrv.logger.Check(zapcore.DebugLevel, "precompressed file not accessible"); c != nil {
+				c.Write(zap.String("filename", compressedFilename), zap.Error(err))
+			}
 			continue
 		}
-		fsrv.logger.Debug("opening compressed sidecar file", zap.String("filename", compressedFilename), zap.Error(err))
+		if c := fsrv.logger.Check(zapcore.DebugLevel, "opening compressed sidecar file"); c != nil {
+			c.Write(zap.String("filename", compressedFilename), zap.Error(err))
+		}
 		file, err = fsrv.openFile(fileSystem, compressedFilename, w)
 		if err != nil {
-			fsrv.logger.Warn("opening precompressed file failed", zap.String("filename", compressedFilename), zap.Error(err))
+			if c := fsrv.logger.Check(zapcore.WarnLevel, "opening precompressed file failed"); c != nil {
+				c.Write(zap.String("filename", compressedFilename), zap.Error(err))
+			}
 			if caddyErr, ok := err.(caddyhttp.HandlerError); ok && caddyErr.StatusCode == http.StatusServiceUnavailable {
 				return err
 			}
@@ -448,7 +475,9 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
 
 	// no precompressed file found, use the actual file
 	if file == nil {
-		fsrv.logger.Debug("opening file", zap.String("filename", filename))
+		if c := fsrv.logger.Check(zapcore.DebugLevel, "opening file"); c != nil {
+			c.Write(zap.String("filename", filename))
+		}
 
 		// open the file
 		file, err = fsrv.openFile(fileSystem, filename, w)
@@ -548,10 +577,14 @@ func (fsrv *FileServer) openFile(fileSystem fs.FS, filename string, w http.Respo
 	if err != nil {
 		err = fsrv.mapDirOpenError(fileSystem, err, filename)
 		if errors.Is(err, fs.ErrNotExist) {
-			fsrv.logger.Debug("file not found", zap.String("filename", filename), zap.Error(err))
+			if c := fsrv.logger.Check(zapcore.DebugLevel, "file not found"); c != nil {
+				c.Write(zap.String("filename", filename), zap.Error(err))
+			}
 			return nil, caddyhttp.Error(http.StatusNotFound, err)
 		} else if errors.Is(err, fs.ErrPermission) {
-			fsrv.logger.Debug("permission denied", zap.String("filename", filename), zap.Error(err))
+			if c := fsrv.logger.Check(zapcore.DebugLevel, "permission denied"); c != nil {
+				c.Write(zap.String("filename", filename), zap.Error(err))
+			}
 			return nil, caddyhttp.Error(http.StatusForbidden, err)
 		}
 		// maybe the server is under load and ran out of file descriptors?
@@ -559,7 +592,9 @@ func (fsrv *FileServer) openFile(fileSystem fs.FS, filename string, w http.Respo
 		//nolint:gosec
 		backoff := weakrand.Intn(maxBackoff-minBackoff) + minBackoff
 		w.Header().Set("Retry-After", strconv.Itoa(backoff))
-		fsrv.logger.Debug("retry after backoff", zap.String("filename", filename), zap.Int("backoff", backoff), zap.Error(err))
+		if c := fsrv.logger.Check(zapcore.DebugLevel, "retry after backoff"); c != nil {
+			c.Write(zap.String("filename", filename), zap.Int("backoff", backoff), zap.Error(err))
+		}
 		return nil, caddyhttp.Error(http.StatusServiceUnavailable, err)
 	}
 	return file, nil
diff --git a/modules/caddyhttp/intercept/intercept.go b/modules/caddyhttp/intercept/intercept.go
index 720a09333..29889dcc0 100644
--- a/modules/caddyhttp/intercept/intercept.go
+++ b/modules/caddyhttp/intercept/intercept.go
@@ -23,6 +23,7 @@ import (
 	"sync"
 
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -165,7 +166,9 @@ func (ir Intercept) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
 	}
 	repl.Set("http.intercept.status_code", rec.Status())
 
-	ir.logger.Debug("handling response", zap.Int("handler", rec.handlerIndex))
+	if c := ir.logger.Check(zapcore.DebugLevel, "handling response"); c != nil {
+		c.Write(zap.Int("handler", rec.handlerIndex))
+	}
 
 	// pass the request through the response handler routes
 	return rec.handler.Routes.Compile(next).ServeHTTP(w, r)
diff --git a/modules/caddyhttp/ip_matchers.go b/modules/caddyhttp/ip_matchers.go
index 2e735cb69..99eb39dff 100644
--- a/modules/caddyhttp/ip_matchers.go
+++ b/modules/caddyhttp/ip_matchers.go
@@ -26,6 +26,7 @@ import (
 	"github.com/google/cel-go/cel"
 	"github.com/google/cel-go/common/types/ref"
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -150,12 +151,17 @@ func (m MatchRemoteIP) Match(r *http.Request) bool {
 	address := r.RemoteAddr
 	clientIP, zoneID, err := parseIPZoneFromString(address)
 	if err != nil {
-		m.logger.Error("getting remote IP", zap.Error(err))
+		if c := m.logger.Check(zapcore.ErrorLevel, "getting remote "); c != nil {
+			c.Write(zap.Error(err))
+		}
+
 		return false
 	}
 	matches, zoneFilter := matchIPByCidrZones(clientIP, zoneID, m.cidrs, m.zones)
 	if !matches && !zoneFilter {
-		m.logger.Debug("zone ID from remote IP did not match", zap.String("zone", zoneID))
+		if c := m.logger.Check(zapcore.DebugLevel, "zone ID from remote IP did not match"); c != nil {
+			c.Write(zap.String("zone", zoneID))
+		}
 	}
 	return matches
 }
diff --git a/modules/caddyhttp/logging.go b/modules/caddyhttp/logging.go
index 823763e91..0a389fe16 100644
--- a/modules/caddyhttp/logging.go
+++ b/modules/caddyhttp/logging.go
@@ -193,7 +193,7 @@ func (sa *StringArray) UnmarshalJSON(b []byte) error {
 // to use, the error log message, and any extra fields.
 // If err is a HandlerError, the returned values will
 // have richer information.
-func errLogValues(err error) (status int, msg string, fields []zapcore.Field) {
+func errLogValues(err error) (status int, msg string, fields func() []zapcore.Field) {
 	var handlerErr HandlerError
 	if errors.As(err, &handlerErr) {
 		status = handlerErr.StatusCode
@@ -202,10 +202,12 @@ func errLogValues(err error) (status int, msg string, fields []zapcore.Field) {
 		} else {
 			msg = handlerErr.Err.Error()
 		}
-		fields = []zapcore.Field{
-			zap.Int("status", handlerErr.StatusCode),
-			zap.String("err_id", handlerErr.ID),
-			zap.String("err_trace", handlerErr.Trace),
+		fields = func() []zapcore.Field {
+			return []zapcore.Field{
+				zap.Int("status", handlerErr.StatusCode),
+				zap.String("err_id", handlerErr.ID),
+				zap.String("err_trace", handlerErr.Trace),
+			}
 		}
 		return
 	}
diff --git a/modules/caddyhttp/push/handler.go b/modules/caddyhttp/push/handler.go
index 031a8991f..1fbe53d83 100644
--- a/modules/caddyhttp/push/handler.go
+++ b/modules/caddyhttp/push/handler.go
@@ -20,6 +20,7 @@ import (
 	"strings"
 
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
@@ -92,14 +93,17 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhtt
 
 	// push first!
 	for _, resource := range h.Resources {
-		h.logger.Debug("pushing resource",
-			zap.String("uri", r.RequestURI),
-			zap.String("push_method", resource.Method),
-			zap.String("push_target", resource.Target),
-			zap.Object("push_headers", caddyhttp.LoggableHTTPHeader{
-				Header:               hdr,
-				ShouldLogCredentials: shouldLogCredentials,
-			}))
+		if c := h.logger.Check(zapcore.DebugLevel, "pushing resource"); c != nil {
+			c.Write(
+				zap.String("uri", r.RequestURI),
+				zap.String("push_method", resource.Method),
+				zap.String("push_target", resource.Target),
+				zap.Object("push_headers", caddyhttp.LoggableHTTPHeader{
+					Header:               hdr,
+					ShouldLogCredentials: shouldLogCredentials,
+				}),
+			)
+		}
 		err := pusher.Push(repl.ReplaceAll(resource.Target, "."), &http.PushOptions{
 			Method: resource.Method,
 			Header: hdr,
@@ -209,7 +213,9 @@ func (lp linkPusher) WriteHeader(statusCode int) {
 	if links, ok := lp.ResponseWriter.Header()["Link"]; ok {
 		// only initiate these pushes if it hasn't been done yet
 		if val := caddyhttp.GetVar(lp.request.Context(), pushedLink); val == nil {
-			lp.handler.logger.Debug("pushing Link resources", zap.Strings("linked", links))
+			if c := lp.handler.logger.Check(zapcore.DebugLevel, "pushing Link resources"); c != nil {
+				c.Write(zap.Strings("linked", links))
+			}
 			caddyhttp.SetVar(lp.request.Context(), pushedLink, true)
 			lp.handler.servePreloadLinks(lp.pusher, lp.header, links)
 		}
diff --git a/modules/caddyhttp/requestbody/requestbody.go b/modules/caddyhttp/requestbody/requestbody.go
index d00455a8c..1c804aa13 100644
--- a/modules/caddyhttp/requestbody/requestbody.go
+++ b/modules/caddyhttp/requestbody/requestbody.go
@@ -20,6 +20,7 @@ import (
 	"time"
 
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
@@ -69,12 +70,16 @@ func (rb RequestBody) ServeHTTP(w http.ResponseWriter, r *http.Request, next cad
 		rc := http.NewResponseController(w)
 		if rb.ReadTimeout > 0 {
 			if err := rc.SetReadDeadline(time.Now().Add(rb.ReadTimeout)); err != nil {
-				rb.logger.Error("could not set read deadline", zap.Error(err))
+				if c := rb.logger.Check(zapcore.ErrorLevel, "could not set read deadline"); c != nil {
+					c.Write(zap.Error(err))
+				}
 			}
 		}
 		if rb.WriteTimeout > 0 {
 			if err := rc.SetWriteDeadline(time.Now().Add(rb.WriteTimeout)); err != nil {
-				rb.logger.Error("could not set write deadline", zap.Error(err))
+				if c := rb.logger.Check(zapcore.ErrorLevel, "could not set write deadline"); c != nil {
+					c.Write(zap.Error(err))
+				}
 			}
 		}
 	}
diff --git a/modules/caddyhttp/reverseproxy/fastcgi/client.go b/modules/caddyhttp/reverseproxy/fastcgi/client.go
index d944c5778..7284fe672 100644
--- a/modules/caddyhttp/reverseproxy/fastcgi/client.go
+++ b/modules/caddyhttp/reverseproxy/fastcgi/client.go
@@ -40,6 +40,7 @@ import (
 	"time"
 
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 )
 
 // FCGIListenSockFileno describes listen socket file number.
@@ -184,10 +185,13 @@ func (f clientCloser) Close() error {
 		return f.rwc.Close()
 	}
 
+	logLevel := zapcore.WarnLevel
 	if f.status >= 400 {
-		f.logger.Error("stderr", zap.ByteString("body", stderr))
-	} else {
-		f.logger.Warn("stderr", zap.ByteString("body", stderr))
+		logLevel = zapcore.ErrorLevel
+	}
+
+	if c := f.logger.Check(logLevel, "stderr"); c != nil {
+		c.Write(zap.ByteString("body", stderr))
 	}
 
 	return f.rwc.Close()
diff --git a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
index 31febdd4e..3985465ba 100644
--- a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
+++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
@@ -148,10 +148,13 @@ func (t Transport) RoundTrip(r *http.Request) (*http.Response, error) {
 		zap.Object("request", loggableReq),
 		zap.Object("env", loggableEnv),
 	)
-	logger.Debug("roundtrip",
-		zap.String("dial", address),
-		zap.Object("env", loggableEnv),
-		zap.Object("request", loggableReq))
+	if c := t.logger.Check(zapcore.DebugLevel, "roundtrip"); c != nil {
+		c.Write(
+			zap.String("dial", address),
+			zap.Object("env", loggableEnv),
+			zap.Object("request", loggableReq),
+		)
+	}
 
 	// connect to the backend
 	dialer := net.Dialer{Timeout: time.Duration(t.DialTimeout)}
diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go
index efa1dbf09..179805f20 100644
--- a/modules/caddyhttp/reverseproxy/healthchecks.go
+++ b/modules/caddyhttp/reverseproxy/healthchecks.go
@@ -28,6 +28,7 @@ import (
 	"time"
 
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
@@ -270,9 +271,12 @@ type CircuitBreaker interface {
 func (h *Handler) activeHealthChecker() {
 	defer func() {
 		if err := recover(); err != nil {
-			h.HealthChecks.Active.logger.Error("active health checker panicked",
-				zap.Any("error", err),
-				zap.ByteString("stack", debug.Stack()))
+			if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "active health checker panicked"); c != nil {
+				c.Write(
+					zap.Any("error", err),
+					zap.ByteString("stack", debug.Stack()),
+				)
+			}
 		}
 	}()
 	ticker := time.NewTicker(time.Duration(h.HealthChecks.Active.Interval))
@@ -295,26 +299,33 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
 		go func(upstream *Upstream) {
 			defer func() {
 				if err := recover(); err != nil {
-					h.HealthChecks.Active.logger.Error("active health check panicked",
-						zap.Any("error", err),
-						zap.ByteString("stack", debug.Stack()))
+					if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "active health checker panicked"); c != nil {
+						c.Write(
+							zap.Any("error", err),
+							zap.ByteString("stack", debug.Stack()),
+						)
+					}
 				}
 			}()
 
 			networkAddr, err := caddy.NewReplacer().ReplaceOrErr(upstream.Dial, true, true)
 			if err != nil {
-				h.HealthChecks.Active.logger.Error("invalid use of placeholders in dial address for active health checks",
-					zap.String("address", networkAddr),
-					zap.Error(err),
-				)
+				if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "invalid use of placeholders in dial address for active health checks"); c != nil {
+					c.Write(
+						zap.String("address", networkAddr),
+						zap.Error(err),
+					)
+				}
 				return
 			}
 			addr, err := caddy.ParseNetworkAddress(networkAddr)
 			if err != nil {
-				h.HealthChecks.Active.logger.Error("bad network address",
-					zap.String("address", networkAddr),
-					zap.Error(err),
-				)
+				if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "bad network address"); c != nil {
+					c.Write(
+						zap.String("address", networkAddr),
+						zap.Error(err),
+					)
+				}
 				return
 			}
 			if hcp := uint(upstream.activeHealthCheckPort); hcp != 0 {
@@ -324,9 +335,11 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
 				addr.StartPort, addr.EndPort = hcp, hcp
 			}
 			if addr.PortRangeSize() != 1 {
-				h.HealthChecks.Active.logger.Error("multiple addresses (upstream must map to only one address)",
-					zap.String("address", networkAddr),
-				)
+				if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "multiple addresses (upstream must map to only one address)"); c != nil {
+					c.Write(
+						zap.String("address", networkAddr),
+					)
+				}
 				return
 			}
 			hostAddr := addr.JoinHostPort(0)
@@ -339,10 +352,12 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
 			}
 			err = h.doActiveHealthCheck(DialInfo{Network: addr.Network, Address: dialAddr}, hostAddr, networkAddr, upstream)
 			if err != nil {
-				h.HealthChecks.Active.logger.Error("active health check failed",
-					zap.String("address", hostAddr),
-					zap.Error(err),
-				)
+				if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "active health check failed"); c != nil {
+					c.Write(
+						zap.String("address", hostAddr),
+						zap.Error(err),
+					)
+				}
 			}
 		}(upstream)
 	}
@@ -441,9 +456,12 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ
 		// increment failures and then check if it has reached the threshold to mark unhealthy
 		err := upstream.Host.countHealthFail(1)
 		if err != nil {
-			h.HealthChecks.Active.logger.Error("could not count active health failure",
-				zap.String("host", upstream.Dial),
-				zap.Error(err))
+			if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "could not count active health failure"); c != nil {
+				c.Write(
+					zap.String("host", upstream.Dial),
+					zap.Error(err),
+				)
+			}
 			return
 		}
 		if upstream.Host.activeHealthFails() >= h.HealthChecks.Active.Fails {
@@ -459,14 +477,19 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ
 		// increment passes and then check if it has reached the threshold to be healthy
 		err := upstream.Host.countHealthPass(1)
 		if err != nil {
-			h.HealthChecks.Active.logger.Error("could not count active health pass",
-				zap.String("host", upstream.Dial),
-				zap.Error(err))
+			if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "could not count active health pass"); c != nil {
+				c.Write(
+					zap.String("host", upstream.Dial),
+					zap.Error(err),
+				)
+			}
 			return
 		}
 		if upstream.Host.activeHealthPasses() >= h.HealthChecks.Active.Passes {
 			if upstream.setHealthy(true) {
-				h.HealthChecks.Active.logger.Info("host is up", zap.String("host", hostAddr))
+				if c := h.HealthChecks.Active.logger.Check(zapcore.InfoLevel, "host is up"); c != nil {
+					c.Write(zap.String("host", hostAddr))
+				}
 				h.events.Emit(h.ctx, "healthy", map[string]any{"host": hostAddr})
 				upstream.Host.resetHealth()
 			}
@@ -476,10 +499,12 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ
 	// do the request, being careful to tame the response body
 	resp, err := h.HealthChecks.Active.httpClient.Do(req)
 	if err != nil {
-		h.HealthChecks.Active.logger.Info("HTTP request failed",
-			zap.String("host", hostAddr),
-			zap.Error(err),
-		)
+		if c := h.HealthChecks.Active.logger.Check(zapcore.InfoLevel, "HTTP request failed"); c != nil {
+			c.Write(
+				zap.String("host", hostAddr),
+				zap.Error(err),
+			)
+		}
 		markUnhealthy()
 		return nil
 	}
@@ -496,18 +521,22 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ
 	// if status code is outside criteria, mark down
 	if h.HealthChecks.Active.ExpectStatus > 0 {
 		if !caddyhttp.StatusCodeMatches(resp.StatusCode, h.HealthChecks.Active.ExpectStatus) {
-			h.HealthChecks.Active.logger.Info("unexpected status code",
-				zap.Int("status_code", resp.StatusCode),
-				zap.String("host", hostAddr),
-			)
+			if c := h.HealthChecks.Active.logger.Check(zapcore.InfoLevel, "unexpected status code"); c != nil {
+				c.Write(
+					zap.Int("status_code", resp.StatusCode),
+					zap.String("host", hostAddr),
+				)
+			}
 			markUnhealthy()
 			return nil
 		}
 	} else if resp.StatusCode < 200 || resp.StatusCode >= 300 {
-		h.HealthChecks.Active.logger.Info("status code out of tolerances",
-			zap.Int("status_code", resp.StatusCode),
-			zap.String("host", hostAddr),
-		)
+		if c := h.HealthChecks.Active.logger.Check(zapcore.InfoLevel, "status code out of tolerances"); c != nil {
+			c.Write(
+				zap.Int("status_code", resp.StatusCode),
+				zap.String("host", hostAddr),
+			)
+		}
 		markUnhealthy()
 		return nil
 	}
@@ -516,17 +545,21 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ
 	if h.HealthChecks.Active.bodyRegexp != nil {
 		bodyBytes, err := io.ReadAll(body)
 		if err != nil {
-			h.HealthChecks.Active.logger.Info("failed to read response body",
-				zap.String("host", hostAddr),
-				zap.Error(err),
-			)
+			if c := h.HealthChecks.Active.logger.Check(zapcore.InfoLevel, "failed to read response body"); c != nil {
+				c.Write(
+					zap.String("host", hostAddr),
+					zap.Error(err),
+				)
+			}
 			markUnhealthy()
 			return nil
 		}
 		if !h.HealthChecks.Active.bodyRegexp.Match(bodyBytes) {
-			h.HealthChecks.Active.logger.Info("response body failed expectations",
-				zap.String("host", hostAddr),
-			)
+			if c := h.HealthChecks.Active.logger.Check(zapcore.InfoLevel, "response body failed expectations"); c != nil {
+				c.Write(
+					zap.String("host", hostAddr),
+				)
+			}
 			markUnhealthy()
 			return nil
 		}
@@ -556,9 +589,12 @@ func (h *Handler) countFailure(upstream *Upstream) {
 	// count failure immediately
 	err := upstream.Host.countFail(1)
 	if err != nil {
-		h.HealthChecks.Passive.logger.Error("could not count failure",
-			zap.String("host", upstream.Dial),
-			zap.Error(err))
+		if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "could not count failure"); c != nil {
+			c.Write(
+				zap.String("host", upstream.Dial),
+				zap.Error(err),
+			)
+		}
 		return
 	}
 
@@ -566,9 +602,12 @@ func (h *Handler) countFailure(upstream *Upstream) {
 	go func(host *Host, failDuration time.Duration) {
 		defer func() {
 			if err := recover(); err != nil {
-				h.HealthChecks.Passive.logger.Error("passive health check failure forgetter panicked",
-					zap.Any("error", err),
-					zap.ByteString("stack", debug.Stack()))
+				if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "passive health check failure forgetter panicked"); c != nil {
+					c.Write(
+						zap.Any("error", err),
+						zap.ByteString("stack", debug.Stack()),
+					)
+				}
 			}
 		}()
 		timer := time.NewTimer(failDuration)
@@ -581,9 +620,12 @@ func (h *Handler) countFailure(upstream *Upstream) {
 		}
 		err := host.countFail(-1)
 		if err != nil {
-			h.HealthChecks.Passive.logger.Error("could not forget failure",
-				zap.String("host", upstream.Dial),
-				zap.Error(err))
+			if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "could not forget failure"); c != nil {
+				c.Write(
+					zap.String("host", upstream.Dial),
+					zap.Error(err),
+				)
+			}
 		}
 	}(upstream.Host, failDuration)
 }
diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go
index 9929ae5d1..144960cf9 100644
--- a/modules/caddyhttp/reverseproxy/httptransport.go
+++ b/modules/caddyhttp/reverseproxy/httptransport.go
@@ -33,6 +33,7 @@ import (
 	"github.com/pires/go-proxyproto"
 	"github.com/quic-go/quic-go/http3"
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 	"golang.org/x/net/http2"
 
 	"github.com/caddyserver/caddy/v2"
@@ -750,7 +751,9 @@ func (c *tcpRWTimeoutConn) Read(b []byte) (int, error) {
 	if c.readTimeout > 0 {
 		err := c.TCPConn.SetReadDeadline(time.Now().Add(c.readTimeout))
 		if err != nil {
-			c.logger.Error("failed to set read deadline", zap.Error(err))
+			if ce := c.logger.Check(zapcore.ErrorLevel, "failed to set read deadline"); ce != nil {
+				ce.Write(zap.Error(err))
+			}
 		}
 	}
 	return c.TCPConn.Read(b)
@@ -760,7 +763,9 @@ func (c *tcpRWTimeoutConn) Write(b []byte) (int, error) {
 	if c.writeTimeout > 0 {
 		err := c.TCPConn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
 		if err != nil {
-			c.logger.Error("failed to set write deadline", zap.Error(err))
+			if ce := c.logger.Check(zapcore.ErrorLevel, "failed to set write deadline"); ce != nil {
+				ce.Write(zap.Error(err))
+			}
 		}
 	}
 	return c.TCPConn.Write(b)
diff --git a/modules/caddyhttp/reverseproxy/metrics.go b/modules/caddyhttp/reverseproxy/metrics.go
index d3c8ee03e..f744756d3 100644
--- a/modules/caddyhttp/reverseproxy/metrics.go
+++ b/modules/caddyhttp/reverseproxy/metrics.go
@@ -8,6 +8,7 @@ import (
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/promauto"
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 )
 
 var reverseProxyMetrics = struct {
@@ -48,9 +49,12 @@ func (m *metricsUpstreamsHealthyUpdater) Init() {
 	go func() {
 		defer func() {
 			if err := recover(); err != nil {
-				reverseProxyMetrics.logger.Error("upstreams healthy metrics updater panicked",
-					zap.Any("error", err),
-					zap.ByteString("stack", debug.Stack()))
+				if c := reverseProxyMetrics.logger.Check(zapcore.ErrorLevel, "upstreams healthy metrics updater panicked"); c != nil {
+					c.Write(
+						zap.Any("error", err),
+						zap.ByteString("stack", debug.Stack()),
+					)
+				}
 			}
 		}()
 
diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go
index 44cc2f9d9..bcbc1ff46 100644
--- a/modules/caddyhttp/reverseproxy/reverseproxy.go
+++ b/modules/caddyhttp/reverseproxy/reverseproxy.go
@@ -33,6 +33,7 @@ import (
 	"time"
 
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 	"golang.org/x/net/http/httpguts"
 
 	"github.com/caddyserver/caddy/v2"
@@ -439,7 +440,9 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
 			if h.LoadBalancing != nil {
 				lbWait = time.Duration(h.LoadBalancing.TryInterval)
 			}
-			h.logger.Debug("retrying", zap.Error(proxyErr), zap.Duration("after", lbWait))
+			if c := h.logger.Check(zapcore.DebugLevel, "retrying"); c != nil {
+				c.Write(zap.Error(proxyErr), zap.Duration("after", lbWait))
+			}
 		}
 		retries++
 	}
@@ -466,13 +469,17 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
 	if h.DynamicUpstreams != nil {
 		dUpstreams, err := h.DynamicUpstreams.GetUpstreams(r)
 		if err != nil {
-			h.logger.Error("failed getting dynamic upstreams; falling back to static upstreams", zap.Error(err))
+			if c := h.logger.Check(zapcore.ErrorLevel, "failed getting dynamic upstreams; falling back to static upstreams"); c != nil {
+				c.Write(zap.Error(err))
+			}
 		} else {
 			upstreams = dUpstreams
 			for _, dUp := range dUpstreams {
 				h.provisionUpstream(dUp)
 			}
-			h.logger.Debug("provisioned dynamic upstreams", zap.Int("count", len(dUpstreams)))
+			if c := h.logger.Check(zapcore.DebugLevel, "provisioned dynamic upstreams"); c != nil {
+				c.Write(zap.Int("count", len(dUpstreams)))
+			}
 			defer func() {
 				// these upstreams are dynamic, so they are only used for this iteration
 				// of the proxy loop; be sure to let them go away when we're done with them
@@ -503,9 +510,12 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
 		return true, fmt.Errorf("making dial info: %v", err)
 	}
 
-	h.logger.Debug("selected upstream",
-		zap.String("dial", dialInfo.Address),
-		zap.Int("total_upstreams", len(upstreams)))
+	if c := h.logger.Check(zapcore.DebugLevel, "selected upstream"); c != nil {
+		c.Write(
+			zap.String("dial", dialInfo.Address),
+			zap.Int("total_upstreams", len(upstreams)),
+		)
+	}
 
 	// attach to the request information about how to dial the upstream;
 	// this is necessary because the information cannot be sufficiently
@@ -811,16 +821,22 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
 			ShouldLogCredentials: shouldLogCredentials,
 		}),
 	)
+
 	if err != nil {
-		logger.Debug("upstream roundtrip", zap.Error(err))
+		if c := logger.Check(zapcore.DebugLevel, "upstream roundtrip"); c != nil {
+			c.Write(zap.Error(err))
+		}
 		return err
 	}
-	logger.Debug("upstream roundtrip",
-		zap.Object("headers", caddyhttp.LoggableHTTPHeader{
-			Header:               res.Header,
-			ShouldLogCredentials: shouldLogCredentials,
-		}),
-		zap.Int("status", res.StatusCode))
+	if c := logger.Check(zapcore.DebugLevel, "upstream roundtrip"); c != nil {
+		c.Write(
+			zap.Object("headers", caddyhttp.LoggableHTTPHeader{
+				Header:               res.Header,
+				ShouldLogCredentials: shouldLogCredentials,
+			}),
+			zap.Int("status", res.StatusCode),
+		)
+	}
 
 	// duration until upstream wrote response headers (roundtrip duration)
 	repl.Set("http.reverse_proxy.upstream.latency", duration)
@@ -879,7 +895,9 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
 		repl.Set("http.reverse_proxy.status_code", res.StatusCode)
 		repl.Set("http.reverse_proxy.status_text", res.Status)
 
-		logger.Debug("handling response", zap.Int("handler", i))
+		if c := logger.Check(zapcore.DebugLevel, "handling response"); c != nil {
+			c.Write(zap.Int("handler", i))
+		}
 
 		// we make some data available via request context to child routes
 		// so that they may inherit some options and functions from the
@@ -975,7 +993,9 @@ func (h *Handler) finalizeResponse(
 	err := h.copyResponse(rw, res.Body, h.flushInterval(req, res), logger)
 	errClose := res.Body.Close() // close now, instead of defer, to populate res.Trailer
 	if h.VerboseLogs || errClose != nil {
-		logger.Debug("closed response body from upstream", zap.Error(errClose))
+		if c := logger.Check(zapcore.DebugLevel, "closed response body from upstream"); c != nil {
+			c.Write(zap.Error(errClose))
+		}
 	}
 	if err != nil {
 		// we're streaming the response and we've already written headers, so
@@ -983,7 +1003,9 @@ func (h *Handler) finalizeResponse(
 		// we'll just log the error and abort the stream here and panic just as
 		// the standard lib's proxy to propagate the stream error.
 		// see issue https://github.com/caddyserver/caddy/issues/5951
-		logger.Warn("aborting with incomplete response", zap.Error(err))
+		if c := logger.Check(zapcore.WarnLevel, "aborting with incomplete response"); c != nil {
+			c.Write(zap.Error(err))
+		}
 		// no extra logging from stdlib
 		panic(http.ErrAbortHandler)
 	}
diff --git a/modules/caddyhttp/reverseproxy/streaming.go b/modules/caddyhttp/reverseproxy/streaming.go
index a696ac7fb..c871a3fa1 100644
--- a/modules/caddyhttp/reverseproxy/streaming.go
+++ b/modules/caddyhttp/reverseproxy/streaming.go
@@ -31,6 +31,7 @@ import (
 	"unsafe"
 
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 	"golang.org/x/net/http/httpguts"
 )
 
@@ -41,14 +42,18 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
 	// Taken from https://github.com/golang/go/commit/5c489514bc5e61ad9b5b07bd7d8ec65d66a0512a
 	// We know reqUpType is ASCII, it's checked by the caller.
 	if !asciiIsPrint(resUpType) {
-		logger.Debug("backend tried to switch to invalid protocol",
-			zap.String("backend_upgrade", resUpType))
+		if c := logger.Check(zapcore.DebugLevel, "backend tried to switch to invalid protocol"); c != nil {
+			c.Write(zap.String("backend_upgrade", resUpType))
+		}
 		return
 	}
 	if !asciiEqualFold(reqUpType, resUpType) {
-		logger.Debug("backend tried to switch to unexpected protocol via Upgrade header",
-			zap.String("backend_upgrade", resUpType),
-			zap.String("requested_upgrade", reqUpType))
+		if c := logger.Check(zapcore.DebugLevel, "backend tried to switch to unexpected protocol via Upgrade header"); c != nil {
+			c.Write(
+				zap.String("backend_upgrade", resUpType),
+				zap.String("requested_upgrade", reqUpType),
+			)
+		}
 		return
 	}
 
@@ -68,12 +73,16 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
 	//nolint:bodyclose
 	conn, brw, hijackErr := http.NewResponseController(rw).Hijack()
 	if errors.Is(hijackErr, http.ErrNotSupported) {
-		h.logger.Error("can't switch protocols using non-Hijacker ResponseWriter", zap.String("type", fmt.Sprintf("%T", rw)))
+		if c := logger.Check(zapcore.ErrorLevel, "can't switch protocols using non-Hijacker ResponseWriter"); c != nil {
+			c.Write(zap.String("type", fmt.Sprintf("%T", rw)))
+		}
 		return
 	}
 
 	if hijackErr != nil {
-		h.logger.Error("hijack failed on protocol switch", zap.Error(hijackErr))
+		if c := logger.Check(zapcore.ErrorLevel, "hijack failed on protocol switch"); c != nil {
+			c.Write(zap.Error(hijackErr))
+		}
 		return
 	}
 
@@ -93,11 +102,15 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
 	start := time.Now()
 	defer func() {
 		conn.Close()
-		logger.Debug("connection closed", zap.Duration("duration", time.Since(start)))
+		if c := logger.Check(zapcore.DebugLevel, "hijack failed on protocol switch"); c != nil {
+			c.Write(zap.Duration("duration", time.Since(start)))
+		}
 	}()
 
 	if err := brw.Flush(); err != nil {
-		logger.Debug("response flush", zap.Error(err))
+		if c := logger.Check(zapcore.DebugLevel, "response flush"); c != nil {
+			c.Write(zap.Error(err))
+		}
 		return
 	}
 
@@ -107,7 +120,9 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
 		data, _ := brw.Peek(buffered)
 		_, err := backConn.Write(data)
 		if err != nil {
-			logger.Debug("backConn write failed", zap.Error(err))
+			if c := logger.Check(zapcore.DebugLevel, "backConn write failed"); c != nil {
+				c.Write(zap.Error(err))
+			}
 			return
 		}
 	}
@@ -148,9 +163,13 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
 	go spc.copyFromBackend(errc)
 	select {
 	case err := <-errc:
-		logger.Debug("streaming error", zap.Error(err))
+		if c := logger.Check(zapcore.DebugLevel, "streaming error"); c != nil {
+			c.Write(zap.Error(err))
+		}
 	case time := <-timeoutc:
-		logger.Debug("stream timed out", zap.Time("timeout", time))
+		if c := logger.Check(zapcore.DebugLevel, "stream timed out"); c != nil {
+			c.Write(zap.Time("timeout", time))
+		}
 	}
 }
 
@@ -247,7 +266,9 @@ func (h Handler) copyBuffer(dst io.Writer, src io.Reader, buf []byte, logger *za
 		logger.Debug("waiting to read from upstream")
 		nr, rerr := src.Read(buf)
 		logger := logger.With(zap.Int("read", nr))
-		logger.Debug("read from upstream", zap.Error(rerr))
+		if c := logger.Check(zapcore.DebugLevel, "read from upstream"); c != nil {
+			c.Write(zap.Error(rerr))
+		}
 		if rerr != nil && rerr != io.EOF && rerr != context.Canceled {
 			// TODO: this could be useful to know (indeed, it revealed an error in our
 			// fastcgi PoC earlier; but it's this single error report here that necessitates
@@ -256,7 +277,9 @@ func (h Handler) copyBuffer(dst io.Writer, src io.Reader, buf []byte, logger *za
 			// something we need to report to the client, but read errors are a problem on our
 			// end for sure. so we need to decide what we want.)
 			// p.logf("copyBuffer: ReverseProxy read error during body copy: %v", rerr)
-			h.logger.Error("reading from backend", zap.Error(rerr))
+			if c := logger.Check(zapcore.ErrorLevel, "reading from backend"); c != nil {
+				c.Write(zap.Error(rerr))
+			}
 		}
 		if nr > 0 {
 			logger.Debug("writing to downstream")
@@ -264,10 +287,13 @@ func (h Handler) copyBuffer(dst io.Writer, src io.Reader, buf []byte, logger *za
 			if nw > 0 {
 				written += int64(nw)
 			}
-			logger.Debug("wrote to downstream",
-				zap.Int("written", nw),
-				zap.Int64("written_total", written),
-				zap.Error(werr))
+			if c := logger.Check(zapcore.DebugLevel, "wrote to downstream"); c != nil {
+				c.Write(
+					zap.Int("written", nw),
+					zap.Int64("written_total", written),
+					zap.Error(werr),
+				)
+			}
 			if werr != nil {
 				return written, fmt.Errorf("writing: %w", werr)
 			}
@@ -347,13 +373,17 @@ func (h *Handler) cleanupConnections() error {
 	if len(h.connections) > 0 {
 		delay := time.Duration(h.StreamCloseDelay)
 		h.connectionsCloseTimer = time.AfterFunc(delay, func() {
-			h.logger.Debug("closing streaming connections after delay",
-				zap.Duration("delay", delay))
+			if c := h.logger.Check(zapcore.DebugLevel, "closing streaming connections after delay"); c != nil {
+				c.Write(zap.Duration("delay", delay))
+			}
 			err := h.closeConnections()
 			if err != nil {
-				h.logger.Error("failed to closed connections after delay",
-					zap.Error(err),
-					zap.Duration("delay", delay))
+				if c := h.logger.Check(zapcore.ErrorLevel, "failed to closed connections after delay"); c != nil {
+					c.Write(
+						zap.Error(err),
+						zap.Duration("delay", delay),
+					)
+				}
 			}
 		})
 	}
@@ -494,7 +524,9 @@ func (m *maxLatencyWriter) Write(p []byte) (n int, err error) {
 	m.mu.Lock()
 	defer m.mu.Unlock()
 	n, err = m.dst.Write(p)
-	m.logger.Debug("wrote bytes", zap.Int("n", n), zap.Error(err))
+	if c := m.logger.Check(zapcore.DebugLevel, "wrote bytes"); c != nil {
+		c.Write(zap.Int("n", n), zap.Error(err))
+	}
 	if m.latency < 0 {
 		m.logger.Debug("flushing immediately")
 		//nolint:errcheck
@@ -510,7 +542,9 @@ func (m *maxLatencyWriter) Write(p []byte) (n int, err error) {
 	} else {
 		m.t.Reset(m.latency)
 	}
-	m.logger.Debug("timer set for delayed flush", zap.Duration("duration", m.latency))
+	if c := m.logger.Check(zapcore.DebugLevel, "timer set for delayed flush"); c != nil {
+		c.Write(zap.Duration("duration", m.latency))
+	}
 	m.flushPending = true
 	return
 }
diff --git a/modules/caddyhttp/reverseproxy/upstreams.go b/modules/caddyhttp/reverseproxy/upstreams.go
index c8ba930d2..aa59dc41b 100644
--- a/modules/caddyhttp/reverseproxy/upstreams.go
+++ b/modules/caddyhttp/reverseproxy/upstreams.go
@@ -12,6 +12,7 @@ import (
 	"time"
 
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 
 	"github.com/caddyserver/caddy/v2"
 )
@@ -136,10 +137,13 @@ func (su SRVUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
 		return allNew(cached.upstreams), nil
 	}
 
-	su.logger.Debug("refreshing SRV upstreams",
-		zap.String("service", service),
-		zap.String("proto", proto),
-		zap.String("name", name))
+	if c := su.logger.Check(zapcore.DebugLevel, "refreshing SRV upstreams"); c != nil {
+		c.Write(
+			zap.String("service", service),
+			zap.String("proto", proto),
+			zap.String("name", name),
+		)
+	}
 
 	_, records, err := su.resolver.LookupSRV(r.Context(), service, proto, name)
 	if err != nil {
@@ -148,23 +152,30 @@ func (su SRVUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
 		// only return an error if no records were also returned.
 		if len(records) == 0 {
 			if su.GracePeriod > 0 {
-				su.logger.Error("SRV lookup failed; using previously cached", zap.Error(err))
+				if c := su.logger.Check(zapcore.ErrorLevel, "SRV lookup failed; using previously cached"); c != nil {
+					c.Write(zap.Error(err))
+				}
 				cached.freshness = time.Now().Add(time.Duration(su.GracePeriod) - time.Duration(su.Refresh))
 				srvs[suAddr] = cached
 				return allNew(cached.upstreams), nil
 			}
 			return nil, err
 		}
-		su.logger.Warn("SRV records filtered", zap.Error(err))
+		if c := su.logger.Check(zapcore.WarnLevel, "SRV records filtered"); c != nil {
+			c.Write(zap.Error(err))
+		}
 	}
 
 	upstreams := make([]Upstream, len(records))
 	for i, rec := range records {
-		su.logger.Debug("discovered SRV record",
-			zap.String("target", rec.Target),
-			zap.Uint16("port", rec.Port),
-			zap.Uint16("priority", rec.Priority),
-			zap.Uint16("weight", rec.Weight))
+		if c := su.logger.Check(zapcore.DebugLevel, "discovered SRV record"); c != nil {
+			c.Write(
+				zap.String("target", rec.Target),
+				zap.Uint16("port", rec.Port),
+				zap.Uint16("priority", rec.Priority),
+				zap.Uint16("weight", rec.Weight),
+			)
+		}
 		addr := net.JoinHostPort(rec.Target, strconv.Itoa(int(rec.Port)))
 		upstreams[i] = Upstream{Dial: addr}
 	}
@@ -361,10 +372,13 @@ func (au AUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
 	name := repl.ReplaceAll(au.Name, "")
 	port := repl.ReplaceAll(au.Port, "")
 
-	au.logger.Debug("refreshing A upstreams",
-		zap.String("version", ipVersion),
-		zap.String("name", name),
-		zap.String("port", port))
+	if c := au.logger.Check(zapcore.DebugLevel, "refreshing A upstreams"); c != nil {
+		c.Write(
+			zap.String("version", ipVersion),
+			zap.String("name", name),
+			zap.String("port", port),
+		)
+	}
 
 	ips, err := au.resolver.LookupIP(r.Context(), ipVersion, name)
 	if err != nil {
@@ -373,8 +387,9 @@ func (au AUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
 
 	upstreams := make([]Upstream, len(ips))
 	for i, ip := range ips {
-		au.logger.Debug("discovered A record",
-			zap.String("ip", ip.String()))
+		if c := au.logger.Check(zapcore.DebugLevel, "discovered A record"); c != nil {
+			c.Write(zap.String("ip", ip.String()))
+		}
 		upstreams[i] = Upstream{
 			Dial: net.JoinHostPort(ip.String(), port),
 		}
@@ -467,11 +482,16 @@ func (mu MultiUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
 
 		up, err := src.GetUpstreams(r)
 		if err != nil {
-			mu.logger.Error("upstream source returned error",
-				zap.Int("source_idx", i),
-				zap.Error(err))
+			if c := mu.logger.Check(zapcore.ErrorLevel, "upstream source returned error"); c != nil {
+				c.Write(
+					zap.Int("source_idx", i),
+					zap.Error(err),
+				)
+			}
 		} else if len(up) == 0 {
-			mu.logger.Warn("upstream source returned 0 upstreams", zap.Int("source_idx", i))
+			if c := mu.logger.Check(zapcore.WarnLevel, "upstream source returned 0 upstreams"); c != nil {
+				c.Write(zap.Int("source_idx", i))
+			}
 		} else {
 			upstreams = append(upstreams, up...)
 		}
diff --git a/modules/caddyhttp/rewrite/rewrite.go b/modules/caddyhttp/rewrite/rewrite.go
index c45ad98a2..e76682729 100644
--- a/modules/caddyhttp/rewrite/rewrite.go
+++ b/modules/caddyhttp/rewrite/rewrite.go
@@ -133,19 +133,17 @@ func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
 	repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
 	const message = "rewrote request"
 
-	if rewr.logger.Check(zap.DebugLevel, message) == nil {
+	c := rewr.logger.Check(zap.DebugLevel, message)
+	if c == nil {
 		rewr.Rewrite(r, repl)
 		return next.ServeHTTP(w, r)
 	}
 
-	logger := rewr.logger.With(
-		zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: r}),
-	)
-
 	changed := rewr.Rewrite(r, repl)
 
 	if changed {
-		logger.Debug(message,
+		c.Write(
+			zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: r}),
 			zap.String("method", r.Method),
 			zap.String("uri", r.RequestURI),
 		)
diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go
index 6caaabcda..9dfa1bb6d 100644
--- a/modules/caddyhttp/server.go
+++ b/modules/caddyhttp/server.go
@@ -275,7 +275,9 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		if r.ProtoMajor < 3 {
 			err := s.h3server.SetQUICHeaders(w.Header())
 			if err != nil {
-				s.logger.Error("setting HTTP/3 Alt-Svc header", zap.Error(err))
+				if c := s.logger.Check(zapcore.ErrorLevel, "setting HTTP/3 Alt-Svc header"); c != nil {
+					c.Write(zap.Error(err))
+				}
 			}
 		}
 	}
@@ -283,9 +285,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	// reject very long methods; probably a mistake or an attack
 	if len(r.Method) > 32 {
 		if s.shouldLogRequest(r) {
-			s.accessLogger.Debug("rejecting request with long method",
-				zap.String("method_trunc", r.Method[:32]),
-				zap.String("remote_addr", r.RemoteAddr))
+			if c := s.accessLogger.Check(zapcore.DebugLevel, "rejecting request with long method"); c != nil {
+				c.Write(
+					zap.String("method_trunc", r.Method[:32]),
+					zap.String("remote_addr", r.RemoteAddr),
+				)
+			}
 		}
 		w.WriteHeader(http.StatusMethodNotAllowed)
 		return
@@ -300,7 +305,9 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		//nolint:bodyclose
 		err := http.NewResponseController(w).EnableFullDuplex()
 		if err != nil {
-			s.logger.Warn("failed to enable full duplex", zap.Error(err))
+			if c := s.logger.Check(zapcore.WarnLevel, "failed to enable full duplex"); c != nil {
+				c.Write(zap.Error(err))
+			}
 		}
 	}
 
@@ -379,6 +386,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	// add HTTP error information to request context
 	r = s.Errors.WithError(r, err)
 
+	var fields []zapcore.Field
 	if s.Errors != nil && len(s.Errors.Routes) > 0 {
 		// execute user-defined error handling route
 		err2 := s.errorHandlerChain.ServeHTTP(w, r)
@@ -386,17 +394,28 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 			// user's error route handled the error response
 			// successfully, so now just log the error
 			for _, logger := range errLoggers {
-				logger.Debug(errMsg, errFields...)
+				if c := logger.Check(zapcore.DebugLevel, errMsg); c != nil {
+					if fields == nil {
+						fields = errFields()
+					}
+
+					c.Write(fields...)
+				}
 			}
 		} else {
 			// well... this is awkward
-			errFields = append([]zapcore.Field{
-				zap.String("error", err2.Error()),
-				zap.Namespace("first_error"),
-				zap.String("msg", errMsg),
-			}, errFields...)
 			for _, logger := range errLoggers {
-				logger.Error("error handling handler error", errFields...)
+				if c := logger.Check(zapcore.ErrorLevel, "error handling handler error"); c != nil {
+					if fields == nil {
+						fields = errFields()
+						fields = append([]zapcore.Field{
+							zap.String("error", err2.Error()),
+							zap.Namespace("first_error"),
+							zap.String("msg", errMsg),
+						}, fields...)
+					}
+					c.Write(fields...)
+				}
 			}
 			if handlerErr, ok := err.(HandlerError); ok {
 				w.WriteHeader(handlerErr.StatusCode)
@@ -405,11 +424,17 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 			}
 		}
 	} else {
+		logLevel := zapcore.DebugLevel
+		if errStatus >= 500 {
+			logLevel = zapcore.ErrorLevel
+		}
+
 		for _, logger := range errLoggers {
-			if errStatus >= 500 {
-				logger.Error(errMsg, errFields...)
-			} else {
-				logger.Debug(errMsg, errFields...)
+			if c := logger.Check(logLevel, errMsg); c != nil {
+				if fields == nil {
+					fields = errFields()
+				}
+				c.Write(fields...)
 			}
 		}
 		w.WriteHeader(errStatus)
@@ -746,7 +771,9 @@ func (s *Server) logTrace(mh MiddlewareHandler) {
 	if s.Logs == nil || !s.Logs.Trace {
 		return
 	}
-	s.traceLogger.Debug(caddy.GetModuleName(mh), zap.Any("module", mh))
+	if c := s.traceLogger.Check(zapcore.DebugLevel, caddy.GetModuleName(mh)); c != nil {
+		c.Write(zap.Any("module", mh))
+	}
 }
 
 // logRequest logs the request to access logs, unless skipped.
@@ -759,50 +786,64 @@ func (s *Server) logRequest(
 		return
 	}
 
-	repl.Set("http.response.status", wrec.Status()) // will be 0 if no response is written by us (Go will write 200 to client)
-	repl.Set("http.response.size", wrec.Size())
+	status := wrec.Status()
+	size := wrec.Size()
+
+	repl.Set("http.response.status", status) // will be 0 if no response is written by us (Go will write 200 to client)
+	repl.Set("http.response.size", size)
 	repl.Set("http.response.duration", duration)
 	repl.Set("http.response.duration_ms", duration.Seconds()*1e3) // multiply seconds to preserve decimal (see #4666)
 
-	userID, _ := repl.GetString("http.auth.user.id")
-
-	reqBodyLength := 0
-	if bodyReader != nil {
-		reqBodyLength = bodyReader.Length
-	}
-
-	extra := r.Context().Value(ExtraLogFieldsCtxKey).(*ExtraLogFields)
-
-	fieldCount := 6
-	fields := make([]zapcore.Field, 0, fieldCount+len(extra.fields))
-	fields = append(fields,
-		zap.Int("bytes_read", reqBodyLength),
-		zap.String("user_id", userID),
-		zap.Duration("duration", *duration),
-		zap.Int("size", wrec.Size()),
-		zap.Int("status", wrec.Status()),
-		zap.Object("resp_headers", LoggableHTTPHeader{
-			Header:               wrec.Header(),
-			ShouldLogCredentials: shouldLogCredentials,
-		}))
-	fields = append(fields, extra.fields...)
-
 	loggers := []*zap.Logger{accLog}
 	if s.Logs != nil {
 		loggers = s.Logs.wrapLogger(accLog, r)
 	}
 
-	// wrapping may return multiple loggers, so we log to all of them
+	message := "handled request"
+	if nop, ok := GetVar(r.Context(), "unhandled").(bool); ok && nop {
+		message = "NOP"
+	}
+
+	logLevel := zapcore.InfoLevel
+	if status >= 500 {
+		logLevel = zapcore.ErrorLevel
+	}
+
+	var fields []zapcore.Field
 	for _, logger := range loggers {
-		logAtLevel := logger.Info
-		if wrec.Status() >= 500 {
-			logAtLevel = logger.Error
+		c := logger.Check(logLevel, message)
+		if c == nil {
+			continue
 		}
-		message := "handled request"
-		if nop, ok := GetVar(r.Context(), "unhandled").(bool); ok && nop {
-			message = "NOP"
+
+		if fields == nil {
+
+			userID, _ := repl.GetString("http.auth.user.id")
+
+			reqBodyLength := 0
+			if bodyReader != nil {
+				reqBodyLength = bodyReader.Length
+			}
+
+			extra := r.Context().Value(ExtraLogFieldsCtxKey).(*ExtraLogFields)
+
+			fieldCount := 6
+			fields = make([]zapcore.Field, 0, fieldCount+len(extra.fields))
+			fields = append(fields,
+				zap.Int("bytes_read", reqBodyLength),
+				zap.String("user_id", userID),
+				zap.Duration("duration", *duration),
+				zap.Int("size", size),
+				zap.Int("status", status),
+				zap.Object("resp_headers", LoggableHTTPHeader{
+					Header:               wrec.Header(),
+					ShouldLogCredentials: shouldLogCredentials,
+				}),
+			)
+			fields = append(fields, extra.fields...)
 		}
-		logAtLevel(message, fields...)
+
+		c.Write(fields...)
 	}
 }
 
diff --git a/modules/caddyhttp/server_test.go b/modules/caddyhttp/server_test.go
index d382a6963..2c8033d45 100644
--- a/modules/caddyhttp/server_test.go
+++ b/modules/caddyhttp/server_test.go
@@ -121,6 +121,29 @@ func BenchmarkServer_LogRequest(b *testing.B) {
 	}
 }
 
+func BenchmarkServer_LogRequest_NopLogger(b *testing.B) {
+	s := &Server{}
+
+	extra := new(ExtraLogFields)
+	ctx := context.WithValue(context.Background(), ExtraLogFieldsCtxKey, extra)
+
+	req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx)
+	rec := httptest.NewRecorder()
+	wrec := NewResponseRecorder(rec, nil, nil)
+
+	duration := 50 * time.Millisecond
+	repl := NewTestReplacer(req)
+	bodyReader := &lengthReader{Source: req.Body}
+
+	accLog := zap.NewNop()
+
+	b.ResetTimer()
+
+	for i := 0; i < b.N; i++ {
+		s.logRequest(accLog, req, wrec, &duration, repl, bodyReader, false)
+	}
+}
+
 func BenchmarkServer_LogRequest_WithTraceID(b *testing.B) {
 	s := &Server{}
 
diff --git a/modules/caddyhttp/tracing/tracerprovider.go b/modules/caddyhttp/tracing/tracerprovider.go
index 035425ed6..0a3766974 100644
--- a/modules/caddyhttp/tracing/tracerprovider.go
+++ b/modules/caddyhttp/tracing/tracerprovider.go
@@ -7,6 +7,7 @@ import (
 
 	sdktrace "go.opentelemetry.io/otel/sdk/trace"
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 )
 
 // globalTracerProvider stores global tracer provider and is responsible for graceful shutdown when nobody is using it.
@@ -47,7 +48,9 @@ func (t *tracerProvider) cleanupTracerProvider(logger *zap.Logger) error {
 		if t.tracerProvider != nil {
 			// tracerProvider.ForceFlush SHOULD be invoked according to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#forceflush
 			if err := t.tracerProvider.ForceFlush(context.Background()); err != nil {
-				logger.Error("forcing flush", zap.Error(err))
+				if c := logger.Check(zapcore.ErrorLevel, "forcing flush"); c != nil {
+					c.Write(zap.Error(err))
+				}
 			}
 
 			// tracerProvider.Shutdown MUST be invoked according to https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#shutdown
diff --git a/modules/caddypki/acmeserver/acmeserver.go b/modules/caddypki/acmeserver/acmeserver.go
index eaf6e930a..aeb4eab8e 100644
--- a/modules/caddypki/acmeserver/acmeserver.go
+++ b/modules/caddypki/acmeserver/acmeserver.go
@@ -35,6 +35,7 @@ import (
 	"github.com/smallstep/certificates/db"
 	"github.com/smallstep/nosql"
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
@@ -243,10 +244,14 @@ func (ash Handler) Cleanup() error {
 	key := ash.getDatabaseKey()
 	deleted, err := databasePool.Delete(key)
 	if deleted {
-		ash.logger.Debug("unloading unused CA database", zap.String("db_key", key))
+		if c := ash.logger.Check(zapcore.DebugLevel, "unloading unused CA database"); c != nil {
+			c.Write(zap.String("db_key", key))
+		}
 	}
 	if err != nil {
-		ash.logger.Error("closing CA database", zap.String("db_key", key), zap.Error(err))
+		if c := ash.logger.Check(zapcore.ErrorLevel, "closing CA database"); c != nil {
+			c.Write(zap.String("db_key", key), zap.Error(err))
+		}
 	}
 	return err
 }
@@ -271,7 +276,9 @@ func (ash Handler) openDatabase() (*db.AuthDB, error) {
 	})
 
 	if loaded {
-		ash.logger.Debug("loaded preexisting CA database", zap.String("db_key", key))
+		if c := ash.logger.Check(zapcore.DebugLevel, "loaded preexisting CA database"); c != nil {
+			c.Write(zap.String("db_key", key))
+		}
 	}
 
 	return database.(databaseCloser).DB, err
diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go
index f46e296e3..9dfeff724 100644
--- a/modules/caddytls/acmeissuer.go
+++ b/modules/caddytls/acmeissuer.go
@@ -30,6 +30,7 @@ import (
 	"github.com/caddyserver/zerossl"
 	"github.com/mholt/acmez/v2/acme"
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/caddy/v2/caddyconfig"
@@ -321,7 +322,9 @@ func (iss *ACMEIssuer) generateZeroSSLEABCredentials(ctx context.Context, acct a
 		return nil, acct, fmt.Errorf("failed getting EAB credentials: HTTP %d", resp.StatusCode)
 	}
 
-	iss.logger.Info("generated EAB credentials", zap.String("key_id", result.EABKID))
+	if c := iss.logger.Check(zapcore.InfoLevel, "generated EAB credentials"); c != nil {
+		c.Write(zap.String("key_id", result.EABKID))
+	}
 
 	return &acme.EAB{
 		KeyID:  result.EABKID,
diff --git a/modules/caddytls/automation.go b/modules/caddytls/automation.go
index 781818ed1..e2e02221d 100644
--- a/modules/caddytls/automation.go
+++ b/modules/caddytls/automation.go
@@ -26,6 +26,7 @@ import (
 	"github.com/caddyserver/certmagic"
 	"github.com/mholt/acmez/v2"
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 
 	"github.com/caddyserver/caddy/v2"
 )
@@ -292,21 +293,30 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
 						remoteIP, _, _ = net.SplitHostPort(remote.String())
 					}
 				}
-				tlsApp.logger.Debug("asking for permission for on-demand certificate",
-					zap.String("remote_ip", remoteIP),
-					zap.String("domain", name))
+				if c := tlsApp.logger.Check(zapcore.DebugLevel, "asking for permission for on-demand certificate"); c != nil {
+					c.Write(
+						zap.String("remote_ip", remoteIP),
+						zap.String("domain", name),
+					)
+				}
 
 				// ask the permission module if this cert is allowed
 				if err := tlsApp.Automation.OnDemand.permission.CertificateAllowed(ctx, name); err != nil {
 					// distinguish true errors from denials, because it's important to elevate actual errors
 					if errors.Is(err, ErrPermissionDenied) {
-						tlsApp.logger.Debug("on-demand certificate issuance denied",
-							zap.String("domain", name),
-							zap.Error(err))
+						if c := tlsApp.logger.Check(zapcore.DebugLevel, "on-demand certificate issuance denied"); c != nil {
+							c.Write(
+								zap.String("domain", name),
+								zap.Error(err),
+							)
+						}
 					} else {
-						tlsApp.logger.Error("failed to get permission for on-demand certificate",
-							zap.String("domain", name),
-							zap.Error(err))
+						if c := tlsApp.logger.Check(zapcore.ErrorLevel, "failed to get permission for on-demand certificate"); c != nil {
+							c.Write(
+								zap.String("domain", name),
+								zap.Error(err),
+							)
+						}
 					}
 					return err
 				}
diff --git a/modules/caddytls/certmanagers.go b/modules/caddytls/certmanagers.go
index f65a78b09..56950bc84 100644
--- a/modules/caddytls/certmanagers.go
+++ b/modules/caddytls/certmanagers.go
@@ -12,6 +12,7 @@ import (
 	"github.com/caddyserver/certmagic"
 	"github.com/tailscale/tscert"
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -46,7 +47,9 @@ func (ts Tailscale) GetCertificate(ctx context.Context, hello *tls.ClientHelloIn
 		return nil, nil // pass-thru: Tailscale can't offer a cert for this name
 	}
 	if err != nil {
-		ts.logger.Warn("could not get status; will try to get certificate anyway", zap.Error(err))
+		if c := ts.logger.Check(zapcore.WarnLevel, "could not get status; will try to get certificate anyway"); c != nil {
+			c.Write(zap.Error(err))
+		}
 	}
 	return tscert.GetCertificateWithContext(ctx, hello)
 }
diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go
index e2890c848..2ff41f7b2 100644
--- a/modules/caddytls/connpolicy.go
+++ b/modules/caddytls/connpolicy.go
@@ -28,6 +28,7 @@ import (
 
 	"github.com/mholt/acmez/v2"
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/caddy/v2/caddyconfig"
@@ -338,8 +339,9 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
 
 		cfg.KeyLogWriter = logFile.(io.Writer)
 
-		tlsApp.logger.Warn("TLS SECURITY COMPROMISED: secrets logging is enabled!",
-			zap.String("log_filename", filename))
+		if c := tlsApp.logger.Check(zapcore.WarnLevel, "TLS SECURITY COMPROMISED: secrets logging is enabled!"); c != nil {
+			c.Write(zap.String("log_filename", filename))
+		}
 	}
 
 	setDefaultTLSParams(cfg)
diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go
index 65bbfa311..f44b4c02a 100644
--- a/modules/caddytls/matchers.go
+++ b/modules/caddytls/matchers.go
@@ -25,6 +25,7 @@ import (
 
 	"github.com/caddyserver/certmagic"
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -291,7 +292,9 @@ func (m MatchRemoteIP) Match(hello *tls.ClientHelloInfo) bool {
 	}
 	ipAddr, err := netip.ParseAddr(ipStr)
 	if err != nil {
-		m.logger.Error("invalid client IP address", zap.String("ip", ipStr))
+		if c := m.logger.Check(zapcore.ErrorLevel, "invalid client IP address"); c != nil {
+			c.Write(zap.String("ip", ipStr))
+		}
 		return false
 	}
 	return (len(m.cidrs) == 0 || m.matches(ipAddr, m.cidrs)) &&
@@ -408,7 +411,9 @@ func (m MatchLocalIP) Match(hello *tls.ClientHelloInfo) bool {
 	}
 	ipAddr, err := netip.ParseAddr(ipStr)
 	if err != nil {
-		m.logger.Error("invalid local IP address", zap.String("ip", ipStr))
+		if c := m.logger.Check(zapcore.ErrorLevel, "invalid local IP address"); c != nil {
+			c.Write(zap.String("ip", ipStr))
+		}
 		return false
 	}
 	return (len(m.cidrs) == 0 || m.matches(ipAddr, m.cidrs))
diff --git a/modules/caddytls/ondemand.go b/modules/caddytls/ondemand.go
index 060a3ac6a..89abfe03f 100644
--- a/modules/caddytls/ondemand.go
+++ b/modules/caddytls/ondemand.go
@@ -26,6 +26,7 @@ import (
 
 	"github.com/caddyserver/certmagic"
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -156,10 +157,13 @@ func (p PermissionByHTTP) CertificateAllowed(ctx context.Context, name string) e
 		remote = chi.Conn.RemoteAddr().String()
 	}
 
-	p.logger.Debug("asking permission endpoint",
-		zap.String("remote", remote),
-		zap.String("domain", name),
-		zap.String("url", askURLString))
+	if c := p.logger.Check(zapcore.DebugLevel, "asking permission endpoint"); c != nil {
+		c.Write(
+			zap.String("remote", remote),
+			zap.String("domain", name),
+			zap.String("url", askURLString),
+		)
+	}
 
 	resp, err := onDemandAskClient.Get(askURLString)
 	if err != nil {
@@ -168,11 +172,14 @@ func (p PermissionByHTTP) CertificateAllowed(ctx context.Context, name string) e
 	}
 	resp.Body.Close()
 
-	p.logger.Debug("response from permission endpoint",
-		zap.String("remote", remote),
-		zap.String("domain", name),
-		zap.String("url", askURLString),
-		zap.Int("status", resp.StatusCode))
+	if c := p.logger.Check(zapcore.DebugLevel, "response from permission endpoint"); c != nil {
+		c.Write(
+			zap.String("remote", remote),
+			zap.String("domain", name),
+			zap.String("url", askURLString),
+			zap.Int("status", resp.StatusCode),
+		)
+	}
 
 	if resp.StatusCode < 200 || resp.StatusCode > 299 {
 		return fmt.Errorf("%s: %w %s - non-2xx status code %d", name, ErrPermissionDenied, askEndpoint, resp.StatusCode)
diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go
index b30b10c24..5f3d0eaeb 100644
--- a/modules/caddytls/tls.go
+++ b/modules/caddytls/tls.go
@@ -27,6 +27,7 @@ import (
 
 	"github.com/caddyserver/certmagic"
 	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
 
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/caddy/v2/modules/caddyevents"
@@ -323,8 +324,9 @@ func (t *TLS) Start() error {
 	if t.Automation.OnDemand == nil || (t.Automation.OnDemand.Ask == "" && t.Automation.OnDemand.permission == nil) {
 		for _, ap := range t.Automation.Policies {
 			if ap.OnDemand && ap.isWildcardOrDefault() {
-				t.logger.Warn("YOUR SERVER MAY BE VULNERABLE TO ABUSE: on-demand TLS is enabled, but no protections are in place",
-					zap.String("docs", "https://caddyserver.com/docs/automatic-https#on-demand-tls"))
+				if c := t.logger.Check(zapcore.WarnLevel, "YOUR SERVER MAY BE VULNERABLE TO ABUSE: on-demand TLS is enabled, but no protections are in place"); c != nil {
+					c.Write(zap.String("docs", "https://caddyserver.com/docs/automatic-https#on-demand-tls"))
+				}
 				break
 			}
 		}
@@ -408,9 +410,12 @@ func (t *TLS) Cleanup() error {
 		// give the new TLS app a "kick" to manage certs that it is configured for
 		// with its own configuration instead of the one we just evicted
 		if err := nextTLSApp.Manage(reManage); err != nil {
-			t.logger.Error("re-managing unloaded certificates with new config",
-				zap.Strings("subjects", reManage),
-				zap.Error(err))
+			if c := t.logger.Check(zapcore.ErrorLevel, "re-managing unloaded certificates with new config"); c != nil {
+				c.Write(
+					zap.Strings("subjects", reManage),
+					zap.Error(err),
+				)
+			}
 		}
 	} else {
 		// no more TLS app running, so delete in-memory cert cache
@@ -653,7 +658,9 @@ func (t *TLS) cleanStorageUnits() {
 
 	id, err := caddy.InstanceID()
 	if err != nil {
-		t.logger.Warn("unable to get instance ID; storage clean stamps will be incomplete", zap.Error(err))
+		if c := t.logger.Check(zapcore.WarnLevel, "unable to get instance ID; storage clean stamps will be incomplete"); c != nil {
+			c.Write(zap.Error(err))
+		}
 	}
 	options := certmagic.CleanStorageOptions{
 		Logger:                 t.logger,
@@ -669,7 +676,9 @@ func (t *TLS) cleanStorageUnits() {
 	if err != nil {
 		// probably don't want to return early, since we should still
 		// see if any other storages can get cleaned up
-		t.logger.Error("could not clean default/global storage", zap.Error(err))
+		if c := t.logger.Check(zapcore.ErrorLevel, "could not clean default/global storage"); c != nil {
+			c.Write(zap.Error(err))
+		}
 	}
 
 	// then clean each storage defined in ACME automation policies
@@ -679,7 +688,9 @@ func (t *TLS) cleanStorageUnits() {
 				continue
 			}
 			if err := certmagic.CleanStorage(t.ctx, ap.storage, options); err != nil {
-				t.logger.Error("could not clean storage configured in automation policy", zap.Error(err))
+				if c := t.logger.Check(zapcore.ErrorLevel, "could not clean storage configured in automation policy"); c != nil {
+					c.Write(zap.Error(err))
+				}
 			}
 		}
 	}

From 6ab9fb6f7424026ed67ceb112d99a4c483ef468b Mon Sep 17 00:00:00 2001
From: Mohammed Al Sahaf <msaa1990@gmail.com>
Date: Mon, 16 Sep 2024 16:50:26 +0300
Subject: [PATCH 48/62] ci: update the linter action version (#6575)

* ci: update the linter action version

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* exclude rule `G115`; disable deprecated linter

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

---------

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
---
 .github/workflows/lint.yml  | 2 +-
 .golangci.yml               | 4 +++-
 modules/caddyhttp/server.go | 1 -
 3 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 94250f88f..22e13973f 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -49,7 +49,7 @@ jobs:
       - name: golangci-lint
         uses: golangci/golangci-lint-action@v6
         with:
-          version: v1.60
+          version: latest
 
           # Windows times out frequently after about 5m50s if we don't set a longer timeout.
           args: --timeout 10m
diff --git a/.golangci.yml b/.golangci.yml
index 74e3503c4..e8b2a55d3 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -35,7 +35,6 @@ linters:
     - errcheck
     - errname
     - exhaustive
-    - exportloopref
     - gci
     - gofmt
     - goimports
@@ -145,6 +144,9 @@ output:
 
 issues:
   exclude-rules:
+    - text: 'G115' # TODO: Either we should fix the issues or nuke the linter if it's bad
+      linters:
+        - gosec
     # we aren't calling unknown URL
     - text: 'G107' # G107: Url provided to HTTP request as taint input
       linters:
diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go
index 9dfa1bb6d..e301bdc68 100644
--- a/modules/caddyhttp/server.go
+++ b/modules/caddyhttp/server.go
@@ -817,7 +817,6 @@ func (s *Server) logRequest(
 		}
 
 		if fields == nil {
-
 			userID, _ := repl.GetString("http.auth.user.id")
 
 			reqBodyLength := 0

From 5b44d6cea87e91723e166eb30041d0ec79c29e66 Mon Sep 17 00:00:00 2001
From: Marten Seemann <martenseemann@gmail.com>
Date: Sat, 21 Sep 2024 05:00:13 +0800
Subject: [PATCH 49/62] update quic-go to v0.47.0 (#6582)

---
 go.mod | 4 ++--
 go.sum | 8 ++++----
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/go.mod b/go.mod
index 6ae4a4a8e..051058f4e 100644
--- a/go.mod
+++ b/go.mod
@@ -19,7 +19,7 @@ require (
 	github.com/klauspost/cpuid/v2 v2.2.7
 	github.com/mholt/acmez/v2 v2.0.1
 	github.com/prometheus/client_golang v1.19.1
-	github.com/quic-go/quic-go v0.46.0
+	github.com/quic-go/quic-go v0.47.0
 	github.com/smallstep/certificates v0.26.1
 	github.com/smallstep/nosql v0.6.1
 	github.com/smallstep/truststore v0.13.0
@@ -62,7 +62,7 @@ require (
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect
 	github.com/onsi/ginkgo/v2 v2.13.2 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/quic-go/qpack v0.4.0 // indirect
+	github.com/quic-go/qpack v0.5.1 // indirect
 	github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 // indirect
 	github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 // indirect
 	github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d // indirect
diff --git a/go.sum b/go.sum
index 5bf39b424..e65ac8b43 100644
--- a/go.sum
+++ b/go.sum
@@ -337,10 +337,10 @@ github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSz
 github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
 github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
 github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
-github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
-github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
-github.com/quic-go/quic-go v0.46.0 h1:uuwLClEEyk1DNvchH8uCByQVjo3yKL9opKulExNDs7Y=
-github.com/quic-go/quic-go v0.46.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI=
+github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
+github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
+github.com/quic-go/quic-go v0.47.0 h1:yXs3v7r2bm1wmPTYNLKAAJTHMYkPEsfYJmTazXrCZ7Y=
+github.com/quic-go/quic-go v0.47.0/go.mod h1:3bCapYsJvXGZcipOHuu7plYtaV6tnF+z7wIFsU0WK9E=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
 github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=

From ff67b971267abb24774d18f323b0d6d43bfcdb3b Mon Sep 17 00:00:00 2001
From: Marten Seemann <martenseemann@gmail.com>
Date: Sat, 21 Sep 2024 11:47:18 +0800
Subject: [PATCH 50/62] caddyhttp: enable qlog, controlled by QLOGDIR env
 (#6581)

---
 go.mod                      |   1 +
 go.sum                      | 144 ++++++++++++++++++++++++++++++++++++
 listeners.go                |   9 ++-
 modules/caddyhttp/server.go |   3 +-
 4 files changed, 155 insertions(+), 2 deletions(-)

diff --git a/go.mod b/go.mod
index 051058f4e..0c4f1edd7 100644
--- a/go.mod
+++ b/go.mod
@@ -51,6 +51,7 @@ require (
 	github.com/Microsoft/go-winio v0.6.0 // indirect
 	github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/francoispqt/gojay v1.2.13 // indirect
 	github.com/fxamacker/cbor/v2 v2.6.0 // indirect
 	github.com/go-jose/go-jose/v3 v3.0.3 // indirect
 	github.com/go-kit/log v0.2.1 // indirect
diff --git a/go.sum b/go.sum
index e65ac8b43..6bf0bd3e2 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,7 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
 cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
 cloud.google.com/go/auth v0.4.1 h1:Z7YNIhlWRtrnKlZke7z3GMqzvuYzdc2z98F9D1NV5Hg=
 cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro=
@@ -12,8 +16,13 @@ cloud.google.com/go/kms v1.16.0 h1:1yZsRPhmargZOmY+fVAh8IKiR9HzCb0U1zsxb5g2nRY=
 cloud.google.com/go/kms v1.16.0/go.mod h1:olQUXy2Xud+1GzYfiBO9N0RhjsJk5IJLU6n/ethLXVc=
 cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU=
 cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
+dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
+dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
+dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
+dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
 github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
 github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@@ -38,6 +47,7 @@ github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv
 github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
 github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
 github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
 github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
 github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
@@ -71,8 +81,11 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.28.7 h1:et3Ta53gotFR4ERLXXHIHl/Uuk1q
 github.com/aws/aws-sdk-go-v2/service/sts v1.28.7/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw=
 github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
 github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
+github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
 github.com/caddyserver/certmagic v0.21.3 h1:pqRRry3yuB4CWBVq9+cUqu+Y6E2z8TswbhNx1AZeYm0=
 github.com/caddyserver/certmagic v0.21.3/go.mod h1:Zq6pklO9nVRl3DIFUw9gVUfXKdpc/0qwTUAQMBlfgtI=
 github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
@@ -93,11 +106,13 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
 github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
 github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
 github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
@@ -128,11 +143,17 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
+github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
+github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
 github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
 github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
 github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
 github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
 github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
 github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@@ -158,17 +179,23 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEe
 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
 github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
 github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68=
 github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
 github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
 github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84=
@@ -176,15 +203,20 @@ github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u
 github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
 github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
 github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
+github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
 github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
 github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
 github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98=
 github.com/google/go-tpm-tools v0.4.4/go.mod h1:T8jXkp2s+eltnCDIsXR84/MTcVU9Ja7bh3Mit0pa4AY=
 github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
 github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20231212022811-ec68065c825e h1:bwOy7hAFd0C91URzMIEBfr6BAz29yk7Qj0cy6S7DJlU=
 github.com/google/pprof v0.0.0-20231212022811-ec68065c825e/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
@@ -195,8 +227,14 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
 github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
+github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU=
+github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
+github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
 github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg=
 github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 h1:RtRsiaGvWxcwd8y3BiRZxsylPT8hLWZ5SPcfI+3IDNk=
 github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0/go.mod h1:TzP6duP4Py2pHLVPPQp42aoYI92+PCrVotyR5e8Vqlk=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
@@ -257,7 +295,10 @@ github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx
 github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
 github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
 github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
@@ -272,6 +313,7 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -284,7 +326,9 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
 github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
+github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
 github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
 github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
@@ -297,10 +341,12 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
 github.com/mholt/acmez/v2 v2.0.1 h1:3/3N0u1pLjMK4sNEAFSI+bcvzbPhRpY383sy1kLHJ6k=
 github.com/mholt/acmez/v2 v2.0.1/go.mod h1:fX4c9r5jYwMyMsC+7tkYRxHibkOTgta5DIFGoe67e1U=
+github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
 github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
 github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
 github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
@@ -313,10 +359,15 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
 github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
 github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
+github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
 github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs=
 github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM=
 github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
 github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
+github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
 github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
@@ -329,12 +380,16 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
 github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
+github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
 github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
 github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
+github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
 github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
 github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
+github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
 github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
 github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
@@ -355,11 +410,34 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
 github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E=
 github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
 github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
 github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
+github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
+github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
+github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
+github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
+github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
+github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
+github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
+github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
+github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
+github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
+github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
+github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
+github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
+github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
+github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
+github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
+github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
+github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
+github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
+github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
 github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
@@ -381,6 +459,8 @@ github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d h1:06LUHn4Ia2X6syjI
 github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d/go.mod h1:4d0ub42ut1mMtvGyMensjuHYEUpRrASvkzLEJvoRQcU=
 github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4=
 github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A=
+github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
+github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
@@ -417,9 +497,12 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ=
 github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
+github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk=
 github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA=
+github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
+github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
 github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
 github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
@@ -438,6 +521,7 @@ github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l
 github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
 go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
 go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
+go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
@@ -497,8 +581,12 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
 go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
 go.uber.org/zap/exp v0.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs=
 go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ=
+go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
+golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
+golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -514,8 +602,12 @@ golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
 golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
 golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw=
 golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
+golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
@@ -523,7 +615,15 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
 golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -534,18 +634,31 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
 golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
 golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
 golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -579,6 +692,7 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
 golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
 golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -590,9 +704,15 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
 golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
 golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
@@ -610,14 +730,30 @@ golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
 google.golang.org/api v0.180.0 h1:M2D87Yo0rGBPWpo1orwfCLehUUL6E7/TYe5gvMQWDh4=
 google.golang.org/api v0.180.0/go.mod h1:51AiyoEg1MJPSZ9zvklA8VnRILPXxn1iVen9v25XHAE=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
+google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw=
 google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw=
 google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae h1:AH34z6WAGVNkllnKs5raNq3yRq93VnjBG6rpfub/jYk=
 google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae/go.mod h1:FfiGhwUm6CJviekPrc0oJ+7h29e+DmWU6UtjX0ZvI7Y=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 h1:DujSIu+2tC9Ht0aPNA7jgj23Iq8Ewi5sgkQ++wdvonE=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
+google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
+google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
+google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
 google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
 google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
@@ -629,9 +765,11 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
 gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
 gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
@@ -639,6 +777,12 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
+honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
 howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
+sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
+sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
diff --git a/listeners.go b/listeners.go
index fa5ac1f56..0d9dd7538 100644
--- a/listeners.go
+++ b/listeners.go
@@ -31,6 +31,7 @@ import (
 
 	"github.com/quic-go/quic-go"
 	"github.com/quic-go/quic-go/http3"
+	"github.com/quic-go/quic-go/qlog"
 	"go.uber.org/zap"
 	"golang.org/x/time/rate"
 
@@ -437,7 +438,13 @@ func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config
 			Conn:                h3ln,
 			VerifySourceAddress: func(addr net.Addr) bool { return !limiter.Allow() },
 		}
-		earlyLn, err := tr.ListenEarly(http3.ConfigureTLSConfig(quicTlsConfig), &quic.Config{Allow0RTT: true})
+		earlyLn, err := tr.ListenEarly(
+			http3.ConfigureTLSConfig(quicTlsConfig),
+			&quic.Config{
+				Allow0RTT: true,
+				Tracer:    qlog.DefaultConnectionTracer,
+			},
+		)
 		if err != nil {
 			return nil, err
 		}
diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go
index e301bdc68..54b1c3b34 100644
--- a/modules/caddyhttp/server.go
+++ b/modules/caddyhttp/server.go
@@ -32,6 +32,7 @@ import (
 	"github.com/caddyserver/certmagic"
 	"github.com/quic-go/quic-go"
 	"github.com/quic-go/quic-go/http3"
+	"github.com/quic-go/quic-go/qlog"
 	"go.uber.org/zap"
 	"go.uber.org/zap/zapcore"
 
@@ -623,9 +624,9 @@ func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error
 			}),
 			TLSConfig:      tlsCfg,
 			MaxHeaderBytes: s.MaxHeaderBytes,
-			// TODO: remove this config when draft versions are no longer supported (we have no need to support drafts)
 			QUICConfig: &quic.Config{
 				Versions: []quic.Version{quic.Version1, quic.Version2},
+				Tracer:   qlog.DefaultConnectionTracer,
 			},
 			IdleTimeout: time.Duration(s.IdleTimeout),
 			ConnContext: func(ctx context.Context, c quic.Connection) context.Context {

From 9dda8fbf846db052243e6ce5ab707650da8c030e Mon Sep 17 00:00:00 2001
From: Francis Lavoie <lavofr@gmail.com>
Date: Wed, 25 Sep 2024 08:00:48 -0400
Subject: [PATCH 51/62] caddytls: Give a better error message when given
 encrypted private keys (#6591)

---
 modules/caddytls/fileloader.go    | 9 +++++++++
 modules/caddytls/folderloader.go  | 6 ++++++
 modules/caddytls/storageloader.go | 9 +++++++++
 3 files changed, 24 insertions(+)

diff --git a/modules/caddytls/fileloader.go b/modules/caddytls/fileloader.go
index 8603bbe65..7d2927e2a 100644
--- a/modules/caddytls/fileloader.go
+++ b/modules/caddytls/fileloader.go
@@ -18,6 +18,7 @@ import (
 	"crypto/tls"
 	"fmt"
 	"os"
+	"strings"
 
 	"github.com/caddyserver/caddy/v2"
 )
@@ -92,8 +93,16 @@ func (fl FileLoader) LoadCertificates() ([]Certificate, error) {
 		switch pair.Format {
 		case "":
 			fallthrough
+
 		case "pem":
+			// if the start of the key file looks like an encrypted private key,
+			// reject it with a helpful error message
+			if strings.Contains(string(keyData[:40]), "ENCRYPTED") {
+				return nil, fmt.Errorf("encrypted private keys are not supported; please decrypt the key first")
+			}
+
 			cert, err = tls.X509KeyPair(certData, keyData)
+
 		default:
 			return nil, fmt.Errorf("unrecognized certificate/key encoding format: %s", pair.Format)
 		}
diff --git a/modules/caddytls/folderloader.go b/modules/caddytls/folderloader.go
index 89e978df6..2df6f4cee 100644
--- a/modules/caddytls/folderloader.go
+++ b/modules/caddytls/folderloader.go
@@ -150,6 +150,12 @@ func tlsCertFromCertAndKeyPEMBundle(bundle []byte) (tls.Certificate, error) {
 		return tls.Certificate{}, fmt.Errorf("no private key block found")
 	}
 
+	// if the start of the key file looks like an encrypted private key,
+	// reject it with a helpful error message
+	if strings.HasPrefix(string(keyPEMBytes[:40]), "ENCRYPTED") {
+		return tls.Certificate{}, fmt.Errorf("encrypted private keys are not supported; please decrypt the key first")
+	}
+
 	cert, err := tls.X509KeyPair(certPEMBytes, keyPEMBytes)
 	if err != nil {
 		return tls.Certificate{}, fmt.Errorf("making X509 key pair: %v", err)
diff --git a/modules/caddytls/storageloader.go b/modules/caddytls/storageloader.go
index f9f0e7e68..c9487e892 100644
--- a/modules/caddytls/storageloader.go
+++ b/modules/caddytls/storageloader.go
@@ -17,6 +17,7 @@ package caddytls
 import (
 	"crypto/tls"
 	"fmt"
+	"strings"
 
 	"github.com/caddyserver/certmagic"
 
@@ -88,8 +89,16 @@ func (sl StorageLoader) LoadCertificates() ([]Certificate, error) {
 		switch pair.Format {
 		case "":
 			fallthrough
+
 		case "pem":
+			// if the start of the key file looks like an encrypted private key,
+			// reject it with a helpful error message
+			if strings.Contains(string(keyData[:40]), "ENCRYPTED") {
+				return nil, fmt.Errorf("encrypted private keys are not supported; please decrypt the key first")
+			}
+
 			cert, err = tls.X509KeyPair(certData, keyData)
+
 		default:
 			return nil, fmt.Errorf("unrecognized certificate/key encoding format: %s", pair.Format)
 		}

From 2faeac0a104c72e9396717406ecb7c353bf9aa64 Mon Sep 17 00:00:00 2001
From: Francis Lavoie <lavofr@gmail.com>
Date: Wed, 25 Sep 2024 16:30:56 -0400
Subject: [PATCH 52/62] chore: Use slices package where possible (#6585)

* chore: Use slices package where possible

* More, mostly using ContainsFunc

* Even more slice operations
---
 admin.go                                      | 19 ++-----
 caddyconfig/caddyfile/importgraph.go          |  8 +--
 caddyconfig/httpcaddyfile/directives.go       | 39 +++----------
 caddyconfig/httpcaddyfile/httptype.go         | 33 +++--------
 caddyconfig/httpcaddyfile/options.go          | 40 ++++++-------
 caddyconfig/httpcaddyfile/serveroptions.go    | 24 +++-----
 caddyconfig/httpcaddyfile/tlsapp.go           | 57 +++++++++----------
 modules/caddyevents/app.go                    | 25 ++++----
 modules/caddyhttp/autohttps.go                | 16 +-----
 modules/caddyhttp/encode/encode.go            | 10 ++--
 .../caddyhttp/fileserver/browsetplcontext.go  | 10 ++--
 modules/caddyhttp/headers/headers.go          | 13 +++--
 modules/caddyhttp/map/map.go                  | 14 +----
 modules/caddyhttp/matchers.go                 |  7 +--
 .../caddyhttp/reverseproxy/healthchecks.go    |  9 +--
 .../caddyhttp/reverseproxy/httptransport.go   | 17 ++----
 modules/caddyhttp/server.go                   | 26 +++------
 modules/caddyhttp/templates/frontmatter.go    |  1 +
 modules/caddytls/automation.go                | 10 ++--
 modules/caddytls/certselection.go             | 13 ++---
 modules/caddytls/matchers.go                  | 19 +++----
 21 files changed, 142 insertions(+), 268 deletions(-)

diff --git a/admin.go b/admin.go
index ef78254ee..e78adeec9 100644
--- a/admin.go
+++ b/admin.go
@@ -34,6 +34,7 @@ import (
 	"os"
 	"path"
 	"regexp"
+	"slices"
 	"strconv"
 	"strings"
 	"sync"
@@ -675,13 +676,7 @@ func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error {
 					// key recognized; make sure its HTTP request is permitted
 					for _, accessPerm := range adminAccess.Permissions {
 						// verify method
-						methodFound := accessPerm.Methods == nil
-						for _, method := range accessPerm.Methods {
-							if method == r.Method {
-								methodFound = true
-								break
-							}
-						}
+						methodFound := accessPerm.Methods == nil || slices.Contains(accessPerm.Methods, r.Method)
 						if !methodFound {
 							return APIError{
 								HTTPStatus: http.StatusForbidden,
@@ -877,13 +872,9 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
 // a trustworthy/expected value. This helps to mitigate DNS
 // rebinding attacks.
 func (h adminHandler) checkHost(r *http.Request) error {
-	var allowed bool
-	for _, allowedOrigin := range h.allowedOrigins {
-		if r.Host == allowedOrigin.Host {
-			allowed = true
-			break
-		}
-	}
+	allowed := slices.ContainsFunc(h.allowedOrigins, func(u *url.URL) bool {
+		return r.Host == u.Host
+	})
 	if !allowed {
 		return APIError{
 			HTTPStatus: http.StatusForbidden,
diff --git a/caddyconfig/caddyfile/importgraph.go b/caddyconfig/caddyfile/importgraph.go
index d5037fe62..ca859299d 100644
--- a/caddyconfig/caddyfile/importgraph.go
+++ b/caddyconfig/caddyfile/importgraph.go
@@ -16,6 +16,7 @@ package caddyfile
 
 import (
 	"fmt"
+	"slices"
 )
 
 type adjacency map[string][]string
@@ -91,12 +92,7 @@ func (i *importGraph) areConnected(from, to string) bool {
 	if !ok {
 		return false
 	}
-	for _, v := range al {
-		if v == to {
-			return true
-		}
-	}
-	return false
+	return slices.Contains(al, to)
 }
 
 func (i *importGraph) willCycle(from, to string) bool {
diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go
index 6972bb674..19ef4bc0e 100644
--- a/caddyconfig/httpcaddyfile/directives.go
+++ b/caddyconfig/httpcaddyfile/directives.go
@@ -17,6 +17,7 @@ package httpcaddyfile
 import (
 	"encoding/json"
 	"net"
+	"slices"
 	"sort"
 	"strconv"
 	"strings"
@@ -100,17 +101,6 @@ var defaultDirectiveOrder = []string{
 // plugins or by the user via the "order" global option.
 var directiveOrder = defaultDirectiveOrder
 
-// directiveIsOrdered returns true if dir is
-// a known, ordered (sorted) directive.
-func directiveIsOrdered(dir string) bool {
-	for _, d := range directiveOrder {
-		if d == dir {
-			return true
-		}
-	}
-	return false
-}
-
 // RegisterDirective registers a unique directive dir with an
 // associated unmarshaling (setup) function. When directive dir
 // is encountered in a Caddyfile, setupFunc will be called to
@@ -161,7 +151,7 @@ func RegisterHandlerDirective(dir string, setupFunc UnmarshalHandlerFunc) {
 // EXPERIMENTAL: This API may change or be removed.
 func RegisterDirectiveOrder(dir string, position Positional, standardDir string) {
 	// check if directive was already ordered
-	if directiveIsOrdered(dir) {
+	if slices.Contains(directiveOrder, dir) {
 		panic("directive '" + dir + "' already ordered")
 	}
 
@@ -172,12 +162,7 @@ func RegisterDirectiveOrder(dir string, position Positional, standardDir string)
 	// check if directive exists in standard distribution, since
 	// we can't allow plugins to depend on one another; we can't
 	// guarantee the order that plugins are loaded in.
-	foundStandardDir := false
-	for _, d := range defaultDirectiveOrder {
-		if d == standardDir {
-			foundStandardDir = true
-		}
-	}
+	foundStandardDir := slices.Contains(defaultDirectiveOrder, standardDir)
 	if !foundStandardDir {
 		panic("the 3rd argument '" + standardDir + "' must be a directive that exists in the standard distribution of Caddy")
 	}
@@ -603,23 +588,17 @@ func (sb serverBlock) hostsFromKeysNotHTTP(httpPort string) []string {
 // hasHostCatchAllKey returns true if sb has a key that
 // omits a host portion, i.e. it "catches all" hosts.
 func (sb serverBlock) hasHostCatchAllKey() bool {
-	for _, addr := range sb.keys {
-		if addr.Host == "" {
-			return true
-		}
-	}
-	return false
+	return slices.ContainsFunc(sb.keys, func(addr Address) bool {
+		return addr.Host == ""
+	})
 }
 
 // isAllHTTP returns true if all sb keys explicitly specify
 // the http:// scheme
 func (sb serverBlock) isAllHTTP() bool {
-	for _, addr := range sb.keys {
-		if addr.Scheme != "http" {
-			return false
-		}
-	}
-	return true
+	return !slices.ContainsFunc(sb.keys, func(addr Address) bool {
+		return addr.Scheme != "http"
+	})
 }
 
 // Positional are the supported modes for ordering directives.
diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go
index a8a2ae5b3..c858ee564 100644
--- a/caddyconfig/httpcaddyfile/httptype.go
+++ b/caddyconfig/httpcaddyfile/httptype.go
@@ -536,7 +536,7 @@ func (st *ServerType) serversFromPairings(
 					if k == j {
 						continue
 					}
-					if sliceContains(sblock2.block.GetKeysText(), key) {
+					if slices.Contains(sblock2.block.GetKeysText(), key) {
 						return nil, fmt.Errorf("ambiguous site definition: %s", key)
 					}
 				}
@@ -720,7 +720,7 @@ func (st *ServerType) serversFromPairings(
 						if srv.AutoHTTPS == nil {
 							srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
 						}
-						if !sliceContains(srv.AutoHTTPS.Skip, addr.Host) {
+						if !slices.Contains(srv.AutoHTTPS.Skip, addr.Host) {
 							srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, addr.Host)
 						}
 					}
@@ -734,7 +734,7 @@ func (st *ServerType) serversFromPairings(
 				// https://caddy.community/t/making-sense-of-auto-https-and-why-disabling-it-still-serves-https-instead-of-http/9761
 				createdTLSConnPolicies, ok := sblock.pile["tls.connection_policy"]
 				hasTLSEnabled := (ok && len(createdTLSConnPolicies) > 0) ||
-					(addr.Host != "" && srv.AutoHTTPS != nil && !sliceContains(srv.AutoHTTPS.Skip, addr.Host))
+					(addr.Host != "" && srv.AutoHTTPS != nil && !slices.Contains(srv.AutoHTTPS.Skip, addr.Host))
 
 				// we'll need to remember if the address qualifies for auto-HTTPS, so we
 				// can add a TLS conn policy if necessary
@@ -1061,7 +1061,7 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti
 				} else if cps[i].CertSelection != nil && cps[j].CertSelection != nil {
 					// if both have one, then combine AnyTag
 					for _, tag := range cps[j].CertSelection.AnyTag {
-						if !sliceContains(cps[i].CertSelection.AnyTag, tag) {
+						if !slices.Contains(cps[i].CertSelection.AnyTag, tag) {
 							cps[i].CertSelection.AnyTag = append(cps[i].CertSelection.AnyTag, tag)
 						}
 					}
@@ -1144,7 +1144,7 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
 func buildSubroute(routes []ConfigValue, groupCounter counter, needsSorting bool) (*caddyhttp.Subroute, error) {
 	if needsSorting {
 		for _, val := range routes {
-			if !directiveIsOrdered(val.directive) {
+			if !slices.Contains(directiveOrder, val.directive) {
 				return nil, fmt.Errorf("directive '%s' is not an ordered HTTP handler, so it cannot be used here - try placing within a route block or using the order global option", val.directive)
 			}
 		}
@@ -1354,17 +1354,8 @@ func (st *ServerType) compileEncodedMatcherSets(sblock serverBlock) ([]caddy.Mod
 
 		// add this server block's keys to the matcher
 		// pair if it doesn't already exist
-		if addr.Host != "" {
-			var found bool
-			for _, h := range chosenMatcherPair.hostm {
-				if h == addr.Host {
-					found = true
-					break
-				}
-			}
-			if !found {
-				chosenMatcherPair.hostm = append(chosenMatcherPair.hostm, addr.Host)
-			}
+		if addr.Host != "" && !slices.Contains(chosenMatcherPair.hostm, addr.Host) {
+			chosenMatcherPair.hostm = append(chosenMatcherPair.hostm, addr.Host)
 		}
 	}
 
@@ -1540,16 +1531,6 @@ func tryDuration(val any, warnings *[]caddyconfig.Warning) caddy.Duration {
 	return durationVal
 }
 
-// sliceContains returns true if needle is in haystack.
-func sliceContains(haystack []string, needle string) bool {
-	for _, s := range haystack {
-		if s == needle {
-			return true
-		}
-	}
-	return false
-}
-
 // listenersUseAnyPortOtherThan returns true if there are any
 // listeners in addresses that use a port which is not otherPort.
 // Mostly borrowed from unexported method in caddyhttp package.
diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go
index db9be52ca..53687d32a 100644
--- a/caddyconfig/httpcaddyfile/options.go
+++ b/caddyconfig/httpcaddyfile/options.go
@@ -15,6 +15,7 @@
 package httpcaddyfile
 
 import (
+	"slices"
 	"strconv"
 
 	"github.com/caddyserver/certmagic"
@@ -110,17 +111,12 @@ func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
 	}
 	pos := Positional(d.Val())
 
-	newOrder := directiveOrder
+	// if directive already had an order, drop it
+	newOrder := slices.DeleteFunc(directiveOrder, func(d string) bool {
+		return d == dirName
+	})
 
-	// if directive exists, first remove it
-	for i, d := range newOrder {
-		if d == dirName {
-			newOrder = append(newOrder[:i], newOrder[i+1:]...)
-			break
-		}
-	}
-
-	// act on the positional
+	// act on the positional; if it's First or Last, we're done right away
 	switch pos {
 	case First:
 		newOrder = append([]string{dirName}, newOrder...)
@@ -129,6 +125,7 @@ func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
 		}
 		directiveOrder = newOrder
 		return newOrder, nil
+
 	case Last:
 		newOrder = append(newOrder, dirName)
 		if d.NextArg() {
@@ -136,8 +133,11 @@ func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
 		}
 		directiveOrder = newOrder
 		return newOrder, nil
+
+	// if it's Before or After, continue
 	case Before:
 	case After:
+
 	default:
 		return nil, d.Errf("unknown positional '%s'", pos)
 	}
@@ -151,17 +151,17 @@ func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
 		return nil, d.ArgErr()
 	}
 
-	// insert directive into proper position
-	for i, d := range newOrder {
-		if d == otherDir {
-			if pos == Before {
-				newOrder = append(newOrder[:i], append([]string{dirName}, newOrder[i:]...)...)
-			} else if pos == After {
-				newOrder = append(newOrder[:i+1], append([]string{dirName}, newOrder[i+1:]...)...)
-			}
-			break
-		}
+	// get the position of the target directive
+	targetIndex := slices.Index(newOrder, otherDir)
+	if targetIndex == -1 {
+		return nil, d.Errf("directive '%s' not found", otherDir)
 	}
+	// if we're inserting after, we need to increment the index to go after
+	if pos == After {
+		targetIndex++
+	}
+	// insert the directive into the new order
+	newOrder = slices.Insert(newOrder, targetIndex, dirName)
 
 	directiveOrder = newOrder
 
diff --git a/caddyconfig/httpcaddyfile/serveroptions.go b/caddyconfig/httpcaddyfile/serveroptions.go
index 4246cd7dc..7087cdba5 100644
--- a/caddyconfig/httpcaddyfile/serveroptions.go
+++ b/caddyconfig/httpcaddyfile/serveroptions.go
@@ -17,6 +17,7 @@ package httpcaddyfile
 import (
 	"encoding/json"
 	"fmt"
+	"slices"
 
 	"github.com/dustin/go-humanize"
 
@@ -180,7 +181,7 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
 				if proto != "h1" && proto != "h2" && proto != "h2c" && proto != "h3" {
 					return nil, d.Errf("unknown protocol '%s': expected h1, h2, h2c, or h3", proto)
 				}
-				if sliceContains(serverOpts.Protocols, proto) {
+				if slices.Contains(serverOpts.Protocols, proto) {
 					return nil, d.Errf("protocol %s specified more than once", proto)
 				}
 				serverOpts.Protocols = append(serverOpts.Protocols, proto)
@@ -229,7 +230,7 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
 		case "client_ip_headers":
 			headers := d.RemainingArgs()
 			for _, header := range headers {
-				if sliceContains(serverOpts.ClientIPHeaders, header) {
+				if slices.Contains(serverOpts.ClientIPHeaders, header) {
 					return nil, d.Errf("client IP header %s specified more than once", header)
 				}
 				serverOpts.ClientIPHeaders = append(serverOpts.ClientIPHeaders, header)
@@ -288,24 +289,15 @@ func applyServerOptions(
 
 	for key, server := range servers {
 		// find the options that apply to this server
-		opts := func() *serverOptions {
-			for _, entry := range serverOpts {
-				if entry.ListenerAddress == "" {
-					return &entry
-				}
-				for _, listener := range server.Listen {
-					if entry.ListenerAddress == listener {
-						return &entry
-					}
-				}
-			}
-			return nil
-		}()
+		optsIndex := slices.IndexFunc(serverOpts, func(s serverOptions) bool {
+			return s.ListenerAddress == "" || slices.Contains(server.Listen, s.ListenerAddress)
+		})
 
 		// if none apply, then move to the next server
-		if opts == nil {
+		if optsIndex == -1 {
 			continue
 		}
+		opts := serverOpts[optsIndex]
 
 		// set all the options
 		server.ListenerWrappersRaw = opts.ListenerWrappersRaw
diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go
index f69e2c54a..157a3113e 100644
--- a/caddyconfig/httpcaddyfile/tlsapp.go
+++ b/caddyconfig/httpcaddyfile/tlsapp.go
@@ -19,6 +19,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"reflect"
+	"slices"
 	"sort"
 	"strconv"
 	"strings"
@@ -57,19 +58,20 @@ func (st ServerType) buildTLSApp(
 		for _, pair := range pairings {
 			for _, sb := range pair.serverBlocks {
 				for _, addr := range sb.keys {
-					if addr.Host == "" {
-						// this server block has a hostless key, now
-						// go through and add all the hosts to the set
-						for _, otherAddr := range sb.keys {
-							if otherAddr.Original == addr.Original {
-								continue
-							}
-							if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
-								httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
-							}
-						}
-						break
+					if addr.Host != "" {
+						continue
 					}
+					// this server block has a hostless key, now
+					// go through and add all the hosts to the set
+					for _, otherAddr := range sb.keys {
+						if otherAddr.Original == addr.Original {
+							continue
+						}
+						if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
+							httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
+						}
+					}
+					break
 				}
 			}
 		}
@@ -465,7 +467,7 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
 	if globalACMECA != nil && acmeIssuer.CA == "" {
 		acmeIssuer.CA = globalACMECA.(string)
 	}
-	if globalACMECARoot != nil && !sliceContains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) {
+	if globalACMECARoot != nil && !slices.Contains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) {
 		acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string))
 	}
 	if globalACMEDNS != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) {
@@ -580,7 +582,7 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
 			if !automationPolicyHasAllPublicNames(aps[i]) {
 				// if this automation policy has internal names, we might as well remove it
 				// so auto-https can implicitly use the internal issuer
-				aps = append(aps[:i], aps[i+1:]...)
+				aps = slices.Delete(aps, i, i+1)
 				i--
 			}
 		}
@@ -597,7 +599,7 @@ outer:
 		for j := i + 1; j < len(aps); j++ {
 			// if they're exactly equal in every way, just keep one of them
 			if reflect.DeepEqual(aps[i], aps[j]) {
-				aps = append(aps[:j], aps[j+1:]...)
+				aps = slices.Delete(aps, j, j+1)
 				// must re-evaluate current i against next j; can't skip it!
 				// even if i decrements to -1, will be incremented to 0 immediately
 				i--
@@ -627,18 +629,18 @@ outer:
 					// cause example.com to be served by the less specific policy for
 					// '*.com', which might be different (yes we've seen this happen)
 					if automationPolicyShadows(i, aps) >= j {
-						aps = append(aps[:i], aps[i+1:]...)
+						aps = slices.Delete(aps, i, i+1)
 						i--
 						continue outer
 					}
 				} else {
 					// avoid repeated subjects
 					for _, subj := range aps[j].SubjectsRaw {
-						if !sliceContains(aps[i].SubjectsRaw, subj) {
+						if !slices.Contains(aps[i].SubjectsRaw, subj) {
 							aps[i].SubjectsRaw = append(aps[i].SubjectsRaw, subj)
 						}
 					}
-					aps = append(aps[:j], aps[j+1:]...)
+					aps = slices.Delete(aps, j, j+1)
 					j--
 				}
 			}
@@ -658,13 +660,9 @@ func automationPolicyIsSubset(a, b *caddytls.AutomationPolicy) bool {
 		return false
 	}
 	for _, aSubj := range a.SubjectsRaw {
-		var inSuperset bool
-		for _, bSubj := range b.SubjectsRaw {
-			if certmagic.MatchWildcard(aSubj, bSubj) {
-				inSuperset = true
-				break
-			}
-		}
+		inSuperset := slices.ContainsFunc(b.SubjectsRaw, func(bSubj string) bool {
+			return certmagic.MatchWildcard(aSubj, bSubj)
+		})
 		if !inSuperset {
 			return false
 		}
@@ -709,12 +707,9 @@ func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) b
 // automationPolicyHasAllPublicNames returns true if all the names on the policy
 // do NOT qualify for public certs OR are tailscale domains.
 func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
-	for _, subj := range ap.SubjectsRaw {
-		if !subjectQualifiesForPublicCert(ap, subj) || isTailscaleDomain(subj) {
-			return false
-		}
-	}
-	return true
+	return !slices.ContainsFunc(ap.SubjectsRaw, func(i string) bool {
+		return !subjectQualifiesForPublicCert(ap, i) || isTailscaleDomain(i)
+	})
 }
 
 func isTailscaleDomain(name string) bool {
diff --git a/modules/caddyevents/app.go b/modules/caddyevents/app.go
index fe76a6757..2e4a6f2c0 100644
--- a/modules/caddyevents/app.go
+++ b/modules/caddyevents/app.go
@@ -124,18 +124,19 @@ func (app *App) Provision(ctx caddy.Context) error {
 	app.subscriptions = make(map[string]map[caddy.ModuleID][]Handler)
 
 	for _, sub := range app.Subscriptions {
-		if sub.HandlersRaw != nil {
-			handlersIface, err := ctx.LoadModule(sub, "HandlersRaw")
-			if err != nil {
-				return fmt.Errorf("loading event subscriber modules: %v", err)
-			}
-			for _, h := range handlersIface.([]any) {
-				sub.Handlers = append(sub.Handlers, h.(Handler))
-			}
-			if len(sub.Handlers) == 0 {
-				// pointless to bind without any handlers
-				return fmt.Errorf("no handlers defined")
-			}
+		if sub.HandlersRaw == nil {
+			continue
+		}
+		handlersIface, err := ctx.LoadModule(sub, "HandlersRaw")
+		if err != nil {
+			return fmt.Errorf("loading event subscriber modules: %v", err)
+		}
+		for _, h := range handlersIface.([]any) {
+			sub.Handlers = append(sub.Handlers, h.(Handler))
+		}
+		if len(sub.Handlers) == 0 {
+			// pointless to bind without any handlers
+			return fmt.Errorf("no handlers defined")
 		}
 	}
 
diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go
index 263cd14c7..ea5a07b3c 100644
--- a/modules/caddyhttp/autohttps.go
+++ b/modules/caddyhttp/autohttps.go
@@ -17,6 +17,7 @@ package caddyhttp
 import (
 	"fmt"
 	"net/http"
+	"slices"
 	"strconv"
 	"strings"
 
@@ -66,17 +67,6 @@ type AutoHTTPSConfig struct {
 	IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"`
 }
 
-// Skipped returns true if name is in skipSlice, which
-// should be either the Skip or SkipCerts field on ahc.
-func (ahc AutoHTTPSConfig) Skipped(name string, skipSlice []string) bool {
-	for _, n := range skipSlice {
-		if name == n {
-			return true
-		}
-	}
-	return false
-}
-
 // automaticHTTPSPhase1 provisions all route matchers, determines
 // which domain names found in the routes qualify for automatic
 // HTTPS, and sets up HTTP->HTTPS redirects. This phase must occur
@@ -158,7 +148,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
 								return fmt.Errorf("%s: route %d, matcher set %d, matcher %d, host matcher %d: %v",
 									srvName, routeIdx, matcherSetIdx, matcherIdx, hostMatcherIdx, err)
 							}
-							if !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.Skip) {
+							if !slices.Contains(srv.AutoHTTPS.Skip, d) {
 								serverDomainSet[d] = struct{}{}
 							}
 						}
@@ -193,7 +183,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
 		} else {
 			for d := range serverDomainSet {
 				if certmagic.SubjectQualifiesForCert(d) &&
-					!srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) {
+					!slices.Contains(srv.AutoHTTPS.SkipCerts, d) {
 					// if a certificate for this name is already loaded,
 					// don't obtain another one for it, unless we are
 					// supposed to ignore loaded certificates
diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go
index 00e507277..f0d56a90d 100644
--- a/modules/caddyhttp/encode/encode.go
+++ b/modules/caddyhttp/encode/encode.go
@@ -24,6 +24,7 @@ import (
 	"io"
 	"math"
 	"net/http"
+	"slices"
 	"sort"
 	"strconv"
 	"strings"
@@ -441,12 +442,9 @@ func AcceptedEncodings(r *http.Request, preferredOrder []string) []string {
 		}
 
 		// set server preference
-		prefOrder := -1
-		for i, p := range preferredOrder {
-			if encName == p {
-				prefOrder = len(preferredOrder) - i
-				break
-			}
+		prefOrder := slices.Index(preferredOrder, encName)
+		if prefOrder > -1 {
+			prefOrder = len(preferredOrder) - prefOrder
 		}
 
 		prefs = append(prefs, encodingPreference{
diff --git a/modules/caddyhttp/fileserver/browsetplcontext.go b/modules/caddyhttp/fileserver/browsetplcontext.go
index 37e1cc3dd..8e5d138f1 100644
--- a/modules/caddyhttp/fileserver/browsetplcontext.go
+++ b/modules/caddyhttp/fileserver/browsetplcontext.go
@@ -21,6 +21,7 @@ import (
 	"os"
 	"path"
 	"path/filepath"
+	"slices"
 	"sort"
 	"strconv"
 	"strings"
@@ -281,12 +282,9 @@ type fileInfo struct {
 
 // HasExt returns true if the filename has any of the given suffixes, case-insensitive.
 func (fi fileInfo) HasExt(exts ...string) bool {
-	for _, ext := range exts {
-		if strings.HasSuffix(strings.ToLower(fi.Name), strings.ToLower(ext)) {
-			return true
-		}
-	}
-	return false
+	return slices.ContainsFunc(exts, func(ext string) bool {
+		return strings.HasSuffix(strings.ToLower(fi.Name), strings.ToLower(ext))
+	})
 }
 
 // HumanSize returns the size of the file as a
diff --git a/modules/caddyhttp/headers/headers.go b/modules/caddyhttp/headers/headers.go
index bdb185f46..a3279d913 100644
--- a/modules/caddyhttp/headers/headers.go
+++ b/modules/caddyhttp/headers/headers.go
@@ -135,13 +135,14 @@ type HeaderOps struct {
 func (ops *HeaderOps) Provision(_ caddy.Context) error {
 	for fieldName, replacements := range ops.Replace {
 		for i, r := range replacements {
-			if r.SearchRegexp != "" {
-				re, err := regexp.Compile(r.SearchRegexp)
-				if err != nil {
-					return fmt.Errorf("replacement %d for header field '%s': %v", i, fieldName, err)
-				}
-				replacements[i].re = re
+			if r.SearchRegexp == "" {
+				continue
 			}
+			re, err := regexp.Compile(r.SearchRegexp)
+			if err != nil {
+				return fmt.Errorf("replacement %d for header field '%s': %v", i, fieldName, err)
+			}
+			replacements[i].re = re
 		}
 	}
 	return nil
diff --git a/modules/caddyhttp/map/map.go b/modules/caddyhttp/map/map.go
index 336f25723..d02085e76 100644
--- a/modules/caddyhttp/map/map.go
+++ b/modules/caddyhttp/map/map.go
@@ -18,6 +18,7 @@ import (
 	"fmt"
 	"net/http"
 	"regexp"
+	"slices"
 	"strings"
 
 	"github.com/caddyserver/caddy/v2"
@@ -126,7 +127,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhtt
 	// defer work until a variable is actually evaluated by using replacer's Map callback
 	repl.Map(func(key string) (any, bool) {
 		// return early if the variable is not even a configured destination
-		destIdx := h.destinationIndex(key)
+		destIdx := slices.Index(h.Destinations, key)
 		if destIdx < 0 {
 			return nil, false
 		}
@@ -170,17 +171,6 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhtt
 	return next.ServeHTTP(w, r)
 }
 
-// destinationIndex returns the positional index of the destination
-// is name is a known destination; otherwise it returns -1.
-func (h Handler) destinationIndex(name string) int {
-	for i, dest := range h.Destinations {
-		if dest == name {
-			return i
-		}
-	}
-	return -1
-}
-
 // Mapping describes a mapping from input to outputs.
 type Mapping struct {
 	// The input value to match. Must be distinct from other mappings.
diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go
index b7952ab69..a0bc6d63a 100644
--- a/modules/caddyhttp/matchers.go
+++ b/modules/caddyhttp/matchers.go
@@ -764,12 +764,7 @@ func (m *MatchMethod) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 
 // Match returns true if r matches m.
 func (m MatchMethod) Match(r *http.Request) bool {
-	for _, method := range m {
-		if r.Method == method {
-			return true
-		}
-	}
-	return false
+	return slices.Contains(m, r.Method)
 }
 
 // CELLibrary produces options that expose this matcher for use in CEL
diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go
index 179805f20..319cc9248 100644
--- a/modules/caddyhttp/reverseproxy/healthchecks.go
+++ b/modules/caddyhttp/reverseproxy/healthchecks.go
@@ -23,6 +23,7 @@ import (
 	"net/url"
 	"regexp"
 	"runtime/debug"
+	"slices"
 	"strconv"
 	"strings"
 	"time"
@@ -397,12 +398,8 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ
 		u.Scheme = "https"
 
 		// if the port is in the except list, flip back to HTTP
-		if ht, ok := h.Transport.(*HTTPTransport); ok {
-			for _, exceptPort := range ht.TLS.ExceptPorts {
-				if exceptPort == port {
-					u.Scheme = "http"
-				}
-			}
+		if ht, ok := h.Transport.(*HTTPTransport); ok && slices.Contains(ht.TLS.ExceptPorts, port) {
+			u.Scheme = "http"
 		}
 	}
 
diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go
index 144960cf9..bc8233788 100644
--- a/modules/caddyhttp/reverseproxy/httptransport.go
+++ b/modules/caddyhttp/reverseproxy/httptransport.go
@@ -27,6 +27,7 @@ import (
 	"net/url"
 	"os"
 	"reflect"
+	"slices"
 	"strings"
 	"time"
 
@@ -381,7 +382,7 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
 		rt.DisableCompression = !*h.Compression
 	}
 
-	if sliceContains(h.Versions, "2") {
+	if slices.Contains(h.Versions, "2") {
 		if err := http2.ConfigureTransport(rt); err != nil {
 			return nil, err
 		}
@@ -400,13 +401,13 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
 				return nil, fmt.Errorf("making TLS client config for HTTP/3 transport: %v", err)
 			}
 		}
-	} else if len(h.Versions) > 1 && sliceContains(h.Versions, "3") {
+	} else if len(h.Versions) > 1 && slices.Contains(h.Versions, "3") {
 		return nil, fmt.Errorf("if HTTP/3 is enabled to the upstream, no other HTTP versions are supported")
 	}
 
 	// if h2c is enabled, configure its transport (std lib http.Transport
 	// does not "HTTP/2 over cleartext TCP")
-	if sliceContains(h.Versions, "h2c") {
+	if slices.Contains(h.Versions, "h2c") {
 		// crafting our own http2.Transport doesn't allow us to utilize
 		// most of the customizations/preferences on the http.Transport,
 		// because, for some reason, only http2.ConfigureTransport()
@@ -783,16 +784,6 @@ func decodeBase64DERCert(certStr string) (*x509.Certificate, error) {
 	return x509.ParseCertificate(derBytes)
 }
 
-// sliceContains returns true if needle is in haystack.
-func sliceContains(haystack []string, needle string) bool {
-	for _, s := range haystack {
-		if s == needle {
-			return true
-		}
-	}
-	return false
-}
-
 // Interface guards
 var (
 	_ caddy.Provisioner  = (*HTTPTransport)(nil)
diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go
index 54b1c3b34..ece3f1cd2 100644
--- a/modules/caddyhttp/server.go
+++ b/modules/caddyhttp/server.go
@@ -25,6 +25,7 @@ import (
 	"net/netip"
 	"net/url"
 	"runtime"
+	"slices"
 	"strings"
 	"sync"
 	"time"
@@ -543,12 +544,9 @@ func (s *Server) hasListenerAddress(fullAddr string) bool {
 }
 
 func (s *Server) hasTLSClientAuth() bool {
-	for _, cp := range s.TLSConnPolicies {
-		if cp.ClientAuthentication != nil && cp.ClientAuthentication.Active() {
-			return true
-		}
-	}
-	return false
+	return slices.ContainsFunc(s.TLSConnPolicies, func(cp *caddytls.ConnectionPolicy) bool {
+		return cp.ClientAuthentication != nil && cp.ClientAuthentication.Active()
+	})
 }
 
 // findLastRouteWithHostMatcher returns the index of the last route
@@ -849,12 +847,7 @@ func (s *Server) logRequest(
 
 // protocol returns true if the protocol proto is configured/enabled.
 func (s *Server) protocol(proto string) bool {
-	for _, p := range s.Protocols {
-		if p == proto {
-			return true
-		}
-	}
-	return false
+	return slices.Contains(s.Protocols, proto)
 }
 
 // Listeners returns the server's listeners. These are active listeners,
@@ -959,12 +952,9 @@ func determineTrustedProxy(r *http.Request, s *Server) (bool, string) {
 // isTrustedClientIP returns true if the given IP address is
 // in the list of trusted IP ranges.
 func isTrustedClientIP(ipAddr netip.Addr, trusted []netip.Prefix) bool {
-	for _, ipRange := range trusted {
-		if ipRange.Contains(ipAddr) {
-			return true
-		}
-	}
-	return false
+	return slices.ContainsFunc(trusted, func(prefix netip.Prefix) bool {
+		return prefix.Contains(ipAddr)
+	})
 }
 
 // trustedRealClientIP finds the client IP from the request assuming it is
diff --git a/modules/caddyhttp/templates/frontmatter.go b/modules/caddyhttp/templates/frontmatter.go
index 3f7bd0cc4..fb62a4184 100644
--- a/modules/caddyhttp/templates/frontmatter.go
+++ b/modules/caddyhttp/templates/frontmatter.go
@@ -40,6 +40,7 @@ func extractFrontMatter(input string) (map[string]any, string, error) {
 		if firstLine == fmType.FenceOpen {
 			closingFence = fmType.FenceClose
 			fmParser = fmType.ParseFunc
+			break
 		}
 	}
 
diff --git a/modules/caddytls/automation.go b/modules/caddytls/automation.go
index e2e02221d..1f1042ba0 100644
--- a/modules/caddytls/automation.go
+++ b/modules/caddytls/automation.go
@@ -21,6 +21,7 @@ import (
 	"errors"
 	"fmt"
 	"net"
+	"slices"
 	"strings"
 
 	"github.com/caddyserver/certmagic"
@@ -373,12 +374,9 @@ func (ap *AutomationPolicy) Subjects() []string {
 
 // AllInternalSubjects returns true if all the subjects on this policy are internal.
 func (ap *AutomationPolicy) AllInternalSubjects() bool {
-	for _, subj := range ap.subjects {
-		if !certmagic.SubjectIsInternal(subj) {
-			return false
-		}
-	}
-	return true
+	return !slices.ContainsFunc(ap.subjects, func(s string) bool {
+		return !certmagic.SubjectIsInternal(s)
+	})
 }
 
 func (ap *AutomationPolicy) onlyInternalIssuer() bool {
diff --git a/modules/caddytls/certselection.go b/modules/caddytls/certselection.go
index 84ca2e118..a561e3a1d 100644
--- a/modules/caddytls/certselection.go
+++ b/modules/caddytls/certselection.go
@@ -20,6 +20,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"math/big"
+	"slices"
 
 	"github.com/caddyserver/certmagic"
 
@@ -72,15 +73,9 @@ nextChoice:
 		}
 
 		if len(p.SubjectOrganization) > 0 {
-			var found bool
-			for _, subjOrg := range p.SubjectOrganization {
-				for _, org := range cert.Leaf.Subject.Organization {
-					if subjOrg == org {
-						found = true
-						break
-					}
-				}
-			}
+			found := slices.ContainsFunc(p.SubjectOrganization, func(s string) bool {
+				return slices.Contains(cert.Leaf.Subject.Organization, s)
+			})
 			if !found {
 				continue
 			}
diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go
index f44b4c02a..a74d6ea80 100644
--- a/modules/caddytls/matchers.go
+++ b/modules/caddytls/matchers.go
@@ -20,6 +20,7 @@ import (
 	"net"
 	"net/netip"
 	"regexp"
+	"slices"
 	"strconv"
 	"strings"
 
@@ -321,12 +322,9 @@ func (MatchRemoteIP) parseIPRange(str string) ([]netip.Prefix, error) {
 }
 
 func (MatchRemoteIP) matches(ip netip.Addr, ranges []netip.Prefix) bool {
-	for _, ipRange := range ranges {
-		if ipRange.Contains(ip) {
-			return true
-		}
-	}
-	return false
+	return slices.ContainsFunc(ranges, func(prefix netip.Prefix) bool {
+		return prefix.Contains(ip)
+	})
 }
 
 // UnmarshalCaddyfile sets up the MatchRemoteIP from Caddyfile tokens. Syntax:
@@ -439,12 +437,9 @@ func (MatchLocalIP) parseIPRange(str string) ([]netip.Prefix, error) {
 }
 
 func (MatchLocalIP) matches(ip netip.Addr, ranges []netip.Prefix) bool {
-	for _, ipRange := range ranges {
-		if ipRange.Contains(ip) {
-			return true
-		}
-	}
-	return false
+	return slices.ContainsFunc(ranges, func(prefix netip.Prefix) bool {
+		return prefix.Contains(ip)
+	})
 }
 
 // UnmarshalCaddyfile sets up the MatchLocalIP from Caddyfile tokens. Syntax:

From 22c98ea165bdfbca33fbc77ce3b2bd22d3ee4626 Mon Sep 17 00:00:00 2001
From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com>
Date: Thu, 26 Sep 2024 20:23:12 +0200
Subject: [PATCH 53/62] caddyhttp: Optimize logs using zap's WithLazy() (#6590)

* uses zap's .WithLazy with a cloned request

* fixes the cloning

* adds comment explaining why cloning is faster
---
 modules/caddyhttp/server.go | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go
index ece3f1cd2..f5478cb37 100644
--- a/modules/caddyhttp/server.go
+++ b/modules/caddyhttp/server.go
@@ -313,16 +313,18 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
-	// encode the request for logging purposes before
+	// clone the request for logging purposes before
 	// it enters any handler chain; this is necessary
 	// to capture the original request in case it gets
 	// modified during handling
+	// cloning the request and using .WithLazy is considerably faster
+	// than using .With, which will JSON encode the request immediately
 	shouldLogCredentials := s.Logs != nil && s.Logs.ShouldLogCredentials
 	loggableReq := zap.Object("request", LoggableHTTPRequest{
-		Request:              r,
+		Request:              r.Clone(r.Context()),
 		ShouldLogCredentials: shouldLogCredentials,
 	})
-	errLog := s.errorLogger.With(loggableReq)
+	errLog := s.errorLogger.WithLazy(loggableReq)
 
 	var duration time.Duration
 

From 1a345b4fa620dfb0909a3b086bd76e35dfdbefa5 Mon Sep 17 00:00:00 2001
From: Mohammed Al Sahaf <msaa1990@gmail.com>
Date: Sun, 29 Sep 2024 12:12:52 +0300
Subject: [PATCH 54/62] doc: remove docs of deprecated directives (#6566)

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
---
 modules/caddytls/connpolicy.go | 10 ++--------
 1 file changed, 2 insertions(+), 8 deletions(-)

diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go
index 2ff41f7b2..2e2d4f748 100644
--- a/modules/caddytls/connpolicy.go
+++ b/modules/caddytls/connpolicy.go
@@ -555,16 +555,10 @@ type ClientAuthentication struct {
 //	 	trust_pool			   <module> {
 //			...
 //		}
-//		trusted_leaf_cert      <base64_der>
-//		trusted_leaf_cert_file <filename>
 //		verifier               <module>
 //	}
 //
-// If `mode` is not provided, it defaults to `require_and_verify` if any of the following are provided:
-// - `trusted_leaf_certs`
-// - `trusted_leaf_cert_file`
-// - `trust_pool`
-//
+// If `mode` is not provided, it defaults to `require_and_verify` if `trust_pool` is provided.
 // Otherwise, it defaults to `require`.
 func (ca *ClientAuthentication) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 	for d.NextArg() {
@@ -768,7 +762,7 @@ func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) erro
 		if len(clientauth.TrustedCACerts) > 0 ||
 			len(clientauth.TrustedCACertPEMFiles) > 0 ||
 			len(clientauth.TrustedLeafCerts) > 0 ||
-			clientauth.CARaw != nil {
+			clientauth.CARaw != nil || clientauth.ca != nil {
 			cfg.ClientAuth = tls.RequireAndVerifyClientCert
 		} else {
 			cfg.ClientAuth = tls.RequireAnyClientCert

From 4b1a9b6cc1aa521e21289afa276d29952a97d8f3 Mon Sep 17 00:00:00 2001
From: Aaron Paterson <9441877+MayCXC@users.noreply.github.com>
Date: Mon, 30 Sep 2024 12:55:03 -0400
Subject: [PATCH 55/62] core: Implement socket activation listeners (#6573)

* caddy adapt for listen_protocols

* adapt listen_socket

* allow multiple listen sockets for port ranges and readd socket fd listen logic

* readd logic to start servers according to listener protocols

* gofmt

* adapt caddytest

* gosec

* fmt and rename listen to listenWithSocket

* fmt and rename listen to listenWithSocket

* more consistent error msg

* non unix listenReusableWithSocketFile

* remove unused func

* doc comment typo

* nonosec

* commit

* doc comments

* more doc comments

* comment was misleading, cardinality did not change

* addressesWithProtocols

* update test

* fd/ and fdgram/

* rm addr

* actually write...

* i guess we doin' "skip": now

* wrong var in placeholder

* wrong var in placeholder II

* update param name in comment

* dont save nil file pointers

* windows

* key -> parsedKey

* osx

* multiple default_bind with protocols

* check for h1 and h2 listener netw
---
 admin.go                                      |   4 +-
 caddyconfig/httpcaddyfile/addresses.go        | 306 +++++++++++-------
 caddyconfig/httpcaddyfile/builtins.go         |  24 +-
 caddyconfig/httpcaddyfile/directives.go       |  14 +-
 caddyconfig/httpcaddyfile/directives_test.go  |   2 +-
 caddyconfig/httpcaddyfile/httptype.go         |  80 ++++-
 caddyconfig/httpcaddyfile/options.go          |  31 +-
 caddyconfig/httpcaddyfile/tlsapp.go           |  14 +-
 .../bind_fd_fdgram_h123.caddyfiletest         | 142 ++++++++
 cmd/commandfuncs.go                           |  12 +-
 cmd/main_test.go                              |   6 +-
 listen.go                                     |  80 ++++-
 listen_unix.go                                | 174 +++++++---
 listeners.go                                  | 113 ++++---
 modules/caddyhttp/app.go                      | 248 +++++++++-----
 .../proxyprotocol/listenerwrapper.go          |   2 +-
 modules/caddyhttp/reverseproxy/addresses.go   |   2 +-
 .../caddyhttp/reverseproxy/healthchecks.go    |   4 +-
 modules/caddyhttp/server.go                   |  44 ++-
 modules/caddyhttp/staticresp.go               |   2 +-
 replacer.go                                   |   6 +-
 21 files changed, 946 insertions(+), 364 deletions(-)
 create mode 100644 caddytest/integration/caddyfile_adapt/bind_fd_fdgram_h123.caddyfiletest

diff --git a/admin.go b/admin.go
index e78adeec9..24c583235 100644
--- a/admin.go
+++ b/admin.go
@@ -313,7 +313,7 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
 	}
 	if admin.Origins == nil {
 		if addr.isLoopback() {
-			if addr.IsUnixNetwork() {
+			if addr.IsUnixNetwork() || addr.IsFdNetwork() {
 				// RFC 2616, Section 14.26:
 				// "A client MUST include a Host header field in all HTTP/1.1 request
 				// messages. If the requested URI does not include an Internet host
@@ -351,7 +351,7 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
 				uniqueOrigins[net.JoinHostPort("127.0.0.1", addr.port())] = struct{}{}
 			}
 		}
-		if !addr.IsUnixNetwork() {
+		if !addr.IsUnixNetwork() && !addr.IsFdNetwork() {
 			uniqueOrigins[addr.JoinHostPort(0)] = struct{}{}
 		}
 	}
diff --git a/caddyconfig/httpcaddyfile/addresses.go b/caddyconfig/httpcaddyfile/addresses.go
index da51fe9b0..1c331eadc 100644
--- a/caddyconfig/httpcaddyfile/addresses.go
+++ b/caddyconfig/httpcaddyfile/addresses.go
@@ -77,10 +77,15 @@ import (
 // repetition may be undesirable, so call consolidateAddrMappings() to map
 // multiple addresses to the same lists of server blocks (a many:many mapping).
 // (Doing this is essentially a map-reduce technique.)
-func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock,
+func (st *ServerType) mapAddressToProtocolToServerBlocks(originalServerBlocks []serverBlock,
 	options map[string]any,
-) (map[string][]serverBlock, error) {
-	sbmap := make(map[string][]serverBlock)
+) (map[string]map[string][]serverBlock, error) {
+	addrToProtocolToServerBlocks := map[string]map[string][]serverBlock{}
+
+	type keyWithParsedKey struct {
+		key       caddyfile.Token
+		parsedKey Address
+	}
 
 	for i, sblock := range originalServerBlocks {
 		// within a server block, we need to map all the listener addresses
@@ -88,27 +93,48 @@ func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBloc
 		// will be served by them; this has the effect of treating each
 		// key of a server block as its own, but without having to repeat its
 		// contents in cases where multiple keys really can be served together
-		addrToKeys := make(map[string][]caddyfile.Token)
+		addrToProtocolToKeyWithParsedKeys := map[string]map[string][]keyWithParsedKey{}
 		for j, key := range sblock.block.Keys {
+			parsedKey, err := ParseAddress(key.Text)
+			if err != nil {
+				return nil, fmt.Errorf("parsing key: %v", err)
+			}
+			parsedKey = parsedKey.Normalize()
+
 			// a key can have multiple listener addresses if there are multiple
 			// arguments to the 'bind' directive (although they will all have
 			// the same port, since the port is defined by the key or is implicit
 			// through automatic HTTPS)
-			addrs, err := st.listenerAddrsForServerBlockKey(sblock, key.Text, options)
+			listeners, err := st.listenersForServerBlockAddress(sblock, parsedKey, options)
 			if err != nil {
 				return nil, fmt.Errorf("server block %d, key %d (%s): determining listener address: %v", i, j, key.Text, err)
 			}
 
-			// associate this key with each listener address it is served on
-			for _, addr := range addrs {
-				addrToKeys[addr] = append(addrToKeys[addr], key)
+			// associate this key with its protocols and each listener address served with them
+			kwpk := keyWithParsedKey{key, parsedKey}
+			for addr, protocols := range listeners {
+				protocolToKeyWithParsedKeys, ok := addrToProtocolToKeyWithParsedKeys[addr]
+				if !ok {
+					protocolToKeyWithParsedKeys = map[string][]keyWithParsedKey{}
+					addrToProtocolToKeyWithParsedKeys[addr] = protocolToKeyWithParsedKeys
+				}
+
+				// an empty protocol indicates the default, a nil or empty value in the ListenProtocols array
+				if len(protocols) == 0 {
+					protocols[""] = struct{}{}
+				}
+				for prot := range protocols {
+					protocolToKeyWithParsedKeys[prot] = append(
+						protocolToKeyWithParsedKeys[prot],
+						kwpk)
+				}
 			}
 		}
 
 		// make a slice of the map keys so we can iterate in sorted order
-		addrs := make([]string, 0, len(addrToKeys))
-		for k := range addrToKeys {
-			addrs = append(addrs, k)
+		addrs := make([]string, 0, len(addrToProtocolToKeyWithParsedKeys))
+		for addr := range addrToProtocolToKeyWithParsedKeys {
+			addrs = append(addrs, addr)
 		}
 		sort.Strings(addrs)
 
@@ -118,85 +144,132 @@ func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBloc
 		// server block are only the ones which use the address; but
 		// the contents (tokens) are of course the same
 		for _, addr := range addrs {
-			keys := addrToKeys[addr]
-			// parse keys so that we only have to do it once
-			parsedKeys := make([]Address, 0, len(keys))
-			for _, key := range keys {
-				addr, err := ParseAddress(key.Text)
-				if err != nil {
-					return nil, fmt.Errorf("parsing key '%s': %v", key.Text, err)
-				}
-				parsedKeys = append(parsedKeys, addr.Normalize())
+			protocolToKeyWithParsedKeys := addrToProtocolToKeyWithParsedKeys[addr]
+
+			prots := make([]string, 0, len(protocolToKeyWithParsedKeys))
+			for prot := range protocolToKeyWithParsedKeys {
+				prots = append(prots, prot)
 			}
-			sbmap[addr] = append(sbmap[addr], serverBlock{
-				block: caddyfile.ServerBlock{
-					Keys:     keys,
-					Segments: sblock.block.Segments,
-				},
-				pile: sblock.pile,
-				keys: parsedKeys,
+			sort.Strings(prots)
+
+			protocolToServerBlocks, ok := addrToProtocolToServerBlocks[addr]
+			if !ok {
+				protocolToServerBlocks = map[string][]serverBlock{}
+				addrToProtocolToServerBlocks[addr] = protocolToServerBlocks
+			}
+
+			for _, prot := range prots {
+				keyWithParsedKeys := protocolToKeyWithParsedKeys[prot]
+
+				keys := make([]caddyfile.Token, len(keyWithParsedKeys))
+				parsedKeys := make([]Address, len(keyWithParsedKeys))
+
+				for k, keyWithParsedKey := range keyWithParsedKeys {
+					keys[k] = keyWithParsedKey.key
+					parsedKeys[k] = keyWithParsedKey.parsedKey
+				}
+
+				protocolToServerBlocks[prot] = append(protocolToServerBlocks[prot], serverBlock{
+					block: caddyfile.ServerBlock{
+						Keys:     keys,
+						Segments: sblock.block.Segments,
+					},
+					pile:       sblock.pile,
+					parsedKeys: parsedKeys,
+				})
+			}
+		}
+	}
+
+	return addrToProtocolToServerBlocks, nil
+}
+
+// consolidateAddrMappings eliminates repetition of identical server blocks in a mapping of
+// single listener addresses to protocols to lists of server blocks. Since multiple addresses
+// may serve multiple protocols to identical sites (server block contents), this function turns
+// a 1:many mapping into a many:many mapping. Server block contents (tokens) must be
+// exactly identical so that reflect.DeepEqual returns true in order for the addresses to be combined.
+// Identical entries are deleted from the addrToServerBlocks map. Essentially, each pairing (each
+// association from multiple addresses to multiple server blocks; i.e. each element of
+// the returned slice) becomes a server definition in the output JSON.
+func (st *ServerType) consolidateAddrMappings(addrToProtocolToServerBlocks map[string]map[string][]serverBlock) []sbAddrAssociation {
+	sbaddrs := make([]sbAddrAssociation, 0, len(addrToProtocolToServerBlocks))
+
+	addrs := make([]string, 0, len(addrToProtocolToServerBlocks))
+	for addr := range addrToProtocolToServerBlocks {
+		addrs = append(addrs, addr)
+	}
+	sort.Strings(addrs)
+
+	for _, addr := range addrs {
+		protocolToServerBlocks := addrToProtocolToServerBlocks[addr]
+
+		prots := make([]string, 0, len(protocolToServerBlocks))
+		for prot := range protocolToServerBlocks {
+			prots = append(prots, prot)
+		}
+		sort.Strings(prots)
+
+		for _, prot := range prots {
+			serverBlocks := protocolToServerBlocks[prot]
+
+			// now find other addresses that map to identical
+			// server blocks and add them to our map of listener
+			// addresses and protocols, while removing them from
+			// the original map
+			listeners := map[string]map[string]struct{}{}
+
+			for otherAddr, otherProtocolToServerBlocks := range addrToProtocolToServerBlocks {
+				for otherProt, otherServerBlocks := range otherProtocolToServerBlocks {
+					if addr == otherAddr && prot == otherProt || reflect.DeepEqual(serverBlocks, otherServerBlocks) {
+						listener, ok := listeners[otherAddr]
+						if !ok {
+							listener = map[string]struct{}{}
+							listeners[otherAddr] = listener
+						}
+						listener[otherProt] = struct{}{}
+						delete(otherProtocolToServerBlocks, otherProt)
+					}
+				}
+			}
+
+			addresses := make([]string, 0, len(listeners))
+			for lnAddr := range listeners {
+				addresses = append(addresses, lnAddr)
+			}
+			sort.Strings(addresses)
+
+			addressesWithProtocols := make([]addressWithProtocols, 0, len(listeners))
+
+			for _, lnAddr := range addresses {
+				lnProts := listeners[lnAddr]
+				prots := make([]string, 0, len(lnProts))
+				for prot := range lnProts {
+					prots = append(prots, prot)
+				}
+				sort.Strings(prots)
+
+				addressesWithProtocols = append(addressesWithProtocols, addressWithProtocols{
+					address:   lnAddr,
+					protocols: prots,
+				})
+			}
+
+			sbaddrs = append(sbaddrs, sbAddrAssociation{
+				addressesWithProtocols: addressesWithProtocols,
+				serverBlocks:           serverBlocks,
 			})
 		}
 	}
 
-	return sbmap, nil
-}
-
-// consolidateAddrMappings eliminates repetition of identical server blocks in a mapping of
-// single listener addresses to lists of server blocks. Since multiple addresses may serve
-// identical sites (server block contents), this function turns a 1:many mapping into a
-// many:many mapping. Server block contents (tokens) must be exactly identical so that
-// reflect.DeepEqual returns true in order for the addresses to be combined. Identical
-// entries are deleted from the addrToServerBlocks map. Essentially, each pairing (each
-// association from multiple addresses to multiple server blocks; i.e. each element of
-// the returned slice) becomes a server definition in the output JSON.
-func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]serverBlock) []sbAddrAssociation {
-	sbaddrs := make([]sbAddrAssociation, 0, len(addrToServerBlocks))
-	for addr, sblocks := range addrToServerBlocks {
-		// we start with knowing that at least this address
-		// maps to these server blocks
-		a := sbAddrAssociation{
-			addresses:    []string{addr},
-			serverBlocks: sblocks,
-		}
-
-		// now find other addresses that map to identical
-		// server blocks and add them to our list of
-		// addresses, while removing them from the map
-		for otherAddr, otherSblocks := range addrToServerBlocks {
-			if addr == otherAddr {
-				continue
-			}
-			if reflect.DeepEqual(sblocks, otherSblocks) {
-				a.addresses = append(a.addresses, otherAddr)
-				delete(addrToServerBlocks, otherAddr)
-			}
-		}
-		sort.Strings(a.addresses)
-
-		sbaddrs = append(sbaddrs, a)
-	}
-
-	// sort them by their first address (we know there will always be at least one)
-	// to avoid problems with non-deterministic ordering (makes tests flaky)
-	sort.Slice(sbaddrs, func(i, j int) bool {
-		return sbaddrs[i].addresses[0] < sbaddrs[j].addresses[0]
-	})
-
 	return sbaddrs
 }
 
-// listenerAddrsForServerBlockKey essentially converts the Caddyfile
-// site addresses to Caddy listener addresses for each server block.
-func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string,
+// listenersForServerBlockAddress essentially converts the Caddyfile site addresses to a map from
+// Caddy listener addresses and the protocols to serve them with to the parsed address for each server block.
+func (st *ServerType) listenersForServerBlockAddress(sblock serverBlock, addr Address,
 	options map[string]any,
-) ([]string, error) {
-	addr, err := ParseAddress(key)
-	if err != nil {
-		return nil, fmt.Errorf("parsing key: %v", err)
-	}
-	addr = addr.Normalize()
-
+) (map[string]map[string]struct{}, error) {
 	switch addr.Scheme {
 	case "wss":
 		return nil, fmt.Errorf("the scheme wss:// is only supported in browsers; use https:// instead")
@@ -230,55 +303,54 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
 
 	// error if scheme and port combination violate convention
 	if (addr.Scheme == "http" && lnPort == httpsPort) || (addr.Scheme == "https" && lnPort == httpPort) {
-		return nil, fmt.Errorf("[%s] scheme and port violate convention", key)
+		return nil, fmt.Errorf("[%s] scheme and port violate convention", addr.String())
 	}
 
-	// the bind directive specifies hosts (and potentially network), but is optional
-	lnHosts := make([]string, 0, len(sblock.pile["bind"]))
+	// the bind directive specifies hosts (and potentially network), and the protocols to serve them with, but is optional
+	lnCfgVals := make([]addressesWithProtocols, 0, len(sblock.pile["bind"]))
 	for _, cfgVal := range sblock.pile["bind"] {
-		lnHosts = append(lnHosts, cfgVal.Value.([]string)...)
+		if val, ok := cfgVal.Value.(addressesWithProtocols); ok {
+			lnCfgVals = append(lnCfgVals, val)
+		}
 	}
-	if len(lnHosts) == 0 {
-		if defaultBind, ok := options["default_bind"].([]string); ok {
-			lnHosts = defaultBind
+	if len(lnCfgVals) == 0 {
+		if defaultBindValues, ok := options["default_bind"].([]ConfigValue); ok {
+			for _, defaultBindValue := range defaultBindValues {
+				lnCfgVals = append(lnCfgVals, defaultBindValue.Value.(addressesWithProtocols))
+			}
 		} else {
-			lnHosts = []string{""}
+			lnCfgVals = []addressesWithProtocols{{
+				addresses: []string{""},
+				protocols: nil,
+			}}
 		}
 	}
 
 	// use a map to prevent duplication
-	listeners := make(map[string]struct{})
-	for _, lnHost := range lnHosts {
-		// normally we would simply append the port,
-		// but if lnHost is IPv6, we need to ensure it
-		// is enclosed in [ ]; net.JoinHostPort does
-		// this for us, but lnHost might also have a
-		// network type in front (e.g. "tcp/") leading
-		// to "[tcp/::1]" which causes parsing failures
-		// later; what we need is "tcp/[::1]", so we have
-		// to split the network and host, then re-combine
-		network, host, ok := strings.Cut(lnHost, "/")
-		if !ok {
-			host = network
-			network = ""
+	listeners := map[string]map[string]struct{}{}
+	for _, lnCfgVal := range lnCfgVals {
+		for _, lnHost := range lnCfgVal.addresses {
+			networkAddr, err := caddy.ParseNetworkAddressFromHostPort(lnHost, lnPort)
+			if err != nil {
+				return nil, fmt.Errorf("parsing network address: %v", err)
+			}
+			if _, ok := listeners[addr.String()]; !ok {
+				listeners[networkAddr.String()] = map[string]struct{}{}
+			}
+			for _, protocol := range lnCfgVal.protocols {
+				listeners[networkAddr.String()][protocol] = struct{}{}
+			}
 		}
-		host = strings.Trim(host, "[]") // IPv6
-		networkAddr := caddy.JoinNetworkAddress(network, host, lnPort)
-		addr, err := caddy.ParseNetworkAddress(networkAddr)
-		if err != nil {
-			return nil, fmt.Errorf("parsing network address: %v", err)
-		}
-		listeners[addr.String()] = struct{}{}
 	}
 
-	// now turn map into list
-	listenersList := make([]string, 0, len(listeners))
-	for lnStr := range listeners {
-		listenersList = append(listenersList, lnStr)
-	}
-	sort.Strings(listenersList)
+	return listeners, nil
+}
 
-	return listenersList, nil
+// addressesWithProtocols associates a list of listen addresses
+// with a list of protocols to serve them with
+type addressesWithProtocols struct {
+	addresses []string
+	protocols []string
 }
 
 // Address represents a site address. It contains
diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go
index e1e95e00a..165c66b25 100644
--- a/caddyconfig/httpcaddyfile/builtins.go
+++ b/caddyconfig/httpcaddyfile/builtins.go
@@ -56,10 +56,30 @@ func init() {
 
 // parseBind parses the bind directive. Syntax:
 //
-//	bind <addresses...>
+//		bind <addresses...> [{
+//	   protocols [h1|h2|h2c|h3] [...]
+//	 }]
 func parseBind(h Helper) ([]ConfigValue, error) {
 	h.Next() // consume directive name
-	return []ConfigValue{{Class: "bind", Value: h.RemainingArgs()}}, nil
+	var addresses, protocols []string
+	addresses = h.RemainingArgs()
+
+	for h.NextBlock(0) {
+		switch h.Val() {
+		case "protocols":
+			protocols = h.RemainingArgs()
+			if len(protocols) == 0 {
+				return nil, h.Errf("protocols requires one or more arguments")
+			}
+		default:
+			return nil, h.Errf("unknown subdirective: %s", h.Val())
+		}
+	}
+
+	return []ConfigValue{{Class: "bind", Value: addressesWithProtocols{
+		addresses: addresses,
+		protocols: protocols,
+	}}}, nil
 }
 
 // parseTLS parses the tls directive. Syntax:
diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go
index 19ef4bc0e..f0687a7e9 100644
--- a/caddyconfig/httpcaddyfile/directives.go
+++ b/caddyconfig/httpcaddyfile/directives.go
@@ -516,9 +516,9 @@ func sortRoutes(routes []ConfigValue) {
 // a "pile" of config values, keyed by class name,
 // as well as its parsed keys for convenience.
 type serverBlock struct {
-	block caddyfile.ServerBlock
-	pile  map[string][]ConfigValue // config values obtained from directives
-	keys  []Address
+	block      caddyfile.ServerBlock
+	pile       map[string][]ConfigValue // config values obtained from directives
+	parsedKeys []Address
 }
 
 // hostsFromKeys returns a list of all the non-empty hostnames found in
@@ -535,7 +535,7 @@ type serverBlock struct {
 func (sb serverBlock) hostsFromKeys(loggerMode bool) []string {
 	// ensure each entry in our list is unique
 	hostMap := make(map[string]struct{})
-	for _, addr := range sb.keys {
+	for _, addr := range sb.parsedKeys {
 		if addr.Host == "" {
 			if !loggerMode {
 				// server block contains a key like ":443", i.e. the host portion
@@ -567,7 +567,7 @@ func (sb serverBlock) hostsFromKeys(loggerMode bool) []string {
 func (sb serverBlock) hostsFromKeysNotHTTP(httpPort string) []string {
 	// ensure each entry in our list is unique
 	hostMap := make(map[string]struct{})
-	for _, addr := range sb.keys {
+	for _, addr := range sb.parsedKeys {
 		if addr.Host == "" {
 			continue
 		}
@@ -588,7 +588,7 @@ func (sb serverBlock) hostsFromKeysNotHTTP(httpPort string) []string {
 // hasHostCatchAllKey returns true if sb has a key that
 // omits a host portion, i.e. it "catches all" hosts.
 func (sb serverBlock) hasHostCatchAllKey() bool {
-	return slices.ContainsFunc(sb.keys, func(addr Address) bool {
+	return slices.ContainsFunc(sb.parsedKeys, func(addr Address) bool {
 		return addr.Host == ""
 	})
 }
@@ -596,7 +596,7 @@ func (sb serverBlock) hasHostCatchAllKey() bool {
 // isAllHTTP returns true if all sb keys explicitly specify
 // the http:// scheme
 func (sb serverBlock) isAllHTTP() bool {
-	return !slices.ContainsFunc(sb.keys, func(addr Address) bool {
+	return !slices.ContainsFunc(sb.parsedKeys, func(addr Address) bool {
 		return addr.Scheme != "http"
 	})
 }
diff --git a/caddyconfig/httpcaddyfile/directives_test.go b/caddyconfig/httpcaddyfile/directives_test.go
index db0282299..2b4d3e6c3 100644
--- a/caddyconfig/httpcaddyfile/directives_test.go
+++ b/caddyconfig/httpcaddyfile/directives_test.go
@@ -78,7 +78,7 @@ func TestHostsFromKeys(t *testing.T) {
 			[]string{"example.com:2015"},
 		},
 	} {
-		sb := serverBlock{keys: tc.keys}
+		sb := serverBlock{parsedKeys: tc.keys}
 
 		// test in normal mode
 		actual := sb.hostsFromKeys(false)
diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go
index c858ee564..6745969ee 100644
--- a/caddyconfig/httpcaddyfile/httptype.go
+++ b/caddyconfig/httpcaddyfile/httptype.go
@@ -171,7 +171,7 @@ func (st ServerType) Setup(
 	}
 
 	// map
-	sbmap, err := st.mapAddressToServerBlocks(originalServerBlocks, options)
+	sbmap, err := st.mapAddressToProtocolToServerBlocks(originalServerBlocks, options)
 	if err != nil {
 		return nil, warnings, err
 	}
@@ -402,6 +402,20 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
 			options[opt] = append(existingOpts, logOpts...)
 			continue
 		}
+		// Also fold multiple "default_bind" options together into an
+		// array so that server blocks can have multiple binds by default.
+		if opt == "default_bind" {
+			existingOpts, ok := options[opt].([]ConfigValue)
+			if !ok {
+				existingOpts = []ConfigValue{}
+			}
+			defaultBindOpts, ok := val.([]ConfigValue)
+			if !ok {
+				return nil, fmt.Errorf("unexpected type from 'default_bind' global options: %T", val)
+			}
+			options[opt] = append(existingOpts, defaultBindOpts...)
+			continue
+		}
 
 		options[opt] = val
 	}
@@ -543,8 +557,40 @@ func (st *ServerType) serversFromPairings(
 			}
 		}
 
+		var (
+			addresses []string
+			protocols [][]string
+		)
+
+		for _, addressWithProtocols := range p.addressesWithProtocols {
+			addresses = append(addresses, addressWithProtocols.address)
+			protocols = append(protocols, addressWithProtocols.protocols)
+		}
+
 		srv := &caddyhttp.Server{
-			Listen: p.addresses,
+			Listen:          addresses,
+			ListenProtocols: protocols,
+		}
+
+		// remove srv.ListenProtocols[j] if it only contains the default protocols
+		for j, lnProtocols := range srv.ListenProtocols {
+			srv.ListenProtocols[j] = nil
+			for _, lnProtocol := range lnProtocols {
+				if lnProtocol != "" {
+					srv.ListenProtocols[j] = lnProtocols
+					break
+				}
+			}
+		}
+
+		// remove srv.ListenProtocols if it only contains the default protocols for all listen addresses
+		listenProtocols := srv.ListenProtocols
+		srv.ListenProtocols = nil
+		for _, lnProtocols := range listenProtocols {
+			if lnProtocols != nil {
+				srv.ListenProtocols = listenProtocols
+				break
+			}
 		}
 
 		// handle the auto_https global option
@@ -566,7 +612,7 @@ func (st *ServerType) serversFromPairings(
 		// See ParseAddress() where parsing should later reject paths
 		// See https://github.com/caddyserver/caddy/pull/4728 for a full explanation
 		for _, sblock := range p.serverBlocks {
-			for _, addr := range sblock.keys {
+			for _, addr := range sblock.parsedKeys {
 				if addr.Path != "" {
 					caddy.Log().Named("caddyfile").Warn("Using a path in a site address is deprecated; please use the 'handle' directive instead", zap.String("address", addr.String()))
 				}
@@ -584,7 +630,7 @@ func (st *ServerType) serversFromPairings(
 			var iLongestPath, jLongestPath string
 			var iLongestHost, jLongestHost string
 			var iWildcardHost, jWildcardHost bool
-			for _, addr := range p.serverBlocks[i].keys {
+			for _, addr := range p.serverBlocks[i].parsedKeys {
 				if strings.Contains(addr.Host, "*") || addr.Host == "" {
 					iWildcardHost = true
 				}
@@ -595,7 +641,7 @@ func (st *ServerType) serversFromPairings(
 					iLongestPath = addr.Path
 				}
 			}
-			for _, addr := range p.serverBlocks[j].keys {
+			for _, addr := range p.serverBlocks[j].parsedKeys {
 				if strings.Contains(addr.Host, "*") || addr.Host == "" {
 					jWildcardHost = true
 				}
@@ -711,7 +757,7 @@ func (st *ServerType) serversFromPairings(
 				}
 			}
 
-			for _, addr := range sblock.keys {
+			for _, addr := range sblock.parsedKeys {
 				// if server only uses HTTP port, auto-HTTPS will not apply
 				if listenersUseAnyPortOtherThan(srv.Listen, httpPort) {
 					// exclude any hosts that were defined explicitly with "http://"
@@ -886,8 +932,7 @@ func (st *ServerType) serversFromPairings(
 		servers[fmt.Sprintf("srv%d", i)] = srv
 	}
 
-	err := applyServerOptions(servers, options, warnings)
-	if err != nil {
+	if err := applyServerOptions(servers, options, warnings); err != nil {
 		return nil, fmt.Errorf("applying global server options: %v", err)
 	}
 
@@ -932,7 +977,7 @@ func detectConflictingSchemes(srv *caddyhttp.Server, serverBlocks []serverBlock,
 	}
 
 	for _, sblock := range serverBlocks {
-		for _, addr := range sblock.keys {
+		for _, addr := range sblock.parsedKeys {
 			if addr.Scheme == "http" || addr.Port == httpPort {
 				if err := checkAndSetHTTP(addr); err != nil {
 					return err
@@ -1322,7 +1367,7 @@ func (st *ServerType) compileEncodedMatcherSets(sblock serverBlock) ([]caddy.Mod
 	var matcherPairs []*hostPathPair
 
 	var catchAllHosts bool
-	for _, addr := range sblock.keys {
+	for _, addr := range sblock.parsedKeys {
 		// choose a matcher pair that should be shared by this
 		// server block; if none exists yet, create one
 		var chosenMatcherPair *hostPathPair
@@ -1594,12 +1639,19 @@ type namedCustomLog struct {
 	noHostname bool
 }
 
+// addressWithProtocols associates a listen address with
+// the protocols to serve it with
+type addressWithProtocols struct {
+	address   string
+	protocols []string
+}
+
 // sbAddrAssociation is a mapping from a list of
-// addresses to a list of server blocks that are
-// served on those addresses.
+// addresses with protocols, and a list of server
+// blocks that are served on those addresses.
 type sbAddrAssociation struct {
-	addresses    []string
-	serverBlocks []serverBlock
+	addressesWithProtocols []addressWithProtocols
+	serverBlocks           []serverBlock
 }
 
 const (
diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go
index 53687d32a..c14208b61 100644
--- a/caddyconfig/httpcaddyfile/options.go
+++ b/caddyconfig/httpcaddyfile/options.go
@@ -31,7 +31,7 @@ func init() {
 	RegisterGlobalOption("debug", parseOptTrue)
 	RegisterGlobalOption("http_port", parseOptHTTPPort)
 	RegisterGlobalOption("https_port", parseOptHTTPSPort)
-	RegisterGlobalOption("default_bind", parseOptStringList)
+	RegisterGlobalOption("default_bind", parseOptDefaultBind)
 	RegisterGlobalOption("grace_period", parseOptDuration)
 	RegisterGlobalOption("shutdown_delay", parseOptDuration)
 	RegisterGlobalOption("default_sni", parseOptSingleString)
@@ -284,13 +284,32 @@ func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) {
 	return val, nil
 }
 
-func parseOptStringList(d *caddyfile.Dispenser, _ any) (any, error) {
+func parseOptDefaultBind(d *caddyfile.Dispenser, _ any) (any, error) {
 	d.Next() // consume option name
-	val := d.RemainingArgs()
-	if len(val) == 0 {
-		return "", d.ArgErr()
+
+	var addresses, protocols []string
+	addresses = d.RemainingArgs()
+
+	if len(addresses) == 0 {
+		addresses = append(addresses, "")
 	}
-	return val, nil
+
+	for d.NextBlock(0) {
+		switch d.Val() {
+		case "protocols":
+			protocols = d.RemainingArgs()
+			if len(protocols) == 0 {
+				return nil, d.Errf("protocols requires one or more arguments")
+			}
+		default:
+			return nil, d.Errf("unknown subdirective: %s", d.Val())
+		}
+	}
+
+	return []ConfigValue{{Class: "bind", Value: addressesWithProtocols{
+		addresses: addresses,
+		protocols: protocols,
+	}}}, nil
 }
 
 func parseOptAdmin(d *caddyfile.Dispenser, _ any) (any, error) {
diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go
index 157a3113e..c6ff81b2b 100644
--- a/caddyconfig/httpcaddyfile/tlsapp.go
+++ b/caddyconfig/httpcaddyfile/tlsapp.go
@@ -57,13 +57,13 @@ func (st ServerType) buildTLSApp(
 	if autoHTTPS != "off" {
 		for _, pair := range pairings {
 			for _, sb := range pair.serverBlocks {
-				for _, addr := range sb.keys {
+				for _, addr := range sb.parsedKeys {
 					if addr.Host != "" {
 						continue
 					}
 					// this server block has a hostless key, now
 					// go through and add all the hosts to the set
-					for _, otherAddr := range sb.keys {
+					for _, otherAddr := range sb.parsedKeys {
 						if otherAddr.Original == addr.Original {
 							continue
 						}
@@ -93,7 +93,11 @@ func (st ServerType) buildTLSApp(
 
 	for _, p := range pairings {
 		// avoid setting up TLS automation policies for a server that is HTTP-only
-		if !listenersUseAnyPortOtherThan(p.addresses, httpPort) {
+		var addresses []string
+		for _, addressWithProtocols := range p.addressesWithProtocols {
+			addresses = append(addresses, addressWithProtocols.address)
+		}
+		if !listenersUseAnyPortOtherThan(addresses, httpPort) {
 			continue
 		}
 
@@ -183,8 +187,8 @@ func (st ServerType) buildTLSApp(
 					if acmeIssuer.Challenges.BindHost == "" {
 						// only binding to one host is supported
 						var bindHost string
-						if bindHosts, ok := cfgVal.Value.([]string); ok && len(bindHosts) > 0 {
-							bindHost = bindHosts[0]
+						if asserted, ok := cfgVal.Value.(addressesWithProtocols); ok && len(asserted.addresses) > 0 {
+							bindHost = asserted.addresses[0]
 						}
 						acmeIssuer.Challenges.BindHost = bindHost
 					}
diff --git a/caddytest/integration/caddyfile_adapt/bind_fd_fdgram_h123.caddyfiletest b/caddytest/integration/caddyfile_adapt/bind_fd_fdgram_h123.caddyfiletest
new file mode 100644
index 000000000..08f30d18a
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/bind_fd_fdgram_h123.caddyfiletest
@@ -0,0 +1,142 @@
+{
+	auto_https disable_redirects
+	admin off
+}
+
+http://localhost {
+	bind fd/{env.CADDY_HTTP_FD} {
+		protocols h1
+	}
+	log
+	respond "Hello, HTTP!"
+}
+
+https://localhost {
+	bind fd/{env.CADDY_HTTPS_FD} {
+		protocols h1 h2
+	}
+	bind fdgram/{env.CADDY_HTTP3_FD} {
+		protocols h3
+	}
+	log
+	respond "Hello, HTTPS!"
+}
+----------
+{
+	"admin": {
+		"disabled": true
+	},
+	"apps": {
+		"http": {
+			"servers": {
+				"srv0": {
+					"listen": [
+						"fd/{env.CADDY_HTTPS_FD}",
+						"fdgram/{env.CADDY_HTTP3_FD}"
+					],
+					"routes": [
+						{
+							"match": [
+								{
+									"host": [
+										"localhost"
+									]
+								}
+							],
+							"handle": [
+								{
+									"handler": "subroute",
+									"routes": [
+										{
+											"handle": [
+												{
+													"body": "Hello, HTTPS!",
+													"handler": "static_response"
+												}
+											]
+										}
+									]
+								}
+							],
+							"terminal": true
+						}
+					],
+					"automatic_https": {
+						"disable_redirects": true
+					},
+					"logs": {
+						"logger_names": {
+							"localhost": [
+								""
+							]
+						}
+					},
+					"listen_protocols": [
+						[
+							"h1",
+							"h2"
+						],
+						[
+							"h3"
+						]
+					]
+				},
+				"srv1": {
+					"automatic_https": {
+						"disable_redirects": true
+					}
+				},
+				"srv2": {
+					"listen": [
+						"fd/{env.CADDY_HTTP_FD}"
+					],
+					"routes": [
+						{
+							"match": [
+								{
+									"host": [
+										"localhost"
+									]
+								}
+							],
+							"handle": [
+								{
+									"handler": "subroute",
+									"routes": [
+										{
+											"handle": [
+												{
+													"body": "Hello, HTTP!",
+													"handler": "static_response"
+												}
+											]
+										}
+									]
+								}
+							],
+							"terminal": true
+						}
+					],
+					"automatic_https": {
+						"disable_redirects": true,
+						"skip": [
+							"localhost"
+						]
+					},
+					"logs": {
+						"logger_names": {
+							"localhost": [
+								""
+							]
+						}
+					},
+					"listen_protocols": [
+						[
+							"h1"
+						]
+					]
+				}
+			}
+		}
+	}
+}
diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go
index 49d0321ef..50e9b110d 100644
--- a/cmd/commandfuncs.go
+++ b/cmd/commandfuncs.go
@@ -660,6 +660,8 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
 			return nil, err
 		}
 		parsedAddr.Host = addr
+	} else if parsedAddr.IsFdNetwork() {
+		origin = "http://127.0.0.1"
 	}
 
 	// form the request
@@ -667,13 +669,13 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
 	if err != nil {
 		return nil, fmt.Errorf("making request: %v", err)
 	}
-	if parsedAddr.IsUnixNetwork() {
+	if parsedAddr.IsUnixNetwork() || parsedAddr.IsFdNetwork() {
 		// We used to conform to RFC 2616 Section 14.26 which requires
 		// an empty host header when there is no host, as is the case
-		// with unix sockets. However, Go required a Host value so we
-		// used a hack of a space character as the host (it would see
-		// the Host was non-empty, then trim the space later). As of
-		// Go 1.20.6 (July 2023), this hack no longer works. See:
+		// with unix sockets and socket fds. However, Go required a
+		// Host value so we used a hack of a space character as the host
+		// (it would see the Host was non-empty, then trim the space later).
+		// As of Go 1.20.6 (July 2023), this hack no longer works. See:
 		// https://github.com/golang/go/issues/60374
 		// See also the discussion here:
 		// https://github.com/golang/go/issues/61431
diff --git a/cmd/main_test.go b/cmd/main_test.go
index 757a58ce6..3b2412c57 100644
--- a/cmd/main_test.go
+++ b/cmd/main_test.go
@@ -235,7 +235,7 @@ func Test_isCaddyfile(t *testing.T) {
 			wantErr: false,
 		},
 		{
-			
+
 			name: "json is not caddyfile but not error",
 			args: args{
 				configFile:  "./Caddyfile.json",
@@ -245,7 +245,7 @@ func Test_isCaddyfile(t *testing.T) {
 			wantErr: false,
 		},
 		{
-			
+
 			name: "prefix of Caddyfile and ./ with any extension is Caddyfile",
 			args: args{
 				configFile:  "./Caddyfile.prd",
@@ -255,7 +255,7 @@ func Test_isCaddyfile(t *testing.T) {
 			wantErr: false,
 		},
 		{
-			
+
 			name: "prefix of Caddyfile without ./ with any extension is Caddyfile",
 			args: args{
 				configFile:  "Caddyfile.prd",
diff --git a/listen.go b/listen.go
index 34812b54f..f5c2086a6 100644
--- a/listen.go
+++ b/listen.go
@@ -18,7 +18,11 @@ package caddy
 
 import (
 	"context"
+	"fmt"
 	"net"
+	"os"
+	"slices"
+	"strconv"
 	"sync"
 	"sync/atomic"
 	"time"
@@ -31,10 +35,49 @@ func reuseUnixSocket(network, addr string) (any, error) {
 }
 
 func listenReusable(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (any, error) {
-	switch network {
-	case "udp", "udp4", "udp6", "unixgram":
+	var socketFile *os.File
+
+	fd := slices.Contains([]string{"fd", "fdgram"}, network)
+	if fd {
+		socketFd, err := strconv.ParseUint(address, 0, strconv.IntSize)
+		if err != nil {
+			return nil, fmt.Errorf("invalid file descriptor: %v", err)
+		}
+
+		func() {
+			socketFilesMu.Lock()
+			defer socketFilesMu.Unlock()
+
+			socketFdWide := uintptr(socketFd)
+			var ok bool
+
+			socketFile, ok = socketFiles[socketFdWide]
+
+			if !ok {
+				socketFile = os.NewFile(socketFdWide, lnKey)
+				if socketFile != nil {
+					socketFiles[socketFdWide] = socketFile
+				}
+			}
+		}()
+
+		if socketFile == nil {
+			return nil, fmt.Errorf("invalid socket file descriptor: %d", socketFd)
+		}
+	}
+
+	datagram := slices.Contains([]string{"udp", "udp4", "udp6", "unixgram", "fdgram"}, network)
+	if datagram {
 		sharedPc, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
-			pc, err := config.ListenPacket(ctx, network, address)
+			var (
+				pc  net.PacketConn
+				err error
+			)
+			if fd {
+				pc, err = net.FilePacketConn(socketFile)
+			} else {
+				pc, err = config.ListenPacket(ctx, network, address)
+			}
 			if err != nil {
 				return nil, err
 			}
@@ -44,20 +87,27 @@ func listenReusable(ctx context.Context, lnKey string, network, address string,
 			return nil, err
 		}
 		return &fakeClosePacketConn{sharedPacketConn: sharedPc.(*sharedPacketConn)}, nil
+	}
 
-	default:
-		sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
-			ln, err := config.Listen(ctx, network, address)
-			if err != nil {
-				return nil, err
-			}
-			return &sharedListener{Listener: ln, key: lnKey}, nil
-		})
+	sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
+		var (
+			ln  net.Listener
+			err error
+		)
+		if fd {
+			ln, err = net.FileListener(socketFile)
+		} else {
+			ln, err = config.Listen(ctx, network, address)
+		}
 		if err != nil {
 			return nil, err
 		}
-		return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: config.KeepAlive}, nil
+		return &sharedListener{Listener: ln, key: lnKey}, nil
+	})
+	if err != nil {
+		return nil, err
 	}
+	return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: config.KeepAlive}, nil
 }
 
 // fakeCloseListener is a private wrapper over a listener that
@@ -260,3 +310,9 @@ var (
 		Unwrap() net.PacketConn
 	}) = (*fakeClosePacketConn)(nil)
 )
+
+// socketFiles is a fd -> *os.File map used to make a FileListener/FilePacketConn from a socket file descriptor.
+var socketFiles = map[uintptr]*os.File{}
+
+// socketFilesMu synchronizes socketFiles insertions
+var socketFilesMu sync.Mutex
diff --git a/listen_unix.go b/listen_unix.go
index 9ec65c399..d6ae0cb8e 100644
--- a/listen_unix.go
+++ b/listen_unix.go
@@ -22,10 +22,14 @@ package caddy
 import (
 	"context"
 	"errors"
+	"fmt"
 	"io"
 	"io/fs"
 	"net"
 	"os"
+	"slices"
+	"strconv"
+	"sync"
 	"sync/atomic"
 	"syscall"
 
@@ -34,12 +38,9 @@ import (
 )
 
 // reuseUnixSocket copies and reuses the unix domain socket (UDS) if we already
-// have it open; if not, unlink it so we can have it. No-op if not a unix network.
+// have it open; if not, unlink it so we can have it.
+// No-op if not a unix network.
 func reuseUnixSocket(network, addr string) (any, error) {
-	if !IsUnixNetwork(network) {
-		return nil, nil
-	}
-
 	socketKey := listenerKey(network, addr)
 
 	socket, exists := unixSockets[socketKey]
@@ -71,7 +72,7 @@ func reuseUnixSocket(network, addr string) (any, error) {
 				return nil, err
 			}
 			atomic.AddInt32(unixSocket.count, 1)
-			unixSockets[socketKey] = &unixConn{pc.(*net.UnixConn), addr, socketKey, unixSocket.count}
+			unixSockets[socketKey] = &unixConn{pc.(*net.UnixConn), socketKey, unixSocket.count}
 		}
 
 		return unixSockets[socketKey], nil
@@ -89,56 +90,107 @@ func reuseUnixSocket(network, addr string) (any, error) {
 	return nil, nil
 }
 
+// listenReusable creates a new listener for the given network and address, and adds it to listenerPool.
 func listenReusable(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (any, error) {
-	// wrap any Control function set by the user so we can also add our reusePort control without clobbering theirs
-	oldControl := config.Control
-	config.Control = func(network, address string, c syscall.RawConn) error {
-		if oldControl != nil {
-			if err := oldControl(network, address, c); err != nil {
-				return err
-			}
-		}
-		return reusePort(network, address, c)
-	}
-
 	// even though SO_REUSEPORT lets us bind the socket multiple times,
 	// we still put it in the listenerPool so we can count how many
 	// configs are using this socket; necessary to ensure we can know
 	// whether to enforce shutdown delays, for example (see #5393).
-	var ln io.Closer
-	var err error
-	switch network {
-	case "udp", "udp4", "udp6", "unixgram":
-		ln, err = config.ListenPacket(ctx, network, address)
-	default:
-		ln, err = config.Listen(ctx, network, address)
+	var (
+		ln         io.Closer
+		err        error
+		socketFile *os.File
+	)
+
+	fd := slices.Contains([]string{"fd", "fdgram"}, network)
+	if fd {
+		socketFd, err := strconv.ParseUint(address, 0, strconv.IntSize)
+		if err != nil {
+			return nil, fmt.Errorf("invalid file descriptor: %v", err)
+		}
+
+		func() {
+			socketFilesMu.Lock()
+			defer socketFilesMu.Unlock()
+
+			socketFdWide := uintptr(socketFd)
+			var ok bool
+
+			socketFile, ok = socketFiles[socketFdWide]
+
+			if !ok {
+				socketFile = os.NewFile(socketFdWide, lnKey)
+				if socketFile != nil {
+					socketFiles[socketFdWide] = socketFile
+				}
+			}
+		}()
+
+		if socketFile == nil {
+			return nil, fmt.Errorf("invalid socket file descriptor: %d", socketFd)
+		}
+	} else {
+		// wrap any Control function set by the user so we can also add our reusePort control without clobbering theirs
+		oldControl := config.Control
+		config.Control = func(network, address string, c syscall.RawConn) error {
+			if oldControl != nil {
+				if err := oldControl(network, address, c); err != nil {
+					return err
+				}
+			}
+			return reusePort(network, address, c)
+		}
 	}
+
+	datagram := slices.Contains([]string{"udp", "udp4", "udp6", "unixgram", "fdgram"}, network)
+	if datagram {
+		if fd {
+			ln, err = net.FilePacketConn(socketFile)
+		} else {
+			ln, err = config.ListenPacket(ctx, network, address)
+		}
+	} else {
+		if fd {
+			ln, err = net.FileListener(socketFile)
+		} else {
+			ln, err = config.Listen(ctx, network, address)
+		}
+	}
+
 	if err == nil {
 		listenerPool.LoadOrStore(lnKey, nil)
 	}
 
-	// if new listener is a unix socket, make sure we can reuse it later
-	// (we do our own "unlink on close" -- not required, but more tidy)
-	one := int32(1)
-	if unix, ok := ln.(*net.UnixListener); ok {
-		unix.SetUnlinkOnClose(false)
-		ln = &unixListener{unix, lnKey, &one}
-		unixSockets[lnKey] = ln.(*unixListener)
-	}
-
-	// TODO: Not 100% sure this is necessary, but we do this for net.UnixListener in listen_unix.go, so...
-	if unix, ok := ln.(*net.UnixConn); ok {
-		ln = &unixConn{unix, address, lnKey, &one}
-		unixSockets[lnKey] = ln.(*unixConn)
-	}
-
-	// lightly wrap the listener so that when it is closed,
-	// we can decrement the usage pool counter
-	switch specificLn := ln.(type) {
-	case net.Listener:
-		return deleteListener{specificLn, lnKey}, err
-	case net.PacketConn:
-		return deletePacketConn{specificLn, lnKey}, err
+	if datagram {
+		if !fd {
+			// TODO: Not 100% sure this is necessary, but we do this for net.UnixListener, so...
+			if unix, ok := ln.(*net.UnixConn); ok {
+				one := int32(1)
+				ln = &unixConn{unix, lnKey, &one}
+				unixSockets[lnKey] = ln.(*unixConn)
+			}
+		}
+		// lightly wrap the connection so that when it is closed,
+		// we can decrement the usage pool counter
+		if specificLn, ok := ln.(net.PacketConn); ok {
+			ln = deletePacketConn{specificLn, lnKey}
+		}
+	} else {
+		if !fd {
+			// if new listener is a unix socket, make sure we can reuse it later
+			// (we do our own "unlink on close" -- not required, but more tidy)
+			if unix, ok := ln.(*net.UnixListener); ok {
+				unix.SetUnlinkOnClose(false)
+				one := int32(1)
+				ln = &unixListener{unix, lnKey, &one}
+				unixSockets[lnKey] = ln.(*unixListener)
+			}
+		}
+		// lightly wrap the listener so that when it is closed,
+		// we can decrement the usage pool counter
+		if specificLn, ok := ln.(net.Listener); ok {
+			ln = deleteListener{specificLn, lnKey}
+		}
 	}
 
 	// other types, I guess we just return them directly
@@ -170,12 +222,18 @@ type unixListener struct {
 func (uln *unixListener) Close() error {
 	newCount := atomic.AddInt32(uln.count, -1)
 	if newCount == 0 {
+		file, err := uln.File()
+		var name string
+		if err == nil {
+			name = file.Name()
+		}
 		defer func() {
-			addr := uln.Addr().String()
 			unixSocketsMu.Lock()
 			delete(unixSockets, uln.mapKey)
 			unixSocketsMu.Unlock()
-			_ = syscall.Unlink(addr)
+			if err == nil {
+				_ = syscall.Unlink(name)
+			}
 		}()
 	}
 	return uln.UnixListener.Close()
@@ -183,19 +241,25 @@ func (uln *unixListener) Close() error {
 
 type unixConn struct {
 	*net.UnixConn
-	filename string
-	mapKey   string
-	count    *int32 // accessed atomically
+	mapKey string
+	count  *int32 // accessed atomically
 }
 
 func (uc *unixConn) Close() error {
 	newCount := atomic.AddInt32(uc.count, -1)
 	if newCount == 0 {
+		file, err := uc.File()
+		var name string
+		if err == nil {
+			name = file.Name()
+		}
 		defer func() {
 			unixSocketsMu.Lock()
 			delete(unixSockets, uc.mapKey)
 			unixSocketsMu.Unlock()
-			_ = syscall.Unlink(uc.filename)
+			if err == nil {
+				_ = syscall.Unlink(name)
+			}
 		}()
 	}
 	return uc.UnixConn.Close()
@@ -211,6 +275,12 @@ var unixSockets = make(map[string]interface {
 	File() (*os.File, error)
 })
 
+// socketFiles is a fd -> *os.File map used to make a FileListener/FilePacketConn from a socket file descriptor.
+var socketFiles = map[uintptr]*os.File{}
+
+// socketFilesMu synchronizes socketFiles insertions
+var socketFilesMu sync.Mutex
+
 // deleteListener is a type that simply deletes itself
 // from the listenerPool when it closes. It is used
 // solely for the purpose of reference counting (i.e.
diff --git a/listeners.go b/listeners.go
index 0d9dd7538..cf7b52018 100644
--- a/listeners.go
+++ b/listeners.go
@@ -58,7 +58,7 @@ type NetworkAddress struct {
 	EndPort   uint
 }
 
-// ListenAll calls Listen() for all addresses represented by this struct, i.e. all ports in the range.
+// 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.
 func (na NetworkAddress) ListenAll(ctx context.Context, config net.ListenConfig) ([]any, error) {
@@ -106,7 +106,8 @@ func (na NetworkAddress) ListenAll(ctx context.Context, config net.ListenConfig)
 // portOffset to the start port. (For network types that do not use ports, the
 // portOffset is ignored.)
 //
-// The provided ListenConfig is used to create the listener. Its Control function,
+// First Listen checks if a plugin can provide a listener from this address. Otherwise,
+// the provided ListenConfig is used to create the listener. Its Control function,
 // if set, may be wrapped by an internally-used Control function. The provided
 // context may be used to cancel long operations early. The context is not used
 // to close the listener after it has been created.
@@ -129,6 +130,8 @@ 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).
+// Listen synchronizes binds to unix domain sockets to avoid race conditions
+// while an existing socket is unlinked.
 func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
 	if na.IsUnixNetwork() {
 		unixSocketsMu.Lock()
@@ -146,54 +149,53 @@ func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net
 
 func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
 	var (
-		ln                   any
-		err                  error
-		address              string
-		unixFileMode         fs.FileMode
-		isAbstractUnixSocket bool
+		ln           any
+		err          error
+		address      string
+		unixFileMode fs.FileMode
 	)
 
 	// split unix socket addr early so lnKey
 	// is independent of permissions bits
 	if na.IsUnixNetwork() {
-		var err error
 		address, unixFileMode, err = internal.SplitUnixSocketPermissionsBits(na.Host)
 		if err != nil {
 			return nil, err
 		}
-		isAbstractUnixSocket = strings.HasPrefix(address, "@")
+	} else if na.IsFdNetwork() {
+		address = na.Host
 	} else {
 		address = na.JoinHostPort(portOffset)
 	}
 
-	// if this is a unix socket, see if we already have it open,
-	// force socket permissions on it and return early
-	if socket, err := reuseUnixSocket(na.Network, address); socket != nil || err != nil {
-		if !isAbstractUnixSocket {
-			if err := os.Chmod(address, unixFileMode); err != nil {
-				return nil, fmt.Errorf("unable to set permissions (%s) on %s: %v", unixFileMode, address, err)
-			}
-		}
-		return socket, err
-	}
-
-	lnKey := listenerKey(na.Network, address)
-
 	if strings.HasPrefix(na.Network, "ip") {
 		ln, err = config.ListenPacket(ctx, na.Network, address)
 	} else {
-		ln, err = listenReusable(ctx, lnKey, na.Network, address, config)
-	}
-	if err != nil {
-		return nil, err
+		if na.IsUnixNetwork() {
+			// if this is a unix socket, see if we already have it open
+			ln, err = reuseUnixSocket(na.Network, address)
+		}
+
+		if ln == nil && err == nil {
+			// otherwise, create a new listener
+			lnKey := listenerKey(na.Network, address)
+			ln, err = listenReusable(ctx, lnKey, na.Network, address, config)
+		}
 	}
+
 	if ln == nil {
 		return nil, fmt.Errorf("unsupported network type: %s", na.Network)
 	}
 
+	if err != nil {
+		return nil, err
+	}
+
 	if IsUnixNetwork(na.Network) {
+		isAbstractUnixSocket := strings.HasPrefix(address, "@")
 		if !isAbstractUnixSocket {
-			if err := os.Chmod(address, unixFileMode); err != nil {
+			err = os.Chmod(address, unixFileMode)
+			if err != nil {
 				return nil, fmt.Errorf("unable to set permissions (%s) on %s: %v", unixFileMode, address, err)
 			}
 		}
@@ -208,13 +210,19 @@ func (na NetworkAddress) IsUnixNetwork() bool {
 	return IsUnixNetwork(na.Network)
 }
 
+// IsUnixNetwork returns true if na.Network is
+// fd or fdgram.
+func (na NetworkAddress) IsFdNetwork() bool {
+	return IsFdNetwork(na.Network)
+}
+
 // JoinHostPort is like net.JoinHostPort, but where the port
 // is StartPort + offset.
 func (na NetworkAddress) JoinHostPort(offset uint) string {
-	if na.IsUnixNetwork() {
+	if na.IsUnixNetwork() || na.IsFdNetwork() {
 		return na.Host
 	}
-	return net.JoinHostPort(na.Host, strconv.Itoa(int(na.StartPort+offset)))
+	return net.JoinHostPort(na.Host, strconv.FormatUint(uint64(na.StartPort+offset), 10))
 }
 
 // Expand returns one NetworkAddress for each port in the port range.
@@ -248,7 +256,7 @@ func (na NetworkAddress) PortRangeSize() uint {
 }
 
 func (na NetworkAddress) isLoopback() bool {
-	if na.IsUnixNetwork() {
+	if na.IsUnixNetwork() || na.IsFdNetwork() {
 		return true
 	}
 	if na.Host == "localhost" {
@@ -292,6 +300,30 @@ func IsUnixNetwork(netw string) bool {
 	return strings.HasPrefix(netw, "unix")
 }
 
+// IsFdNetwork returns true if the netw is a fd network.
+func IsFdNetwork(netw string) bool {
+	return strings.HasPrefix(netw, "fd")
+}
+
+// normally we would simply append the port,
+// but if host is IPv6, we need to ensure it
+// is enclosed in [ ]; net.JoinHostPort does
+// this for us, but host might also have a
+// network type in front (e.g. "tcp/") leading
+// to "[tcp/::1]" which causes parsing failures
+// later; what we need is "tcp/[::1]", so we have
+// to split the network and host, then re-combine
+func ParseNetworkAddressFromHostPort(host, port string) (NetworkAddress, error) {
+	network, addr, ok := strings.Cut(host, "/")
+	if !ok {
+		addr = network
+		network = ""
+	}
+	addr = strings.Trim(addr, "[]") // IPv6
+	networkAddr := JoinNetworkAddress(network, addr, port)
+	return ParseNetworkAddress(networkAddr)
+}
+
 // ParseNetworkAddress parses addr into its individual
 // components. The input string is expected to be of
 // the form "network/host:port-range" where any part is
@@ -322,6 +354,12 @@ func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort ui
 			Host:    host,
 		}, err
 	}
+	if IsFdNetwork(network) {
+		return NetworkAddress{
+			Network: network,
+			Host:    host,
+		}, nil
+	}
 	var start, end uint64
 	if port == "" {
 		start = uint64(defaultPort)
@@ -362,7 +400,7 @@ func SplitNetworkAddress(a string) (network, host, port string, err error) {
 		network = strings.ToLower(strings.TrimSpace(beforeSlash))
 		a = afterSlash
 	}
-	if IsUnixNetwork(network) {
+	if IsUnixNetwork(network) || IsFdNetwork(network) {
 		host = a
 		return
 	}
@@ -393,7 +431,7 @@ func JoinNetworkAddress(network, host, port string) string {
 	if network != "" {
 		a = network + "/"
 	}
-	if (host != "" && port == "") || IsUnixNetwork(network) {
+	if (host != "" && port == "") || IsUnixNetwork(network) || IsFdNetwork(network) {
 		a += host
 	} else if port != "" {
 		a += net.JoinHostPort(host, port)
@@ -401,9 +439,11 @@ func JoinNetworkAddress(network, host, port string) string {
 	return a
 }
 
-// ListenQUIC returns a quic.EarlyListener suitable for use in a Caddy module.
-// The network will be transformed into a QUIC-compatible type (if unix, then
-// unixgram will be used; otherwise, udp will be used).
+// ListenQUIC returns a http3.QUICEarlyListener suitable for use in a Caddy module.
+//
+// The network will be transformed into a QUIC-compatible type if the same address can be used with
+// different networks. Currently this just means that for tcp, udp will be used with the same
+// address instead.
 //
 // NOTE: This API is EXPERIMENTAL and may be changed or removed.
 func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config net.ListenConfig, tlsConf *tls.Config) (http3.QUICEarlyListener, error) {
@@ -617,7 +657,8 @@ func RegisterNetwork(network string, getListener ListenerFunc) {
 	if network == "tcp" || network == "tcp4" || network == "tcp6" ||
 		network == "udp" || network == "udp4" || network == "udp6" ||
 		network == "unix" || network == "unixpacket" || network == "unixgram" ||
-		strings.HasPrefix("ip:", network) || strings.HasPrefix("ip4:", network) || strings.HasPrefix("ip6:", network) {
+		strings.HasPrefix("ip:", network) || strings.HasPrefix("ip4:", network) || strings.HasPrefix("ip6:", network) ||
+		network == "fd" || network == "fdgram" {
 		panic("network type " + network + " is reserved")
 	}
 
diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go
index 5dbecf9b2..673ebcb8e 100644
--- a/modules/caddyhttp/app.go
+++ b/modules/caddyhttp/app.go
@@ -18,6 +18,7 @@ import (
 	"context"
 	"crypto/tls"
 	"fmt"
+	"maps"
 	"net"
 	"net/http"
 	"strconv"
@@ -203,17 +204,75 @@ func (app *App) Provision(ctx caddy.Context) error {
 			}
 		}
 
-		// the Go standard library does not let us serve only HTTP/2 using
-		// http.Server; we would probably need to write our own server
-		if !srv.protocol("h1") && (srv.protocol("h2") || srv.protocol("h2c")) {
-			return fmt.Errorf("server %s: cannot enable HTTP/2 or H2C without enabling HTTP/1.1; add h1 to protocols or remove h2/h2c", srvName)
-		}
-
 		// if no protocols configured explicitly, enable all except h2c
 		if len(srv.Protocols) == 0 {
 			srv.Protocols = []string{"h1", "h2", "h3"}
 		}
 
+		srvProtocolsUnique := map[string]struct{}{}
+		for _, srvProtocol := range srv.Protocols {
+			srvProtocolsUnique[srvProtocol] = struct{}{}
+		}
+		_, h1ok := srvProtocolsUnique["h1"]
+		_, h2ok := srvProtocolsUnique["h2"]
+		_, h2cok := srvProtocolsUnique["h2c"]
+
+		// the Go standard library does not let us serve only HTTP/2 using
+		// http.Server; we would probably need to write our own server
+		if !h1ok && (h2ok || h2cok) {
+			return fmt.Errorf("server %s: cannot enable HTTP/2 or H2C without enabling HTTP/1.1; add h1 to protocols or remove h2/h2c", srvName)
+		}
+
+		if srv.ListenProtocols != nil {
+			if len(srv.ListenProtocols) != len(srv.Listen) {
+				return fmt.Errorf("server %s: listener protocols count does not match address count: %d != %d",
+					srvName, len(srv.ListenProtocols), len(srv.Listen))
+			}
+
+			for i, lnProtocols := range srv.ListenProtocols {
+				if lnProtocols != nil {
+					// populate empty listen protocols with server protocols
+					lnProtocolsDefault := false
+					var lnProtocolsInclude []string
+					srvProtocolsInclude := maps.Clone(srvProtocolsUnique)
+
+					// keep existing listener protocols unless they are empty
+					for _, lnProtocol := range lnProtocols {
+						if lnProtocol == "" {
+							lnProtocolsDefault = true
+						} else {
+							lnProtocolsInclude = append(lnProtocolsInclude, lnProtocol)
+							delete(srvProtocolsInclude, lnProtocol)
+						}
+					}
+
+					// append server protocols to listener protocols if any listener protocols were empty
+					if lnProtocolsDefault {
+						for _, srvProtocol := range srv.Protocols {
+							if _, ok := srvProtocolsInclude[srvProtocol]; ok {
+								lnProtocolsInclude = append(lnProtocolsInclude, srvProtocol)
+							}
+						}
+					}
+
+					lnProtocolsIncludeUnique := map[string]struct{}{}
+					for _, lnProtocol := range lnProtocolsInclude {
+						lnProtocolsIncludeUnique[lnProtocol] = struct{}{}
+					}
+					_, h1ok := lnProtocolsIncludeUnique["h1"]
+					_, h2ok := lnProtocolsIncludeUnique["h2"]
+					_, h2cok := lnProtocolsIncludeUnique["h2c"]
+
+					// check if any listener protocols contain h2 or h2c without h1
+					if !h1ok && (h2ok || h2cok) {
+						return fmt.Errorf("server %s, listener %d: cannot enable HTTP/2 or H2C without enabling HTTP/1.1; add h1 to protocols or remove h2/h2c", srvName, i)
+					}
+
+					srv.ListenProtocols[i] = lnProtocolsInclude
+				}
+			}
+		}
+
 		// if not explicitly configured by the user, disallow TLS
 		// client auth bypass (domain fronting) which could
 		// otherwise be exploited by sending an unprotected SNI
@@ -344,7 +403,7 @@ func (app *App) Validate() error {
 			// check that every address in the port range is unique to this server;
 			// we do not use <= here because PortRangeSize() adds 1 to EndPort for us
 			for i := uint(0); i < listenAddr.PortRangeSize(); i++ {
-				addr := caddy.JoinNetworkAddress(listenAddr.Network, listenAddr.Host, strconv.Itoa(int(listenAddr.StartPort+i)))
+				addr := caddy.JoinNetworkAddress(listenAddr.Network, listenAddr.Host, strconv.FormatUint(uint64(listenAddr.StartPort+i), 10))
 				if sn, ok := lnAddrs[addr]; ok {
 					return fmt.Errorf("server %s: listener address repeated: %s (already claimed by server '%s')", srvName, addr, sn)
 				}
@@ -422,99 +481,118 @@ func (app *App) Start() error {
 			srv.server.Handler = h2c.NewHandler(srv, h2server)
 		}
 
-		for _, lnAddr := range srv.Listen {
+		for lnIndex, lnAddr := range srv.Listen {
 			listenAddr, err := caddy.ParseNetworkAddress(lnAddr)
 			if err != nil {
 				return fmt.Errorf("%s: parsing listen address '%s': %v", srvName, lnAddr, err)
 			}
+
 			srv.addresses = append(srv.addresses, listenAddr)
 
-			for portOffset := uint(0); portOffset < listenAddr.PortRangeSize(); portOffset++ {
-				// create the listener for this socket
-				hostport := listenAddr.JoinHostPort(portOffset)
-				lnAny, err := listenAddr.Listen(app.ctx, portOffset, net.ListenConfig{KeepAlive: time.Duration(srv.KeepAliveInterval)})
-				if err != nil {
-					return fmt.Errorf("listening on %s: %v", listenAddr.At(portOffset), err)
-				}
-				ln := lnAny.(net.Listener)
+			protocols := srv.Protocols
+			if srv.ListenProtocols != nil && srv.ListenProtocols[lnIndex] != nil {
+				protocols = srv.ListenProtocols[lnIndex]
+			}
 
-				// wrap listener before TLS (up to the TLS placeholder wrapper)
-				var lnWrapperIdx int
-				for i, lnWrapper := range srv.listenerWrappers {
-					if _, ok := lnWrapper.(*tlsPlaceholderWrapper); ok {
-						lnWrapperIdx = i + 1 // mark the next wrapper's spot
-						break
-					}
-					ln = lnWrapper.WrapListener(ln)
-				}
+			protocolsUnique := map[string]struct{}{}
+			for _, protocol := range protocols {
+				protocolsUnique[protocol] = struct{}{}
+			}
+			_, h1ok := protocolsUnique["h1"]
+			_, h2ok := protocolsUnique["h2"]
+			_, h2cok := protocolsUnique["h2c"]
+			_, h3ok := protocolsUnique["h3"]
+
+			for portOffset := uint(0); portOffset < listenAddr.PortRangeSize(); portOffset++ {
+				hostport := listenAddr.JoinHostPort(portOffset)
 
 				// enable TLS if there is a policy and if this is not the HTTP port
 				useTLS := len(srv.TLSConnPolicies) > 0 && int(listenAddr.StartPort+portOffset) != app.httpPort()
-				if useTLS {
-					// create TLS listener - this enables and terminates TLS
-					ln = tls.NewListener(ln, tlsCfg)
 
-					// enable HTTP/3 if configured
-					if srv.protocol("h3") {
-						// Can't serve HTTP/3 on the same socket as HTTP/1 and 2 because it uses
-						// a different transport mechanism... which is fine, but the OS doesn't
-						// differentiate between a SOCK_STREAM file and a SOCK_DGRAM file; they
-						// are still one file on the system. So even though "unixpacket" and
-						// "unixgram" are different network types just as "tcp" and "udp" are,
-						// the OS will not let us use the same file as both STREAM and DGRAM.
-						if len(srv.Protocols) > 1 && listenAddr.IsUnixNetwork() {
-							app.logger.Warn("HTTP/3 disabled because Unix can't multiplex STREAM and DGRAM on same socket",
-								zap.String("file", hostport))
-							for i := range srv.Protocols {
-								if srv.Protocols[i] == "h3" {
-									srv.Protocols = append(srv.Protocols[:i], srv.Protocols[i+1:]...)
-									break
-								}
-							}
-						} else {
-							app.logger.Info("enabling HTTP/3 listener", zap.String("addr", hostport))
-							if err := srv.serveHTTP3(listenAddr.At(portOffset), tlsCfg); err != nil {
-								return err
-							}
+				// enable HTTP/3 if configured
+				if h3ok && useTLS {
+					app.logger.Info("enabling HTTP/3 listener", zap.String("addr", hostport))
+					if err := srv.serveHTTP3(listenAddr.At(portOffset), tlsCfg); err != nil {
+						return err
+					}
+				}
+
+				if h3ok && !useTLS {
+					// Can only serve h3 with TLS enabled
+					app.logger.Warn("HTTP/3 skipped because it requires TLS",
+						zap.String("network", listenAddr.Network),
+						zap.String("addr", hostport))
+				}
+
+				if h1ok || h2ok && useTLS || h2cok {
+					// create the listener for this socket
+					lnAny, err := listenAddr.Listen(app.ctx, portOffset, net.ListenConfig{KeepAlive: time.Duration(srv.KeepAliveInterval)})
+					if err != nil {
+						return fmt.Errorf("listening on %s: %v", listenAddr.At(portOffset), err)
+					}
+					ln, ok := lnAny.(net.Listener)
+					if !ok {
+						return fmt.Errorf("network '%s' cannot handle HTTP/1 or HTTP/2 connections", listenAddr.Network)
+					}
+
+					if useTLS {
+						// create TLS listener - this enables and terminates TLS
+						ln = tls.NewListener(ln, tlsCfg)
+					}
+
+					// wrap listener before TLS (up to the TLS placeholder wrapper)
+					var lnWrapperIdx int
+					for i, lnWrapper := range srv.listenerWrappers {
+						if _, ok := lnWrapper.(*tlsPlaceholderWrapper); ok {
+							lnWrapperIdx = i + 1 // mark the next wrapper's spot
+							break
 						}
+						ln = lnWrapper.WrapListener(ln)
+					}
+
+					// finish wrapping listener where we left off before TLS
+					for i := lnWrapperIdx; i < len(srv.listenerWrappers); i++ {
+						ln = srv.listenerWrappers[i].WrapListener(ln)
+					}
+
+					// handle http2 if use tls listener wrapper
+					if h2ok {
+						http2lnWrapper := &http2Listener{
+							Listener: ln,
+							server:   srv.server,
+							h2server: h2server,
+						}
+						srv.h2listeners = append(srv.h2listeners, http2lnWrapper)
+						ln = http2lnWrapper
+					}
+
+					// if binding to port 0, the OS chooses a port for us;
+					// but the user won't know the port unless we print it
+					if !listenAddr.IsUnixNetwork() && !listenAddr.IsFdNetwork() && listenAddr.StartPort == 0 && listenAddr.EndPort == 0 {
+						app.logger.Info("port 0 listener",
+							zap.String("input_address", lnAddr),
+							zap.String("actual_address", ln.Addr().String()))
+					}
+
+					app.logger.Debug("starting server loop",
+						zap.String("address", ln.Addr().String()),
+						zap.Bool("tls", useTLS),
+						zap.Bool("http3", srv.h3server != nil))
+
+					srv.listeners = append(srv.listeners, ln)
+
+					// enable HTTP/1 if configured
+					if h1ok {
+						//nolint:errcheck
+						go srv.server.Serve(ln)
 					}
 				}
 
-				// finish wrapping listener where we left off before TLS
-				for i := lnWrapperIdx; i < len(srv.listenerWrappers); i++ {
-					ln = srv.listenerWrappers[i].WrapListener(ln)
-				}
-
-				// handle http2 if use tls listener wrapper
-				if useTLS {
-					http2lnWrapper := &http2Listener{
-						Listener: ln,
-						server:   srv.server,
-						h2server: h2server,
-					}
-					srv.h2listeners = append(srv.h2listeners, http2lnWrapper)
-					ln = http2lnWrapper
-				}
-
-				// if binding to port 0, the OS chooses a port for us;
-				// but the user won't know the port unless we print it
-				if !listenAddr.IsUnixNetwork() && listenAddr.StartPort == 0 && listenAddr.EndPort == 0 {
-					app.logger.Info("port 0 listener",
-						zap.String("input_address", lnAddr),
-						zap.String("actual_address", ln.Addr().String()))
-				}
-
-				app.logger.Debug("starting server loop",
-					zap.String("address", ln.Addr().String()),
-					zap.Bool("tls", useTLS),
-					zap.Bool("http3", srv.h3server != nil))
-
-				srv.listeners = append(srv.listeners, ln)
-
-				// enable HTTP/1 if configured
-				if srv.protocol("h1") {
-					//nolint:errcheck
-					go srv.server.Serve(ln)
+				if h2ok && !useTLS {
+					// Can only serve h2 with TLS enabled
+					app.logger.Warn("HTTP/2 skipped because it requires TLS",
+						zap.String("network", listenAddr.Network),
+						zap.String("addr", hostport))
 				}
 			}
 		}
diff --git a/modules/caddyhttp/proxyprotocol/listenerwrapper.go b/modules/caddyhttp/proxyprotocol/listenerwrapper.go
index 440e70710..f5f2099ce 100644
--- a/modules/caddyhttp/proxyprotocol/listenerwrapper.go
+++ b/modules/caddyhttp/proxyprotocol/listenerwrapper.go
@@ -72,7 +72,7 @@ func (pp *ListenerWrapper) Provision(ctx caddy.Context) error {
 
 	pp.policy = func(options goproxy.ConnPolicyOptions) (goproxy.Policy, error) {
 		// trust unix sockets
-		if network := options.Upstream.Network(); caddy.IsUnixNetwork(network) {
+		if network := options.Upstream.Network(); caddy.IsUnixNetwork(network) || caddy.IsFdNetwork(network) {
 			return goproxy.USE, nil
 		}
 		ret := pp.FallbackPolicy
diff --git a/modules/caddyhttp/reverseproxy/addresses.go b/modules/caddyhttp/reverseproxy/addresses.go
index 82c1c7994..31f4aeb35 100644
--- a/modules/caddyhttp/reverseproxy/addresses.go
+++ b/modules/caddyhttp/reverseproxy/addresses.go
@@ -137,7 +137,7 @@ func parseUpstreamDialAddress(upstreamAddr string) (parsedAddr, error) {
 		}
 		// we can assume a port if only a hostname is specified, but use of a
 		// placeholder without a port likely means a port will be filled in
-		if port == "" && !strings.Contains(host, "{") && !caddy.IsUnixNetwork(network) {
+		if port == "" && !strings.Contains(host, "{") && !caddy.IsUnixNetwork(network) && !caddy.IsFdNetwork(network) {
 			port = "80"
 		}
 	}
diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go
index 319cc9248..1735e45a4 100644
--- a/modules/caddyhttp/reverseproxy/healthchecks.go
+++ b/modules/caddyhttp/reverseproxy/healthchecks.go
@@ -330,7 +330,7 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
 				return
 			}
 			if hcp := uint(upstream.activeHealthCheckPort); hcp != 0 {
-				if addr.IsUnixNetwork() {
+				if addr.IsUnixNetwork() || addr.IsFdNetwork() {
 					addr.Network = "tcp" // I guess we just assume TCP since we are using a port??
 				}
 				addr.StartPort, addr.EndPort = hcp, hcp
@@ -345,7 +345,7 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
 			}
 			hostAddr := addr.JoinHostPort(0)
 			dialAddr := hostAddr
-			if addr.IsUnixNetwork() {
+			if addr.IsUnixNetwork() || addr.IsFdNetwork() {
 				// this will be used as the Host portion of a http.Request URL, and
 				// paths to socket files would produce an error when creating URL,
 				// so use a fake Host value instead; unix sockets are usually local
diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go
index f5478cb37..5aa7e0f63 100644
--- a/modules/caddyhttp/server.go
+++ b/modules/caddyhttp/server.go
@@ -220,6 +220,10 @@ type Server struct {
 	// Default: `[h1 h2 h3]`
 	Protocols []string `json:"protocols,omitempty"`
 
+	// ListenProtocols overrides Protocols for each parallel address in Listen.
+	// A nil value or element indicates that Protocols will be used instead.
+	ListenProtocols [][]string `json:"listen_protocols,omitempty"`
+
 	// If set, metrics observations will be enabled.
 	// This setting is EXPERIMENTAL and subject to change.
 	Metrics *Metrics `json:"metrics,omitempty"`
@@ -597,7 +601,11 @@ func (s *Server) findLastRouteWithHostMatcher() int {
 // not already done, and then uses that server to serve HTTP/3 over
 // the listener, with Server s as the handler.
 func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error {
-	addr.Network = getHTTP3Network(addr.Network)
+	h3net, err := getHTTP3Network(addr.Network)
+	if err != nil {
+		return fmt.Errorf("starting HTTP/3 QUIC listener: %v", err)
+	}
+	addr.Network = h3net
 	h3ln, err := addr.ListenQUIC(s.ctx, 0, net.ListenConfig{}, tlsCfg)
 	if err != nil {
 		return fmt.Errorf("starting HTTP/3 QUIC listener: %v", err)
@@ -849,7 +857,21 @@ func (s *Server) logRequest(
 
 // protocol returns true if the protocol proto is configured/enabled.
 func (s *Server) protocol(proto string) bool {
-	return slices.Contains(s.Protocols, proto)
+	if s.ListenProtocols == nil {
+		if slices.Contains(s.Protocols, proto) {
+			return true
+		}
+	} else {
+		for _, lnProtocols := range s.ListenProtocols {
+			for _, lnProtocol := range lnProtocols {
+				if lnProtocol == "" && slices.Contains(s.Protocols, proto) || lnProtocol == proto {
+					return true
+				}
+			}
+		}
+	}
+
+	return false
 }
 
 // Listeners returns the server's listeners. These are active listeners,
@@ -1089,9 +1111,14 @@ const (
 )
 
 var networkTypesHTTP3 = map[string]string{
-	"unix": "unixgram",
-	"tcp4": "udp4",
-	"tcp6": "udp6",
+	"unixgram": "unixgram",
+	"udp":      "udp",
+	"udp4":     "udp4",
+	"udp6":     "udp6",
+	"tcp":      "udp",
+	"tcp4":     "udp4",
+	"tcp6":     "udp6",
+	"fdgram":   "fdgram",
 }
 
 // RegisterNetworkHTTP3 registers a mapping from non-HTTP/3 network to HTTP/3
@@ -1106,11 +1133,10 @@ func RegisterNetworkHTTP3(originalNetwork, h3Network string) {
 	networkTypesHTTP3[originalNetwork] = h3Network
 }
 
-func getHTTP3Network(originalNetwork string) string {
+func getHTTP3Network(originalNetwork string) (string, error) {
 	h3Network, ok := networkTypesHTTP3[strings.ToLower(originalNetwork)]
 	if !ok {
-		// TODO: Maybe a better default is to not enable HTTP/3 if we do not know the network?
-		return "udp"
+		return "", fmt.Errorf("network '%s' cannot handle HTTP/3 connections", originalNetwork)
 	}
-	return h3Network
+	return h3Network, nil
 }
diff --git a/modules/caddyhttp/staticresp.go b/modules/caddyhttp/staticresp.go
index 1fea6978f..1b93ede4b 100644
--- a/modules/caddyhttp/staticresp.go
+++ b/modules/caddyhttp/staticresp.go
@@ -387,7 +387,7 @@ func cmdRespond(fl caddycmd.Flags) (int, error) {
 		return caddy.ExitCodeFailedStartup, err
 	}
 
-	if !listenAddr.IsUnixNetwork() {
+	if !listenAddr.IsUnixNetwork() && !listenAddr.IsFdNetwork() {
 		listenAddrs := make([]string, 0, listenAddr.PortRangeSize())
 		for offset := uint(0); offset < listenAddr.PortRangeSize(); offset++ {
 			listenAddrs = append(listenAddrs, listenAddr.JoinHostPort(offset))
diff --git a/replacer.go b/replacer.go
index 65815c92a..297dd935c 100644
--- a/replacer.go
+++ b/replacer.go
@@ -299,11 +299,11 @@ func ToString(val any) string {
 	case int64:
 		return strconv.Itoa(int(v))
 	case uint:
-		return strconv.Itoa(int(v))
+		return strconv.FormatUint(uint64(v), 10)
 	case uint32:
-		return strconv.Itoa(int(v))
+		return strconv.FormatUint(uint64(v), 10)
 	case uint64:
-		return strconv.Itoa(int(v))
+		return strconv.FormatUint(v, 10)
 	case float32:
 		return strconv.FormatFloat(float64(v), 'f', -1, 32)
 	case float64:

From 0e829bc4187e265d1e4ccb1bec24996088acf0a5 Mon Sep 17 00:00:00 2001
From: Aaron Paterson <9441877+MayCXC@users.noreply.github.com>
Date: Tue, 1 Oct 2024 01:47:21 -0400
Subject: [PATCH 56/62] caddyhttp: Fix listener wrapper regression from #6573
 (#6599)

---
 listeners.go             |  8 ++++----
 modules/caddyhttp/app.go | 10 +++++-----
 2 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/listeners.go b/listeners.go
index cf7b52018..3a2a5180f 100644
--- a/listeners.go
+++ b/listeners.go
@@ -183,14 +183,14 @@ func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net
 		}
 	}
 
-	if ln == nil {
-		return nil, fmt.Errorf("unsupported network type: %s", na.Network)
-	}
-
 	if err != nil {
 		return nil, err
 	}
 
+	if ln == nil {
+		return nil, fmt.Errorf("unsupported network type: %s", na.Network)
+	}
+
 	if IsUnixNetwork(na.Network) {
 		isAbstractUnixSocket := strings.HasPrefix(address, "@")
 		if !isAbstractUnixSocket {
diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go
index 673ebcb8e..7a5c10623 100644
--- a/modules/caddyhttp/app.go
+++ b/modules/caddyhttp/app.go
@@ -535,11 +535,6 @@ func (app *App) Start() error {
 						return fmt.Errorf("network '%s' cannot handle HTTP/1 or HTTP/2 connections", listenAddr.Network)
 					}
 
-					if useTLS {
-						// create TLS listener - this enables and terminates TLS
-						ln = tls.NewListener(ln, tlsCfg)
-					}
-
 					// wrap listener before TLS (up to the TLS placeholder wrapper)
 					var lnWrapperIdx int
 					for i, lnWrapper := range srv.listenerWrappers {
@@ -550,6 +545,11 @@ func (app *App) Start() error {
 						ln = lnWrapper.WrapListener(ln)
 					}
 
+					if useTLS {
+						// create TLS listener - this enables and terminates TLS
+						ln = tls.NewListener(ln, tlsCfg)
+					}
+
 					// finish wrapping listener where we left off before TLS
 					for i := lnWrapperIdx; i < len(srv.listenerWrappers); i++ {
 						ln = srv.listenerWrappers[i].WrapListener(ln)

From 571f88d86f244d1d0211379c5aaad1ddada87fb1 Mon Sep 17 00:00:00 2001
From: Francis Lavoie <lavofr@gmail.com>
Date: Tue, 1 Oct 2024 12:56:30 -0400
Subject: [PATCH 57/62] chore: Adjust incorrect `reverse_proxy` Caddyfile
 comment (#6598)

---
 modules/caddyhttp/reverseproxy/caddyfile.go | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go
index 12e2b9b97..ab1dcdd02 100644
--- a/modules/caddyhttp/reverseproxy/caddyfile.go
+++ b/modules/caddyhttp/reverseproxy/caddyfile.go
@@ -93,12 +93,11 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
 //
 //	    # streaming
 //	    flush_interval     <duration>
-//	    buffer_requests
-//	    buffer_responses
-//	    max_buffer_size    <size>
+//	    request_buffers    <size>
+//	    response_buffers   <size>
 //	    stream_timeout     <duration>
 //	    stream_close_delay <duration>
-//	    trace_logs
+//	    verbose_logs
 //
 //	    # request manipulation
 //	    trusted_proxies [private_ranges] <ranges...>

From f3aead0e4df57716d525f3a25fb11f70bc72b20a Mon Sep 17 00:00:00 2001
From: WeidiDeng <weidi_deng@icloud.com>
Date: Wed, 2 Oct 2024 01:19:03 +0800
Subject: [PATCH 58/62] http: ReponseWriter prefer ReadFrom if available
 (#6565)

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
---
 modules/caddyhttp/responsewriter.go | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/modules/caddyhttp/responsewriter.go b/modules/caddyhttp/responsewriter.go
index 808d2de3a..3c0f89d0b 100644
--- a/modules/caddyhttp/responsewriter.go
+++ b/modules/caddyhttp/responsewriter.go
@@ -42,9 +42,13 @@ func (rww *ResponseWriterWrapper) Push(target string, opts *http.PushOptions) er
 	return ErrNotImplemented
 }
 
-// ReadFrom implements io.ReaderFrom. It simply calls io.Copy,
-// which uses io.ReaderFrom if available.
+// ReadFrom implements io.ReaderFrom. It retries to use io.ReaderFrom if available,
+// then fallback to io.Copy.
+// see: https://github.com/caddyserver/caddy/issues/6546
 func (rww *ResponseWriterWrapper) ReadFrom(r io.Reader) (n int64, err error) {
+	if rf, ok := rww.ResponseWriter.(io.ReaderFrom); ok {
+		return rf.ReadFrom(r)
+	}
 	return io.Copy(rww.ResponseWriter, r)
 }
 

From 9b4acc2449b10ff7c32eff4ddf1838cd2d93c9cf Mon Sep 17 00:00:00 2001
From: Matt Holt <mholt@users.noreply.github.com>
Date: Tue, 1 Oct 2024 17:18:17 -0600
Subject: [PATCH 59/62] caddytls: Support new tls.context module (#6369)

* caddytls: Support new tls.context module

This allows modules to manipulate the context passed into CertMagic's GetCertificate function, which can be useful for tracing/metrics, or other
custom logic.

This is experimental and may resolve the request of a sponsor, so we'll see how it goes!

* Derpy derp
---
 modules/caddytls/connpolicy.go | 38 ++++++++++++++++++++++++++++++++--
 1 file changed, 36 insertions(+), 2 deletions(-)

diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go
index 2e2d4f748..f415fffa0 100644
--- a/modules/caddytls/connpolicy.go
+++ b/modules/caddytls/connpolicy.go
@@ -15,6 +15,7 @@
 package caddytls
 
 import (
+	"context"
 	"crypto/tls"
 	"crypto/x509"
 	"encoding/base64"
@@ -78,6 +79,14 @@ func (cp ConnectionPolicies) Provision(ctx caddy.Context) error {
 				cp[i].ClientAuthentication.verifiers = append(cp[i].ClientAuthentication.verifiers, validator.(ClientCertificateVerifier))
 			}
 		}
+
+		if len(pol.HandshakeContextRaw) > 0 {
+			modIface, err := ctx.LoadModule(pol, "HandshakeContextRaw")
+			if err != nil {
+				return fmt.Errorf("loading handshake context module: %v", err)
+			}
+			cp[i].handshakeContext = modIface.(HandshakeContext)
+		}
 	}
 
 	return nil
@@ -137,6 +146,7 @@ type ConnectionPolicy struct {
 	// How to match this policy with a TLS ClientHello. If
 	// this policy is the first to match, it will be used.
 	MatchersRaw caddy.ModuleMap `json:"match,omitempty" caddy:"namespace=tls.handshake_match"`
+	matchers    []ConnectionMatcher
 
 	// How to choose a certificate if more than one matched
 	// the given ServerName (SNI) value.
@@ -192,6 +202,12 @@ type ConnectionPolicy struct {
 	// This feature is EXPERIMENTAL and subject to change or removal.
 	InsecureSecretsLog string `json:"insecure_secrets_log,omitempty"`
 
+	// A module that can manipulate the context passed into CertMagic's
+	// certificate management functions during TLS handshakes.
+	// EXPERIMENTAL - subject to change or removal.
+	HandshakeContextRaw json.RawMessage `json:"handshake_context,omitempty" caddy:"namespace=tls.context inline_key=module"`
+	handshakeContext    HandshakeContext
+
 	// TLSConfig is the fully-formed, standard lib TLS config
 	// used to serve TLS connections. Provision all
 	// ConnectionPolicies to populate this. It is exported only
@@ -199,8 +215,15 @@ type ConnectionPolicy struct {
 	// if necessary (like to adjust NextProtos to disable HTTP/2),
 	// and may be unexported in the future.
 	TLSConfig *tls.Config `json:"-"`
+}
 
-	matchers []ConnectionMatcher
+type HandshakeContext interface {
+	// HandshakeContext returns a context to pass into CertMagic's
+	// GetCertificate function used to serve, load, and manage certs
+	// during TLS handshakes. Generally you'll start with the context
+	// from the ClientHelloInfo, but you may use other information
+	// from it as well. Return an error to abort the handshake.
+	HandshakeContext(*tls.ClientHelloInfo) (context.Context, error)
 }
 
 func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
@@ -240,7 +263,18 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
 			}
 			cfg.DefaultServerName = p.DefaultSNI
 			cfg.FallbackServerName = p.FallbackSNI
-			return cfg.GetCertificate(hello)
+
+			// TODO: experimental: if a handshake context module is configured, allow it
+			// to modify the context before passing it into CertMagic's GetCertificate
+			ctx := hello.Context()
+			if p.handshakeContext != nil {
+				ctx, err = p.handshakeContext.HandshakeContext(hello)
+				if err != nil {
+					return nil, fmt.Errorf("handshake context: %v", err)
+				}
+			}
+
+			return cfg.GetCertificateWithContext(ctx, hello)
 		},
 		MinVersion: tls.VersionTLS12,
 		MaxVersion: tls.VersionTLS13,

From c8adb1b553412253d5f166065635ab809d3eef33 Mon Sep 17 00:00:00 2001
From: Matt Holt <mholt@users.noreply.github.com>
Date: Tue, 1 Oct 2024 20:31:30 -0600
Subject: [PATCH 60/62] cmd: Better error handling when reloading (#6601)

* caddyhttp: Limit auto-HTTPS error logs to 100 domains

* Improve error message and increase error size limit
---
 cmd/commandfuncs.go            | 2 +-
 modules/caddyhttp/autohttps.go | 2 +-
 modules/caddytls/tls.go        | 4 ++++
 3 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go
index 50e9b110d..a5c357cd7 100644
--- a/cmd/commandfuncs.go
+++ b/cmd/commandfuncs.go
@@ -716,7 +716,7 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
 
 	// if it didn't work, let the user know
 	if resp.StatusCode >= 400 {
-		respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1024*10))
+		respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024*2))
 		if err != nil {
 			return nil, fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
 		}
diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go
index ea5a07b3c..79fdfd6ec 100644
--- a/modules/caddyhttp/autohttps.go
+++ b/modules/caddyhttp/autohttps.go
@@ -732,7 +732,7 @@ func (app *App) automaticHTTPSPhase2() error {
 	)
 	err := app.tlsApp.Manage(app.allCertDomains)
 	if err != nil {
-		return fmt.Errorf("managing certificates for %v: %s", app.allCertDomains, err)
+		return fmt.Errorf("managing certificates for %d domains: %s", len(app.allCertDomains), err)
 	}
 	app.allCertDomains = nil // no longer needed; allow GC to deallocate
 	return nil
diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go
index 5f3d0eaeb..f04beb2ee 100644
--- a/modules/caddytls/tls.go
+++ b/modules/caddytls/tls.go
@@ -447,6 +447,10 @@ func (t *TLS) Manage(names []string) error {
 	for ap, names := range policyToNames {
 		err := ap.magic.ManageAsync(t.ctx.Context, names)
 		if err != nil {
+			const maxNamesToDisplay = 100
+			if len(names) > maxNamesToDisplay {
+				names = append(names[:maxNamesToDisplay], fmt.Sprintf("(%d more...)", len(names)-maxNamesToDisplay))
+			}
 			return fmt.Errorf("automate: manage %v: %v", names, err)
 		}
 		for _, name := range names {

From 792f1c7ed759b97ee6dc80246cf2de054c09a12f Mon Sep 17 00:00:00 2001
From: Francis Lavoie <lavofr@gmail.com>
Date: Wed, 2 Oct 2024 08:34:04 -0400
Subject: [PATCH 61/62] caddyhttp: Escaping placeholders in CEL, add `vars` and
 `vars_regexp` (#6594)

* caddyhttp: Escaping placeholders in CEL

* Simplify some of the test cases

* Implement vars and vars_regexp in CEL

* dupl lint is dumb

* Better consts for the placeholder CEL shortcut

* Bump CEL version, register a few extensions

* Refactor s390x test script for readability

* Add retries for s390x to smooth over flakiness

* Switch to `ph` for the CEL shortcut (match it in templates cause why not)
---
 .github/workflows/ci.yml                  |  28 ++++-
 .golangci.yml                             |   6 +
 go.mod                                    |   2 +-
 go.sum                                    |   4 +-
 modules/caddyhttp/celmatcher.go           |  51 ++++++---
 modules/caddyhttp/celmatcher_test.go      | 133 +++++++++++++++++-----
 modules/caddyhttp/fileserver/matcher.go   |  10 +-
 modules/caddyhttp/matchers.go             |   4 +-
 modules/caddyhttp/templates/templates.go  |   6 +
 modules/caddyhttp/templates/tplcontext.go |   1 +
 modules/caddyhttp/vars.go                 |  89 +++++++++++++++
 11 files changed, 276 insertions(+), 58 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2c7a67c5b..3a74d8cc6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -156,13 +156,35 @@ jobs:
           # short sha is enough?
           short_sha=$(git rev-parse --short HEAD)
 
+          # To shorten the following lines
+          ssh_opts="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
+          ssh_host="$CI_USER@ci-s390x.caddyserver.com"
+
           # The environment is fresh, so there's no point in keeping accepting and adding the key.
-          rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . "$CI_USER"@ci-s390x.caddyserver.com:/var/tmp/"$short_sha"
-          ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t "$CI_USER"@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -p 1 -tags nobadger -v ./..."
+          rsync -arz -e "ssh $ssh_opts" --progress --delete --exclude '.git' . "$ssh_host":/var/tmp/"$short_sha"
+          ssh $ssh_opts -t "$ssh_host" bash <<EOF
+          cd /var/tmp/$short_sha
+          go version
+          go env
+          printf "\n\n"
+          retries=3
+          exit_code=0
+          while ((retries > 0)); do
+            CGO_ENABLED=0 go test -p 1 -tags nobadger -v ./...
+            exit_code=$?
+            if ((exit_code == 0)); then
+              break
+            fi
+            echo "\n\nTest failed: \$exit_code, retrying..."
+            ((retries--))
+          done
+          echo "Remote exit code: \$exit_code"
+          exit \$exit_code
+          EOF
           test_result=$?
 
           # There's no need leaving the files around
-          ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$CI_USER"@ci-s390x.caddyserver.com "rm -rf /var/tmp/'$short_sha'"
+          ssh $ssh_opts "$ssh_host" "rm -rf /var/tmp/'$short_sha'"
 
           echo "Test exit code: $test_result"
           exit $test_result
diff --git a/.golangci.yml b/.golangci.yml
index e8b2a55d3..aecff563e 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -171,6 +171,12 @@ issues:
     - path: modules/logging/filters.go
       linters:
         - dupl
+    - path: modules/caddyhttp/matchers.go
+      linters:
+        - dupl
+    - path: modules/caddyhttp/vars.go
+      linters:
+        - dupl
     - path: _test\.go
       linters:
         - errcheck
diff --git a/go.mod b/go.mod
index 0c4f1edd7..62fe30dee 100644
--- a/go.mod
+++ b/go.mod
@@ -13,7 +13,7 @@ require (
 	github.com/caddyserver/zerossl v0.1.3
 	github.com/dustin/go-humanize v1.0.1
 	github.com/go-chi/chi/v5 v5.0.12
-	github.com/google/cel-go v0.20.1
+	github.com/google/cel-go v0.21.0
 	github.com/google/uuid v1.6.0
 	github.com/klauspost/compress v1.17.8
 	github.com/klauspost/cpuid/v2 v2.2.7
diff --git a/go.sum b/go.sum
index 6bf0bd3e2..1f741c104 100644
--- a/go.sum
+++ b/go.sum
@@ -198,8 +198,8 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
 github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
-github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84=
-github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
+github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI=
+github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc=
 github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
 github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
 github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
diff --git a/modules/caddyhttp/celmatcher.go b/modules/caddyhttp/celmatcher.go
index a5565eb98..2a03ebba7 100644
--- a/modules/caddyhttp/celmatcher.go
+++ b/modules/caddyhttp/celmatcher.go
@@ -126,6 +126,10 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
 	// light (and possibly naïve) syntactic sugar
 	m.expandedExpr = placeholderRegexp.ReplaceAllString(m.Expr, placeholderExpansion)
 
+	// as a second pass, we'll strip the escape character from an escaped
+	// placeholder, so that it can be used as an input to other CEL functions
+	m.expandedExpr = escapedPlaceholderRegexp.ReplaceAllString(m.expandedExpr, escapedPlaceholderExpansion)
+
 	// our type adapter expands CEL's standard type support
 	m.ta = celTypeAdapter{}
 
@@ -159,14 +163,17 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
 
 	// create the CEL environment
 	env, err := cel.NewEnv(
-		cel.Function(placeholderFuncName, cel.SingletonBinaryBinding(m.caddyPlaceholderFunc), cel.Overload(
-			placeholderFuncName+"_httpRequest_string",
+		cel.Function(CELPlaceholderFuncName, cel.SingletonBinaryBinding(m.caddyPlaceholderFunc), cel.Overload(
+			CELPlaceholderFuncName+"_httpRequest_string",
 			[]*cel.Type{httpRequestObjectType, cel.StringType},
 			cel.AnyType,
 		)),
-		cel.Variable("request", httpRequestObjectType),
+		cel.Variable(CELRequestVarName, httpRequestObjectType),
 		cel.CustomTypeAdapter(m.ta),
 		ext.Strings(),
+		ext.Bindings(),
+		ext.Lists(),
+		ext.Math(),
 		matcherLib,
 	)
 	if err != nil {
@@ -247,7 +254,7 @@ func (m MatchExpression) caddyPlaceholderFunc(lhs, rhs ref.Val) ref.Val {
 		return types.NewErr(
 			"invalid request of type '%v' to %s(request, placeholderVarName)",
 			lhs.Type(),
-			placeholderFuncName,
+			CELPlaceholderFuncName,
 		)
 	}
 	phStr, ok := rhs.(types.String)
@@ -255,7 +262,7 @@ func (m MatchExpression) caddyPlaceholderFunc(lhs, rhs ref.Val) ref.Val {
 		return types.NewErr(
 			"invalid placeholder variable name of type '%v' to %s(request, placeholderVarName)",
 			rhs.Type(),
-			placeholderFuncName,
+			CELPlaceholderFuncName,
 		)
 	}
 
@@ -275,7 +282,7 @@ var httpRequestCELType = cel.ObjectType("http.Request", traits.ReceiverType)
 type celHTTPRequest struct{ *http.Request }
 
 func (cr celHTTPRequest) ResolveName(name string) (any, bool) {
-	if name == "request" {
+	if name == CELRequestVarName {
 		return cr, true
 	}
 	return nil, false
@@ -457,15 +464,15 @@ func CELMatcherDecorator(funcName string, fac CELMatcherFactory) interpreter.Int
 		callArgs := call.Args()
 		reqAttr, ok := callArgs[0].(interpreter.InterpretableAttribute)
 		if !ok {
-			return nil, errors.New("missing 'request' argument")
+			return nil, errors.New("missing 'req' argument")
 		}
 		nsAttr, ok := reqAttr.Attr().(interpreter.NamespacedAttribute)
 		if !ok {
-			return nil, errors.New("missing 'request' argument")
+			return nil, errors.New("missing 'req' argument")
 		}
 		varNames := nsAttr.CandidateVariableNames()
-		if len(varNames) != 1 || len(varNames) == 1 && varNames[0] != "request" {
-			return nil, errors.New("missing 'request' argument")
+		if len(varNames) != 1 || len(varNames) == 1 && varNames[0] != CELRequestVarName {
+			return nil, errors.New("missing 'req' argument")
 		}
 		matcherData, ok := callArgs[1].(interpreter.InterpretableConst)
 		if !ok {
@@ -524,7 +531,7 @@ func celMatcherStringListMacroExpander(funcName string) cel.MacroFactory {
 				return nil, eh.NewError(arg.ID(), "matcher arguments must be string constants")
 			}
 		}
-		return eh.NewCall(funcName, eh.NewIdent("request"), eh.NewList(matchArgs...)), nil
+		return eh.NewCall(funcName, eh.NewIdent(CELRequestVarName), eh.NewList(matchArgs...)), nil
 	}
 }
 
@@ -538,7 +545,7 @@ func celMatcherStringMacroExpander(funcName string) parser.MacroExpander {
 			return nil, eh.NewError(0, "matcher requires one argument")
 		}
 		if isCELStringExpr(args[0]) {
-			return eh.NewCall(funcName, eh.NewIdent("request"), args[0]), nil
+			return eh.NewCall(funcName, eh.NewIdent(CELRequestVarName), args[0]), nil
 		}
 		return nil, eh.NewError(args[0].ID(), "matcher argument must be a string literal")
 	}
@@ -572,7 +579,7 @@ func celMatcherJSONMacroExpander(funcName string) parser.MacroExpander {
 					return nil, eh.NewError(entry.AsMapEntry().Value().ID(), "matcher map values must be string or list literals")
 				}
 			}
-			return eh.NewCall(funcName, eh.NewIdent("request"), arg), nil
+			return eh.NewCall(funcName, eh.NewIdent(CELRequestVarName), arg), nil
 		case ast.UnspecifiedExprKind, ast.CallKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.SelectKind:
 			// appeasing the linter :)
 		}
@@ -646,7 +653,7 @@ func isCELCaddyPlaceholderCall(e ast.Expr) bool {
 	switch e.Kind() {
 	case ast.CallKind:
 		call := e.AsCall()
-		if call.FunctionName() == "caddyPlaceholder" {
+		if call.FunctionName() == CELPlaceholderFuncName {
 			return true
 		}
 	case ast.UnspecifiedExprKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.MapKind, ast.SelectKind, ast.StructKind:
@@ -701,8 +708,15 @@ func isCELStringListLiteral(e ast.Expr) bool {
 // expressions with a proper CEL function call; this is
 // just for syntactic sugar.
 var (
-	placeholderRegexp    = regexp.MustCompile(`{([a-zA-Z][\w.-]+)}`)
-	placeholderExpansion = `caddyPlaceholder(request, "${1}")`
+	// The placeholder may not be preceded by a backslash; the expansion
+	// will include the preceding character if it is not a backslash.
+	placeholderRegexp    = regexp.MustCompile(`([^\\]|^){([a-zA-Z][\w.-]+)}`)
+	placeholderExpansion = `${1}ph(req, "${2}")`
+
+	// As a second pass, we need to strip the escape character in front of
+	// the placeholder, if it exists.
+	escapedPlaceholderRegexp    = regexp.MustCompile(`\\{([a-zA-Z][\w.-]+)}`)
+	escapedPlaceholderExpansion = `{${1}}`
 
 	CELTypeJSON = cel.MapType(cel.StringType, cel.DynType)
 )
@@ -710,7 +724,10 @@ var (
 var httpRequestObjectType = cel.ObjectType("http.Request")
 
 // The name of the CEL function which accesses Replacer values.
-const placeholderFuncName = "caddyPlaceholder"
+const CELPlaceholderFuncName = "ph"
+
+// The name of the CEL request variable.
+const CELRequestVarName = "req"
 
 const MatcherNameCtxKey = "matcher_name"
 
diff --git a/modules/caddyhttp/celmatcher_test.go b/modules/caddyhttp/celmatcher_test.go
index 25fdcf45e..26491b7ca 100644
--- a/modules/caddyhttp/celmatcher_test.go
+++ b/modules/caddyhttp/celmatcher_test.go
@@ -70,13 +70,36 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
 			wantResult: true,
 		},
 		{
-			name: "header error (MatchHeader)",
+			name: "header matches an escaped placeholder value (MatchHeader)",
+			expression: &MatchExpression{
+				Expr: `header({'Field': '\\\{foobar}'})`,
+			},
+			urlTarget:  "https://example.com/foo",
+			httpHeader: &http.Header{"Field": []string{"{foobar}"}},
+			wantResult: true,
+		},
+		{
+			name: "header matches an placeholder replaced during the header matcher (MatchHeader)",
+			expression: &MatchExpression{
+				Expr: `header({'Field': '\{http.request.uri.path}'})`,
+			},
+			urlTarget:  "https://example.com/foo",
+			httpHeader: &http.Header{"Field": []string{"/foo"}},
+			wantResult: true,
+		},
+		{
+			name: "header error, invalid escape sequence (MatchHeader)",
+			expression: &MatchExpression{
+				Expr: `header({'Field': '\\{foobar}'})`,
+			},
+			wantErr: true,
+		},
+		{
+			name: "header error, needs to be JSON syntax with field as key (MatchHeader)",
 			expression: &MatchExpression{
 				Expr: `header('foo')`,
 			},
-			urlTarget:  "https://example.com/foo",
-			httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
-			wantErr:    true,
+			wantErr: true,
 		},
 		{
 			name: "header_regexp matches (MatchHeaderRE)",
@@ -110,9 +133,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
 			expression: &MatchExpression{
 				Expr: `header_regexp('foo')`,
 			},
-			urlTarget:  "https://example.com/foo",
-			httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
-			wantErr:    true,
+			wantErr: true,
 		},
 		{
 			name: "host matches localhost (MatchHost)",
@@ -143,8 +164,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
 			expression: &MatchExpression{
 				Expr: `host(80)`,
 			},
-			urlTarget: "http://localhost:80",
-			wantErr:   true,
+			wantErr: true,
 		},
 		{
 			name: "method does not match (MatchMethod)",
@@ -169,9 +189,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
 			expression: &MatchExpression{
 				Expr: `method()`,
 			},
-			urlTarget:  "https://foo.example.com",
-			httpMethod: "PUT",
-			wantErr:    true,
+			wantErr: true,
 		},
 		{
 			name: "path matches substring (MatchPath)",
@@ -266,24 +284,21 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
 			expression: &MatchExpression{
 				Expr: `protocol()`,
 			},
-			urlTarget: "https://example.com",
-			wantErr:   true,
+			wantErr: true,
 		},
 		{
 			name: "protocol invocation error too many args (MatchProtocol)",
 			expression: &MatchExpression{
 				Expr: `protocol('grpc', 'https')`,
 			},
-			urlTarget: "https://example.com",
-			wantErr:   true,
+			wantErr: true,
 		},
 		{
 			name: "protocol invocation error wrong arg type (MatchProtocol)",
 			expression: &MatchExpression{
 				Expr: `protocol(true)`,
 			},
-			urlTarget: "https://example.com",
-			wantErr:   true,
+			wantErr: true,
 		},
 		{
 			name: "query does not match against a specific value (MatchQuery)",
@@ -330,40 +345,35 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
 			expression: &MatchExpression{
 				Expr: `query({1: "1"})`,
 			},
-			urlTarget: "https://example.com/foo",
-			wantErr:   true,
+			wantErr: true,
 		},
 		{
 			name: "query error typed struct instead of map (MatchQuery)",
 			expression: &MatchExpression{
 				Expr: `query(Message{field: "1"})`,
 			},
-			urlTarget: "https://example.com/foo",
-			wantErr:   true,
+			wantErr: true,
 		},
 		{
 			name: "query error bad map value type (MatchQuery)",
 			expression: &MatchExpression{
 				Expr: `query({"debug": 1})`,
 			},
-			urlTarget: "https://example.com/foo/?debug=1",
-			wantErr:   true,
+			wantErr: true,
 		},
 		{
 			name: "query error no args (MatchQuery)",
 			expression: &MatchExpression{
 				Expr: `query()`,
 			},
-			urlTarget: "https://example.com/foo/?debug=1",
-			wantErr:   true,
+			wantErr: true,
 		},
 		{
 			name: "remote_ip error no args (MatchRemoteIP)",
 			expression: &MatchExpression{
 				Expr: `remote_ip()`,
 			},
-			urlTarget: "https://example.com/foo",
-			wantErr:   true,
+			wantErr: true,
 		},
 		{
 			name: "remote_ip single IP match (MatchRemoteIP)",
@@ -373,6 +383,67 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
 			urlTarget:  "https://example.com/foo",
 			wantResult: true,
 		},
+		{
+			name: "vars value (VarsMatcher)",
+			expression: &MatchExpression{
+				Expr: `vars({'foo': 'bar'})`,
+			},
+			urlTarget:  "https://example.com/foo",
+			wantResult: true,
+		},
+		{
+			name: "vars matches placeholder, needs escape (VarsMatcher)",
+			expression: &MatchExpression{
+				Expr: `vars({'\{http.request.uri.path}': '/foo'})`,
+			},
+			urlTarget:  "https://example.com/foo",
+			wantResult: true,
+		},
+		{
+			name: "vars error wrong syntax (VarsMatcher)",
+			expression: &MatchExpression{
+				Expr: `vars('foo', 'bar')`,
+			},
+			wantErr: true,
+		},
+		{
+			name: "vars error no args (VarsMatcher)",
+			expression: &MatchExpression{
+				Expr: `vars()`,
+			},
+			wantErr: true,
+		},
+		{
+			name: "vars_regexp value (MatchVarsRE)",
+			expression: &MatchExpression{
+				Expr: `vars_regexp('foo', 'ba?r')`,
+			},
+			urlTarget:  "https://example.com/foo",
+			wantResult: true,
+		},
+		{
+			name: "vars_regexp value with name (MatchVarsRE)",
+			expression: &MatchExpression{
+				Expr: `vars_regexp('name', 'foo', 'ba?r')`,
+			},
+			urlTarget:  "https://example.com/foo",
+			wantResult: true,
+		},
+		{
+			name: "vars_regexp matches placeholder, needs escape (MatchVarsRE)",
+			expression: &MatchExpression{
+				Expr: `vars_regexp('\{http.request.uri.path}', '/fo?o')`,
+			},
+			urlTarget:  "https://example.com/foo",
+			wantResult: true,
+		},
+		{
+			name: "vars_regexp error no args (MatchVarsRE)",
+			expression: &MatchExpression{
+				Expr: `vars_regexp()`,
+			},
+			wantErr: true,
+		},
 	}
 )
 
@@ -396,6 +467,9 @@ func TestMatchExpressionMatch(t *testing.T) {
 			}
 			repl := caddy.NewReplacer()
 			ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
+			ctx = context.WithValue(ctx, VarsCtxKey, map[string]any{
+				"foo": "bar",
+			})
 			req = req.WithContext(ctx)
 			addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
 
@@ -436,6 +510,9 @@ func BenchmarkMatchExpressionMatch(b *testing.B) {
 			}
 			repl := caddy.NewReplacer()
 			ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
+			ctx = context.WithValue(ctx, VarsCtxKey, map[string]any{
+				"foo": "bar",
+			})
 			req = req.WithContext(ctx)
 			addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
 			if tc.clientCertificate != nil {
diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go
index 6ab2180ad..71de1db29 100644
--- a/modules/caddyhttp/fileserver/matcher.go
+++ b/modules/caddyhttp/fileserver/matcher.go
@@ -225,7 +225,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
 	return func(eh parser.ExprHelper, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) {
 		if len(args) == 0 {
 			return eh.NewCall("file",
-				eh.NewIdent("request"),
+				eh.NewIdent(caddyhttp.CELRequestVarName),
 				eh.NewMap(),
 			), nil
 		}
@@ -233,7 +233,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
 			arg := args[0]
 			if isCELStringLiteral(arg) || isCELCaddyPlaceholderCall(arg) {
 				return eh.NewCall("file",
-					eh.NewIdent("request"),
+					eh.NewIdent(caddyhttp.CELRequestVarName),
 					eh.NewMap(eh.NewMapEntry(
 						eh.NewLiteral(types.String("try_files")),
 						eh.NewList(arg),
@@ -242,7 +242,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
 				), nil
 			}
 			if isCELTryFilesLiteral(arg) {
-				return eh.NewCall("file", eh.NewIdent("request"), arg), nil
+				return eh.NewCall("file", eh.NewIdent(caddyhttp.CELRequestVarName), arg), nil
 			}
 			return nil, &common.Error{
 				Location: eh.OffsetLocation(arg.ID()),
@@ -259,7 +259,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
 			}
 		}
 		return eh.NewCall("file",
-			eh.NewIdent("request"),
+			eh.NewIdent(caddyhttp.CELRequestVarName),
 			eh.NewMap(eh.NewMapEntry(
 				eh.NewLiteral(types.String("try_files")),
 				eh.NewList(args...),
@@ -634,7 +634,7 @@ func isCELCaddyPlaceholderCall(e ast.Expr) bool {
 	switch e.Kind() {
 	case ast.CallKind:
 		call := e.AsCall()
-		if call.FunctionName() == "caddyPlaceholder" {
+		if call.FunctionName() == caddyhttp.CELPlaceholderFuncName {
 			return true
 		}
 	case ast.UnspecifiedExprKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.MapKind, ast.SelectKind, ast.StructKind:
diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go
index a0bc6d63a..93a36237b 100644
--- a/modules/caddyhttp/matchers.go
+++ b/modules/caddyhttp/matchers.go
@@ -1562,8 +1562,8 @@ var (
 	_ CELLibraryProducer = (*MatchHeader)(nil)
 	_ CELLibraryProducer = (*MatchHeaderRE)(nil)
 	_ CELLibraryProducer = (*MatchProtocol)(nil)
-	// _ CELLibraryProducer = (*VarsMatcher)(nil)
-	// _ CELLibraryProducer = (*MatchVarsRE)(nil)
+	_ CELLibraryProducer = (*VarsMatcher)(nil)
+	_ CELLibraryProducer = (*MatchVarsRE)(nil)
 
 	_ json.Marshaler   = (*MatchNot)(nil)
 	_ json.Unmarshaler = (*MatchNot)(nil)
diff --git a/modules/caddyhttp/templates/templates.go b/modules/caddyhttp/templates/templates.go
index 0eba4870f..eb6488659 100644
--- a/modules/caddyhttp/templates/templates.go
+++ b/modules/caddyhttp/templates/templates.go
@@ -81,6 +81,12 @@ func init() {
 // {{placeholder "http.error.status_code"}}
 // ```
 //
+// As a shortcut, `ph` is an alias for `placeholder`.
+//
+// ```
+// {{ph "http.request.method"}}
+// ```
+//
 // ##### `.Host`
 //
 // Returns the hostname portion (no port) of the Host header of the HTTP request.
diff --git a/modules/caddyhttp/templates/tplcontext.go b/modules/caddyhttp/templates/tplcontext.go
index 2324586be..1b1020f1b 100644
--- a/modules/caddyhttp/templates/tplcontext.go
+++ b/modules/caddyhttp/templates/tplcontext.go
@@ -88,6 +88,7 @@ func (c *TemplateContext) NewTemplate(tplName string) *template.Template {
 		"fileStat":         c.funcFileStat,
 		"env":              c.funcEnv,
 		"placeholder":      c.funcPlaceholder,
+		"ph":               c.funcPlaceholder, // shortcut
 		"fileExists":       c.funcFileExists,
 		"httpError":        c.funcHTTPError,
 		"humanize":         c.funcHumanize,
diff --git a/modules/caddyhttp/vars.go b/modules/caddyhttp/vars.go
index 9e86dd716..77e06e3cb 100644
--- a/modules/caddyhttp/vars.go
+++ b/modules/caddyhttp/vars.go
@@ -18,8 +18,12 @@ import (
 	"context"
 	"fmt"
 	"net/http"
+	"reflect"
 	"strings"
 
+	"github.com/google/cel-go/cel"
+	"github.com/google/cel-go/common/types/ref"
+
 	"github.com/caddyserver/caddy/v2"
 	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
 )
@@ -203,6 +207,28 @@ func (m VarsMatcher) Match(r *http.Request) bool {
 	return false
 }
 
+// CELLibrary produces options that expose this matcher for use in CEL
+// expression matchers.
+//
+// Example:
+//
+//	expression vars({'{magic_number}': ['3', '5']})
+//	expression vars({'{foo}': 'single_value'})
+func (VarsMatcher) CELLibrary(_ caddy.Context) (cel.Library, error) {
+	return CELMatcherImpl(
+		"vars",
+		"vars_matcher_request_map",
+		[]*cel.Type{CELTypeJSON},
+		func(data ref.Val) (RequestMatcher, error) {
+			mapStrListStr, err := CELValueToMapStrList(data)
+			if err != nil {
+				return nil, err
+			}
+			return VarsMatcher(mapStrListStr), nil
+		},
+	)
+}
+
 // MatchVarsRE matches the value of the context variables by a given regular expression.
 //
 // Upon a match, it adds placeholders to the request: `{http.regexp.name.capture_group}`
@@ -302,6 +328,69 @@ func (m MatchVarsRE) Match(r *http.Request) bool {
 	return false
 }
 
+// CELLibrary produces options that expose this matcher for use in CEL
+// expression matchers.
+//
+// Example:
+//
+//	expression vars_regexp('foo', '{magic_number}', '[0-9]+')
+//	expression vars_regexp('{magic_number}', '[0-9]+')
+func (MatchVarsRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
+	unnamedPattern, err := CELMatcherImpl(
+		"vars_regexp",
+		"vars_regexp_request_string_string",
+		[]*cel.Type{cel.StringType, cel.StringType},
+		func(data ref.Val) (RequestMatcher, error) {
+			refStringList := reflect.TypeOf([]string{})
+			params, err := data.ConvertToNative(refStringList)
+			if err != nil {
+				return nil, err
+			}
+			strParams := params.([]string)
+			matcher := MatchVarsRE{}
+			matcher[strParams[0]] = &MatchRegexp{
+				Pattern: strParams[1],
+				Name:    ctx.Value(MatcherNameCtxKey).(string),
+			}
+			err = matcher.Provision(ctx)
+			return matcher, err
+		},
+	)
+	if err != nil {
+		return nil, err
+	}
+	namedPattern, err := CELMatcherImpl(
+		"vars_regexp",
+		"vars_regexp_request_string_string_string",
+		[]*cel.Type{cel.StringType, cel.StringType, cel.StringType},
+		func(data ref.Val) (RequestMatcher, error) {
+			refStringList := reflect.TypeOf([]string{})
+			params, err := data.ConvertToNative(refStringList)
+			if err != nil {
+				return nil, err
+			}
+			strParams := params.([]string)
+			name := strParams[0]
+			if name == "" {
+				name = ctx.Value(MatcherNameCtxKey).(string)
+			}
+			matcher := MatchVarsRE{}
+			matcher[strParams[1]] = &MatchRegexp{
+				Pattern: strParams[2],
+				Name:    name,
+			}
+			err = matcher.Provision(ctx)
+			return matcher, err
+		},
+	)
+	if err != nil {
+		return nil, err
+	}
+	envOpts := append(unnamedPattern.CompileOptions(), namedPattern.CompileOptions()...)
+	prgOpts := append(unnamedPattern.ProgramOptions(), namedPattern.ProgramOptions()...)
+	return NewMatcherCELLibrary(envOpts, prgOpts), nil
+}
+
 // Validate validates m's regular expressions.
 func (m MatchVarsRE) Validate() error {
 	for _, rm := range m {

From 16724842d9b9096b800326d0b7667a4361552552 Mon Sep 17 00:00:00 2001
From: Francis Lavoie <lavofr@gmail.com>
Date: Wed, 2 Oct 2024 09:31:58 -0400
Subject: [PATCH 62/62] caddyhttp: Implement `auto_https prefer_wildcard`
 option (#6146)

* Allow specifying multiple `auto_https` options

* Implement `auto_https prefer_wildcard` option

* Adapt tests, add mock DNS module for config testing

* Rebase fix
---
 caddyconfig/httpcaddyfile/httptype.go         |  56 ++++++-
 caddyconfig/httpcaddyfile/options.go          |  21 ++-
 caddyconfig/httpcaddyfile/tlsapp.go           |  41 ++++-
 .../auto_https_prefer_wildcard.caddyfiletest  | 106 ++++++++++++
 .../wildcard_pattern.caddyfiletest            | 157 ++++++++++++++++++
 caddytest/integration/mockdns_test.go         |  61 +++++++
 modules/caddyhttp/autohttps.go                |  27 +++
 7 files changed, 449 insertions(+), 20 deletions(-)
 create mode 100644 caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard.caddyfiletest
 create mode 100644 caddytest/integration/caddyfile_adapt/wildcard_pattern.caddyfiletest
 create mode 100644 caddytest/integration/mockdns_test.go

diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go
index 6745969ee..4dacd9058 100644
--- a/caddyconfig/httpcaddyfile/httptype.go
+++ b/caddyconfig/httpcaddyfile/httptype.go
@@ -534,8 +534,8 @@ func (st *ServerType) serversFromPairings(
 	if hsp, ok := options["https_port"].(int); ok {
 		httpsPort = strconv.Itoa(hsp)
 	}
-	autoHTTPS := "on"
-	if ah, ok := options["auto_https"].(string); ok {
+	autoHTTPS := []string{}
+	if ah, ok := options["auto_https"].([]string); ok {
 		autoHTTPS = ah
 	}
 
@@ -594,17 +594,37 @@ func (st *ServerType) serversFromPairings(
 		}
 
 		// handle the auto_https global option
-		if autoHTTPS != "on" {
-			srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
-			switch autoHTTPS {
+		for _, val := range autoHTTPS {
+			switch val {
 			case "off":
+				if srv.AutoHTTPS == nil {
+					srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
+				}
 				srv.AutoHTTPS.Disabled = true
+
 			case "disable_redirects":
+				if srv.AutoHTTPS == nil {
+					srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
+				}
 				srv.AutoHTTPS.DisableRedir = true
+
 			case "disable_certs":
+				if srv.AutoHTTPS == nil {
+					srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
+				}
 				srv.AutoHTTPS.DisableCerts = true
+
 			case "ignore_loaded_certs":
+				if srv.AutoHTTPS == nil {
+					srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
+				}
 				srv.AutoHTTPS.IgnoreLoadedCerts = true
+
+			case "prefer_wildcard":
+				if srv.AutoHTTPS == nil {
+					srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
+				}
+				srv.AutoHTTPS.PreferWildcard = true
 			}
 		}
 
@@ -673,7 +693,7 @@ func (st *ServerType) serversFromPairings(
 		})
 
 		var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
-		autoHTTPSWillAddConnPolicy := autoHTTPS != "off"
+		autoHTTPSWillAddConnPolicy := srv.AutoHTTPS == nil || !srv.AutoHTTPS.Disabled
 
 		// if needed, the ServerLogConfig is initialized beforehand so
 		// that all server blocks can populate it with data, even when not
@@ -757,6 +777,13 @@ func (st *ServerType) serversFromPairings(
 				}
 			}
 
+			wildcardHosts := []string{}
+			for _, addr := range sblock.parsedKeys {
+				if strings.HasPrefix(addr.Host, "*.") {
+					wildcardHosts = append(wildcardHosts, addr.Host[2:])
+				}
+			}
+
 			for _, addr := range sblock.parsedKeys {
 				// if server only uses HTTP port, auto-HTTPS will not apply
 				if listenersUseAnyPortOtherThan(srv.Listen, httpPort) {
@@ -772,6 +799,18 @@ func (st *ServerType) serversFromPairings(
 					}
 				}
 
+				// If prefer wildcard is enabled, then we add hosts that are
+				// already covered by the wildcard to the skip list
+				if srv.AutoHTTPS != nil && srv.AutoHTTPS.PreferWildcard && addr.Scheme == "https" {
+					baseDomain := addr.Host
+					if idx := strings.Index(baseDomain, "."); idx != -1 {
+						baseDomain = baseDomain[idx+1:]
+					}
+					if !strings.HasPrefix(addr.Host, "*.") && slices.Contains(wildcardHosts, baseDomain) {
+						srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, addr.Host)
+					}
+				}
+
 				// If TLS is specified as directive, it will also result in 1 or more connection policy being created
 				// Thus, catch-all address with non-standard port, e.g. :8443, can have TLS enabled without
 				// specifying prefix "https://"
@@ -919,7 +958,10 @@ func (st *ServerType) serversFromPairings(
 		if addressQualifiesForTLS &&
 			!hasCatchAllTLSConnPolicy &&
 			(len(srv.TLSConnPolicies) > 0 || !autoHTTPSWillAddConnPolicy || defaultSNI != "" || fallbackSNI != "") {
-			srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{DefaultSNI: defaultSNI, FallbackSNI: fallbackSNI})
+			srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{
+				DefaultSNI:  defaultSNI,
+				FallbackSNI: fallbackSNI,
+			})
 		}
 
 		// tidy things up a bit
diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go
index c14208b61..abbe8f418 100644
--- a/caddyconfig/httpcaddyfile/options.go
+++ b/caddyconfig/httpcaddyfile/options.go
@@ -452,15 +452,22 @@ func parseOptPersistConfig(d *caddyfile.Dispenser, _ any) (any, error) {
 
 func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) {
 	d.Next() // consume option name
-	if !d.Next() {
+	val := d.RemainingArgs()
+	if len(val) == 0 {
 		return "", d.ArgErr()
 	}
-	val := d.Val()
-	if d.Next() {
-		return "", d.ArgErr()
-	}
-	if val != "off" && val != "disable_redirects" && val != "disable_certs" && val != "ignore_loaded_certs" {
-		return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', or 'ignore_loaded_certs'")
+	for _, v := range val {
+		switch v {
+		case "off":
+		case "disable_redirects":
+		case "disable_certs":
+		case "ignore_loaded_certs":
+		case "prefer_wildcard":
+			break
+
+		default:
+			return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', 'ignore_loaded_certs', or 'prefer_wildcard'")
+		}
 	}
 	return val, nil
 }
diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go
index c6ff81b2b..ed708524d 100644
--- a/caddyconfig/httpcaddyfile/tlsapp.go
+++ b/caddyconfig/httpcaddyfile/tlsapp.go
@@ -45,8 +45,8 @@ func (st ServerType) buildTLSApp(
 	if hp, ok := options["http_port"].(int); ok {
 		httpPort = strconv.Itoa(hp)
 	}
-	autoHTTPS := "on"
-	if ah, ok := options["auto_https"].(string); ok {
+	autoHTTPS := []string{}
+	if ah, ok := options["auto_https"].([]string); ok {
 		autoHTTPS = ah
 	}
 
@@ -54,13 +54,14 @@ func (st ServerType) buildTLSApp(
 	// key, so that they don't get forgotten/omitted by auto-HTTPS
 	// (since they won't appear in route matchers)
 	httpsHostsSharedWithHostlessKey := make(map[string]struct{})
-	if autoHTTPS != "off" {
+	if !slices.Contains(autoHTTPS, "off") {
 		for _, pair := range pairings {
 			for _, sb := range pair.serverBlocks {
 				for _, addr := range sb.parsedKeys {
 					if addr.Host != "" {
 						continue
 					}
+
 					// this server block has a hostless key, now
 					// go through and add all the hosts to the set
 					for _, otherAddr := range sb.parsedKeys {
@@ -350,7 +351,7 @@ func (st ServerType) buildTLSApp(
 	internalAP := &caddytls.AutomationPolicy{
 		IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
 	}
-	if autoHTTPS != "off" && autoHTTPS != "disable_certs" {
+	if !slices.Contains(autoHTTPS, "off") && !slices.Contains(autoHTTPS, "disable_certs") {
 		for h := range httpsHostsSharedWithHostlessKey {
 			al = append(al, h)
 			if !certmagic.SubjectQualifiesForPublicCert(h) {
@@ -417,7 +418,10 @@ func (st ServerType) buildTLSApp(
 		}
 
 		// consolidate automation policies that are the exact same
-		tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
+		tlsApp.Automation.Policies = consolidateAutomationPolicies(
+			tlsApp.Automation.Policies,
+			slices.Contains(autoHTTPS, "prefer_wildcard"),
+		)
 
 		// ensure automation policies don't overlap subjects (this should be
 		// an error at provision-time as well, but catch it in the adapt phase
@@ -563,7 +567,7 @@ func newBaseAutomationPolicy(
 
 // consolidateAutomationPolicies combines automation policies that are the same,
 // for a cleaner overall output.
-func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls.AutomationPolicy {
+func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy, preferWildcard bool) []*caddytls.AutomationPolicy {
 	// sort from most specific to least specific; we depend on this ordering
 	sort.SliceStable(aps, func(i, j int) bool {
 		if automationPolicyIsSubset(aps[i], aps[j]) {
@@ -648,6 +652,31 @@ outer:
 					j--
 				}
 			}
+
+			if preferWildcard {
+				// remove subjects from i if they're covered by a wildcard in j
+				iSubjs := aps[i].SubjectsRaw
+				for iSubj := 0; iSubj < len(iSubjs); iSubj++ {
+					for jSubj := range aps[j].SubjectsRaw {
+						if !strings.HasPrefix(aps[j].SubjectsRaw[jSubj], "*.") {
+							continue
+						}
+						if certmagic.MatchWildcard(aps[i].SubjectsRaw[iSubj], aps[j].SubjectsRaw[jSubj]) {
+							iSubjs = slices.Delete(iSubjs, iSubj, iSubj+1)
+							iSubj--
+							break
+						}
+					}
+				}
+				aps[i].SubjectsRaw = iSubjs
+
+				// remove i if it has no subjects left
+				if len(aps[i].SubjectsRaw) == 0 {
+					aps = slices.Delete(aps, i, i+1)
+					i--
+					continue outer
+				}
+			}
 		}
 	}
 
diff --git a/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard.caddyfiletest b/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard.caddyfiletest
new file mode 100644
index 000000000..8880d71ae
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard.caddyfiletest
@@ -0,0 +1,106 @@
+{
+	auto_https prefer_wildcard
+}
+
+*.example.com {
+	tls {
+		dns mock
+	}
+	respond "fallback"
+}
+
+foo.example.com {
+	respond "foo"
+}
+----------
+{
+	"apps": {
+		"http": {
+			"servers": {
+				"srv0": {
+					"listen": [
+						":443"
+					],
+					"routes": [
+						{
+							"match": [
+								{
+									"host": [
+										"foo.example.com"
+									]
+								}
+							],
+							"handle": [
+								{
+									"handler": "subroute",
+									"routes": [
+										{
+											"handle": [
+												{
+													"body": "foo",
+													"handler": "static_response"
+												}
+											]
+										}
+									]
+								}
+							],
+							"terminal": true
+						},
+						{
+							"match": [
+								{
+									"host": [
+										"*.example.com"
+									]
+								}
+							],
+							"handle": [
+								{
+									"handler": "subroute",
+									"routes": [
+										{
+											"handle": [
+												{
+													"body": "fallback",
+													"handler": "static_response"
+												}
+											]
+										}
+									]
+								}
+							],
+							"terminal": true
+						}
+					],
+					"automatic_https": {
+						"prefer_wildcard": true
+					}
+				}
+			}
+		},
+		"tls": {
+			"automation": {
+				"policies": [
+					{
+						"subjects": [
+							"*.example.com"
+						],
+						"issuers": [
+							{
+								"challenges": {
+									"dns": {
+										"provider": {
+											"name": "mock"
+										}
+									}
+								},
+								"module": "acme"
+							}
+						]
+					}
+				]
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/caddytest/integration/caddyfile_adapt/wildcard_pattern.caddyfiletest b/caddytest/integration/caddyfile_adapt/wildcard_pattern.caddyfiletest
new file mode 100644
index 000000000..1a9ccea74
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/wildcard_pattern.caddyfiletest
@@ -0,0 +1,157 @@
+*.example.com {
+	tls foo@example.com {
+		dns mock
+	}
+
+	@foo host foo.example.com
+	handle @foo {
+		respond "Foo!"
+	}
+
+	@bar host bar.example.com
+	handle @bar {
+		respond "Bar!"
+	}
+
+	# Fallback for otherwise unhandled domains
+	handle {
+		abort
+	}
+}
+----------
+{
+	"apps": {
+		"http": {
+			"servers": {
+				"srv0": {
+					"listen": [
+						":443"
+					],
+					"routes": [
+						{
+							"match": [
+								{
+									"host": [
+										"*.example.com"
+									]
+								}
+							],
+							"handle": [
+								{
+									"handler": "subroute",
+									"routes": [
+										{
+											"group": "group3",
+											"handle": [
+												{
+													"handler": "subroute",
+													"routes": [
+														{
+															"handle": [
+																{
+																	"body": "Foo!",
+																	"handler": "static_response"
+																}
+															]
+														}
+													]
+												}
+											],
+											"match": [
+												{
+													"host": [
+														"foo.example.com"
+													]
+												}
+											]
+										},
+										{
+											"group": "group3",
+											"handle": [
+												{
+													"handler": "subroute",
+													"routes": [
+														{
+															"handle": [
+																{
+																	"body": "Bar!",
+																	"handler": "static_response"
+																}
+															]
+														}
+													]
+												}
+											],
+											"match": [
+												{
+													"host": [
+														"bar.example.com"
+													]
+												}
+											]
+										},
+										{
+											"group": "group3",
+											"handle": [
+												{
+													"handler": "subroute",
+													"routes": [
+														{
+															"handle": [
+																{
+																	"abort": true,
+																	"handler": "static_response"
+																}
+															]
+														}
+													]
+												}
+											]
+										}
+									]
+								}
+							],
+							"terminal": true
+						}
+					]
+				}
+			}
+		},
+		"tls": {
+			"automation": {
+				"policies": [
+					{
+						"subjects": [
+							"*.example.com"
+						],
+						"issuers": [
+							{
+								"challenges": {
+									"dns": {
+										"provider": {
+											"name": "mock"
+										}
+									}
+								},
+								"email": "foo@example.com",
+								"module": "acme"
+							},
+							{
+								"ca": "https://acme.zerossl.com/v2/DV90",
+								"challenges": {
+									"dns": {
+										"provider": {
+											"name": "mock"
+										}
+									}
+								},
+								"email": "foo@example.com",
+								"module": "acme"
+							}
+						]
+					}
+				]
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/caddytest/integration/mockdns_test.go b/caddytest/integration/mockdns_test.go
new file mode 100644
index 000000000..1b2efb653
--- /dev/null
+++ b/caddytest/integration/mockdns_test.go
@@ -0,0 +1,61 @@
+package integration
+
+import (
+	"context"
+
+	"github.com/caddyserver/caddy/v2"
+	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+	"github.com/caddyserver/certmagic"
+	"github.com/libdns/libdns"
+)
+
+func init() {
+	caddy.RegisterModule(MockDNSProvider{})
+}
+
+// MockDNSProvider is a mock DNS provider, for testing config with DNS modules.
+type MockDNSProvider struct{}
+
+// CaddyModule returns the Caddy module information.
+func (MockDNSProvider) CaddyModule() caddy.ModuleInfo {
+	return caddy.ModuleInfo{
+		ID:  "dns.providers.mock",
+		New: func() caddy.Module { return new(MockDNSProvider) },
+	}
+}
+
+// Provision sets up the module.
+func (MockDNSProvider) Provision(ctx caddy.Context) error {
+	return nil
+}
+
+// UnmarshalCaddyfile sets up the module from Caddyfile tokens.
+func (MockDNSProvider) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
+	return nil
+}
+
+// AppendsRecords appends DNS records to the zone.
+func (MockDNSProvider) AppendRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
+	return nil, nil
+}
+
+// DeleteRecords deletes DNS records from the zone.
+func (MockDNSProvider) DeleteRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
+	return nil, nil
+}
+
+// GetRecords gets DNS records from the zone.
+func (MockDNSProvider) GetRecords(ctx context.Context, zone string) ([]libdns.Record, error) {
+	return nil, nil
+}
+
+// SetRecords sets DNS records in the zone.
+func (MockDNSProvider) SetRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
+	return nil, nil
+}
+
+// Interface guard
+var _ caddyfile.Unmarshaler = (*MockDNSProvider)(nil)
+var _ certmagic.DNSProvider = (*MockDNSProvider)(nil)
+var _ caddy.Provisioner = (*MockDNSProvider)(nil)
+var _ caddy.Module = (*MockDNSProvider)(nil)
diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go
index 79fdfd6ec..ccb610327 100644
--- a/modules/caddyhttp/autohttps.go
+++ b/modules/caddyhttp/autohttps.go
@@ -65,6 +65,12 @@ type AutoHTTPSConfig struct {
 	// enabled. To force automated certificate management
 	// regardless of loaded certificates, set this to true.
 	IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"`
+
+	// If true, automatic HTTPS will prefer wildcard names
+	// and ignore non-wildcard names if both are available.
+	// This allows for writing a config with top-level host
+	// matchers without having those names produce certificates.
+	PreferWildcard bool `json:"prefer_wildcard,omitempty"`
 }
 
 // automaticHTTPSPhase1 provisions all route matchers, determines
@@ -157,6 +163,27 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
 			}
 		}
 
+		if srv.AutoHTTPS.PreferWildcard {
+			wildcards := make(map[string]struct{})
+			for d := range serverDomainSet {
+				if strings.HasPrefix(d, "*.") {
+					wildcards[d[2:]] = struct{}{}
+				}
+			}
+			for d := range serverDomainSet {
+				if strings.HasPrefix(d, "*.") {
+					continue
+				}
+				base := d
+				if idx := strings.Index(d, "."); idx != -1 {
+					base = d[idx+1:]
+				}
+				if _, ok := wildcards[base]; ok {
+					delete(serverDomainSet, d)
+				}
+			}
+		}
+
 		// nothing more to do here if there are no domains that qualify for
 		// automatic HTTPS and there are no explicit TLS connection policies:
 		// if there is at least one domain but no TLS conn policy (F&&T), we'll