aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMatt Holt <[email protected]>2024-12-20 10:55:02 -0700
committerGitHub <[email protected]>2024-12-20 10:55:02 -0700
commit86da4e8f56f3518bc053dd4f68a78c538a4aab5f (patch)
treebaaf259e90a81d56bf8a2999809d396a02fe1ea9
parent130c868e95dfd1a8b1d39fd217bc6378f6b72ec0 (diff)
parented1c594cdbddf89829eaf1174f414028577b432d (diff)
downloadcaddy-86da4e8f56f3518bc053dd4f68a78c538a4aab5f.tar.gz
caddy-86da4e8f56f3518bc053dd4f68a78c538a4aab5f.zip
Merge branch 'master' into transfer-encoding-matchtransfer-encoding-match
-rw-r--r--.github/SECURITY.md10
-rw-r--r--.github/workflows/ci.yml5
-rw-r--r--.github/workflows/cross-build.yml2
-rw-r--r--.goreleaser.yml5
-rw-r--r--README.md2
-rw-r--r--admin.go2
-rw-r--r--caddyconfig/caddyfile/parse.go4
-rw-r--r--caddyconfig/httpcaddyfile/addresses.go10
-rw-r--r--caddyconfig/httpcaddyfile/builtins.go46
-rw-r--r--caddyconfig/httpcaddyfile/builtins_test.go14
-rw-r--r--caddyconfig/httpcaddyfile/httptype.go77
-rw-r--r--caddyconfig/httpcaddyfile/options.go59
-rw-r--r--caddyconfig/httpcaddyfile/serveroptions.go1
-rw-r--r--caddyconfig/httpcaddyfile/shorthands.go14
-rw-r--r--caddyconfig/httpcaddyfile/tlsapp.go103
-rw-r--r--caddyconfig/httploader.go2
-rw-r--r--caddytest/integration/acme_test.go10
-rw-r--r--caddytest/integration/acmeserver_test.go10
-rw-r--r--caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard.caddyfiletest3
-rw-r--r--caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard_multi.caddyfiletest268
-rw-r--r--caddytest/integration/caddyfile_adapt/encode_options.caddyfiletest13
-rw-r--r--caddytest/integration/caddyfile_adapt/file_server_file_limit.caddyfiletest36
-rw-r--r--caddytest/integration/caddyfile_adapt/file_server_precompressed.caddyfiletest20
-rw-r--r--caddytest/integration/caddyfile_adapt/forward_auth_authelia.caddyfiletest102
-rw-r--r--caddytest/integration/caddyfile_adapt/forward_auth_rename_headers.caddyfiletest132
-rw-r--r--caddytest/integration/caddyfile_adapt/global_options.caddyfiletest6
-rw-r--r--caddytest/integration/caddyfile_adapt/global_options_log_sampling.caddyfiletest23
-rw-r--r--caddytest/integration/caddyfile_adapt/log_sampling.caddyfiletest45
-rw-r--r--caddytest/integration/caddyfile_adapt/metrics_merge_options.caddyfiletest39
-rw-r--r--caddytest/integration/caddyfile_adapt/metrics_perhost.caddyfiletest8
-rw-r--r--caddytest/integration/caddyfile_adapt/php_fastcgi_expanded_form.caddyfiletest6
-rw-r--r--caddytest/integration/caddyfile_adapt/php_fastcgi_handle_response.caddyfiletest3
-rw-r--r--caddytest/integration/caddyfile_adapt/php_fastcgi_matcher.caddyfiletest5
-rw-r--r--caddytest/integration/caddyfile_adapt/php_fastcgi_subdirectives.caddyfiletest3
-rw-r--r--caddytest/integration/caddyfile_adapt/php_fastcgi_try_files_override.caddyfiletest2
-rw-r--r--caddytest/integration/caddyfile_adapt/php_fastcgi_try_files_override_no_dir_index.caddyfiletest95
-rw-r--r--caddytest/integration/mockdns_test.go2
-rw-r--r--cmd/caddy/main.go5
-rw-r--r--cmd/commandfuncs.go9
-rw-r--r--cmd/commands.go8
-rw-r--r--cmd/packagesfuncs.go58
-rw-r--r--cmd/storagefuncs.go9
-rw-r--r--context.go10
-rw-r--r--filepath.go39
-rw-r--r--filepath_windows.go27
-rw-r--r--filesystem.go14
-rw-r--r--go.mod64
-rw-r--r--go.sum142
-rw-r--r--listen.go2
-rw-r--r--listeners.go60
-rw-r--r--listeners_test.go12
-rw-r--r--modules/caddyevents/app.go2
-rw-r--r--modules/caddyhttp/app.go60
-rw-r--r--modules/caddyhttp/caddyhttp.go16
-rw-r--r--modules/caddyhttp/celmatcher.go135
-rw-r--r--modules/caddyhttp/celmatcher_test.go8
-rw-r--r--modules/caddyhttp/encode/caddyfile.go36
-rw-r--r--modules/caddyhttp/encode/encode.go30
-rw-r--r--modules/caddyhttp/encode/encode_test.go2
-rw-r--r--modules/caddyhttp/fileserver/browse.go13
-rw-r--r--modules/caddyhttp/fileserver/caddyfile.go29
-rw-r--r--modules/caddyhttp/fileserver/command.go4
-rw-r--r--modules/caddyhttp/fileserver/matcher.go86
-rw-r--r--modules/caddyhttp/fileserver/matcher_test.go37
-rw-r--r--modules/caddyhttp/fileserver/staticfiles.go6
-rw-r--r--modules/caddyhttp/fileserver/testdata/large.txt3
-rw-r--r--modules/caddyhttp/headers/headers.go4
-rw-r--r--modules/caddyhttp/ip_matchers.go62
-rw-r--r--modules/caddyhttp/matchers.go219
-rw-r--r--modules/caddyhttp/matchers_test.go53
-rw-r--r--modules/caddyhttp/metrics.go5
-rw-r--r--modules/caddyhttp/replacer.go11
-rw-r--r--modules/caddyhttp/requestbody/requestbody.go4
-rw-r--r--modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go72
-rw-r--r--modules/caddyhttp/reverseproxy/fastcgi/client.go11
-rw-r--r--modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go2
-rw-r--r--modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go67
-rw-r--r--modules/caddyhttp/reverseproxy/healthchecks.go2
-rw-r--r--modules/caddyhttp/reverseproxy/httptransport.go4
-rw-r--r--modules/caddyhttp/reverseproxy/reverseproxy.go85
-rw-r--r--modules/caddyhttp/reverseproxy/selectionpolicies.go19
-rw-r--r--modules/caddyhttp/reverseproxy/selectionpolicies_test.go52
-rw-r--r--modules/caddyhttp/reverseproxy/streaming.go84
-rw-r--r--modules/caddyhttp/rewrite/caddyfile.go3
-rw-r--r--modules/caddyhttp/rewrite/rewrite.go3
-rw-r--r--modules/caddyhttp/rewrite/rewrite_test.go5
-rw-r--r--modules/caddyhttp/routes.go97
-rw-r--r--modules/caddyhttp/server.go26
-rw-r--r--modules/caddyhttp/server_test.go9
-rw-r--r--modules/caddyhttp/tracing/tracer.go6
-rw-r--r--modules/caddyhttp/vars.go38
-rw-r--r--modules/caddytls/acmeissuer.go2
-rw-r--r--modules/caddytls/automation.go2
-rw-r--r--modules/caddytls/connpolicy.go25
-rw-r--r--modules/caddytls/ondemand.go2
-rw-r--r--modules/caddytls/tls.go17
-rw-r--r--modules/logging/filewriter.go3
-rw-r--r--sigtrap_posix.go4
98 files changed, 2424 insertions, 687 deletions
diff --git a/.github/SECURITY.md b/.github/SECURITY.md
index 44cc5b7cd..557b4bac1 100644
--- a/.github/SECURITY.md
+++ b/.github/SECURITY.md
@@ -5,11 +5,11 @@ The Caddy project would like to make sure that it stays on top of all practicall
## Supported Versions
-| Version | Supported |
-| ------- | ------------------ |
-| 2.x | ✔️ |
-| 1.x | :x: |
-| < 1.x | :x: |
+| Version | Supported |
+| -------- | ----------|
+| 2.latest | ✔️ |
+| 1.x | :x: |
+| < 1.x | :x: |
## Acceptable Scope
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a88bd17a4..c16af8db5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -143,7 +143,7 @@ jobs:
s390x-test:
name: test (s390x on IBM Z)
runs-on: ubuntu-latest
- if: github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]'
+ if: github.event.pull_request.head.repo.full_name == 'caddyserver/caddy' && github.actor != 'dependabot[bot]'
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
steps:
- name: Checkout code
@@ -194,6 +194,7 @@ jobs:
goreleaser-check:
runs-on: ubuntu-latest
+ if: github.event.pull_request.head.repo.full_name == 'caddyserver/caddy' && github.actor != 'dependabot[bot]'
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -216,4 +217,4 @@ jobs:
version: latest
args: build --single-target --snapshot
env:
- TAG: "master"
+ TAG: ${{ github.head_ref || github.ref_name }}
diff --git a/.github/workflows/cross-build.yml b/.github/workflows/cross-build.yml
index e77e4e992..af0394603 100644
--- a/.github/workflows/cross-build.yml
+++ b/.github/workflows/cross-build.yml
@@ -70,4 +70,4 @@ jobs:
continue-on-error: true
working-directory: ./cmd/caddy
run: |
- GOOS=$GOOS GOARCH=$GOARCH go build -tags nobadger -trimpath -o caddy-"$GOOS"-$GOARCH 2> /dev/null
+ GOOS=$GOOS GOARCH=$GOARCH go build -tags=nobadger,nomysql,nopgx -trimpath -o caddy-"$GOOS"-$GOARCH 2> /dev/null
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 5c1f7df40..005fdbaf3 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -83,6 +83,8 @@ builds:
- -s -w
tags:
- nobadger
+ - nomysql
+ - nopgx
signs:
- cmd: cosign
@@ -190,6 +192,9 @@ nfpms:
preremove: ./caddy-dist/scripts/preremove.sh
postremove: ./caddy-dist/scripts/postremove.sh
+ provides:
+ - httpd
+
release:
github:
owner: caddyserver
diff --git a/README.md b/README.md
index 2185eccd8..abc136f68 100644
--- a/README.md
+++ b/README.md
@@ -131,7 +131,7 @@ $ xcaddy build
4. Initialize a Go module: `go mod init caddy`
5. (Optional) Pin Caddy version: `go get github.com/caddyserver/caddy/v2@version` replacing `version` with a git tag, commit, or branch name.
6. (Optional) Add plugins by adding their import: `_ "import/path/here"`
-7. Compile: `go build`
+7. Compile: `go build -tags=nobadger,nomysql,nopgx`
diff --git a/admin.go b/admin.go
index 0d5ecc92c..89fce1d28 100644
--- a/admin.go
+++ b/admin.go
@@ -214,7 +214,7 @@ type AdminPermissions struct {
// newAdminHandler reads admin's config and returns an http.Handler suitable
// for use in an admin endpoint server, which will be listening on listenAddr.
-func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, ctx Context) adminHandler {
+func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Context) adminHandler {
muxWrap := adminHandler{mux: http.NewServeMux()}
// secure the local or remote endpoint respectively
diff --git a/caddyconfig/caddyfile/parse.go b/caddyconfig/caddyfile/parse.go
index e19b3b97d..d04a1ac46 100644
--- a/caddyconfig/caddyfile/parse.go
+++ b/caddyconfig/caddyfile/parse.go
@@ -423,7 +423,7 @@ func (p *parser) doImport(nesting int) error {
// make path relative to the file of the _token_ being processed rather
// than current working directory (issue #867) and then use glob to get
// list of matching filenames
- absFile, err := filepath.Abs(p.Dispenser.File())
+ absFile, err := caddy.FastAbs(p.Dispenser.File())
if err != nil {
return p.Errf("Failed to get absolute path of file: %s: %v", p.Dispenser.File(), err)
}
@@ -622,7 +622,7 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
// Tack the file path onto these tokens so errors show the imported file's name
// (we use full, absolute path to avoid bugs: issue #1892)
- filename, err := filepath.Abs(importFile)
+ filename, err := caddy.FastAbs(importFile)
if err != nil {
return nil, p.Errf("Failed to get absolute path of file: %s: %v", importFile, err)
}
diff --git a/caddyconfig/httpcaddyfile/addresses.go b/caddyconfig/httpcaddyfile/addresses.go
index 1c331eadc..1121776d9 100644
--- a/caddyconfig/httpcaddyfile/addresses.go
+++ b/caddyconfig/httpcaddyfile/addresses.go
@@ -31,7 +31,7 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
-// mapAddressToServerBlocks returns a map of listener address to list of server
+// mapAddressToProtocolToServerBlocks returns a map of listener address to list of server
// blocks that will be served on that address. To do this, each server block is
// expanded so that each one is considered individually, although keys of a
// server block that share the same address stay grouped together so the config
@@ -329,8 +329,12 @@ func (st *ServerType) listenersForServerBlockAddress(sblock serverBlock, addr Ad
// use a map to prevent duplication
listeners := map[string]map[string]struct{}{}
for _, lnCfgVal := range lnCfgVals {
- for _, lnHost := range lnCfgVal.addresses {
- networkAddr, err := caddy.ParseNetworkAddressFromHostPort(lnHost, lnPort)
+ for _, lnAddr := range lnCfgVal.addresses {
+ lnNetw, lnHost, _, err := caddy.SplitNetworkAddress(lnAddr)
+ if err != nil {
+ return nil, fmt.Errorf("splitting listener address: %v", err)
+ }
+ networkAddr, err := caddy.ParseNetworkAddress(caddy.JoinNetworkAddress(lnNetw, lnHost, lnPort))
if err != nil {
return nil, fmt.Errorf("parsing network address: %v", err)
}
diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go
index 165c66b25..99a4d916a 100644
--- a/caddyconfig/httpcaddyfile/builtins.go
+++ b/caddyconfig/httpcaddyfile/builtins.go
@@ -24,7 +24,7 @@ import (
"time"
"github.com/caddyserver/certmagic"
- "github.com/mholt/acmez/v2/acme"
+ "github.com/mholt/acmez/v3/acme"
"go.uber.org/zap/zapcore"
"github.com/caddyserver/caddy/v2"
@@ -981,6 +981,50 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
}
cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings)
+ case "sampling":
+ d := h.Dispenser.NewFromNextSegment()
+ for d.NextArg() {
+ // consume any tokens on the same line, if any.
+ }
+
+ sampling := &caddy.LogSampling{}
+ for nesting := d.Nesting(); d.NextBlock(nesting); {
+ subdir := d.Val()
+ switch subdir {
+ case "interval":
+ if !d.NextArg() {
+ return nil, d.ArgErr()
+ }
+ interval, err := time.ParseDuration(d.Val() + "ns")
+ if err != nil {
+ return nil, d.Errf("failed to parse interval: %v", err)
+ }
+ sampling.Interval = interval
+ case "first":
+ if !d.NextArg() {
+ return nil, d.ArgErr()
+ }
+ first, err := strconv.Atoi(d.Val())
+ if err != nil {
+ return nil, d.Errf("failed to parse first: %v", err)
+ }
+ sampling.First = first
+ case "thereafter":
+ if !d.NextArg() {
+ return nil, d.ArgErr()
+ }
+ thereafter, err := strconv.Atoi(d.Val())
+ if err != nil {
+ return nil, d.Errf("failed to parse thereafter: %v", err)
+ }
+ sampling.Thereafter = thereafter
+ default:
+ return nil, d.Errf("unrecognized subdirective: %s", subdir)
+ }
+ }
+
+ cl.Sampling = sampling
+
case "core":
if !h.NextArg() {
return nil, h.ArgErr()
diff --git a/caddyconfig/httpcaddyfile/builtins_test.go b/caddyconfig/httpcaddyfile/builtins_test.go
index cf7463484..c23531f22 100644
--- a/caddyconfig/httpcaddyfile/builtins_test.go
+++ b/caddyconfig/httpcaddyfile/builtins_test.go
@@ -62,6 +62,20 @@ func TestLogDirectiveSyntax(t *testing.T) {
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.name-override"]},"name-override":{"writer":{"filename":"foo.log","output":"file"},"core":{"module":"mock"},"include":["http.log.access.name-override"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"name-override"}}}}}}`,
expectError: false,
},
+ {
+ input: `:8080 {
+ log {
+ sampling {
+ interval 2
+ first 3
+ thereafter 4
+ }
+ }
+ }
+ `,
+ output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"sampling":{"interval":2,"first":3,"thereafter":4},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
+ expectError: false,
+ },
} {
adapter := caddyfile.Adapter{
diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go
index 4dacd9058..c169b92af 100644
--- a/caddyconfig/httpcaddyfile/httptype.go
+++ b/caddyconfig/httpcaddyfile/httptype.go
@@ -15,6 +15,7 @@
package httpcaddyfile
import (
+ "cmp"
"encoding/json"
"fmt"
"net"
@@ -186,12 +187,25 @@ func (st ServerType) Setup(
return nil, warnings, err
}
+ // hoist the metrics config from per-server to global
+ metrics, _ := options["metrics"].(*caddyhttp.Metrics)
+ for _, s := range servers {
+ if s.Metrics != nil {
+ metrics = cmp.Or[*caddyhttp.Metrics](metrics, &caddyhttp.Metrics{})
+ metrics = &caddyhttp.Metrics{
+ PerHost: metrics.PerHost || s.Metrics.PerHost,
+ }
+ s.Metrics = nil // we don't need it anymore
+ }
+ }
+
// now that each server is configured, make the HTTP app
httpApp := caddyhttp.App{
HTTPPort: tryInt(options["http_port"], &warnings),
HTTPSPort: tryInt(options["https_port"], &warnings),
GracePeriod: tryDuration(options["grace_period"], &warnings),
ShutdownDelay: tryDuration(options["shutdown_delay"], &warnings),
+ Metrics: metrics,
Servers: servers,
}
@@ -692,6 +706,16 @@ func (st *ServerType) serversFromPairings(
return specificity(iLongestHost) > specificity(jLongestHost)
})
+ // collect all hosts that have a wildcard in them
+ wildcardHosts := []string{}
+ for _, sblock := range p.serverBlocks {
+ for _, addr := range sblock.parsedKeys {
+ if strings.HasPrefix(addr.Host, "*.") {
+ wildcardHosts = append(wildcardHosts, addr.Host[2:])
+ }
+ }
+ }
+
var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
autoHTTPSWillAddConnPolicy := srv.AutoHTTPS == nil || !srv.AutoHTTPS.Disabled
@@ -777,13 +801,6 @@ 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) {
@@ -799,18 +816,6 @@ 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://"
@@ -827,6 +832,19 @@ func (st *ServerType) serversFromPairings(
(addr.Scheme != "http" && addr.Port != httpPort && hasTLSEnabled) {
addressQualifiesForTLS = true
}
+
+ // If prefer wildcard is enabled, then we add hosts that are
+ // already covered by the wildcard to the skip list
+ if addressQualifiesForTLS && srv.AutoHTTPS != nil && srv.AutoHTTPS.PreferWildcard {
+ 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.SkipCerts = append(srv.AutoHTTPS.SkipCerts, addr.Host)
+ }
+ }
+
// predict whether auto-HTTPS will add the conn policy for us; if so, we
// may not need to add one for this server
autoHTTPSWillAddConnPolicy = autoHTTPSWillAddConnPolicy &&
@@ -1448,9 +1466,9 @@ func (st *ServerType) compileEncodedMatcherSets(sblock serverBlock) ([]caddy.Mod
// iterate each pairing of host and path matchers and
// put them into a map for JSON encoding
- var matcherSets []map[string]caddyhttp.RequestMatcher
+ var matcherSets []map[string]caddyhttp.RequestMatcherWithError
for _, mp := range matcherPairs {
- matcherSet := make(map[string]caddyhttp.RequestMatcher)
+ matcherSet := make(map[string]caddyhttp.RequestMatcherWithError)
if len(mp.hostm) > 0 {
matcherSet["host"] = mp.hostm
}
@@ -1509,12 +1527,17 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.M
if err != nil {
return err
}
- rm, ok := unm.(caddyhttp.RequestMatcher)
- if !ok {
- return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
+
+ if rm, ok := unm.(caddyhttp.RequestMatcherWithError); ok {
+ matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
+ return nil
}
- matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
- return nil
+ // nolint:staticcheck
+ if rm, ok := unm.(caddyhttp.RequestMatcher); ok {
+ matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
+ return nil
+ }
+ return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
}
// if the next token is quoted, we can assume it's not a matcher name
@@ -1558,7 +1581,7 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.M
return nil
}
-func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (caddy.ModuleMap, error) {
+func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcherWithError) (caddy.ModuleMap, error) {
msEncoded := make(caddy.ModuleMap)
for matcherName, val := range matchers {
jsonBytes, err := json.Marshal(val)
diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go
index 9bae760c0..d4a424624 100644
--- a/caddyconfig/httpcaddyfile/options.go
+++ b/caddyconfig/httpcaddyfile/options.go
@@ -19,11 +19,12 @@ import (
"strconv"
"github.com/caddyserver/certmagic"
- "github.com/mholt/acmez/v2/acme"
+ "github.com/mholt/acmez/v3/acme"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddytls"
)
@@ -38,7 +39,8 @@ func init() {
RegisterGlobalOption("fallback_sni", parseOptSingleString)
RegisterGlobalOption("order", parseOptOrder)
RegisterGlobalOption("storage", parseOptStorage)
- RegisterGlobalOption("storage_clean_interval", parseOptDuration)
+ RegisterGlobalOption("storage_check", parseStorageCheck)
+ RegisterGlobalOption("storage_clean_interval", parseStorageCleanInterval)
RegisterGlobalOption("renew_interval", parseOptDuration)
RegisterGlobalOption("ocsp_interval", parseOptDuration)
RegisterGlobalOption("acme_ca", parseOptSingleString)
@@ -53,6 +55,7 @@ func init() {
RegisterGlobalOption("local_certs", parseOptTrue)
RegisterGlobalOption("key_type", parseOptSingleString)
RegisterGlobalOption("auto_https", parseOptAutoHTTPS)
+ RegisterGlobalOption("metrics", parseMetricsOptions)
RegisterGlobalOption("servers", parseServerOptions)
RegisterGlobalOption("ocsp_stapling", parseOCSPStaplingOptions)
RegisterGlobalOption("cert_lifetime", parseOptDuration)
@@ -187,6 +190,40 @@ func parseOptStorage(d *caddyfile.Dispenser, _ any) (any, error) {
return storage, nil
}
+func parseStorageCheck(d *caddyfile.Dispenser, _ any) (any, error) {
+ d.Next() // consume option name
+ if !d.Next() {
+ return "", d.ArgErr()
+ }
+ val := d.Val()
+ if d.Next() {
+ return "", d.ArgErr()
+ }
+ if val != "off" {
+ return "", d.Errf("storage_check must be 'off'")
+ }
+ return val, nil
+}
+
+func parseStorageCleanInterval(d *caddyfile.Dispenser, _ any) (any, error) {
+ d.Next() // consume option name
+ if !d.Next() {
+ return "", d.ArgErr()
+ }
+ val := d.Val()
+ if d.Next() {
+ return "", d.ArgErr()
+ }
+ if val == "off" {
+ return false, nil
+ }
+ dur, err := caddy.ParseDuration(d.Val())
+ if err != nil {
+ return nil, d.Errf("failed to parse storage_clean_interval, must be a duration or 'off' %w", err)
+ }
+ return caddy.Duration(dur), nil
+}
+
func parseOptDuration(d *caddyfile.Dispenser, _ any) (any, error) {
if !d.Next() { // consume option name
return nil, d.ArgErr()
@@ -446,6 +483,24 @@ func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) {
return val, nil
}
+func unmarshalCaddyfileMetricsOptions(d *caddyfile.Dispenser) (any, error) {
+ d.Next() // consume option name
+ metrics := new(caddyhttp.Metrics)
+ for d.NextBlock(0) {
+ switch d.Val() {
+ case "per_host":
+ metrics.PerHost = true
+ default:
+ return nil, d.Errf("unrecognized servers option '%s'", d.Val())
+ }
+ }
+ return metrics, nil
+}
+
+func parseMetricsOptions(d *caddyfile.Dispenser, _ any) (any, error) {
+ return unmarshalCaddyfileMetricsOptions(d)
+}
+
func parseServerOptions(d *caddyfile.Dispenser, _ any) (any, error) {
return unmarshalCaddyfileServerOptions(d)
}
diff --git a/caddyconfig/httpcaddyfile/serveroptions.go b/caddyconfig/httpcaddyfile/serveroptions.go
index b05af47f5..40a8af209 100644
--- a/caddyconfig/httpcaddyfile/serveroptions.go
+++ b/caddyconfig/httpcaddyfile/serveroptions.go
@@ -240,6 +240,7 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
}
case "metrics":
+ caddy.Log().Warn("The nested 'metrics' option inside `servers` is deprecated and will be removed in the next major version. Use the global 'metrics' option instead.")
serverOpts.Metrics = new(caddyhttp.Metrics)
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
diff --git a/caddyconfig/httpcaddyfile/shorthands.go b/caddyconfig/httpcaddyfile/shorthands.go
index 5d9ef31eb..ca6e4f92c 100644
--- a/caddyconfig/httpcaddyfile/shorthands.go
+++ b/caddyconfig/httpcaddyfile/shorthands.go
@@ -52,19 +52,27 @@ func NewShorthandReplacer() ShorthandReplacer {
// be used in the Caddyfile, and the right is the replacement.
func placeholderShorthands() []string {
return []string{
- "{dir}", "{http.request.uri.path.dir}",
- "{file}", "{http.request.uri.path.file}",
"{host}", "{http.request.host}",
"{hostport}", "{http.request.hostport}",
"{port}", "{http.request.port}",
+ "{orig_method}", "{http.request.orig_method}",
+ "{orig_uri}", "{http.request.orig_uri}",
+ "{orig_path}", "{http.request.orig_uri.path}",
+ "{orig_dir}", "{http.request.orig_uri.path.dir}",
+ "{orig_file}", "{http.request.orig_uri.path.file}",
+ "{orig_query}", "{http.request.orig_uri.query}",
+ "{orig_?query}", "{http.request.orig_uri.prefixed_query}",
"{method}", "{http.request.method}",
+ "{uri}", "{http.request.uri}",
"{path}", "{http.request.uri.path}",
+ "{dir}", "{http.request.uri.path.dir}",
+ "{file}", "{http.request.uri.path.file}",
"{query}", "{http.request.uri.query}",
+ "{?query}", "{http.request.uri.prefixed_query}",
"{remote}", "{http.request.remote}",
"{remote_host}", "{http.request.remote.host}",
"{remote_port}", "{http.request.remote.port}",
"{scheme}", "{http.request.scheme}",
- "{uri}", "{http.request.uri}",
"{uuid}", "{http.request.uuid}",
"{tls_cipher}", "{http.request.tls.cipher_suite}",
"{tls_version}", "{http.request.tls.version}",
diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go
index ed708524d..397323f71 100644
--- a/caddyconfig/httpcaddyfile/tlsapp.go
+++ b/caddyconfig/httpcaddyfile/tlsapp.go
@@ -25,7 +25,7 @@ import (
"strings"
"github.com/caddyserver/certmagic"
- "github.com/mholt/acmez/v2/acme"
+ "github.com/mholt/acmez/v3/acme"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
@@ -92,6 +92,25 @@ func (st ServerType) buildTLSApp(
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP)
}
+ // collect all hosts that have a wildcard in them, and arent HTTP
+ wildcardHosts := []string{}
+ for _, p := range pairings {
+ var addresses []string
+ for _, addressWithProtocols := range p.addressesWithProtocols {
+ addresses = append(addresses, addressWithProtocols.address)
+ }
+ if !listenersUseAnyPortOtherThan(addresses, httpPort) {
+ continue
+ }
+ for _, sblock := range p.serverBlocks {
+ for _, addr := range sblock.parsedKeys {
+ if strings.HasPrefix(addr.Host, "*.") {
+ wildcardHosts = append(wildcardHosts, addr.Host[2:])
+ }
+ }
+ }
+ }
+
for _, p := range pairings {
// avoid setting up TLS automation policies for a server that is HTTP-only
var addresses []string
@@ -115,6 +134,12 @@ func (st ServerType) buildTLSApp(
return nil, warnings, err
}
+ // make a plain copy so we can compare whether we made any changes
+ apCopy, err := newBaseAutomationPolicy(options, warnings, true)
+ if err != nil {
+ return nil, warnings, err
+ }
+
sblockHosts := sblock.hostsFromKeys(false)
if len(sblockHosts) == 0 && catchAllAP != nil {
ap = catchAllAP
@@ -217,9 +242,21 @@ func (st ServerType) buildTLSApp(
catchAllAP = ap
}
+ hostsNotHTTP := sblock.hostsFromKeysNotHTTP(httpPort)
+ sort.Strings(hostsNotHTTP) // solely for deterministic test results
+
+ // if the we prefer wildcards and the AP is unchanged,
+ // then we can skip this AP because it should be covered
+ // by an AP with a wildcard
+ if slices.Contains(autoHTTPS, "prefer_wildcard") {
+ if hostsCoveredByWildcard(hostsNotHTTP, wildcardHosts) &&
+ reflect.DeepEqual(ap, apCopy) {
+ continue
+ }
+ }
+
// associate our new automation policy with this server block's hosts
- ap.SubjectsRaw = sblock.hostsFromKeysNotHTTP(httpPort)
- sort.Strings(ap.SubjectsRaw) // solely for deterministic test results
+ ap.SubjectsRaw = hostsNotHTTP
// if a combination of public and internal names were given
// for this same server block and no issuer was specified, we
@@ -258,6 +295,7 @@ func (st ServerType) buildTLSApp(
ap2.IssuersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)}
}
}
+
if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig)
}
@@ -311,6 +349,16 @@ func (st ServerType) buildTLSApp(
tlsApp.Automation.OnDemand = onDemand
}
+ // if the storage clean interval is a boolean, then it's "off" to disable cleaning
+ if sc, ok := options["storage_check"].(string); ok && sc == "off" {
+ tlsApp.DisableStorageCheck = true
+ }
+
+ // if the storage clean interval is a boolean, then it's "off" to disable cleaning
+ if sci, ok := options["storage_clean_interval"].(bool); ok && !sci {
+ tlsApp.DisableStorageClean = true
+ }
+
// set the storage clean interval if configured
if storageCleanInterval, ok := options["storage_clean_interval"].(caddy.Duration); ok {
if tlsApp.Automation == nil {
@@ -418,10 +466,7 @@ func (st ServerType) buildTLSApp(
}
// consolidate automation policies that are the exact same
- tlsApp.Automation.Policies = consolidateAutomationPolicies(
- tlsApp.Automation.Policies,
- slices.Contains(autoHTTPS, "prefer_wildcard"),
- )
+ tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
// ensure automation policies don't overlap subjects (this should be
// an error at provision-time as well, but catch it in the adapt phase
@@ -567,7 +612,7 @@ func newBaseAutomationPolicy(
// consolidateAutomationPolicies combines automation policies that are the same,
// for a cleaner overall output.
-func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy, preferWildcard bool) []*caddytls.AutomationPolicy {
+func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*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]) {
@@ -652,31 +697,6 @@ 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
- }
- }
}
}
@@ -748,3 +768,20 @@ func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
func isTailscaleDomain(name string) bool {
return strings.HasSuffix(strings.ToLower(name), ".ts.net")
}
+
+func hostsCoveredByWildcard(hosts []string, wildcards []string) bool {
+ if len(hosts) == 0 || len(wildcards) == 0 {
+ return false
+ }
+ for _, host := range hosts {
+ for _, wildcard := range wildcards {
+ if strings.HasPrefix(host, "*.") {
+ continue
+ }
+ if certmagic.MatchWildcard(host, "*."+wildcard) {
+ return true
+ }
+ }
+ }
+ return false
+}
diff --git a/caddyconfig/httploader.go b/caddyconfig/httploader.go
index 55fadf65b..a25041a34 100644
--- a/caddyconfig/httploader.go
+++ b/caddyconfig/httploader.go
@@ -35,7 +35,7 @@ func init() {
// If the response is not a JSON config, a config adapter must be specified
// either in the loader config (`adapter`), or in the Content-Type HTTP header
// returned in the HTTP response from the server. The Content-Type header is
-// read just like the admin API's `/load` endpoint. Uf you don't have control
+// read just like the admin API's `/load` endpoint. If you don't have control
// over the HTTP server (but can still trust its response), you can override
// the Content-Type header by setting the `adapter` property in this config.
type HTTPLoader struct {
diff --git a/caddytest/integration/acme_test.go b/caddytest/integration/acme_test.go
index ceacd1db0..d7e4c296d 100644
--- a/caddytest/integration/acme_test.go
+++ b/caddytest/integration/acme_test.go
@@ -6,6 +6,7 @@ import (
"crypto/elliptic"
"crypto/rand"
"fmt"
+ "log/slog"
"net"
"net/http"
"strings"
@@ -13,10 +14,11 @@ import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddytest"
- "github.com/mholt/acmez/v2"
- "github.com/mholt/acmez/v2/acme"
+ "github.com/mholt/acmez/v3"
+ "github.com/mholt/acmez/v3/acme"
smallstepacme "github.com/smallstep/certificates/acme"
"go.uber.org/zap"
+ "go.uber.org/zap/exp/zapslog"
)
const acmeChallengePort = 9081
@@ -48,7 +50,7 @@ func TestACMEServerWithDefaults(t *testing.T) {
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
- Logger: logger,
+ Logger: slog.New(zapslog.NewHandler(logger.Core())),
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
@@ -117,7 +119,7 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) {
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
- Logger: logger,
+ Logger: slog.New(zapslog.NewHandler(logger.Core())),
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
diff --git a/caddytest/integration/acmeserver_test.go b/caddytest/integration/acmeserver_test.go
index 22b716f84..ca5845f87 100644
--- a/caddytest/integration/acmeserver_test.go
+++ b/caddytest/integration/acmeserver_test.go
@@ -5,13 +5,15 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
+ "log/slog"
"strings"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
- "github.com/mholt/acmez/v2"
- "github.com/mholt/acmez/v2/acme"
+ "github.com/mholt/acmez/v3"
+ "github.com/mholt/acmez/v3/acme"
"go.uber.org/zap"
+ "go.uber.org/zap/exp/zapslog"
)
func TestACMEServerDirectory(t *testing.T) {
@@ -76,7 +78,7 @@ func TestACMEServerAllowPolicy(t *testing.T) {
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
- Logger: logger,
+ Logger: slog.New(zapslog.NewHandler(logger.Core())),
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
@@ -165,7 +167,7 @@ func TestACMEServerDenyPolicy(t *testing.T) {
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
- Logger: logger,
+ Logger: slog.New(zapslog.NewHandler(logger.Core())),
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
diff --git a/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard.caddyfiletest b/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard.caddyfiletest
index 8880d71ae..04f2c4665 100644
--- a/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard.caddyfiletest
@@ -74,6 +74,9 @@ foo.example.com {
}
],
"automatic_https": {
+ "skip_certificates": [
+ "foo.example.com"
+ ],
"prefer_wildcard": true
}
}
diff --git a/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard_multi.caddyfiletest b/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard_multi.caddyfiletest
new file mode 100644
index 000000000..4f8c26a5d
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/auto_https_prefer_wildcard_multi.caddyfiletest
@@ -0,0 +1,268 @@
+{
+ auto_https prefer_wildcard
+}
+
+# Covers two domains
+*.one.example.com {
+ tls {
+ dns mock
+ }
+ respond "one fallback"
+}
+
+# Is covered, should not get its own AP
+foo.one.example.com {
+ respond "foo one"
+}
+
+# This one has its own tls config so it doesn't get covered (escape hatch)
+bar.one.example.com {
+ respond "bar one"
+}
+
+# Covers nothing but AP gets consolidated with the first
+*.two.example.com {
+ tls {
+ dns mock
+ }
+ respond "two fallback"
+}
+
+# Is HTTP so it should not cover
+http://*.three.example.com {
+ respond "three fallback"
+}
+
+# Has no wildcard coverage so it gets an AP
+foo.three.example.com {
+ respond "foo three"
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":443"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "foo.three.example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "foo three",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ },
+ {
+ "match": [
+ {
+ "host": [
+ "foo.one.example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "foo one",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ },
+ {
+ "match": [
+ {
+ "host": [
+ "bar.one.example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "bar one",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ },
+ {
+ "match": [
+ {
+ "host": [
+ "*.one.example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "one fallback",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ },
+ {
+ "match": [
+ {
+ "host": [
+ "*.two.example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "two fallback",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ],
+ "automatic_https": {
+ "skip_certificates": [
+ "foo.one.example.com",
+ "bar.one.example.com"
+ ],
+ "prefer_wildcard": true
+ }
+ },
+ "srv1": {
+ "listen": [
+ ":80"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "*.three.example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "three fallback",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ],
+ "automatic_https": {
+ "prefer_wildcard": true
+ }
+ }
+ }
+ },
+ "tls": {
+ "automation": {
+ "policies": [
+ {
+ "subjects": [
+ "foo.three.example.com"
+ ]
+ },
+ {
+ "subjects": [
+ "bar.one.example.com"
+ ],
+ "issuers": [
+ {
+ "email": "[email protected]",
+ "module": "acme"
+ },
+ {
+ "ca": "https://acme.zerossl.com/v2/DV90",
+ "email": "[email protected]",
+ "module": "acme"
+ }
+ ]
+ },
+ {
+ "subjects": [
+ "*.one.example.com",
+ "*.two.example.com"
+ ],
+ "issuers": [
+ {
+ "challenges": {
+ "dns": {
+ "provider": {
+ "name": "mock"
+ }
+ }
+ },
+ "module": "acme"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/caddytest/integration/caddyfile_adapt/encode_options.caddyfiletest b/caddytest/integration/caddyfile_adapt/encode_options.caddyfiletest
index 181bc2264..ea9038ef8 100644
--- a/caddytest/integration/caddyfile_adapt/encode_options.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/encode_options.caddyfiletest
@@ -21,6 +21,8 @@ encode {
zstd
gzip 5
}
+
+encode
----------
{
"apps": {
@@ -76,6 +78,17 @@ encode {
"zstd",
"gzip"
]
+ },
+ {
+ "encodings": {
+ "gzip": {},
+ "zstd": {}
+ },
+ "handler": "encode",
+ "prefer": [
+ "zstd",
+ "gzip"
+ ]
}
]
}
diff --git a/caddytest/integration/caddyfile_adapt/file_server_file_limit.caddyfiletest b/caddytest/integration/caddyfile_adapt/file_server_file_limit.caddyfiletest
new file mode 100644
index 000000000..cd73fbff6
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/file_server_file_limit.caddyfiletest
@@ -0,0 +1,36 @@
+:80
+
+file_server {
+ browse {
+ file_limit 4000
+ }
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":80"
+ ],
+ "routes": [
+ {
+ "handle": [
+ {
+ "browse": {
+ "file_limit": 4000
+ },
+ "handler": "file_server",
+ "hide": [
+ "./Caddyfile"
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/caddytest/integration/caddyfile_adapt/file_server_precompressed.caddyfiletest b/caddytest/integration/caddyfile_adapt/file_server_precompressed.caddyfiletest
index ac7d7eddd..3154de96e 100644
--- a/caddytest/integration/caddyfile_adapt/file_server_precompressed.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/file_server_precompressed.caddyfiletest
@@ -3,6 +3,10 @@
file_server {
precompressed zstd br gzip
}
+
+file_server {
+ precompressed
+}
----------
{
"apps": {
@@ -30,6 +34,22 @@ file_server {
"br",
"gzip"
]
+ },
+ {
+ "handler": "file_server",
+ "hide": [
+ "./Caddyfile"
+ ],
+ "precompressed": {
+ "br": {},
+ "gzip": {},
+ "zstd": {}
+ },
+ "precompressed_order": [
+ "br",
+ "zstd",
+ "gzip"
+ ]
}
]
}
diff --git a/caddytest/integration/caddyfile_adapt/forward_auth_authelia.caddyfiletest b/caddytest/integration/caddyfile_adapt/forward_auth_authelia.caddyfiletest
index a05703280..240bdc62f 100644
--- a/caddytest/integration/caddyfile_adapt/forward_auth_authelia.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/forward_auth_authelia.caddyfiletest
@@ -1,6 +1,6 @@
app.example.com {
forward_auth authelia:9091 {
- uri /api/verify?rd=https://authelia.example.com
+ uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
}
@@ -42,24 +42,116 @@ app.example.com {
{
"handle": [
{
+ "handler": "vars"
+ }
+ ]
+ },
+ {
+ "handle": [
+ {
"handler": "headers",
"request": {
"set": {
"Remote-Email": [
"{http.reverse_proxy.header.Remote-Email}"
- ],
+ ]
+ }
+ }
+ }
+ ],
+ "match": [
+ {
+ "not": [
+ {
+ "vars": {
+ "{http.reverse_proxy.header.Remote-Email}": [
+ ""
+ ]
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "handle": [
+ {
+ "handler": "headers",
+ "request": {
+ "set": {
"Remote-Groups": [
"{http.reverse_proxy.header.Remote-Groups}"
- ],
+ ]
+ }
+ }
+ }
+ ],
+ "match": [
+ {
+ "not": [
+ {
+ "vars": {
+ "{http.reverse_proxy.header.Remote-Groups}": [
+ ""
+ ]
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "handle": [
+ {
+ "handler": "headers",
+ "request": {
+ "set": {
"Remote-Name": [
"{http.reverse_proxy.header.Remote-Name}"
- ],
+ ]
+ }
+ }
+ }
+ ],
+ "match": [
+ {
+ "not": [
+ {
+ "vars": {
+ "{http.reverse_proxy.header.Remote-Name}": [
+ ""
+ ]
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "handle": [
+ {
+ "handler": "headers",
+ "request": {
+ "set": {
"Remote-User": [
"{http.reverse_proxy.header.Remote-User}"
]
}
}
}
+ ],
+ "match": [
+ {
+ "not": [
+ {
+ "vars": {
+ "{http.reverse_proxy.header.Remote-User}": [
+ ""
+ ]
+ }
+ }
+ ]
+ }
]
}
]
@@ -80,7 +172,7 @@ app.example.com {
},
"rewrite": {
"method": "GET",
- "uri": "/api/verify?rd=https://authelia.example.com"
+ "uri": "/api/authz/forward-auth"
},
"upstreams": [
{
diff --git a/caddytest/integration/caddyfile_adapt/forward_auth_rename_headers.caddyfiletest b/caddytest/integration/caddyfile_adapt/forward_auth_rename_headers.caddyfiletest
index 65228174d..c2be2ed43 100644
--- a/caddytest/integration/caddyfile_adapt/forward_auth_rename_headers.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/forward_auth_rename_headers.caddyfiletest
@@ -31,27 +31,143 @@ forward_auth localhost:9000 {
{
"handle": [
{
+ "handler": "vars"
+ }
+ ]
+ },
+ {
+ "handle": [
+ {
"handler": "headers",
"request": {
"set": {
"1": [
"{http.reverse_proxy.header.A}"
- ],
- "3": [
- "{http.reverse_proxy.header.C}"
- ],
- "5": [
- "{http.reverse_proxy.header.E}"
- ],
+ ]
+ }
+ }
+ }
+ ],
+ "match": [
+ {
+ "not": [
+ {
+ "vars": {
+ "{http.reverse_proxy.header.A}": [
+ ""
+ ]
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "handle": [
+ {
+ "handler": "headers",
+ "request": {
+ "set": {
"B": [
"{http.reverse_proxy.header.B}"
- ],
+ ]
+ }
+ }
+ }
+ ],
+ "match": [
+ {
+ "not": [
+ {
+ "vars": {
+ "{http.reverse_proxy.header.B}": [
+ ""
+ ]
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "handle": [
+ {
+ "handler": "headers",
+ "request": {
+ "set": {
+ "3": [
+ "{http.reverse_proxy.header.C}"
+ ]
+ }
+ }
+ }
+ ],
+ "match": [
+ {
+ "not": [
+ {
+ "vars": {
+ "{http.reverse_proxy.header.C}": [
+ ""
+ ]
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "handle": [
+ {
+ "handler": "headers",
+ "request": {
+ "set": {
"D": [
"{http.reverse_proxy.header.D}"
]
}
}
}
+ ],
+ "match": [
+ {
+ "not": [
+ {
+ "vars": {
+ "{http.reverse_proxy.header.D}": [
+ ""
+ ]
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "handle": [
+ {
+ "handler": "headers",
+ "request": {
+ "set": {
+ "5": [
+ "{http.reverse_proxy.header.E}"
+ ]
+ }
+ }
+ }
+ ],
+ "match": [
+ {
+ "not": [
+ {
+ "vars": {
+ "{http.reverse_proxy.header.E}": [
+ ""
+ ]
+ }
+ }
+ ]
+ }
]
}
]
diff --git a/caddytest/integration/caddyfile_adapt/global_options.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options.caddyfiletest
index af301615b..99f45cdd5 100644
--- a/caddytest/integration/caddyfile_adapt/global_options.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/global_options.caddyfiletest
@@ -9,6 +9,8 @@
storage file_system {
root /data
}
+ storage_check off
+ storage_clean_interval off
acme_ca https://example.com
acme_ca_root /path/to/ca.crt
ocsp_stapling off
@@ -73,7 +75,9 @@
}
}
},
- "disable_ocsp_stapling": true
+ "disable_ocsp_stapling": true,
+ "disable_storage_check": true,
+ "disable_storage_clean": true
}
}
}
diff --git a/caddytest/integration/caddyfile_adapt/global_options_log_sampling.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options_log_sampling.caddyfiletest
new file mode 100644
index 000000000..12b73b2b7
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/global_options_log_sampling.caddyfiletest
@@ -0,0 +1,23 @@
+{
+ log {
+ sampling {
+ interval 300
+ first 50
+ thereafter 40
+ }
+ }
+}
+----------
+{
+ "logging": {
+ "logs": {
+ "default": {
+ "sampling": {
+ "interval": 300,
+ "first": 50,
+ "thereafter": 40
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/caddytest/integration/caddyfile_adapt/log_sampling.caddyfiletest b/caddytest/integration/caddyfile_adapt/log_sampling.caddyfiletest
new file mode 100644
index 000000000..b58622572
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/log_sampling.caddyfiletest
@@ -0,0 +1,45 @@
+:80 {
+ log {
+ sampling {
+ interval 300
+ first 50
+ thereafter 40
+ }
+ }
+}
+----------
+{
+ "logging": {
+ "logs": {
+ "default": {
+ "exclude": [
+ "http.log.access.log0"
+ ]
+ },
+ "log0": {
+ "sampling": {
+ "interval": 300,
+ "first": 50,
+ "thereafter": 40
+ },
+ "include": [
+ "http.log.access.log0"
+ ]
+ }
+ }
+ },
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":80"
+ ],
+ "logs": {
+ "default_logger_name": "log0"
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/caddytest/integration/caddyfile_adapt/metrics_merge_options.caddyfiletest b/caddytest/integration/caddyfile_adapt/metrics_merge_options.caddyfiletest
new file mode 100644
index 000000000..946b3d0c8
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/metrics_merge_options.caddyfiletest
@@ -0,0 +1,39 @@
+{
+ metrics
+ servers :80 {
+ metrics {
+ per_host
+ }
+ }
+}
+:80 {
+ respond "Hello"
+}
+
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":80"
+ ],
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Hello",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ "metrics": {
+ "per_host": true
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/caddytest/integration/caddyfile_adapt/metrics_perhost.caddyfiletest b/caddytest/integration/caddyfile_adapt/metrics_perhost.caddyfiletest
index 499215515..e362cecc9 100644
--- a/caddytest/integration/caddyfile_adapt/metrics_perhost.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/metrics_perhost.caddyfiletest
@@ -26,11 +26,11 @@
}
]
}
- ],
- "metrics": {
- "per_host": true
- }
+ ]
}
+ },
+ "metrics": {
+ "per_host": true
}
}
}
diff --git a/caddytest/integration/caddyfile_adapt/php_fastcgi_expanded_form.caddyfiletest b/caddytest/integration/caddyfile_adapt/php_fastcgi_expanded_form.caddyfiletest
index 8a57b9e37..df2e24885 100644
--- a/caddytest/integration/caddyfile_adapt/php_fastcgi_expanded_form.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/php_fastcgi_expanded_form.caddyfiletest
@@ -8,7 +8,7 @@ route {
}
not path */
}
- redir @canonicalPath {http.request.orig_uri.path}/ 308
+ redir @canonicalPath {orig_path}/{orig_?query} 308
# If the requested file does not exist, try index files
@indexFiles {
@@ -17,7 +17,7 @@ route {
split_path .php
}
}
- rewrite @indexFiles {http.matchers.file.relative}
+ rewrite @indexFiles {file_match.relative}
# Proxy PHP files to the FastCGI responder
@phpFiles {
@@ -50,7 +50,7 @@ route {
"handler": "static_response",
"headers": {
"Location": [
- "{http.request.orig_uri.path}/"
+ "{http.request.orig_uri.path}/{http.request.orig_uri.prefixed_query}"
]
},
"status_code": 308
diff --git a/caddytest/integration/caddyfile_adapt/php_fastcgi_handle_response.caddyfiletest b/caddytest/integration/caddyfile_adapt/php_fastcgi_handle_response.caddyfiletest
index 70a0780d1..3a857654f 100644
--- a/caddytest/integration/caddyfile_adapt/php_fastcgi_handle_response.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/php_fastcgi_handle_response.caddyfiletest
@@ -42,7 +42,7 @@
"handler": "static_response",
"headers": {
"Location": [
- "{http.request.orig_uri.path}/"
+ "{http.request.orig_uri.path}/{http.request.orig_uri.prefixed_query}"
]
},
"status_code": 308
@@ -58,6 +58,7 @@
"{http.request.uri.path}/index.php",
"index.php"
],
+ "try_policy": "first_exist_fallback",
"split_path": [
".php"
]
diff --git a/caddytest/integration/caddyfile_adapt/php_fastcgi_matcher.caddyfiletest b/caddytest/integration/caddyfile_adapt/php_fastcgi_matcher.caddyfiletest
index e5b331e31..4d1298fcc 100644
--- a/caddytest/integration/caddyfile_adapt/php_fastcgi_matcher.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/php_fastcgi_matcher.caddyfiletest
@@ -33,7 +33,7 @@ php_fastcgi @test localhost:9000
"handler": "static_response",
"headers": {
"Location": [
- "{http.request.orig_uri.path}/"
+ "{http.request.orig_uri.path}/{http.request.orig_uri.prefixed_query}"
]
},
"status_code": 308
@@ -73,7 +73,8 @@ php_fastcgi @test localhost:9000
"{http.request.uri.path}",
"{http.request.uri.path}/index.php",
"index.php"
- ]
+ ],
+ "try_policy": "first_exist_fallback"
}
}
]
diff --git a/caddytest/integration/caddyfile_adapt/php_fastcgi_subdirectives.caddyfiletest b/caddytest/integration/caddyfile_adapt/php_fastcgi_subdirectives.caddyfiletest
index a04d66fe4..9a9ab5ab0 100644
--- a/caddytest/integration/caddyfile_adapt/php_fastcgi_subdirectives.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/php_fastcgi_subdirectives.caddyfiletest
@@ -43,7 +43,7 @@ php_fastcgi localhost:9000 {
"handler": "static_response",
"headers": {
"Location": [
- "{http.request.orig_uri.path}/"
+ "{http.request.orig_uri.path}/{http.request.orig_uri.prefixed_query}"
]
},
"status_code": 308
@@ -59,6 +59,7 @@ php_fastcgi localhost:9000 {
"{http.request.uri.path}/index.php5",
"index.php5"
],
+ "try_policy": "first_exist_fallback",
"split_path": [
".php",
".php5"
diff --git a/caddytest/integration/caddyfile_adapt/php_fastcgi_try_files_override.caddyfiletest b/caddytest/integration/caddyfile_adapt/php_fastcgi_try_files_override.caddyfiletest
index a3381f675..75487a937 100644
--- a/caddytest/integration/caddyfile_adapt/php_fastcgi_try_files_override.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/php_fastcgi_try_files_override.caddyfiletest
@@ -46,7 +46,7 @@ php_fastcgi localhost:9000 {
"handler": "static_response",
"headers": {
"Location": [
- "{http.request.orig_uri.path}/"
+ "{http.request.orig_uri.path}/{http.request.orig_uri.prefixed_query}"
]
},
"status_code": 308
diff --git a/caddytest/integration/caddyfile_adapt/php_fastcgi_try_files_override_no_dir_index.caddyfiletest b/caddytest/integration/caddyfile_adapt/php_fastcgi_try_files_override_no_dir_index.caddyfiletest
new file mode 100644
index 000000000..203ab3b63
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/php_fastcgi_try_files_override_no_dir_index.caddyfiletest
@@ -0,0 +1,95 @@
+:8884
+
+php_fastcgi localhost:9000 {
+ # some php_fastcgi-specific subdirectives
+ split .php .php5
+ env VAR1 value1
+ env VAR2 value2
+ root /var/www
+ try_files {path} index.php
+ dial_timeout 3s
+ read_timeout 10s
+ write_timeout 20s
+
+ # passed through to reverse_proxy (directive order doesn't matter!)
+ lb_policy random
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":8884"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "file": {
+ "try_files": [
+ "{http.request.uri.path}",
+ "index.php"
+ ],
+ "try_policy": "first_exist_fallback",
+ "split_path": [
+ ".php",
+ ".php5"
+ ]
+ }
+ }
+ ],
+ "handle": [
+ {
+ "handler": "rewrite",
+ "uri": "{http.matchers.file.relative}"
+ }
+ ]
+ },
+ {
+ "match": [
+ {
+ "path": [
+ "*.php",
+ "*.php5"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "load_balancing": {
+ "selection_policy": {
+ "policy": "random"
+ }
+ },
+ "transport": {
+ "dial_timeout": 3000000000,
+ "env": {
+ "VAR1": "value1",
+ "VAR2": "value2"
+ },
+ "protocol": "fastcgi",
+ "read_timeout": 10000000000,
+ "root": "/var/www",
+ "split_path": [
+ ".php",
+ ".php5"
+ ],
+ "write_timeout": 20000000000
+ },
+ "upstreams": [
+ {
+ "dial": "localhost:9000"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/caddytest/integration/mockdns_test.go b/caddytest/integration/mockdns_test.go
index 1b2efb653..615116a3a 100644
--- a/caddytest/integration/mockdns_test.go
+++ b/caddytest/integration/mockdns_test.go
@@ -34,7 +34,7 @@ func (MockDNSProvider) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return nil
}
-// AppendsRecords appends DNS records to the zone.
+// AppendRecords appends DNS records to the zone.
func (MockDNSProvider) AppendRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
return nil, nil
}
diff --git a/cmd/caddy/main.go b/cmd/caddy/main.go
index f1aeda0a4..48fa149aa 100644
--- a/cmd/caddy/main.go
+++ b/cmd/caddy/main.go
@@ -1,8 +1,3 @@
-// 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/cmd/commandfuncs.go b/cmd/commandfuncs.go
index a5c357cd7..2adf95bb3 100644
--- a/cmd/commandfuncs.go
+++ b/cmd/commandfuncs.go
@@ -560,10 +560,15 @@ func cmdValidateConfig(fl Flags) (int, error) {
func cmdFmt(fl Flags) (int, error) {
configFile := fl.Arg(0)
- if configFile == "" {
+ configFlag := fl.String("config")
+ if (len(fl.Args()) > 1) || (configFlag != "" && configFile != "") {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("fmt does not support multiple files %s %s", configFlag, strings.Join(fl.Args(), " "))
+ }
+ if configFile == "" && configFlag == "" {
configFile = "Caddyfile"
+ } else if configFile == "" {
+ configFile = configFlag
}
-
// as a special case, read from stdin if the file name is "-"
if configFile == "-" {
input, err := io.ReadAll(os.Stdin)
diff --git a/cmd/commands.go b/cmd/commands.go
index ab4b66d77..259dd358f 100644
--- a/cmd/commands.go
+++ b/cmd/commands.go
@@ -388,6 +388,7 @@ When reading from stdin, the --overwrite flag has no effect: the result
is always printed to stdout.
`,
CobraFunc: func(cmd *cobra.Command) {
+ cmd.Flags().StringP("config", "c", "", "Configuration file")
cmd.Flags().BoolP("overwrite", "w", false, "Overwrite the input file with the results")
cmd.Flags().BoolP("diff", "d", false, "Print the differences between the input file and the formatted output")
cmd.RunE = WrapCommandFuncForCobra(cmdFmt)
@@ -409,12 +410,13 @@ latest versions. EXPERIMENTAL: May be changed or removed.
RegisterCommand(Command{
Name: "add-package",
- Usage: "<packages...>",
+ Usage: "<package[@version]...>",
Short: "Adds Caddy packages (EXPERIMENTAL)",
Long: `
Downloads an updated Caddy binary with the specified packages (module/plugin)
-added. Retains existing packages. Returns an error if the any of packages are
-already included. EXPERIMENTAL: May be changed or removed.
+added, with an optional version specified (e.g., "package@version"). Retains
+existing packages. Returns an error if any of the specified packages are already
+included. EXPERIMENTAL: May be changed or removed.
`,
CobraFunc: func(cmd *cobra.Command) {
cmd.Flags().BoolP("keep-backup", "k", false, "Keep the backed up binary, instead of deleting it")
diff --git a/cmd/packagesfuncs.go b/cmd/packagesfuncs.go
index 0784f7ee6..695232001 100644
--- a/cmd/packagesfuncs.go
+++ b/cmd/packagesfuncs.go
@@ -46,6 +46,25 @@ func cmdUpgrade(fl Flags) (int, error) {
return upgradeBuild(pluginPkgs, fl)
}
+func splitModule(arg string) (module, version string, err error) {
+ const versionSplit = "@"
+
+ // accommodate module paths that have @ in them, but we can only tolerate that if there's also
+ // a version, otherwise we don't know if it's a version separator or part of the file path
+ lastVersionSplit := strings.LastIndex(arg, versionSplit)
+ if lastVersionSplit < 0 {
+ module = arg
+ } else {
+ module, version = arg[:lastVersionSplit], arg[lastVersionSplit+1:]
+ }
+
+ if module == "" {
+ err = fmt.Errorf("module name is required")
+ }
+
+ return
+}
+
func cmdAddPackage(fl Flags) (int, error) {
if len(fl.Args()) == 0 {
return caddy.ExitCodeFailedStartup, fmt.Errorf("at least one package name must be specified")
@@ -60,10 +79,15 @@ func cmdAddPackage(fl Flags) (int, error) {
}
for _, arg := range fl.Args() {
- if _, ok := pluginPkgs[arg]; ok {
+ module, version, err := splitModule(arg)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid module name: %v", err)
+ }
+ // only allow a version to be specified if it's different from the existing version
+ if _, ok := pluginPkgs[module]; ok && !(version != "" && pluginPkgs[module].Version != version) {
return caddy.ExitCodeFailedStartup, fmt.Errorf("package is already added")
}
- pluginPkgs[arg] = struct{}{}
+ pluginPkgs[module] = pluginPackage{Version: version, Path: module}
}
return upgradeBuild(pluginPkgs, fl)
@@ -83,7 +107,11 @@ func cmdRemovePackage(fl Flags) (int, error) {
}
for _, arg := range fl.Args() {
- if _, ok := pluginPkgs[arg]; !ok {
+ module, _, err := splitModule(arg)
+ if err != nil {
+ return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid module name: %v", err)
+ }
+ if _, ok := pluginPkgs[module]; !ok {
// package does not exist
return caddy.ExitCodeFailedStartup, fmt.Errorf("package is not added")
}
@@ -93,7 +121,7 @@ func cmdRemovePackage(fl Flags) (int, error) {
return upgradeBuild(pluginPkgs, fl)
}
-func upgradeBuild(pluginPkgs map[string]struct{}, fl Flags) (int, error) {
+func upgradeBuild(pluginPkgs map[string]pluginPackage, fl Flags) (int, error) {
l := caddy.Log()
thisExecPath, err := os.Executable()
@@ -120,8 +148,8 @@ func upgradeBuild(pluginPkgs map[string]struct{}, fl Flags) (int, error) {
"os": {runtime.GOOS},
"arch": {runtime.GOARCH},
}
- for pkg := range pluginPkgs {
- qs.Add("p", pkg)
+ for _, pkgInfo := range pluginPkgs {
+ qs.Add("p", pkgInfo.String())
}
// initiate the build
@@ -276,14 +304,14 @@ func downloadBuild(qs url.Values) (*http.Response, error) {
return resp, nil
}
-func getPluginPackages(modules []moduleInfo) (map[string]struct{}, error) {
- pluginPkgs := make(map[string]struct{})
+func getPluginPackages(modules []moduleInfo) (map[string]pluginPackage, error) {
+ pluginPkgs := make(map[string]pluginPackage)
for _, mod := range modules {
if mod.goModule.Replace != nil {
return nil, fmt.Errorf("cannot auto-upgrade when Go module has been replaced: %s => %s",
mod.goModule.Path, mod.goModule.Replace.Path)
}
- pluginPkgs[mod.goModule.Path] = struct{}{}
+ pluginPkgs[mod.goModule.Path] = pluginPackage{Version: mod.goModule.Version, Path: mod.goModule.Path}
}
return pluginPkgs, nil
}
@@ -312,3 +340,15 @@ func writeCaddyBinary(path string, body *io.ReadCloser, fileInfo os.FileInfo) er
}
const downloadPath = "https://caddyserver.com/api/download"
+
+type pluginPackage struct {
+ Version string
+ Path string
+}
+
+func (p pluginPackage) String() string {
+ if p.Version == "" {
+ return p.Path
+ }
+ return p.Path + "@" + p.Version
+}
diff --git a/cmd/storagefuncs.go b/cmd/storagefuncs.go
index 60e25485d..3c4219719 100644
--- a/cmd/storagefuncs.go
+++ b/cmd/storagefuncs.go
@@ -21,6 +21,7 @@ import (
"errors"
"fmt"
"io"
+ "io/fs"
"os"
"github.com/caddyserver/certmagic"
@@ -190,12 +191,20 @@ func cmdExportStorage(fl Flags) (int, error) {
for _, k := range keys {
info, err := stor.Stat(ctx, k)
if err != nil {
+ if errors.Is(err, fs.ErrNotExist) {
+ caddy.Log().Warn(fmt.Sprintf("key: %s removed while export is in-progress", k))
+ continue
+ }
return caddy.ExitCodeFailedQuit, err
}
if info.IsTerminal {
v, err := stor.Load(ctx, k)
if err != nil {
+ if errors.Is(err, fs.ErrNotExist) {
+ caddy.Log().Warn(fmt.Sprintf("key: %s removed while export is in-progress", k))
+ continue
+ }
return caddy.ExitCodeFailedQuit, err
}
diff --git a/context.go b/context.go
index 17a8aa4f8..d4d7afacf 100644
--- a/context.go
+++ b/context.go
@@ -110,6 +110,8 @@ func (ctx *Context) GetMetricsRegistry() *prometheus.Registry {
func (ctx *Context) initMetrics() {
ctx.metricsRegistry.MustRegister(
collectors.NewBuildInfoCollector(),
+ collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
+ collectors.NewGoCollector(),
adminMetrics.requestCount,
adminMetrics.requestErrors,
globalMetrics.configSuccess,
@@ -555,12 +557,8 @@ func (ctx Context) Slogger() *slog.Logger {
if mod == nil {
return slog.New(zapslog.NewHandler(Log().Core(), nil))
}
-
- return slog.New(zapslog.NewHandler(
- ctx.cfg.Logging.Logger(mod).Core(),
- &zapslog.HandlerOptions{
- LoggerName: string(mod.CaddyModule().ID),
- },
+ return slog.New(zapslog.NewHandler(ctx.cfg.Logging.Logger(mod).Core(),
+ zapslog.WithName(string(mod.CaddyModule().ID)),
))
}
diff --git a/filepath.go b/filepath.go
new file mode 100644
index 000000000..aad907799
--- /dev/null
+++ b/filepath.go
@@ -0,0 +1,39 @@
+// Copyright 2015 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build !windows
+
+package caddy
+
+import (
+ "os"
+ "path/filepath"
+)
+
+// FastAbs is an optimized version of filepath.Abs for Unix systems,
+// since we don't expect the working directory to ever change once
+// Caddy is running. Avoid the os.Getwd() syscall overhead.
+// It's overall the same as stdlib's implementation, the difference
+// being cached working directory.
+func FastAbs(path string) (string, error) {
+ if filepath.IsAbs(path) {
+ return filepath.Clean(path), nil
+ }
+ if wderr != nil {
+ return "", wderr
+ }
+ return filepath.Join(wd, path), nil
+}
+
+var wd, wderr = os.Getwd()
diff --git a/filepath_windows.go b/filepath_windows.go
new file mode 100644
index 000000000..aa70955e9
--- /dev/null
+++ b/filepath_windows.go
@@ -0,0 +1,27 @@
+// Copyright 2015 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package caddy
+
+import (
+ "path/filepath"
+)
+
+// FastAbs can't be optimized on Windows because there
+// are special file paths that require the use of syscall.FullPath
+// to handle correctly.
+// Just call stdlib's implementation which uses that function.
+func FastAbs(path string) (string, error) {
+ return filepath.Abs(path)
+}
diff --git a/filesystem.go b/filesystem.go
index 9785f57d4..d6679e90b 100644
--- a/filesystem.go
+++ b/filesystem.go
@@ -1,3 +1,17 @@
+// Copyright 2015 Matthew Holt and The Caddy Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
package caddy
import "io/fs"
diff --git a/go.mod b/go.mod
index 67fe8e59c..eac017d10 100644
--- a/go.mod
+++ b/go.mod
@@ -9,17 +9,17 @@ require (
github.com/Masterminds/sprig/v3 v3.3.0
github.com/alecthomas/chroma/v2 v2.14.0
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
- github.com/caddyserver/certmagic v0.21.4
+ github.com/caddyserver/certmagic v0.21.5-0.20241219182349-258b5328e49e
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.21.0
github.com/google/uuid v1.6.0
- github.com/klauspost/compress v1.17.10
- github.com/klauspost/cpuid/v2 v2.2.8
- github.com/mholt/acmez/v2 v2.0.3
+ github.com/klauspost/compress v1.17.11
+ github.com/klauspost/cpuid/v2 v2.2.9
+ github.com/mholt/acmez/v3 v3.0.0
github.com/prometheus/client_golang v1.19.1
- github.com/quic-go/quic-go v0.48.0
+ github.com/quic-go/quic-go v0.48.2
github.com/smallstep/certificates v0.26.1
github.com/smallstep/nosql v0.6.1
github.com/smallstep/truststore v0.13.0
@@ -27,22 +27,22 @@ require (
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.9.0
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53
- github.com/yuin/goldmark v1.7.4
+ github.com/yuin/goldmark v1.7.8
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0
go.opentelemetry.io/contrib/propagators/autoprop v0.42.0
- go.opentelemetry.io/otel v1.24.0
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0
- go.opentelemetry.io/otel/sdk v1.21.0
+ go.opentelemetry.io/otel v1.31.0
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0
+ go.opentelemetry.io/otel/sdk v1.31.0
go.uber.org/automaxprocs v1.6.0
go.uber.org/zap v1.27.0
- go.uber.org/zap/exp v0.2.0
- golang.org/x/crypto v0.27.0
- golang.org/x/crypto/x509roots/fallback v0.0.0-20240930154113-a0819fbb0244
- golang.org/x/net v0.29.0
- golang.org/x/sync v0.8.0
- golang.org/x/term v0.24.0
- golang.org/x/time v0.6.0
+ go.uber.org/zap/exp v0.3.0
+ golang.org/x/crypto v0.31.0
+ golang.org/x/crypto/x509roots/fallback v0.0.0-20241104001025-71ed71b4faf9
+ golang.org/x/net v0.33.0
+ golang.org/x/sync v0.10.0
+ golang.org/x/term v0.27.0
+ golang.org/x/time v0.7.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
)
@@ -56,12 +56,12 @@ require (
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
- github.com/golang/glog v1.2.0 // indirect
+ github.com/golang/glog v1.2.2 // indirect
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect
github.com/google/go-tpm v0.9.0 // indirect
github.com/google/go-tspi v0.3.0 // indirect
github.com/google/pprof v0.0.0-20231212022811-ec68065c825e // indirect
- github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.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.5.1 // indirect
@@ -76,8 +76,8 @@ require (
go.opentelemetry.io/contrib/propagators/ot v1.17.0 // indirect
go.uber.org/mock v0.4.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect
)
require (
@@ -86,9 +86,9 @@ require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
- github.com/cenkalti/backoff/v4 v4.2.1 // indirect
+ github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
- github.com/cespare/xxhash/v2 v2.2.0
+ github.com/cespare/xxhash/v2 v2.3.0
github.com/chzyer/readline v1.5.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/dgraph-io/badger v1.6.2 // indirect
@@ -99,7 +99,7 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-kit/kit v0.13.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
- github.com/go-logr/logr v1.4.1 // indirect
+ github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
@@ -139,19 +139,19 @@ require (
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/urfave/cli v1.22.14 // indirect
go.etcd.io/bbolt v1.3.9 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
- go.opentelemetry.io/otel/metric v1.24.0 // indirect
- go.opentelemetry.io/otel/trace v1.24.0
- go.opentelemetry.io/proto/otlp v1.0.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 // indirect
+ go.opentelemetry.io/otel/metric v1.31.0 // indirect
+ go.opentelemetry.io/otel/trace v1.31.0
+ go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.step.sm/cli-utils v0.9.0 // indirect
go.step.sm/crypto v0.45.0
go.step.sm/linkedca v0.20.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/mod v0.18.0 // indirect
- golang.org/x/sys v0.25.0
- golang.org/x/text v0.18.0 // indirect
+ golang.org/x/sys v0.28.0
+ golang.org/x/text v0.21.0 // indirect
golang.org/x/tools v0.22.0 // indirect
- google.golang.org/grpc v1.63.2 // indirect
- google.golang.org/protobuf v1.34.1 // indirect
+ google.golang.org/grpc v1.67.1 // indirect
+ google.golang.org/protobuf v1.35.1 // indirect
howett.net/plist v1.0.0 // indirect
)
diff --git a/go.sum b/go.sum
index 0956dffd2..4855f2e0a 100644
--- a/go.sum
+++ b/go.sum
@@ -7,9 +7,9 @@ cloud.google.com/go/auth v0.4.1 h1:Z7YNIhlWRtrnKlZke7z3GMqzvuYzdc2z98F9D1NV5Hg=
cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro=
cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
-cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg=
-cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
-cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
+cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
+cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
+cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0=
cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE=
cloud.google.com/go/kms v1.16.0 h1:1yZsRPhmargZOmY+fVAh8IKiR9HzCb0U1zsxb5g2nRY=
@@ -89,17 +89,17 @@ 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.4 h1:e7VobB8rffHv8ZZpSiZtEwnLDHUwLVYLWzWSa1FfKI0=
-github.com/caddyserver/certmagic v0.21.4/go.mod h1:swUXjQ1T9ZtMv95qj7/InJvWLXURU85r+CfG0T+ZbDE=
+github.com/caddyserver/certmagic v0.21.5-0.20241219182349-258b5328e49e h1:AFPVZ2IOgM6NdL2GwMMf+V7NDU3IQ9t4aPbcNbHsitY=
+github.com/caddyserver/certmagic v0.21.5-0.20241219182349-258b5328e49e/go.mod h1:n1sCo7zV1Ez2j+89wrzDxo4N/T1Ws/Vx8u5NvuBFabw=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
-github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
-github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
+github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
-github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
@@ -172,8 +172,8 @@ github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KE
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
-github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
@@ -186,8 +186,8 @@ github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPh
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/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY=
+github.com/golang/glog v1.2.2/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=
@@ -239,8 +239,8 @@ github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoF
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/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
@@ -302,10 +302,10 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV
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.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0=
-github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
-github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
-github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
+github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
+github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
+github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -344,8 +344,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
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.3 h1:CgDBlEwg3QBp6s45tPQmFIBrkRIkBT4rW4orMM6p4sw=
-github.com/mholt/acmez/v2 v2.0.3/go.mod h1:pQ1ysaDeGrIMvJ9dfJMk5kJNkn7L2sb3UhyrX6Q91cw=
+github.com/mholt/acmez/v3 v3.0.0 h1:r1NcjuWR0VaKP2BTjDK9LRFBw/WvURx3jlaEUl9Ht8E=
+github.com/mholt/acmez/v3 v3.0.0/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
@@ -392,11 +392,11 @@ 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.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
-github.com/quic-go/quic-go v0.48.0 h1:2TCyvBrMu1Z25rvIAlnp2dPT4lgh/uTqLqiXVpp5AeU=
-github.com/quic-go/quic-go v0.48.0/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
+github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
+github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
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=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
@@ -506,8 +506,8 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
-github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
+github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
@@ -524,8 +524,8 @@ 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=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.opentelemetry.io/contrib/propagators/autoprop v0.42.0 h1:s2RzYOAqHVgG23q8fPWYChobUoZM6rJZ98EnylJr66w=
go.opentelemetry.io/contrib/propagators/autoprop v0.42.0/go.mod h1:Mv/tWNtZn+NbALDb2XcItP0OM3lWWZjAfSroINxfW+Y=
go.opentelemetry.io/contrib/propagators/aws v1.17.0 h1:IX8d7l2uRw61BlmZBOTQFaK+y22j6vytMVTs9wFrO+c=
@@ -536,20 +536,20 @@ go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 h1:Zbpbmwav32Ea5jSotpmkWE
go.opentelemetry.io/contrib/propagators/jaeger v1.17.0/go.mod h1:tcTUAlmO8nuInPDSBVfG+CP6Mzjy5+gNV4mPxMbL0IA=
go.opentelemetry.io/contrib/propagators/ot v1.17.0 h1:ufo2Vsz8l76eI47jFjuVyjyB3Ae2DmfiCV/o6Vc8ii0=
go.opentelemetry.io/contrib/propagators/ot v1.17.0/go.mod h1:SbKPj5XGp8K/sGm05XblaIABgMgw2jDczP8gGeuaVLk=
-go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
-go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0=
-go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
-go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
-go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
-go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
-go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
-go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
-go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
-go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
+go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
+go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 h1:FFeLy03iVTXP6ffeN2iXrxfGsZGCjVx0/4KlizjyBwU=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0/go.mod h1:TMu73/k1CP8nBUpDLc71Wj/Kf7ZS9FK5b53VapRsP9o=
+go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
+go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
+go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
+go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
+go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
+go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
+go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
+go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.step.sm/cli-utils v0.9.0 h1:55jYcsQbnArNqepZyAwcato6Zy2MoZDRkWW+jF+aPfQ=
go.step.sm/cli-utils v0.9.0/go.mod h1:Y/CRoWl1FVR9j+7PnAewufAwKmBOTzR6l9+7EYGAnp8=
go.step.sm/crypto v0.45.0 h1:Z0WYAaaOYrJmKP9sJkPW+6wy3pgN3Ija8ek/D4serjc=
@@ -577,8 +577,8 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
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=
+go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
+go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
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=
@@ -595,10 +595,10 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
-golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
-golang.org/x/crypto/x509roots/fallback v0.0.0-20240930154113-a0819fbb0244 h1:3uziZWNwkTfxhMOxJB13NpTR+svHLMMVDhTrEyZOd3k=
-golang.org/x/crypto/x509roots/fallback v0.0.0-20240930154113-a0819fbb0244/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
+golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/crypto/x509roots/fallback v0.0.0-20241104001025-71ed71b4faf9 h1:4cEcP5+OjGppY79LCQ5Go2B1Boix2x0v6pvA01P3FoA=
+golang.org/x/crypto/x509roots/fallback v0.0.0-20241104001025-71ed71b4faf9/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=
@@ -628,14 +628,14 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
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.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
-golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
+golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
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/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
+golang.org/x/oauth2 v0.22.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=
@@ -644,8 +644,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
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/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
+golang.org/x/sync v0.10.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=
@@ -675,16 +675,16 @@ 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.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
-golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
+golang.org/x/sys v0.28.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=
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.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
-golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
+golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
+golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
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=
@@ -695,12 +695,12 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
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.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
-golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
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.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
-golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
+golang.org/x/time v0.7.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=
@@ -739,18 +739,18 @@ google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk
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/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg=
+google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
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=
-google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
+google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
+google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
+google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/listen.go b/listen.go
index f5c2086a6..1a7051bbf 100644
--- a/listen.go
+++ b/listen.go
@@ -30,7 +30,7 @@ import (
"go.uber.org/zap"
)
-func reuseUnixSocket(network, addr string) (any, error) {
+func reuseUnixSocket(_, _ string) (any, error) {
return nil, nil
}
diff --git a/listeners.go b/listeners.go
index 3a2a5180f..b22df77ba 100644
--- a/listeners.go
+++ b/listeners.go
@@ -139,7 +139,7 @@ func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net
}
// check to see if plugin provides listener
- if ln, err := getListenerFromPlugin(ctx, na.Network, na.JoinHostPort(portOffset), config); ln != nil || err != nil {
+ if ln, err := getListenerFromPlugin(ctx, na.Network, na.Host, na.port(), portOffset, config); ln != nil || err != nil {
return ln, err
}
@@ -305,25 +305,6 @@ 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
@@ -399,25 +380,28 @@ func SplitNetworkAddress(a string) (network, host, port string, err error) {
if slashFound {
network = strings.ToLower(strings.TrimSpace(beforeSlash))
a = afterSlash
+ if IsUnixNetwork(network) || IsFdNetwork(network) {
+ host = a
+ return
+ }
}
- if IsUnixNetwork(network) || IsFdNetwork(network) {
- host = a
- return
- }
+
host, port, err = net.SplitHostPort(a)
- if err == nil || a == "" {
- return
- }
- // in general, if there was an error, it was likely "missing port",
- // so try adding a bogus port to take advantage of standard library's
- // robust parser, then strip the artificial port before returning
- // (don't overwrite original error though; might still be relevant)
- var err2 error
- host, port, err2 = net.SplitHostPort(a + ":0")
- if err2 == nil {
- err = nil
+ firstErr := err
+
+ if err != nil {
+ // in general, if there was an error, it was likely "missing port",
+ // so try removing square brackets around an IPv6 host, adding a bogus
+ // port to take advantage of standard library's robust parser, then
+ // strip the artificial port.
+ host, _, err = net.SplitHostPort(net.JoinHostPort(strings.Trim(a, "[]"), "0"))
port = ""
}
+
+ if err != nil {
+ err = errors.Join(firstErr, err)
+ }
+
return
}
@@ -674,11 +658,11 @@ var unixSocketsMu sync.Mutex
// getListenerFromPlugin returns a listener on the given network and address
// if a plugin has registered the network name. It may return (nil, nil) if
// no plugin can provide a listener.
-func getListenerFromPlugin(ctx context.Context, network, addr string, config net.ListenConfig) (any, error) {
+func getListenerFromPlugin(ctx context.Context, network, host, port string, portOffset uint, config net.ListenConfig) (any, error) {
// get listener from plugin if network type is registered
if getListener, ok := networkTypes[network]; ok {
Log().Debug("getting listener from plugin", zap.String("network", network))
- return getListener(ctx, network, addr, config)
+ return getListener(ctx, network, host, port, portOffset, config)
}
return nil, nil
@@ -692,7 +676,7 @@ func listenerKey(network, addr string) string {
// The listeners must be capable of overlapping: with Caddy, new configs are loaded
// before old ones are unloaded, so listeners may overlap briefly if the configs
// both need the same listener. EXPERIMENTAL and subject to change.
-type ListenerFunc func(ctx context.Context, network, addr string, cfg net.ListenConfig) (any, error)
+type ListenerFunc func(ctx context.Context, network, host, portRange string, portOffset uint, cfg net.ListenConfig) (any, error)
var networkTypes = map[string]ListenerFunc{}
diff --git a/listeners_test.go b/listeners_test.go
index f8a13cafc..03945308e 100644
--- a/listeners_test.go
+++ b/listeners_test.go
@@ -31,7 +31,7 @@ func TestSplitNetworkAddress(t *testing.T) {
}{
{
input: "",
- expectErr: true,
+ expectHost: "",
},
{
input: "foo",
@@ -42,7 +42,7 @@ func TestSplitNetworkAddress(t *testing.T) {
},
{
input: "::",
- expectErr: true,
+ expectHost: "::",
},
{
input: "[::]",
@@ -77,7 +77,7 @@ func TestSplitNetworkAddress(t *testing.T) {
{
input: "udp/",
expectNetwork: "udp",
- expectErr: true,
+ expectHost: "",
},
{
input: "unix//foo/bar",
@@ -185,7 +185,8 @@ func TestParseNetworkAddress(t *testing.T) {
}{
{
input: "",
- expectErr: true,
+ expectAddr: NetworkAddress{
+ },
},
{
input: ":",
@@ -311,7 +312,8 @@ func TestParseNetworkAddressWithDefaults(t *testing.T) {
}{
{
input: "",
- expectErr: true,
+ expectAddr: NetworkAddress{
+ },
},
{
input: ":",
diff --git a/modules/caddyevents/app.go b/modules/caddyevents/app.go
index 2e4a6f2c0..e78b00f8c 100644
--- a/modules/caddyevents/app.go
+++ b/modules/caddyevents/app.go
@@ -262,7 +262,7 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E
return nil, false
})
- logger = logger.With(zap.Any("data", e.Data))
+ logger = logger.WithLazy(zap.Any("data", e.Data))
logger.Debug("event")
diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go
index 7dc2bee72..850d3aa8f 100644
--- a/modules/caddyhttp/app.go
+++ b/modules/caddyhttp/app.go
@@ -15,6 +15,7 @@
package caddyhttp
import (
+ "cmp"
"context"
"crypto/tls"
"fmt"
@@ -142,6 +143,10 @@ type App struct {
// affect functionality.
Servers map[string]*Server `json:"servers,omitempty"`
+ // If set, metrics observations will be enabled.
+ // This setting is EXPERIMENTAL and subject to change.
+ Metrics *Metrics `json:"metrics,omitempty"`
+
ctx caddy.Context
logger *zap.Logger
tlsApp *caddytls.TLS
@@ -184,6 +189,10 @@ func (app *App) Provision(ctx caddy.Context) error {
return err
}
+ if app.Metrics != nil {
+ app.Metrics.init = sync.Once{}
+ app.Metrics.httpMetrics = &httpMetrics{}
+ }
// prepare each server
oldContext := ctx.Context
for srvName, srv := range app.Servers {
@@ -196,6 +205,15 @@ func (app *App) Provision(ctx caddy.Context) error {
srv.errorLogger = app.logger.Named("log.error")
srv.shutdownAtMu = new(sync.RWMutex)
+ if srv.Metrics != nil {
+ srv.logger.Warn("per-server 'metrics' is deprecated; use 'metrics' in the root 'http' app instead")
+ app.Metrics = cmp.Or[*Metrics](app.Metrics, &Metrics{
+ init: sync.Once{},
+ httpMetrics: &httpMetrics{},
+ })
+ app.Metrics.PerHost = app.Metrics.PerHost || srv.Metrics.PerHost
+ }
+
// only enable access logs if configured
if srv.Logs != nil {
srv.accessLogger = app.logger.Named("log.access")
@@ -342,16 +360,11 @@ func (app *App) Provision(ctx caddy.Context) error {
srv.listenerWrappers = append([]caddy.ListenerWrapper{new(tlsPlaceholderWrapper)}, srv.listenerWrappers...)
}
}
-
// pre-compile the primary handler chain, and be sure to wrap it in our
// route handler so that important security checks are done, etc.
primaryRoute := emptyHandler
if srv.Routes != nil {
- if srv.Metrics != nil {
- srv.Metrics.init = sync.Once{}
- srv.Metrics.httpMetrics = &httpMetrics{}
- }
- err := srv.Routes.ProvisionHandlers(ctx, srv.Metrics)
+ err := srv.Routes.ProvisionHandlers(ctx, app.Metrics)
if err != nil {
return fmt.Errorf("server %s: setting up route handlers: %v", srvName, err)
}
@@ -370,7 +383,7 @@ func (app *App) Provision(ctx caddy.Context) error {
// provision the named routes (they get compiled at runtime)
for name, route := range srv.NamedRoutes {
- err := route.Provision(ctx, srv.Metrics)
+ err := route.Provision(ctx, app.Metrics)
if err != nil {
return fmt.Errorf("server %s: setting up named route '%s' handlers: %v", name, srvName, err)
}
@@ -388,6 +401,9 @@ func (app *App) Provision(ctx caddy.Context) error {
if srv.IdleTimeout == 0 {
srv.IdleTimeout = defaultIdleTimeout
}
+ if srv.ReadHeaderTimeout == 0 {
+ srv.ReadHeaderTimeout = defaultReadHeaderTimeout // see #6663
+ }
}
ctx.Context = oldContext
return nil
@@ -689,16 +705,7 @@ func (app *App) Stop() error {
return
}
- // First close h3server then close listeners unlike stdlib for several reasons:
- // 1, udp has only a single socket, once closed, no more data can be read and
- // written. In contrast, closing tcp listeners won't affect established connections.
- // This have something to do with graceful shutdown when upstream implements it.
- // 2, h3server will only close listeners it's registered (quic listeners). Closing
- // listener first and these listeners maybe unregistered thus won't be closed. caddy
- // distinguishes quic-listener and underlying datagram sockets.
-
- // TODO: CloseGracefully, once implemented upstream (see https://github.com/quic-go/quic-go/issues/2103)
- if err := server.h3server.Close(); err != nil {
+ if err := server.h3server.Shutdown(ctx); err != nil {
app.logger.Error("HTTP/3 server shutdown",
zap.Error(err),
zap.Strings("addresses", server.Listen))
@@ -766,11 +773,20 @@ func (app *App) httpsPort() int {
return app.HTTPSPort
}
-// defaultIdleTimeout is the default HTTP server timeout
-// for closing idle connections; useful to avoid resource
-// exhaustion behind hungry CDNs, for example (we've had
-// several complaints without this).
-const defaultIdleTimeout = caddy.Duration(5 * time.Minute)
+const (
+ // defaultIdleTimeout is the default HTTP server timeout
+ // for closing idle connections; useful to avoid resource
+ // exhaustion behind hungry CDNs, for example (we've had
+ // several complaints without this).
+ defaultIdleTimeout = caddy.Duration(5 * time.Minute)
+
+ // defaultReadHeaderTimeout is the default timeout for
+ // reading HTTP headers from clients. Headers are generally
+ // small, often less than 1 KB, so it shouldn't take a
+ // long time even on legitimately slow connections or
+ // busy servers to read it.
+ defaultReadHeaderTimeout = caddy.Duration(time.Minute)
+)
// Interface guards
var (
diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go
index e1e71f4a0..aacafc92e 100644
--- a/modules/caddyhttp/caddyhttp.go
+++ b/modules/caddyhttp/caddyhttp.go
@@ -36,10 +36,26 @@ func init() {
// RequestMatcher is a type that can match to a request.
// A route matcher MUST NOT modify the request, with the
// only exception being its context.
+//
+// Deprecated: Matchers should now implement RequestMatcherWithError.
+// You may remove any interface guards for RequestMatcher
+// but keep your Match() methods for backwards compatibility.
type RequestMatcher interface {
Match(*http.Request) bool
}
+// RequestMatcherWithError is like RequestMatcher but can return an error.
+// An error during matching will abort the request middleware chain and
+// invoke the error middleware chain.
+//
+// This will eventually replace RequestMatcher. Matcher modules
+// should implement both interfaces, and once all modules have
+// been updated to use RequestMatcherWithError, the RequestMatcher
+// interface may eventually be dropped.
+type RequestMatcherWithError interface {
+ MatchWithError(*http.Request) (bool, error)
+}
+
// Handler is like http.Handler except ServeHTTP may return an error.
//
// If any handler encounters an error, it should be returned for proper
diff --git a/modules/caddyhttp/celmatcher.go b/modules/caddyhttp/celmatcher.go
index 2a03ebba7..3d118ea79 100644
--- a/modules/caddyhttp/celmatcher.go
+++ b/modules/caddyhttp/celmatcher.go
@@ -202,17 +202,25 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
// Match returns true if r matches m.
func (m MatchExpression) Match(r *http.Request) bool {
+ match, err := m.MatchWithError(r)
+ if err != nil {
+ SetVar(r.Context(), MatcherErrorVarKey, err)
+ }
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchExpression) MatchWithError(r *http.Request) (bool, error) {
celReq := celHTTPRequest{r}
out, _, err := m.prg.Eval(celReq)
if err != nil {
m.log.Error("evaluating expression", zap.Error(err))
- SetVar(r.Context(), MatcherErrorVarKey, err)
- return false
+ return false, err
}
if outBool, ok := out.Value().(bool); ok {
- return outBool
+ return outBool, nil
}
- return false
+ return false, nil
}
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
@@ -380,7 +388,7 @@ type CELLibraryProducer interface {
// limited set of function signatures. For strong type validation you may need
// to provide a custom macro which does a more detailed analysis of the CEL
// literal provided to the macro as an argument.
-func CELMatcherImpl(macroName, funcName string, matcherDataTypes []*cel.Type, fac CELMatcherFactory) (cel.Library, error) {
+func CELMatcherImpl(macroName, funcName string, matcherDataTypes []*cel.Type, fac any) (cel.Library, error) {
requestType := cel.ObjectType("http.Request")
var macro parser.Macro
switch len(matcherDataTypes) {
@@ -424,7 +432,11 @@ func CELMatcherImpl(macroName, funcName string, matcherDataTypes []*cel.Type, fa
}
// CELMatcherFactory converts a constant CEL value into a RequestMatcher.
-type CELMatcherFactory func(data ref.Val) (RequestMatcher, error)
+// Deprecated: Use CELMatcherWithErrorFactory instead.
+type CELMatcherFactory = func(data ref.Val) (RequestMatcher, error)
+
+// CELMatcherWithErrorFactory converts a constant CEL value into a RequestMatcherWithError.
+type CELMatcherWithErrorFactory = func(data ref.Val) (RequestMatcherWithError, error)
// matcherCELLibrary is a simplistic configurable cel.Library implementation.
type matcherCELLibrary struct {
@@ -452,7 +464,7 @@ func (lib *matcherCELLibrary) ProgramOptions() []cel.ProgramOption {
// that takes a single argument, and optimizes the implementation to precompile
// the matcher and return a function that references the precompiled and
// provisioned matcher.
-func CELMatcherDecorator(funcName string, fac CELMatcherFactory) interpreter.InterpretableDecorator {
+func CELMatcherDecorator(funcName string, fac any) interpreter.InterpretableDecorator {
return func(i interpreter.Interpretable) (interpreter.Interpretable, error) {
call, ok := i.(interpreter.InterpretableCall)
if !ok {
@@ -481,35 +493,92 @@ func CELMatcherDecorator(funcName string, fac CELMatcherFactory) interpreter.Int
// and matcher provisioning should be handled at dynamically.
return i, nil
}
- matcher, err := fac(matcherData.Value())
- if err != nil {
- return nil, err
+
+ if factory, ok := fac.(CELMatcherWithErrorFactory); ok {
+ matcher, err := factory(matcherData.Value())
+ if err != nil {
+ return nil, err
+ }
+ return interpreter.NewCall(
+ i.ID(), funcName, funcName+"_opt",
+ []interpreter.Interpretable{reqAttr},
+ func(args ...ref.Val) ref.Val {
+ // The request value, guaranteed to be of type celHTTPRequest
+ celReq := args[0]
+ // If needed this call could be changed to convert the value
+ // to a *http.Request using CEL's ConvertToNative method.
+ httpReq := celReq.Value().(celHTTPRequest)
+ match, err := matcher.MatchWithError(httpReq.Request)
+ if err != nil {
+ return types.WrapErr(err)
+ }
+ return types.Bool(match)
+ },
+ ), nil
}
- return interpreter.NewCall(
- i.ID(), funcName, funcName+"_opt",
- []interpreter.Interpretable{reqAttr},
- func(args ...ref.Val) ref.Val {
- // The request value, guaranteed to be of type celHTTPRequest
- celReq := args[0]
- // If needed this call could be changed to convert the value
- // to a *http.Request using CEL's ConvertToNative method.
- httpReq := celReq.Value().(celHTTPRequest)
- return types.Bool(matcher.Match(httpReq.Request))
- },
- ), nil
+
+ if factory, ok := fac.(CELMatcherFactory); ok {
+ matcher, err := factory(matcherData.Value())
+ if err != nil {
+ return nil, err
+ }
+ return interpreter.NewCall(
+ i.ID(), funcName, funcName+"_opt",
+ []interpreter.Interpretable{reqAttr},
+ func(args ...ref.Val) ref.Val {
+ // The request value, guaranteed to be of type celHTTPRequest
+ celReq := args[0]
+ // If needed this call could be changed to convert the value
+ // to a *http.Request using CEL's ConvertToNative method.
+ httpReq := celReq.Value().(celHTTPRequest)
+ if m, ok := matcher.(RequestMatcherWithError); ok {
+ match, err := m.MatchWithError(httpReq.Request)
+ if err != nil {
+ return types.WrapErr(err)
+ }
+ return types.Bool(match)
+ }
+ return types.Bool(matcher.Match(httpReq.Request))
+ },
+ ), nil
+ }
+
+ return nil, fmt.Errorf("invalid matcher factory, must be CELMatcherFactory or CELMatcherWithErrorFactory: %T", fac)
}
}
// CELMatcherRuntimeFunction creates a function binding for when the input to the matcher
// is dynamically resolved rather than a set of static constant values.
-func CELMatcherRuntimeFunction(funcName string, fac CELMatcherFactory) functions.BinaryOp {
+func CELMatcherRuntimeFunction(funcName string, fac any) functions.BinaryOp {
return func(celReq, matcherData ref.Val) ref.Val {
- matcher, err := fac(matcherData)
- if err != nil {
- return types.WrapErr(err)
+ if factory, ok := fac.(CELMatcherWithErrorFactory); ok {
+ matcher, err := factory(matcherData)
+ if err != nil {
+ return types.WrapErr(err)
+ }
+ httpReq := celReq.Value().(celHTTPRequest)
+ match, err := matcher.MatchWithError(httpReq.Request)
+ if err != nil {
+ return types.WrapErr(err)
+ }
+ return types.Bool(match)
+ }
+ if factory, ok := fac.(CELMatcherFactory); ok {
+ matcher, err := factory(matcherData)
+ if err != nil {
+ return types.WrapErr(err)
+ }
+ httpReq := celReq.Value().(celHTTPRequest)
+ if m, ok := matcher.(RequestMatcherWithError); ok {
+ match, err := m.MatchWithError(httpReq.Request)
+ if err != nil {
+ return types.WrapErr(err)
+ }
+ return types.Bool(match)
+ }
+ return types.Bool(matcher.Match(httpReq.Request))
}
- httpReq := celReq.Value().(celHTTPRequest)
- return types.Bool(matcher.Match(httpReq.Request))
+ return types.NewErr("CELMatcherRuntimeFunction invalid matcher factory: %T", fac)
}
}
@@ -733,9 +802,9 @@ const MatcherNameCtxKey = "matcher_name"
// Interface guards
var (
- _ caddy.Provisioner = (*MatchExpression)(nil)
- _ RequestMatcher = (*MatchExpression)(nil)
- _ caddyfile.Unmarshaler = (*MatchExpression)(nil)
- _ json.Marshaler = (*MatchExpression)(nil)
- _ json.Unmarshaler = (*MatchExpression)(nil)
+ _ caddy.Provisioner = (*MatchExpression)(nil)
+ _ RequestMatcherWithError = (*MatchExpression)(nil)
+ _ caddyfile.Unmarshaler = (*MatchExpression)(nil)
+ _ json.Marshaler = (*MatchExpression)(nil)
+ _ json.Unmarshaler = (*MatchExpression)(nil)
)
diff --git a/modules/caddyhttp/celmatcher_test.go b/modules/caddyhttp/celmatcher_test.go
index 26491b7ca..a7e91529c 100644
--- a/modules/caddyhttp/celmatcher_test.go
+++ b/modules/caddyhttp/celmatcher_test.go
@@ -489,7 +489,11 @@ func TestMatchExpressionMatch(t *testing.T) {
}
}
- if tc.expression.Match(req) != tc.wantResult {
+ matches, err := tc.expression.MatchWithError(req)
+ if err != nil {
+ t.Errorf("MatchExpression.Match() error = %v", err)
+ }
+ if matches != tc.wantResult {
t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tc.wantResult, tc.expression.Expr)
}
})
@@ -532,7 +536,7 @@ func BenchmarkMatchExpressionMatch(b *testing.B) {
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
- tc.expression.Match(req)
+ tc.expression.MatchWithError(req)
}
})
}
diff --git a/modules/caddyhttp/encode/caddyfile.go b/modules/caddyhttp/encode/caddyfile.go
index e8ea4b807..8b8657080 100644
--- a/modules/caddyhttp/encode/caddyfile.go
+++ b/modules/caddyhttp/encode/caddyfile.go
@@ -57,21 +57,7 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume directive name
prefer := []string{}
- for _, arg := range d.RemainingArgs() {
- mod, err := caddy.GetModule("http.encoders." + arg)
- if err != nil {
- return d.Errf("finding encoder module '%s': %v", mod, err)
- }
- encoding, ok := mod.New().(Encoding)
- if !ok {
- return d.Errf("module %s is not an HTTP encoding", mod)
- }
- if enc.EncodingsRaw == nil {
- enc.EncodingsRaw = make(caddy.ModuleMap)
- }
- enc.EncodingsRaw[arg] = caddyconfig.JSON(encoding, nil)
- prefer = append(prefer, arg)
- }
+ remainingArgs := d.RemainingArgs()
responseMatchers := make(map[string]caddyhttp.ResponseMatcher)
for d.NextBlock(0) {
@@ -111,6 +97,26 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
}
+ if len(prefer) == 0 && len(remainingArgs) == 0 {
+ remainingArgs = []string{"zstd", "gzip"}
+ }
+
+ for _, arg := range remainingArgs {
+ mod, err := caddy.GetModule("http.encoders." + arg)
+ if err != nil {
+ return d.Errf("finding encoder module '%s': %v", mod, err)
+ }
+ encoding, ok := mod.New().(Encoding)
+ if !ok {
+ return d.Errf("module %s is not an HTTP encoding", mod)
+ }
+ if enc.EncodingsRaw == nil {
+ enc.EncodingsRaw = make(caddy.ModuleMap)
+ }
+ enc.EncodingsRaw[arg] = caddyconfig.JSON(encoding, nil)
+ prefer = append(prefer, arg)
+ }
+
// use the order in which the encoders were defined.
enc.Prefer = prefer
diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go
index f0d56a90d..597772ccc 100644
--- a/modules/caddyhttp/encode/encode.go
+++ b/modules/caddyhttp/encode/encode.go
@@ -156,7 +156,7 @@ func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh
if _, ok := enc.writerPools[encName]; !ok {
continue // encoding not offered
}
- w = enc.openResponseWriter(encName, w)
+ w = enc.openResponseWriter(encName, w, r.Method == http.MethodConnect)
defer w.(*responseWriter).Close()
// to comply with RFC 9110 section 8.8.3(.3), we modify the Etag when encoding
@@ -201,14 +201,14 @@ func (enc *Encode) addEncoding(e Encoding) error {
// openResponseWriter creates a new response writer that may (or may not)
// encode the response with encodingName. The returned response writer MUST
// be closed after the handler completes.
-func (enc *Encode) openResponseWriter(encodingName string, w http.ResponseWriter) *responseWriter {
+func (enc *Encode) openResponseWriter(encodingName string, w http.ResponseWriter, isConnect bool) *responseWriter {
var rw responseWriter
- return enc.initResponseWriter(&rw, encodingName, w)
+ return enc.initResponseWriter(&rw, encodingName, w, isConnect)
}
// initResponseWriter initializes the responseWriter instance
// allocated in openResponseWriter, enabling mid-stack inlining.
-func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter) *responseWriter {
+func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter, isConnect bool) *responseWriter {
if rww, ok := wrappedRW.(*caddyhttp.ResponseWriterWrapper); ok {
rw.ResponseWriter = rww
} else {
@@ -216,6 +216,7 @@ func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, w
}
rw.encodingName = encodingName
rw.config = enc
+ rw.isConnect = isConnect
return rw
}
@@ -230,6 +231,7 @@ type responseWriter struct {
config *Encode
statusCode int
wroteHeader bool
+ isConnect bool
}
// WriteHeader stores the status to write when the time comes
@@ -245,6 +247,14 @@ func (rw *responseWriter) WriteHeader(status int) {
rw.Header().Add("Vary", "Accept-Encoding")
}
+ // write status immediately if status is 2xx and the request is CONNECT
+ // since it means the response is successful.
+ // see: https://github.com/caddyserver/caddy/issues/6733#issuecomment-2525058845
+ if rw.isConnect && 200 <= status && status <= 299 {
+ rw.ResponseWriter.WriteHeader(status)
+ rw.wroteHeader = true
+ }
+
// write status immediately when status code is informational
// see: https://caddy.community/t/disappear-103-early-hints-response-with-encode-enable-caddy-v2-7-6/23081/5
if 100 <= status && status <= 199 {
@@ -260,6 +270,12 @@ func (enc *Encode) Match(rw *responseWriter) bool {
// FlushError is an alternative Flush returning an error. It delays the actual Flush of the underlying
// ResponseWriterWrapper until headers were written.
func (rw *responseWriter) FlushError() error {
+ // WriteHeader wasn't called and is a CONNECT request, treat it as a success.
+ // otherwise, wait until header is written.
+ if rw.isConnect && !rw.wroteHeader && rw.statusCode == 0 {
+ rw.WriteHeader(http.StatusOK)
+ }
+
if !rw.wroteHeader {
// flushing the underlying ResponseWriter will write header and status code,
// but we need to delay that until we can determine if we must encode and
@@ -288,6 +304,12 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
return 0, nil
}
+ // WriteHeader wasn't called and is a CONNECT request, treat it as a success.
+ // otherwise, determine if the response should be compressed.
+ if rw.isConnect && !rw.wroteHeader && rw.statusCode == 0 {
+ rw.WriteHeader(http.StatusOK)
+ }
+
// sniff content-type and determine content-length
if !rw.wroteHeader && rw.config.MinLength > 0 {
var gtMinLength bool
diff --git a/modules/caddyhttp/encode/encode_test.go b/modules/caddyhttp/encode/encode_test.go
index d76945498..83effa58c 100644
--- a/modules/caddyhttp/encode/encode_test.go
+++ b/modules/caddyhttp/encode/encode_test.go
@@ -9,7 +9,7 @@ import (
func BenchmarkOpenResponseWriter(b *testing.B) {
enc := new(Encode)
for n := 0; n < b.N; n++ {
- enc.openResponseWriter("test", nil)
+ enc.openResponseWriter("test", nil, false)
}
}
diff --git a/modules/caddyhttp/fileserver/browse.go b/modules/caddyhttp/fileserver/browse.go
index a19b4e17a..0a623c79e 100644
--- a/modules/caddyhttp/fileserver/browse.go
+++ b/modules/caddyhttp/fileserver/browse.go
@@ -66,8 +66,15 @@ type Browse struct {
// - `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"`
+
+ // FileLimit limits the number of up to n DirEntry values in directory order.
+ FileLimit int `json:"file_limit,omitempty"`
}
+const (
+ defaultDirEntryLimit = 10000
+)
+
func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
if c := fsrv.logger.Check(zapcore.DebugLevel, "browse enabled; listing directory contents"); c != nil {
c.Write(zap.String("path", dirPath), zap.String("root", root))
@@ -206,7 +213,11 @@ func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w ht
}
func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, fileSystem fs.FS, dir fs.ReadDirFile, root, urlPath string, repl *caddy.Replacer) (*browseTemplateContext, error) {
- files, err := dir.ReadDir(10000) // TODO: this limit should probably be configurable
+ dirLimit := defaultDirEntryLimit
+ if fsrv.Browse.FileLimit != 0 {
+ dirLimit = fsrv.Browse.FileLimit
+ }
+ files, err := dir.ReadDir(dirLimit)
if err != nil && err != io.EOF {
return nil, err
}
diff --git a/modules/caddyhttp/fileserver/caddyfile.go b/modules/caddyhttp/fileserver/caddyfile.go
index 71b7638e4..80a37322b 100644
--- a/modules/caddyhttp/fileserver/caddyfile.go
+++ b/modules/caddyhttp/fileserver/caddyfile.go
@@ -16,6 +16,7 @@ package fileserver
import (
"path/filepath"
+ "strconv"
"strings"
"github.com/caddyserver/caddy/v2"
@@ -78,7 +79,7 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return d.ArgErr()
}
- for d.NextBlock(0) {
+ for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "fs":
if !d.NextArg() {
@@ -129,15 +130,29 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return d.Errf("unknown sort option '%s'", dVal)
}
}
+ case "file_limit":
+ fileLimit := d.RemainingArgs()
+ if len(fileLimit) != 1 {
+ return d.Err("file_limit should have an integer value")
+ }
+ val, _ := strconv.Atoi(fileLimit[0])
+ if fsrv.Browse.FileLimit != 0 {
+ return d.Err("file_limit is already enabled")
+ }
+ fsrv.Browse.FileLimit = val
default:
return d.Errf("unknown subdirective '%s'", d.Val())
}
}
case "precompressed":
- var order []string
- for d.NextArg() {
- modID := "http.precompressed." + d.Val()
+ fsrv.PrecompressedOrder = d.RemainingArgs()
+ if len(fsrv.PrecompressedOrder) == 0 {
+ fsrv.PrecompressedOrder = []string{"br", "zstd", "gzip"}
+ }
+
+ for _, format := range fsrv.PrecompressedOrder {
+ modID := "http.precompressed." + format
mod, err := caddy.GetModule(modID)
if err != nil {
return d.Errf("getting module named '%s': %v", modID, err)
@@ -150,10 +165,8 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if fsrv.PrecompressedRaw == nil {
fsrv.PrecompressedRaw = make(caddy.ModuleMap)
}
- fsrv.PrecompressedRaw[d.Val()] = caddyconfig.JSON(precompress, nil)
- order = append(order, d.Val())
+ fsrv.PrecompressedRaw[format] = caddyconfig.JSON(precompress, nil)
}
- fsrv.PrecompressedOrder = order
case "status":
if !d.NextArg() {
@@ -263,7 +276,7 @@ func parseTryFiles(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
tryPolicy = h.Val()
switch tryPolicy {
- case tryPolicyFirstExist, tryPolicyLargestSize, tryPolicySmallestSize, tryPolicyMostRecentlyMod:
+ case tryPolicyFirstExist, tryPolicyFirstExistFallback, tryPolicyLargestSize, tryPolicySmallestSize, tryPolicyMostRecentlyMod:
default:
return nil, h.Errf("unrecognized try policy: %s", tryPolicy)
}
diff --git a/modules/caddyhttp/fileserver/command.go b/modules/caddyhttp/fileserver/command.go
index a76998405..a04d7cade 100644
--- a/modules/caddyhttp/fileserver/command.go
+++ b/modules/caddyhttp/fileserver/command.go
@@ -66,6 +66,7 @@ respond with a file listing.`,
cmd.Flags().BoolP("templates", "t", false, "Enable template rendering")
cmd.Flags().BoolP("access-log", "a", false, "Enable the access log")
cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
+ cmd.Flags().IntP("file-limit", "f", defaultDirEntryLimit, "Max directories to read")
cmd.Flags().BoolP("no-compress", "", false, "Disable Zstandard and Gzip compression")
cmd.Flags().StringSliceP("precompressed", "p", []string{}, "Specify precompression file extensions. Compression preference implied from flag order.")
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdFileServer)
@@ -91,6 +92,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
browse := fs.Bool("browse")
templates := fs.Bool("templates")
accessLog := fs.Bool("access-log")
+ fileLimit := fs.Int("file-limit")
debug := fs.Bool("debug")
revealSymlinks := fs.Bool("reveal-symlinks")
compress := !fs.Bool("no-compress")
@@ -151,7 +153,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
}
if browse {
- handler.Browse = &Browse{RevealSymlinks: revealSymlinks}
+ handler.Browse = &Browse{RevealSymlinks: revealSymlinks, FileLimit: fileLimit}
}
handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "file_server", nil))
diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go
index 71de1db29..2bc665d4f 100644
--- a/modules/caddyhttp/fileserver/matcher.go
+++ b/modules/caddyhttp/fileserver/matcher.go
@@ -90,6 +90,7 @@ type MatchFile struct {
// How to choose a file in TryFiles. Can be:
//
// - first_exist
+ // - first_exist_fallback
// - smallest_size
// - largest_size
// - most_recently_modified
@@ -173,7 +174,7 @@ func (m *MatchFile) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
func (MatchFile) CELLibrary(ctx caddy.Context) (cel.Library, error) {
requestType := cel.ObjectType("http.Request")
- matcherFactory := func(data ref.Val) (caddyhttp.RequestMatcher, error) {
+ matcherFactory := func(data ref.Val) (caddyhttp.RequestMatcherWithError, error) {
values, err := caddyhttp.CELValueToMapStrList(data)
if err != nil {
return nil, err
@@ -191,7 +192,7 @@ func (MatchFile) CELLibrary(ctx caddy.Context) (cel.Library, error) {
var try_policy string
if len(values["try_policy"]) > 0 {
- root = values["try_policy"][0]
+ try_policy = values["try_policy"][0]
}
m := MatchFile{
@@ -296,6 +297,7 @@ func (m MatchFile) Validate() error {
switch m.TryPolicy {
case "",
tryPolicyFirstExist,
+ tryPolicyFirstExistFallback,
tryPolicyLargestSize,
tryPolicySmallestSize,
tryPolicyMostRecentlyMod:
@@ -313,12 +315,22 @@ func (m MatchFile) Validate() error {
// - http.matchers.file.type: file or directory
// - http.matchers.file.remainder: Portion remaining after splitting file path (if configured)
func (m MatchFile) Match(r *http.Request) bool {
+ match, err := m.selectFile(r)
+ if err != nil {
+ // nolint:staticcheck
+ caddyhttp.SetVar(r.Context(), caddyhttp.MatcherErrorVarKey, err)
+ }
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchFile) MatchWithError(r *http.Request) (bool, error) {
return m.selectFile(r)
}
// selectFile chooses a file according to m.TryPolicy by appending
// the paths in m.TryFiles to m.Root, with placeholder replacements.
-func (m MatchFile) selectFile(r *http.Request) (matched bool) {
+func (m MatchFile) selectFile(r *http.Request) (bool, error) {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
root := filepath.Clean(repl.ReplaceAll(m.Root, "."))
@@ -330,7 +342,7 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
if c := m.logger.Check(zapcore.ErrorLevel, "use of unregistered filesystem"); c != nil {
c.Write(zap.String("fs", fsName))
}
- return false
+ return false, nil
}
type matchCandidate struct {
fullpath, relative, splitRemainder string
@@ -405,13 +417,13 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
}
// setPlaceholders creates the placeholders for the matched file
- setPlaceholders := func(candidate matchCandidate, info fs.FileInfo) {
+ setPlaceholders := func(candidate matchCandidate, isDir bool) {
repl.Set("http.matchers.file.relative", filepath.ToSlash(candidate.relative))
repl.Set("http.matchers.file.absolute", filepath.ToSlash(candidate.fullpath))
repl.Set("http.matchers.file.remainder", filepath.ToSlash(candidate.splitRemainder))
fileType := "file"
- if info.IsDir() {
+ if isDir {
fileType = "directory"
}
repl.Set("http.matchers.file.type", fileType)
@@ -419,17 +431,32 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
// match file according to the configured policy
switch m.TryPolicy {
- case "", tryPolicyFirstExist:
- for _, pattern := range m.TryFiles {
+ case "", tryPolicyFirstExist, tryPolicyFirstExistFallback:
+ maxI := -1
+ if m.TryPolicy == tryPolicyFirstExistFallback {
+ maxI = len(m.TryFiles) - 1
+ }
+
+ for i, pattern := range m.TryFiles {
+ // If the pattern is a status code, emit an error,
+ // which short-circuits the middleware pipeline and
+ // writes an HTTP error response.
if err := parseErrorCode(pattern); err != nil {
- caddyhttp.SetVar(r.Context(), caddyhttp.MatcherErrorVarKey, err)
- return
+ return false, err
}
+
candidates := makeCandidates(pattern)
for _, c := range candidates {
+ // Skip the IO if using fallback policy and it's the latest item
+ if i == maxI {
+ setPlaceholders(c, false)
+
+ return true, nil
+ }
+
if info, exists := m.strictFileExists(fileSystem, c.fullpath); exists {
- setPlaceholders(c, info)
- return true
+ setPlaceholders(c, info.IsDir())
+ return true, nil
}
}
}
@@ -450,10 +477,10 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
}
}
if largestInfo == nil {
- return false
+ return false, nil
}
- setPlaceholders(largest, largestInfo)
- return true
+ setPlaceholders(largest, largestInfo.IsDir())
+ return true, nil
case tryPolicySmallestSize:
var smallestSize int64
@@ -471,10 +498,10 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
}
}
if smallestInfo == nil {
- return false
+ return false, nil
}
- setPlaceholders(smallest, smallestInfo)
- return true
+ setPlaceholders(smallest, smallestInfo.IsDir())
+ return true, nil
case tryPolicyMostRecentlyMod:
var recent matchCandidate
@@ -491,13 +518,13 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
}
}
if recentInfo == nil {
- return false
+ return false, nil
}
- setPlaceholders(recent, recentInfo)
- return true
+ setPlaceholders(recent, recentInfo.IsDir())
+ return true, nil
}
- return
+ return false, nil
}
// parseErrorCode checks if the input is a status
@@ -695,15 +722,16 @@ var globSafeRepl = strings.NewReplacer(
)
const (
- tryPolicyFirstExist = "first_exist"
- tryPolicyLargestSize = "largest_size"
- tryPolicySmallestSize = "smallest_size"
- tryPolicyMostRecentlyMod = "most_recently_modified"
+ tryPolicyFirstExist = "first_exist"
+ tryPolicyFirstExistFallback = "first_exist_fallback"
+ tryPolicyLargestSize = "largest_size"
+ tryPolicySmallestSize = "smallest_size"
+ tryPolicyMostRecentlyMod = "most_recently_modified"
)
// Interface guards
var (
- _ caddy.Validator = (*MatchFile)(nil)
- _ caddyhttp.RequestMatcher = (*MatchFile)(nil)
- _ caddyhttp.CELLibraryProducer = (*MatchFile)(nil)
+ _ caddy.Validator = (*MatchFile)(nil)
+ _ caddyhttp.RequestMatcherWithError = (*MatchFile)(nil)
+ _ caddyhttp.CELLibraryProducer = (*MatchFile)(nil)
)
diff --git a/modules/caddyhttp/fileserver/matcher_test.go b/modules/caddyhttp/fileserver/matcher_test.go
index 527c16bd1..b6697b9d8 100644
--- a/modules/caddyhttp/fileserver/matcher_test.go
+++ b/modules/caddyhttp/fileserver/matcher_test.go
@@ -130,7 +130,10 @@ func TestFileMatcher(t *testing.T) {
req := &http.Request{URL: u}
repl := caddyhttp.NewTestReplacer(req)
- result := m.Match(req)
+ result, err := m.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Test %d: unexpected error: %v", i, err)
+ }
if result != tc.matched {
t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result)
}
@@ -240,7 +243,10 @@ func TestPHPFileMatcher(t *testing.T) {
req := &http.Request{URL: u}
repl := caddyhttp.NewTestReplacer(req)
- result := m.Match(req)
+ result, err := m.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Test %d: unexpected error: %v", i, err)
+ }
if result != tc.matched {
t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result)
}
@@ -289,6 +295,7 @@ var expressionTests = []struct {
wantErr bool
wantResult bool
clientCertificate []byte
+ expectedPath string
}{
{
name: "file error no args (MatchFile)",
@@ -354,6 +361,15 @@ var expressionTests = []struct {
urlTarget: "https://example.com/nopenope.txt",
wantResult: false,
},
+ {
+ name: "file match long pattern foo.txt with try_policy (MatchFile)",
+ expression: &caddyhttp.MatchExpression{
+ Expr: `file({"root": "./testdata", "try_policy": "largest_size", "try_files": ["foo.txt", "large.txt"]})`,
+ },
+ urlTarget: "https://example.com/",
+ wantResult: true,
+ expectedPath: "/large.txt",
+ },
}
func TestMatchExpressionMatch(t *testing.T) {
@@ -379,9 +395,24 @@ func TestMatchExpressionMatch(t *testing.T) {
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
- if tc.expression.Match(req) != tc.wantResult {
+ matches, err := tc.expression.MatchWithError(req)
+ if err != nil {
+ t.Errorf("MatchExpression.Match() error = %v", err)
+ return
+ }
+ if matches != tc.wantResult {
t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tc.wantResult, tc.expression.Expr)
}
+
+ if tc.expectedPath != "" {
+ path, ok := repl.Get("http.matchers.file.relative")
+ if !ok {
+ t.Errorf("MatchExpression.Match() expected to return path '%s', but got none", tc.expectedPath)
+ }
+ if path != tc.expectedPath {
+ t.Errorf("MatchExpression.Match() expected to return path '%s', but got '%s'", tc.expectedPath, path)
+ }
+ }
})
}
}
diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go
index 4ae69b647..2b0caecfc 100644
--- a/modules/caddyhttp/fileserver/staticfiles.go
+++ b/modules/caddyhttp/fileserver/staticfiles.go
@@ -204,7 +204,7 @@ func (fsrv *FileServer) Provision(ctx caddy.Context) error {
// absolute paths before the server starts for very slight performance improvement
for i, h := range fsrv.Hide {
if !strings.Contains(h, "{") && strings.Contains(h, separator) {
- if abs, err := filepath.Abs(h); err == nil {
+ if abs, err := caddy.FastAbs(h); err == nil {
fsrv.Hide[i] = abs
}
}
@@ -636,7 +636,7 @@ func (fsrv *FileServer) transformHidePaths(repl *caddy.Replacer) []string {
for i := range fsrv.Hide {
hide[i] = repl.ReplaceAll(fsrv.Hide[i], "")
if strings.Contains(hide[i], separator) {
- abs, err := filepath.Abs(hide[i])
+ abs, err := caddy.FastAbs(hide[i])
if err == nil {
hide[i] = abs
}
@@ -655,7 +655,7 @@ func fileHidden(filename string, hide []string) bool {
}
// all path comparisons use the complete absolute path if possible
- filenameAbs, err := filepath.Abs(filename)
+ filenameAbs, err := caddy.FastAbs(filename)
if err == nil {
filename = filenameAbs
}
diff --git a/modules/caddyhttp/fileserver/testdata/large.txt b/modules/caddyhttp/fileserver/testdata/large.txt
new file mode 100644
index 000000000..c36623744
--- /dev/null
+++ b/modules/caddyhttp/fileserver/testdata/large.txt
@@ -0,0 +1,3 @@
+This is a file with more content than the other files in this directory
+such that tests using the largest_size policy pick this file, or the
+smallest_size policy avoids this file. \ No newline at end of file
diff --git a/modules/caddyhttp/headers/headers.go b/modules/caddyhttp/headers/headers.go
index a3279d913..c66bd4144 100644
--- a/modules/caddyhttp/headers/headers.go
+++ b/modules/caddyhttp/headers/headers.go
@@ -200,9 +200,7 @@ func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) {
for _, fieldName := range ops.Delete {
fieldName = repl.ReplaceKnown(fieldName, "")
if fieldName == "*" {
- for existingField := range hdr {
- delete(hdr, existingField)
- }
+ clear(hdr)
}
}
diff --git a/modules/caddyhttp/ip_matchers.go b/modules/caddyhttp/ip_matchers.go
index 99eb39dff..5e0b356e7 100644
--- a/modules/caddyhttp/ip_matchers.go
+++ b/modules/caddyhttp/ip_matchers.go
@@ -108,7 +108,7 @@ func (MatchRemoteIP) CELLibrary(ctx caddy.Context) (cel.Library, error) {
// internal data type of the MatchPath value.
[]*cel.Type{cel.ListType(cel.StringType)},
// function to convert a constant list of strings to a MatchPath instance.
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
refStringList := reflect.TypeOf([]string{})
strList, err := data.ConvertToNative(refStringList)
if err != nil {
@@ -145,9 +145,23 @@ func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
// Match returns true if r matches m.
func (m MatchRemoteIP) Match(r *http.Request) bool {
+ match, err := m.MatchWithError(r)
+ if err != nil {
+ SetVar(r.Context(), MatcherErrorVarKey, err)
+ }
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchRemoteIP) MatchWithError(r *http.Request) (bool, error) {
+ // if handshake is not finished, we infer 0-RTT that has
+ // not verified remote IP; could be spoofed, so we throw
+ // HTTP 425 status to tell the client to try again after
+ // the handshake is complete
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
+ return false, Error(http.StatusTooEarly, fmt.Errorf("TLS handshake not complete, remote IP cannot be verified"))
}
+
address := r.RemoteAddr
clientIP, zoneID, err := parseIPZoneFromString(address)
if err != nil {
@@ -155,7 +169,7 @@ func (m MatchRemoteIP) Match(r *http.Request) bool {
c.Write(zap.Error(err))
}
- return false
+ return false, nil
}
matches, zoneFilter := matchIPByCidrZones(clientIP, zoneID, m.cidrs, m.zones)
if !matches && !zoneFilter {
@@ -163,7 +177,7 @@ func (m MatchRemoteIP) Match(r *http.Request) bool {
c.Write(zap.String("zone", zoneID))
}
}
- return matches
+ return matches, nil
}
// CaddyModule returns the Caddy module information.
@@ -207,7 +221,7 @@ func (MatchClientIP) CELLibrary(ctx caddy.Context) (cel.Library, error) {
// internal data type of the MatchPath value.
[]*cel.Type{cel.ListType(cel.StringType)},
// function to convert a constant list of strings to a MatchPath instance.
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
refStringList := reflect.TypeOf([]string{})
strList, err := data.ConvertToNative(refStringList)
if err != nil {
@@ -238,20 +252,34 @@ func (m *MatchClientIP) Provision(ctx caddy.Context) error {
// Match returns true if r matches m.
func (m MatchClientIP) Match(r *http.Request) bool {
+ match, err := m.MatchWithError(r)
+ if err != nil {
+ SetVar(r.Context(), MatcherErrorVarKey, err)
+ }
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchClientIP) MatchWithError(r *http.Request) (bool, error) {
+ // if handshake is not finished, we infer 0-RTT that has
+ // not verified remote IP; could be spoofed, so we throw
+ // HTTP 425 status to tell the client to try again after
+ // the handshake is complete
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
+ return false, Error(http.StatusTooEarly, fmt.Errorf("TLS handshake not complete, remote IP cannot be verified"))
}
+
address := GetVar(r.Context(), ClientIPVarKey).(string)
clientIP, zoneID, err := parseIPZoneFromString(address)
if err != nil {
m.logger.Error("getting client IP", zap.Error(err))
- return false
+ return false, nil
}
matches, zoneFilter := matchIPByCidrZones(clientIP, zoneID, m.cidrs, m.zones)
if !matches && !zoneFilter {
m.logger.Debug("zone ID from client IP did not match", zap.String("zone", zoneID))
}
- return matches
+ return matches, nil
}
func provisionCidrsZonesFromRanges(ranges []string) ([]*netip.Prefix, []string, error) {
@@ -326,13 +354,13 @@ func matchIPByCidrZones(clientIP netip.Addr, zoneID string, cidrs []*netip.Prefi
// Interface guards
var (
- _ RequestMatcher = (*MatchRemoteIP)(nil)
- _ caddy.Provisioner = (*MatchRemoteIP)(nil)
- _ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
- _ CELLibraryProducer = (*MatchRemoteIP)(nil)
-
- _ RequestMatcher = (*MatchClientIP)(nil)
- _ caddy.Provisioner = (*MatchClientIP)(nil)
- _ caddyfile.Unmarshaler = (*MatchClientIP)(nil)
- _ CELLibraryProducer = (*MatchClientIP)(nil)
+ _ RequestMatcherWithError = (*MatchRemoteIP)(nil)
+ _ caddy.Provisioner = (*MatchRemoteIP)(nil)
+ _ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
+ _ CELLibraryProducer = (*MatchRemoteIP)(nil)
+
+ _ RequestMatcherWithError = (*MatchClientIP)(nil)
+ _ caddy.Provisioner = (*MatchClientIP)(nil)
+ _ caddyfile.Unmarshaler = (*MatchClientIP)(nil)
+ _ CELLibraryProducer = (*MatchClientIP)(nil)
)
diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go
index 7f62bdd3a..e5ca28b95 100644
--- a/modules/caddyhttp/matchers.go
+++ b/modules/caddyhttp/matchers.go
@@ -296,6 +296,12 @@ func (m MatchHost) Provision(_ caddy.Context) error {
// Match returns true if r matches m.
func (m MatchHost) Match(r *http.Request) bool {
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchHost) MatchWithError(r *http.Request) (bool, error) {
reqHost, _, err := net.SplitHostPort(r.Host)
if err != nil {
// OK; probably didn't have a port
@@ -315,7 +321,7 @@ func (m MatchHost) Match(r *http.Request) bool {
return m[i] >= reqHost
})
if pos < len(m) && m[pos] == reqHost {
- return true
+ return true, nil
}
}
@@ -346,13 +352,13 @@ outer:
continue outer
}
}
- return true
+ return true, nil
} else if strings.EqualFold(reqHost, host) {
- return true
+ return true, nil
}
}
- return false
+ return false, nil
}
// CELLibrary produces options that expose this matcher for use in CEL
@@ -366,7 +372,7 @@ func (MatchHost) CELLibrary(ctx caddy.Context) (cel.Library, error) {
"host",
"host_match_request_list",
[]*cel.Type{cel.ListType(cel.StringType)},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
refStringList := reflect.TypeOf([]string{})
strList, err := data.ConvertToNative(refStringList)
if err != nil {
@@ -411,6 +417,12 @@ func (m MatchPath) Provision(_ caddy.Context) error {
// Match returns true if r matches m.
func (m MatchPath) Match(r *http.Request) bool {
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchPath) MatchWithError(r *http.Request) (bool, error) {
// Even though RFC 9110 says that path matching is case-sensitive
// (https://www.rfc-editor.org/rfc/rfc9110.html#section-4.2.3),
// we do case-insensitive matching to mitigate security issues
@@ -436,7 +448,7 @@ func (m MatchPath) Match(r *http.Request) bool {
// special case: whole path is wildcard; this is unnecessary
// as it matches all requests, which is the same as no matcher
if matchPattern == "*" {
- return true
+ return true, nil
}
// Clean the path, merge doubled slashes, etc.
@@ -464,7 +476,7 @@ func (m MatchPath) Match(r *http.Request) bool {
if strings.Contains(matchPattern, "%") {
reqPathForPattern := CleanPath(r.URL.EscapedPath(), mergeSlashes)
if m.matchPatternWithEscapeSequence(reqPathForPattern, matchPattern) {
- return true
+ return true, nil
}
// doing prefix/suffix/substring matches doesn't make sense
@@ -483,7 +495,7 @@ func (m MatchPath) Match(r *http.Request) bool {
strings.HasPrefix(matchPattern, "*") &&
strings.HasSuffix(matchPattern, "*") {
if strings.Contains(reqPathForPattern, matchPattern[1:len(matchPattern)-1]) {
- return true
+ return true, nil
}
continue
}
@@ -495,7 +507,7 @@ func (m MatchPath) Match(r *http.Request) bool {
// treat it as a fast suffix match
if strings.HasPrefix(matchPattern, "*") {
if strings.HasSuffix(reqPathForPattern, matchPattern[1:]) {
- return true
+ return true, nil
}
continue
}
@@ -504,7 +516,7 @@ func (m MatchPath) Match(r *http.Request) bool {
// treat it as a fast prefix match
if strings.HasSuffix(matchPattern, "*") {
if strings.HasPrefix(reqPathForPattern, matchPattern[:len(matchPattern)-1]) {
- return true
+ return true, nil
}
continue
}
@@ -515,10 +527,10 @@ func (m MatchPath) Match(r *http.Request) bool {
// because we can't handle it anyway
matches, _ := path.Match(matchPattern, reqPathForPattern)
if matches {
- return true
+ return true, nil
}
}
- return false
+ return false, nil
}
func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) bool {
@@ -642,7 +654,7 @@ func (MatchPath) CELLibrary(ctx caddy.Context) (cel.Library, error) {
// internal data type of the MatchPath value.
[]*cel.Type{cel.ListType(cel.StringType)},
// function to convert a constant list of strings to a MatchPath instance.
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
refStringList := reflect.TypeOf([]string{})
strList, err := data.ConvertToNative(refStringList)
if err != nil {
@@ -677,6 +689,12 @@ func (MatchPathRE) CaddyModule() caddy.ModuleInfo {
// Match returns true if r matches m.
func (m MatchPathRE) Match(r *http.Request) bool {
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchPathRE) MatchWithError(r *http.Request) (bool, error) {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
// Clean the path, merges doubled slashes, etc.
@@ -684,7 +702,7 @@ func (m MatchPathRE) Match(r *http.Request) bool {
// the path matcher. See #4407
cleanedPath := cleanPath(r.URL.Path)
- return m.MatchRegexp.Match(cleanedPath, repl)
+ return m.MatchRegexp.Match(cleanedPath, repl), nil
}
// CELLibrary produces options that expose this matcher for use in CEL
@@ -698,7 +716,7 @@ func (MatchPathRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
"path_regexp",
"path_regexp_request_string",
[]*cel.Type{cel.StringType},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
pattern := data.(types.String)
matcher := MatchPathRE{MatchRegexp{
Name: ctx.Value(MatcherNameCtxKey).(string),
@@ -715,7 +733,7 @@ func (MatchPathRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
"path_regexp",
"path_regexp_request_string_string",
[]*cel.Type{cel.StringType, cel.StringType},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
refStringList := reflect.TypeOf([]string{})
params, err := data.ConvertToNative(refStringList)
if err != nil {
@@ -764,7 +782,13 @@ func (m *MatchMethod) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Match returns true if r matches m.
func (m MatchMethod) Match(r *http.Request) bool {
- return slices.Contains(m, r.Method)
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchMethod) MatchWithError(r *http.Request) (bool, error) {
+ return slices.Contains(m, r.Method), nil
}
// CELLibrary produces options that expose this matcher for use in CEL
@@ -778,7 +802,7 @@ func (MatchMethod) CELLibrary(_ caddy.Context) (cel.Library, error) {
"method",
"method_request_list",
[]*cel.Type{cel.ListType(cel.StringType)},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
refStringList := reflect.TypeOf([]string{})
strList, err := data.ConvertToNative(refStringList)
if err != nil {
@@ -823,10 +847,17 @@ func (m *MatchQuery) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Match returns true if r matches m. An empty m matches an empty query string.
func (m MatchQuery) Match(r *http.Request) bool {
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+// An empty m matches an empty query string.
+func (m MatchQuery) MatchWithError(r *http.Request) (bool, error) {
// If no query keys are configured, this only
// matches an empty query string.
if len(m) == 0 {
- return len(r.URL.Query()) == 0
+ return len(r.URL.Query()) == 0, nil
}
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
@@ -843,7 +874,7 @@ func (m MatchQuery) Match(r *http.Request) bool {
// "Relying on parser alignment for security is doomed." Overall conclusion is that
// splitting on & and rejecting ; in key=value pairs is safer than accepting raw ;.
// We regard the Go team's decision as sound and thus reject malformed query strings.
- return false
+ return false, nil
}
// Count the amount of matched keys, to ensure we AND
@@ -854,7 +885,7 @@ func (m MatchQuery) Match(r *http.Request) bool {
param = repl.ReplaceAll(param, "")
paramVal, found := parsed[param]
if !found {
- return false
+ return false, nil
}
for _, v := range vals {
v = repl.ReplaceAll(v, "")
@@ -864,7 +895,7 @@ func (m MatchQuery) Match(r *http.Request) bool {
}
}
}
- return matchedKeys == len(m)
+ return matchedKeys == len(m), nil
}
// CELLibrary produces options that expose this matcher for use in CEL
@@ -878,7 +909,7 @@ func (MatchQuery) CELLibrary(_ caddy.Context) (cel.Library, error) {
"query",
"query_matcher_request_map",
[]*cel.Type{CELTypeJSON},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
mapStrListStr, err := CELValueToMapStrList(data)
if err != nil {
return nil, err
@@ -940,8 +971,14 @@ func (m *MatchHeader) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Match returns true if r matches m.
func (m MatchHeader) Match(r *http.Request) bool {
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchHeader) MatchWithError(r *http.Request) (bool, error) {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
- return matchHeaders(r.Header, http.Header(m), r.Host, r.TransferEncoding, repl)
+ return matchHeaders(r.Header, http.Header(m), r.Host, r.TransferEncoding, repl), nil
}
// CELLibrary produces options that expose this matcher for use in CEL
@@ -956,7 +993,7 @@ func (MatchHeader) CELLibrary(_ caddy.Context) (cel.Library, error) {
"header",
"header_matcher_request_map",
[]*cel.Type{CELTypeJSON},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
mapStrListStr, err := CELValueToMapStrList(data)
if err != nil {
return nil, err
@@ -1079,6 +1116,12 @@ func (m *MatchHeaderRE) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Match returns true if r matches m.
func (m MatchHeaderRE) Match(r *http.Request) bool {
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchHeaderRE) MatchWithError(r *http.Request) (bool, error) {
for field, rm := range m {
actualFieldVals := getHeaderFieldVals(r.Header, field, r.Host, r.TransferEncoding)
match := false
@@ -1091,10 +1134,10 @@ func (m MatchHeaderRE) Match(r *http.Request) bool {
}
}
if !match {
- return false
+ return false, nil
}
}
- return true
+ return true, nil
}
// Provision compiles m's regular expressions.
@@ -1130,7 +1173,7 @@ func (MatchHeaderRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
"header_regexp",
"header_regexp_request_string_string",
[]*cel.Type{cel.StringType, cel.StringType},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
refStringList := reflect.TypeOf([]string{})
params, err := data.ConvertToNative(refStringList)
if err != nil {
@@ -1153,7 +1196,7 @@ func (MatchHeaderRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
"header_regexp",
"header_regexp_request_string_string_string",
[]*cel.Type{cel.StringType, cel.StringType, cel.StringType},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
refStringList := reflect.TypeOf([]string{})
params, err := data.ConvertToNative(refStringList)
if err != nil {
@@ -1191,31 +1234,37 @@ func (MatchProtocol) CaddyModule() caddy.ModuleInfo {
// Match returns true if r matches m.
func (m MatchProtocol) Match(r *http.Request) bool {
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchProtocol) MatchWithError(r *http.Request) (bool, error) {
switch string(m) {
case "grpc":
- return strings.HasPrefix(r.Header.Get("content-type"), "application/grpc")
+ return strings.HasPrefix(r.Header.Get("content-type"), "application/grpc"), nil
case "https":
- return r.TLS != nil
+ return r.TLS != nil, nil
case "http":
- return r.TLS == nil
+ return r.TLS == nil, nil
case "http/1.0":
- return r.ProtoMajor == 1 && r.ProtoMinor == 0
+ return r.ProtoMajor == 1 && r.ProtoMinor == 0, nil
case "http/1.0+":
- return r.ProtoAtLeast(1, 0)
+ return r.ProtoAtLeast(1, 0), nil
case "http/1.1":
- return r.ProtoMajor == 1 && r.ProtoMinor == 1
+ return r.ProtoMajor == 1 && r.ProtoMinor == 1, nil
case "http/1.1+":
- return r.ProtoAtLeast(1, 1)
+ return r.ProtoAtLeast(1, 1), nil
case "http/2":
- return r.ProtoMajor == 2
+ return r.ProtoMajor == 2, nil
case "http/2+":
- return r.ProtoAtLeast(2, 0)
+ return r.ProtoAtLeast(2, 0), nil
case "http/3":
- return r.ProtoMajor == 3
+ return r.ProtoMajor == 3, nil
case "http/3+":
- return r.ProtoAtLeast(3, 0)
+ return r.ProtoAtLeast(3, 0), nil
}
- return false
+ return false, nil
}
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
@@ -1242,7 +1291,7 @@ func (MatchProtocol) CELLibrary(_ caddy.Context) (cel.Library, error) {
"protocol",
"protocol_request_string",
[]*cel.Type{cel.StringType},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
protocolStr, ok := data.(types.String)
if !ok {
return nil, errors.New("protocol argument was not a string")
@@ -1262,16 +1311,22 @@ func (MatchTLS) CaddyModule() caddy.ModuleInfo {
// Match returns true if r matches m.
func (m MatchTLS) Match(r *http.Request) bool {
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchTLS) MatchWithError(r *http.Request) (bool, error) {
if r.TLS == nil {
- return false
+ return false, nil
}
if m.HandshakeComplete != nil {
if (!*m.HandshakeComplete && r.TLS.HandshakeComplete) ||
(*m.HandshakeComplete && !r.TLS.HandshakeComplete) {
- return false
+ return false, nil
}
}
- return true
+ return true, nil
}
// UnmarshalCaddyfile parses Caddyfile tokens for this matcher. Syntax:
@@ -1341,7 +1396,15 @@ func (m *MatchNot) Provision(ctx caddy.Context) error {
for _, modMap := range matcherSets.([]map[string]any) {
var ms MatcherSet
for _, modIface := range modMap {
- ms = append(ms, modIface.(RequestMatcher))
+ if mod, ok := modIface.(RequestMatcherWithError); ok {
+ ms = append(ms, mod)
+ continue
+ }
+ if mod, ok := modIface.(RequestMatcher); ok {
+ ms = append(ms, mod)
+ continue
+ }
+ return fmt.Errorf("module is not a request matcher: %T", modIface)
}
m.MatcherSets = append(m.MatcherSets, ms)
}
@@ -1352,12 +1415,24 @@ func (m *MatchNot) Provision(ctx caddy.Context) error {
// the embedded matchers, false is returned if any of its matcher
// sets return true.
func (m MatchNot) Match(r *http.Request) bool {
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m. Since this matcher
+// negates the embedded matchers, false is returned if any of its
+// matcher sets return true.
+func (m MatchNot) MatchWithError(r *http.Request) (bool, error) {
for _, ms := range m.MatcherSets {
- if ms.Match(r) {
- return false
+ matches, err := ms.MatchWithError(r)
+ if err != nil {
+ return false, err
+ }
+ if matches {
+ return false, nil
}
}
- return true
+ return true, nil
}
// MatchRegexp is an embedable type for matching
@@ -1473,7 +1548,7 @@ func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// ParseCaddyfileNestedMatcher 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]RequestMatcher)
+ matcherMap := make(map[string]any)
// in case there are multiple instances of the same matcher, concatenate
// their tokens (we expect that UnmarshalCaddyfile should be able to
@@ -1498,11 +1573,15 @@ func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, er
if err != nil {
return nil, err
}
- rm, ok := unm.(RequestMatcher)
- if !ok {
- return nil, fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
+ if rm, ok := unm.(RequestMatcherWithError); ok {
+ matcherMap[matcherName] = rm
+ continue
}
- matcherMap[matcherName] = rm
+ if rm, ok := unm.(RequestMatcher); ok {
+ matcherMap[matcherName] = rm
+ continue
+ }
+ return nil, fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
}
// we should now have a functional matcher, but we also
@@ -1528,24 +1607,28 @@ const regexpPlaceholderPrefix = "http.regexp"
// holds an optional error emitted from a request matcher,
// to short-circuit the handler chain, since matchers cannot
// return errors via the RequestMatcher interface.
+//
+// Deprecated: Matchers should implement RequestMatcherWithError
+// which can return an error directly, instead of smuggling it
+// through the vars map.
const MatcherErrorVarKey = "matchers.error"
// Interface guards
var (
- _ RequestMatcher = (*MatchHost)(nil)
- _ caddy.Provisioner = (*MatchHost)(nil)
- _ RequestMatcher = (*MatchPath)(nil)
- _ RequestMatcher = (*MatchPathRE)(nil)
- _ caddy.Provisioner = (*MatchPathRE)(nil)
- _ RequestMatcher = (*MatchMethod)(nil)
- _ RequestMatcher = (*MatchQuery)(nil)
- _ RequestMatcher = (*MatchHeader)(nil)
- _ RequestMatcher = (*MatchHeaderRE)(nil)
- _ caddy.Provisioner = (*MatchHeaderRE)(nil)
- _ RequestMatcher = (*MatchProtocol)(nil)
- _ RequestMatcher = (*MatchNot)(nil)
- _ caddy.Provisioner = (*MatchNot)(nil)
- _ caddy.Provisioner = (*MatchRegexp)(nil)
+ _ RequestMatcherWithError = (*MatchHost)(nil)
+ _ caddy.Provisioner = (*MatchHost)(nil)
+ _ RequestMatcherWithError = (*MatchPath)(nil)
+ _ RequestMatcherWithError = (*MatchPathRE)(nil)
+ _ caddy.Provisioner = (*MatchPathRE)(nil)
+ _ RequestMatcherWithError = (*MatchMethod)(nil)
+ _ RequestMatcherWithError = (*MatchQuery)(nil)
+ _ RequestMatcherWithError = (*MatchHeader)(nil)
+ _ RequestMatcherWithError = (*MatchHeaderRE)(nil)
+ _ caddy.Provisioner = (*MatchHeaderRE)(nil)
+ _ RequestMatcherWithError = (*MatchProtocol)(nil)
+ _ RequestMatcherWithError = (*MatchNot)(nil)
+ _ caddy.Provisioner = (*MatchNot)(nil)
+ _ caddy.Provisioner = (*MatchRegexp)(nil)
_ caddyfile.Unmarshaler = (*MatchHost)(nil)
_ caddyfile.Unmarshaler = (*MatchPath)(nil)
diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go
index 05eaade5b..f7be6909e 100644
--- a/modules/caddyhttp/matchers_test.go
+++ b/modules/caddyhttp/matchers_test.go
@@ -158,7 +158,10 @@ func TestHostMatcher(t *testing.T) {
t.Errorf("Test %d %v: provisioning failed: %v", i, tc.match, err)
}
- actual := tc.match.Match(req)
+ actual, err := tc.match.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
+ }
if actual != tc.expect {
t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
continue
@@ -430,7 +433,10 @@ func TestPathMatcher(t *testing.T) {
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
- actual := tc.match.Match(req)
+ actual, err := tc.match.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
+ }
if actual != tc.expect {
t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
continue
@@ -451,7 +457,10 @@ func TestPathMatcherWindows(t *testing.T) {
req = req.WithContext(ctx)
match := MatchPath{"*.php"}
- matched := match.Match(req)
+ matched, err := match.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Expected no error, but got: %v", err)
+ }
if !matched {
t.Errorf("Expected to match; should ignore trailing dots and spaces")
}
@@ -555,7 +564,10 @@ func TestPathREMatcher(t *testing.T) {
req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
- actual := tc.match.Match(req)
+ actual, err := tc.match.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
+ }
if actual != tc.expect {
t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'",
i, tc.match.Pattern, tc.expect, actual, tc.input)
@@ -691,7 +703,10 @@ func TestHeaderMatcher(t *testing.T) {
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
- actual := tc.match.Match(req)
+ actual, err := tc.match.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
+ }
if actual != tc.expect {
t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
continue
@@ -818,7 +833,10 @@ func TestQueryMatcher(t *testing.T) {
repl.Set("http.vars.debug", "1")
repl.Set("http.vars.key", "somekey")
req = req.WithContext(ctx)
- actual := tc.match.Match(req)
+ actual, err := tc.match.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
+ }
if actual != tc.expect {
t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
continue
@@ -887,7 +905,10 @@ func TestHeaderREMatcher(t *testing.T) {
req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
- actual := tc.match.Match(req)
+ actual, err := tc.match.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
+ }
if actual != tc.expect {
t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'",
i, tc.match, tc.expect, actual, tc.input)
@@ -927,7 +948,7 @@ func BenchmarkHeaderREMatcher(b *testing.B) {
req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
for run := 0; run < b.N; run++ {
- match.Match(req)
+ match.MatchWithError(req)
}
}
@@ -998,7 +1019,10 @@ func TestVarREMatcher(t *testing.T) {
tc.input.ServeHTTP(httptest.NewRecorder(), req, emptyHandler)
- actual := tc.match.Match(req)
+ actual, err := tc.match.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
+ }
if actual != tc.expect {
t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'",
i, tc.match, tc.expect, actual, tc.input)
@@ -1123,7 +1147,10 @@ func TestNotMatcher(t *testing.T) {
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
- actual := tc.match.Match(req)
+ actual, err := tc.match.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
+ }
if actual != tc.expect {
t.Errorf("Test %d %+v: Expected %t, got %t for: host=%s path=%s'", i, tc.match, tc.expect, actual, tc.host, tc.path)
continue
@@ -1155,7 +1182,7 @@ func BenchmarkLargeHostMatcher(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
- matcher.Match(req)
+ matcher.MatchWithError(req)
}
}
@@ -1169,7 +1196,7 @@ func BenchmarkHostMatcherWithoutPlaceholder(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
- match.Match(req)
+ match.MatchWithError(req)
}
}
@@ -1187,6 +1214,6 @@ func BenchmarkHostMatcherWithPlaceholder(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
- match.Match(req)
+ match.MatchWithError(req)
}
}
diff --git a/modules/caddyhttp/metrics.go b/modules/caddyhttp/metrics.go
index 947721429..9bb97e0b4 100644
--- a/modules/caddyhttp/metrics.go
+++ b/modules/caddyhttp/metrics.go
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"net/http"
+ "strings"
"sync"
"time"
@@ -133,8 +134,8 @@ func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
statusLabels := prometheus.Labels{"server": server, "handler": h.handler, "method": method, "code": ""}
if h.metrics.PerHost {
- labels["host"] = r.Host
- statusLabels["host"] = r.Host
+ labels["host"] = strings.ToLower(r.Host)
+ statusLabels["host"] = strings.ToLower(r.Host)
}
inFlight := h.metrics.httpMetrics.requestInFlight.With(labels)
diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go
index 2c0f32357..776aa6294 100644
--- a/modules/caddyhttp/replacer.go
+++ b/modules/caddyhttp/replacer.go
@@ -186,6 +186,11 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
return path.Ext(req.URL.Path), true
case "http.request.uri.query":
return req.URL.RawQuery, true
+ case "http.request.uri.prefixed_query":
+ if req.URL.RawQuery == "" {
+ return "", true
+ }
+ return "?" + req.URL.RawQuery, true
case "http.request.duration":
start := GetVar(req.Context(), "start_time").(time.Time)
return time.Since(start), true
@@ -239,6 +244,12 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
case "http.request.orig_uri.query":
or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
return or.URL.RawQuery, true
+ case "http.request.orig_uri.prefixed_query":
+ or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
+ if or.URL.RawQuery == "" {
+ return "", true
+ }
+ return "?" + or.URL.RawQuery, true
}
// remote IP range/prefix (e.g. keep top 24 bits of 1.2.3.4 => "1.2.3.0/24")
diff --git a/modules/caddyhttp/requestbody/requestbody.go b/modules/caddyhttp/requestbody/requestbody.go
index 1c804aa13..830050416 100644
--- a/modules/caddyhttp/requestbody/requestbody.go
+++ b/modules/caddyhttp/requestbody/requestbody.go
@@ -15,6 +15,7 @@
package requestbody
import (
+ "errors"
"io"
"net/http"
"time"
@@ -94,7 +95,8 @@ type errorWrapper struct {
func (ew errorWrapper) Read(p []byte) (n int, err error) {
n, err = ew.ReadCloser.Read(p)
- if err != nil && err.Error() == "http: request body too large" {
+ var mbe *http.MaxBytesError
+ if errors.As(err, &mbe) {
err = caddyhttp.Error(http.StatusRequestEntityTooLarge, err)
}
return
diff --git a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
index 68eee32be..6fe7df3fd 100644
--- a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
+++ b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
@@ -18,6 +18,7 @@ import (
"encoding/json"
"net/http"
"strconv"
+ "strings"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
@@ -179,7 +180,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
indexFile := "index.php"
// set up for explicitly overriding try_files
- tryFiles := []string{}
+ var tryFiles []string
// if the user specified a matcher token, use that
// matcher in a route that wraps both of our routes;
@@ -310,37 +311,60 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
// if the index is turned off, we skip the redirect and try_files
if indexFile != "off" {
- // route to redirect to canonical path if index PHP file
- redirMatcherSet := caddy.ModuleMap{
- "file": h.JSON(fileserver.MatchFile{
- TryFiles: []string{"{http.request.uri.path}/" + indexFile},
- }),
- "not": h.JSON(caddyhttp.MatchNot{
- MatcherSetsRaw: []caddy.ModuleMap{
- {
- "path": h.JSON(caddyhttp.MatchPath{"*/"}),
- },
- },
- }),
- }
- redirHandler := caddyhttp.StaticResponse{
- StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
- Headers: http.Header{"Location": []string{"{http.request.orig_uri.path}/"}},
- }
- redirRoute := caddyhttp.Route{
- MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet},
- HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)},
- }
+ dirRedir := false
+ dirIndex := "{http.request.uri.path}/" + indexFile
+ tryPolicy := "first_exist_fallback"
// if tryFiles wasn't overridden, use a reasonable default
if len(tryFiles) == 0 {
- tryFiles = []string{"{http.request.uri.path}", "{http.request.uri.path}/" + indexFile, indexFile}
+ tryFiles = []string{"{http.request.uri.path}", dirIndex, indexFile}
+ dirRedir = true
+ } else {
+ if !strings.HasSuffix(tryFiles[len(tryFiles)-1], ".php") {
+ // use first_exist strategy if the last file is not a PHP file
+ tryPolicy = ""
+ }
+
+ for _, tf := range tryFiles {
+ if tf == dirIndex {
+ dirRedir = true
+
+ break
+ }
+ }
+ }
+
+ if dirRedir {
+ // route to redirect to canonical path if index PHP file
+ redirMatcherSet := caddy.ModuleMap{
+ "file": h.JSON(fileserver.MatchFile{
+ TryFiles: []string{dirIndex},
+ }),
+ "not": h.JSON(caddyhttp.MatchNot{
+ MatcherSetsRaw: []caddy.ModuleMap{
+ {
+ "path": h.JSON(caddyhttp.MatchPath{"*/"}),
+ },
+ },
+ }),
+ }
+ redirHandler := caddyhttp.StaticResponse{
+ StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
+ Headers: http.Header{"Location": []string{"{http.request.orig_uri.path}/{http.request.orig_uri.prefixed_query}"}},
+ }
+ redirRoute := caddyhttp.Route{
+ MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet},
+ HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)},
+ }
+
+ routes = append(routes, redirRoute)
}
// route to rewrite to PHP index file
rewriteMatcherSet := caddy.ModuleMap{
"file": h.JSON(fileserver.MatchFile{
TryFiles: tryFiles,
+ TryPolicy: tryPolicy,
SplitPath: extensions,
}),
}
@@ -352,7 +376,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, "handler", "rewrite", nil)},
}
- routes = append(routes, redirRoute, rewriteRoute)
+ routes = append(routes, rewriteRoute)
}
// route to actually reverse proxy requests to PHP files;
diff --git a/modules/caddyhttp/reverseproxy/fastcgi/client.go b/modules/caddyhttp/reverseproxy/fastcgi/client.go
index 7284fe672..684394f53 100644
--- a/modules/caddyhttp/reverseproxy/fastcgi/client.go
+++ b/modules/caddyhttp/reverseproxy/fastcgi/client.go
@@ -41,6 +41,8 @@ import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
+
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
// FCGIListenSockFileno describes listen socket file number.
@@ -136,6 +138,15 @@ type client struct {
// Do made the request and returns a io.Reader that translates the data read
// from fcgi responder out of fcgi packet before returning it.
func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error) {
+ // check for CONTENT_LENGTH, since the lack of it or wrong value will cause the backend to hang
+ if clStr, ok := p["CONTENT_LENGTH"]; !ok {
+ return nil, caddyhttp.Error(http.StatusLengthRequired, nil)
+ } else if _, err := strconv.ParseUint(clStr, 10, 64); err != nil {
+ // stdlib won't return a negative Content-Length, but we check just in case,
+ // the most likely cause is from a missing content length, which is -1
+ return nil, caddyhttp.Error(http.StatusLengthRequired, err)
+ }
+
writer := &streamWriter{c: c}
writer.buf = bufPool.Get().(*bytes.Buffer)
writer.buf.Reset()
diff --git a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
index 3985465ba..d451dd380 100644
--- a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
+++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
@@ -228,7 +228,7 @@ func (t Transport) buildEnv(r *http.Request) (envVars, error) {
ip = strings.Replace(ip, "]", "", 1)
// make sure file root is absolute
- root, err := filepath.Abs(repl.ReplaceAll(t.Root, "."))
+ root, err := caddy.FastAbs(repl.ReplaceAll(t.Root, "."))
if err != nil {
return nil, err
}
diff --git a/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go b/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go
index 8350096ae..347f6dfbf 100644
--- a/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go
+++ b/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go
@@ -17,6 +17,7 @@ package forwardauth
import (
"encoding/json"
"net/http"
+ "sort"
"strings"
"github.com/caddyserver/caddy/v2"
@@ -170,42 +171,66 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
return nil, dispenser.Errf("the 'uri' subdirective is required")
}
- // set up handler for good responses; when a response
- // has 2xx status, then we will copy some headers from
- // the response onto the original request, and allow
- // handling to continue down the middleware chain,
- // by _not_ executing a terminal handler.
+ // Set up handler for good responses; when a response has 2xx status,
+ // then we will copy some headers from the response onto the original
+ // request, and allow handling to continue down the middleware chain,
+ // by _not_ executing a terminal handler. We must have at least one
+ // route in the response handler, even if it's no-op, so that the
+ // response handling logic in reverse_proxy doesn't skip this entry.
goodResponseHandler := caddyhttp.ResponseHandler{
Match: &caddyhttp.ResponseMatcher{
StatusCode: []int{2},
},
- Routes: []caddyhttp.Route{},
- }
-
- handler := &headers.Handler{
- Request: &headers.HeaderOps{
- Set: http.Header{},
+ Routes: []caddyhttp.Route{
+ {
+ HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
+ &caddyhttp.VarsMiddleware{},
+ "handler",
+ "vars",
+ nil,
+ )},
+ },
},
}
- // the list of headers to copy may be empty, but that's okay; we
- // need at least one handler in the routes for the response handling
- // logic in reverse_proxy to not skip this entry as empty.
- for from, to := range headersToCopy {
- handler.Request.Set.Set(to, "{http.reverse_proxy.header."+http.CanonicalHeaderKey(from)+"}")
+ // Sort the headers so that the order in the JSON output is deterministic.
+ sortedHeadersToCopy := make([]string, 0, len(headersToCopy))
+ for k := range headersToCopy {
+ sortedHeadersToCopy = append(sortedHeadersToCopy, k)
}
+ sort.Strings(sortedHeadersToCopy)
- goodResponseHandler.Routes = append(
- goodResponseHandler.Routes,
- caddyhttp.Route{
+ // Set up handlers to copy headers from the auth response onto the
+ // original request. We use vars matchers to test that the placeholder
+ // values aren't empty, because the header handler would not replace
+ // placeholders which have no value.
+ copyHeaderRoutes := []caddyhttp.Route{}
+ for _, from := range sortedHeadersToCopy {
+ to := http.CanonicalHeaderKey(headersToCopy[from])
+ placeholderName := "http.reverse_proxy.header." + http.CanonicalHeaderKey(from)
+ handler := &headers.Handler{
+ Request: &headers.HeaderOps{
+ Set: http.Header{
+ to: []string{"{" + placeholderName + "}"},
+ },
+ },
+ }
+ copyHeaderRoutes = append(copyHeaderRoutes, caddyhttp.Route{
+ MatcherSetsRaw: []caddy.ModuleMap{{
+ "not": h.JSON(caddyhttp.MatchNot{MatcherSetsRaw: []caddy.ModuleMap{{
+ "vars": h.JSON(caddyhttp.VarsMatcher{"{" + placeholderName + "}": []string{""}}),
+ }}}),
+ }},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
handler,
"handler",
"headers",
nil,
)},
- },
- )
+ })
+ }
+
+ goodResponseHandler.Routes = append(goodResponseHandler.Routes, copyHeaderRoutes...)
// note that when a response has any other status than 2xx, then we
// use the reverse proxy's default behaviour of copying the response
diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go
index 1735e45a4..f0ffee5b8 100644
--- a/modules/caddyhttp/reverseproxy/healthchecks.go
+++ b/modules/caddyhttp/reverseproxy/healthchecks.go
@@ -72,7 +72,7 @@ type HealthChecks struct {
// health checks (that is, health checks which occur in a
// background goroutine independently).
type ActiveHealthChecks struct {
- // DEPRECATED: Use 'uri' instead. This field will be removed. TODO: remove this field
+ // Deprecated: Use 'uri' instead. This field will be removed. TODO: remove this field
Path string `json:"path,omitempty"`
// The URI (path and query) to use for health checks
diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go
index 2b4c3f094..910033ca1 100644
--- a/modules/caddyhttp/reverseproxy/httptransport.go
+++ b/modules/caddyhttp/reverseproxy/httptransport.go
@@ -545,11 +545,11 @@ type TLSConfig struct {
// Certificate authority module which provides the certificate pool of trusted certificates
CARaw json.RawMessage `json:"ca,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"`
- // DEPRECATED: Use the `ca` field with the `tls.ca_pool.source.inline` module instead.
+ // Deprecated: Use the `ca` field with the `tls.ca_pool.source.inline` module instead.
// Optional list of base64-encoded DER-encoded CA certificates to trust.
RootCAPool []string `json:"root_ca_pool,omitempty"`
- // DEPRECATED: Use the `ca` field with the `tls.ca_pool.source.file` module instead.
+ // Deprecated: Use the `ca` field with the `tls.ca_pool.source.file` module instead.
// List of PEM-encoded CA certificate files to add to the same trust
// store as RootCAPool (or root_ca_pool in the JSON).
RootCAPEMFiles []string `json:"root_ca_pem_files,omitempty"`
diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go
index 123bf774b..f9485c570 100644
--- a/modules/caddyhttp/reverseproxy/reverseproxy.go
+++ b/modules/caddyhttp/reverseproxy/reverseproxy.go
@@ -17,6 +17,8 @@ package reverseproxy
import (
"bytes"
"context"
+ "crypto/rand"
+ "encoding/base64"
"encoding/json"
"errors"
"fmt"
@@ -108,11 +110,6 @@ type Handler struct {
// response is recognized as a streaming response, or if its
// content length is -1; for such responses, writes are flushed
// to the client immediately.
- //
- // Normally, a request will be canceled if the client disconnects
- // before the response is received from the backend. If explicitly
- // set to -1, client disconnection will be ignored and the request
- // will be completed to help facilitate low-latency streaming.
FlushInterval caddy.Duration `json:"flush_interval,omitempty"`
// A list of IP ranges (supports CIDR notation) from which
@@ -399,6 +396,23 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
return caddyhttp.Error(http.StatusInternalServerError,
fmt.Errorf("preparing request for upstream round-trip: %v", err))
}
+ // websocket over http2, assuming backend doesn't support this, the request will be modified to http1.1 upgrade
+ // TODO: once we can reliably detect backend support this, it can be removed for those backends
+ if r.ProtoMajor == 2 && r.Method == http.MethodConnect && r.Header.Get(":protocol") == "websocket" {
+ clonedReq.Header.Del(":protocol")
+ // keep the body for later use. http1.1 upgrade uses http.NoBody
+ caddyhttp.SetVar(clonedReq.Context(), "h2_websocket_body", clonedReq.Body)
+ clonedReq.Body = http.NoBody
+ clonedReq.Method = http.MethodGet
+ clonedReq.Header.Set("Upgrade", "websocket")
+ clonedReq.Header.Set("Connection", "Upgrade")
+ key := make([]byte, 16)
+ _, randErr := rand.Read(key)
+ if randErr != nil {
+ return randErr
+ }
+ clonedReq.Header["Sec-WebSocket-Key"] = []string{base64.StdEncoding.EncodeToString(key)}
+ }
// we will need the original headers and Host value if
// header operations are configured; this is so that each
@@ -496,7 +510,7 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
if proxyErr == nil {
proxyErr = caddyhttp.Error(http.StatusServiceUnavailable, errNoUpstream)
}
- if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r) {
+ if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r, h.logger) {
return true, proxyErr
}
return false, proxyErr
@@ -554,7 +568,7 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
// ding the health status of the upstream (an error can still
// occur after the roundtrip if, for example, a response handler
// after the roundtrip returns an error)
- if succ, ok := proxyErr.(roundtripSucceeded); ok {
+ if succ, ok := proxyErr.(roundtripSucceededError); ok {
return true, succ.error
}
@@ -562,7 +576,7 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
h.countFailure(upstream)
// if we've tried long enough, break
- if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r) {
+ if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r, h.logger) {
return true, proxyErr
}
@@ -625,7 +639,8 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http.
if h.RequestBuffers != 0 && req.Body != nil {
var readBytes int64
req.Body, readBytes = h.bufferedBody(req.Body, h.RequestBuffers)
- if h.RequestBuffers == -1 {
+ // set Content-Length when body is fully buffered
+ if b, ok := req.Body.(bodyReadCloser); ok && b.body == nil {
req.ContentLength = readBytes
req.Header.Set("Content-Length", strconv.FormatInt(req.ContentLength, 10))
}
@@ -807,37 +822,44 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
shouldLogCredentials := server.Logs != nil && server.Logs.ShouldLogCredentials
// Forward 1xx status codes, backported from https://github.com/golang/go/pull/53164
+ var (
+ roundTripMutex sync.Mutex
+ roundTripDone bool
+ )
trace := &httptrace.ClientTrace{
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
+ roundTripMutex.Lock()
+ defer roundTripMutex.Unlock()
+ if roundTripDone {
+ // If RoundTrip has returned, don't try to further modify
+ // the ResponseWriter's header map.
+ return nil
+ }
h := rw.Header()
copyHeader(h, http.Header(header))
rw.WriteHeader(code)
// Clear headers coming from the backend
// (it's not automatically done by ResponseWriter.WriteHeader() for 1xx responses)
- for k := range header {
- delete(h, k)
- }
+ clear(h)
return nil
},
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
- // if FlushInterval is explicitly configured to -1 (i.e. flush continuously to achieve
- // low-latency streaming), don't let the transport cancel the request if the client
- // disconnects: user probably wants us to finish sending the data to the upstream
- // regardless, and we should expect client disconnection in low-latency streaming
- // scenarios (see issue #4922)
- if h.FlushInterval == -1 {
- req = req.WithContext(context.WithoutCancel(req.Context()))
- }
-
- // do the round-trip; emit debug log with values we know are
- // safe, or if there is no error, emit fuller log entry
+ // do the round-trip
start := time.Now()
res, err := h.Transport.RoundTrip(req)
duration := time.Since(start)
+
+ // record that the round trip is done for the 1xx response handler
+ roundTripMutex.Lock()
+ roundTripDone = true
+ roundTripMutex.Unlock()
+
+ // emit debug log with values we know are safe,
+ // or if there is no error, emit fuller log entry
logger := h.logger.With(
zap.String("upstream", di.Upstream.String()),
zap.Duration("duration", duration),
@@ -951,10 +973,10 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
res.Body.Close()
}
- // wrap any route error in roundtripSucceeded so caller knows that
+ // wrap any route error in roundtripSucceededError so caller knows that
// the roundtrip was successful and to not retry
if routeErr != nil {
- return roundtripSucceeded{routeErr}
+ return roundtripSucceededError{routeErr}
}
// we're done handling the response, and we don't want to
@@ -1073,7 +1095,7 @@ func (h *Handler) finalizeResponse(
// If true is returned, it has already blocked long enough before
// the next retry (i.e. no more sleeping is needed). If false is
// returned, the handler should stop trying to proxy the request.
-func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int, proxyErr error, req *http.Request) bool {
+func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int, proxyErr error, req *http.Request, logger *zap.Logger) bool {
// no retries are configured
if lb.TryDuration == 0 && lb.Retries == 0 {
return false
@@ -1108,7 +1130,12 @@ func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int
return false
}
- if !lb.RetryMatch.AnyMatch(req) {
+ match, err := lb.RetryMatch.AnyMatchWithError(req)
+ if err != nil {
+ logger.Error("error matching request for retry", zap.Error(err))
+ return false
+ }
+ if !match {
return false
}
}
@@ -1426,9 +1453,9 @@ type TLSTransport interface {
EnableTLS(base *TLSConfig) error
}
-// roundtripSucceeded is an error type that is returned if the
+// roundtripSucceededError is an error type that is returned if the
// roundtrip succeeded, but an error occurred after-the-fact.
-type roundtripSucceeded struct{ error }
+type roundtripSucceededError struct{ error }
// bodyReadCloser is a reader that, upon closing, will return
// its buffer to the pool and close the underlying body reader.
diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies.go b/modules/caddyhttp/reverseproxy/selectionpolicies.go
index 293ff75e2..fcf7f90f6 100644
--- a/modules/caddyhttp/reverseproxy/selectionpolicies.go
+++ b/modules/caddyhttp/reverseproxy/selectionpolicies.go
@@ -111,8 +111,8 @@ func (r *WeightedRoundRobinSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser)
if err != nil {
return d.Errf("invalid weight value '%s': %v", weight, err)
}
- if weightInt < 1 {
- return d.Errf("invalid weight value '%s': weight should be non-zero and positive", weight)
+ if weightInt < 0 {
+ return d.Errf("invalid weight value '%s': weight should be non-negative", weight)
}
r.Weights = append(r.Weights, weightInt)
}
@@ -136,8 +136,15 @@ func (r *WeightedRoundRobinSelection) Select(pool UpstreamPool, _ *http.Request,
return pool[0]
}
var index, totalWeight int
+ var weights []int
+
+ for _, w := range r.Weights {
+ if w > 0 {
+ weights = append(weights, w)
+ }
+ }
currentWeight := int(atomic.AddUint32(&r.index, 1)) % r.totalWeight
- for i, weight := range r.Weights {
+ for i, weight := range weights {
totalWeight += weight
if currentWeight < totalWeight {
index = i
@@ -145,9 +152,9 @@ func (r *WeightedRoundRobinSelection) Select(pool UpstreamPool, _ *http.Request,
}
}
- upstreams := make([]*Upstream, 0, len(r.Weights))
- for _, upstream := range pool {
- if !upstream.Available() {
+ upstreams := make([]*Upstream, 0, len(weights))
+ for i, upstream := range pool {
+ if !upstream.Available() || r.Weights[i] == 0 {
continue
}
upstreams = append(upstreams, upstream)
diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies_test.go b/modules/caddyhttp/reverseproxy/selectionpolicies_test.go
index a4701ce86..580abbdde 100644
--- a/modules/caddyhttp/reverseproxy/selectionpolicies_test.go
+++ b/modules/caddyhttp/reverseproxy/selectionpolicies_test.go
@@ -131,6 +131,58 @@ func TestWeightedRoundRobinPolicy(t *testing.T) {
}
}
+func TestWeightedRoundRobinPolicyWithZeroWeight(t *testing.T) {
+ pool := testPool()
+ wrrPolicy := WeightedRoundRobinSelection{
+ Weights: []int{0, 2, 1},
+ totalWeight: 3,
+ }
+ req, _ := http.NewRequest("GET", "/", nil)
+
+ h := wrrPolicy.Select(pool, req, nil)
+ if h != pool[1] {
+ t.Error("Expected first weighted round robin host to be second host in the pool.")
+ }
+
+ h = wrrPolicy.Select(pool, req, nil)
+ if h != pool[2] {
+ t.Error("Expected second weighted round robin host to be third host in the pool.")
+ }
+
+ h = wrrPolicy.Select(pool, req, nil)
+ if h != pool[1] {
+ t.Error("Expected third weighted round robin host to be second host in the pool.")
+ }
+
+ // mark second host as down
+ pool[1].setHealthy(false)
+ h = wrrPolicy.Select(pool, req, nil)
+ if h != pool[2] {
+ t.Error("Expect select next available host.")
+ }
+
+ h = wrrPolicy.Select(pool, req, nil)
+ if h != pool[2] {
+ t.Error("Expect select only available host.")
+ }
+ // mark second host as up
+ pool[1].setHealthy(true)
+
+ h = wrrPolicy.Select(pool, req, nil)
+ if h != pool[1] {
+ t.Error("Expect select first host on availability.")
+ }
+
+ // test next select in full cycle
+ expected := []*Upstream{pool[1], pool[2], pool[1], pool[1], pool[2], pool[1]}
+ for i, want := range expected {
+ got := wrrPolicy.Select(pool, req, nil)
+ if want != got {
+ t.Errorf("Selection %d: got host[%s], want host[%s]", i+1, got, want)
+ }
+ }
+}
+
func TestLeastConnPolicy(t *testing.T) {
pool := testPool()
lcPolicy := LeastConnSelection{}
diff --git a/modules/caddyhttp/reverseproxy/streaming.go b/modules/caddyhttp/reverseproxy/streaming.go
index 3fde10b35..d697eb402 100644
--- a/modules/caddyhttp/reverseproxy/streaming.go
+++ b/modules/caddyhttp/reverseproxy/streaming.go
@@ -19,6 +19,7 @@
package reverseproxy
import (
+ "bufio"
"context"
"errors"
"fmt"
@@ -33,8 +34,29 @@ import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"golang.org/x/net/http/httpguts"
+
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
+type h2ReadWriteCloser struct {
+ io.ReadCloser
+ http.ResponseWriter
+}
+
+func (rwc h2ReadWriteCloser) Write(p []byte) (n int, err error) {
+ n, err = rwc.ResponseWriter.Write(p)
+ if err != nil {
+ return 0, err
+ }
+
+ //nolint:bodyclose
+ err = http.NewResponseController(rwc.ResponseWriter).Flush()
+ if err != nil {
+ return 0, err
+ }
+ return n, nil
+}
+
func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup, rw http.ResponseWriter, req *http.Request, res *http.Response) {
reqUpType := upgradeType(req.Header)
resUpType := upgradeType(res.Header)
@@ -67,24 +89,58 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
// like the rest of handler chain.
copyHeader(rw.Header(), res.Header)
normalizeWebsocketHeaders(rw.Header())
- rw.WriteHeader(res.StatusCode)
- logger.Debug("upgrading connection")
+ var (
+ conn io.ReadWriteCloser
+ brw *bufio.ReadWriter
+ )
+ // websocket over http2, assuming backend doesn't support this, the request will be modified to http1.1 upgrade
+ // TODO: once we can reliably detect backend support this, it can be removed for those backends
+ if body, ok := caddyhttp.GetVar(req.Context(), "h2_websocket_body").(io.ReadCloser); ok {
+ req.Body = body
+ rw.Header().Del("Upgrade")
+ rw.Header().Del("Connection")
+ delete(rw.Header(), "Sec-WebSocket-Accept")
+ rw.WriteHeader(http.StatusOK)
+
+ if c := logger.Check(zap.DebugLevel, "upgrading connection"); c != nil {
+ c.Write(zap.Int("http_version", 2))
+ }
- //nolint:bodyclose
- conn, brw, hijackErr := http.NewResponseController(rw).Hijack()
- if errors.Is(hijackErr, http.ErrNotSupported) {
- 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)))
+ //nolint:bodyclose
+ flushErr := http.NewResponseController(rw).Flush()
+ if flushErr != nil {
+ if c := h.logger.Check(zap.ErrorLevel, "failed to flush http2 websocket response"); c != nil {
+ c.Write(zap.Error(flushErr))
+ }
+ return
}
- return
- }
+ conn = h2ReadWriteCloser{req.Body, rw}
+ // bufio is not needed, use minimal buffer
+ brw = bufio.NewReadWriter(bufio.NewReaderSize(conn, 1), bufio.NewWriterSize(conn, 1))
+ } else {
+ rw.WriteHeader(res.StatusCode)
- if hijackErr != nil {
- if c := logger.Check(zapcore.ErrorLevel, "hijack failed on protocol switch"); c != nil {
- c.Write(zap.Error(hijackErr))
+ if c := logger.Check(zap.DebugLevel, "upgrading connection"); c != nil {
+ c.Write(zap.Int("http_version", req.ProtoMajor))
+ }
+
+ var hijackErr error
+ //nolint:bodyclose
+ conn, brw, hijackErr = http.NewResponseController(rw).Hijack()
+ if errors.Is(hijackErr, http.ErrNotSupported) {
+ if c := h.logger.Check(zap.ErrorLevel, "can't switch protocols using non-Hijacker ResponseWriter"); c != nil {
+ c.Write(zap.String("type", fmt.Sprintf("%T", rw)))
+ }
+ return
+ }
+
+ if hijackErr != nil {
+ if c := h.logger.Check(zap.ErrorLevel, "hijack failed on protocol switch"); c != nil {
+ c.Write(zap.Error(hijackErr))
+ }
+ return
}
- return
}
// adopted from https://github.com/golang/go/commit/8bcf2834afdf6a1f7937390903a41518715ef6f5
@@ -103,7 +159,7 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
start := time.Now()
defer func() {
conn.Close()
- if c := logger.Check(zapcore.DebugLevel, "hijack failed on protocol switch"); c != nil {
+ if c := logger.Check(zapcore.DebugLevel, "connection closed"); c != nil {
c.Write(zap.Duration("duration", time.Since(start)))
}
}()
diff --git a/modules/caddyhttp/rewrite/caddyfile.go b/modules/caddyhttp/rewrite/caddyfile.go
index 89f44c79b..5f9b97adf 100644
--- a/modules/caddyhttp/rewrite/caddyfile.go
+++ b/modules/caddyhttp/rewrite/caddyfile.go
@@ -110,9 +110,6 @@ func parseCaddyfileURI(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, err
return nil, h.ArgErr()
}
rewr.StripPathPrefix = args[1]
- if !strings.HasPrefix(rewr.StripPathPrefix, "/") {
- rewr.StripPathPrefix = "/" + rewr.StripPathPrefix
- }
case "strip_suffix":
if len(args) != 2 {
diff --git a/modules/caddyhttp/rewrite/rewrite.go b/modules/caddyhttp/rewrite/rewrite.go
index e76682729..31ebfb430 100644
--- a/modules/caddyhttp/rewrite/rewrite.go
+++ b/modules/caddyhttp/rewrite/rewrite.go
@@ -259,6 +259,9 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
// strip path prefix or suffix
if rewr.StripPathPrefix != "" {
prefix := repl.ReplaceAll(rewr.StripPathPrefix, "")
+ if !strings.HasPrefix(prefix, "/") {
+ prefix = "/" + prefix
+ }
mergeSlashes := !strings.Contains(prefix, "//")
changePath(r, func(escapedPath string) string {
escapedPath = caddyhttp.CleanPath(escapedPath, mergeSlashes)
diff --git a/modules/caddyhttp/rewrite/rewrite_test.go b/modules/caddyhttp/rewrite/rewrite_test.go
index aaa142bc2..81360baee 100644
--- a/modules/caddyhttp/rewrite/rewrite_test.go
+++ b/modules/caddyhttp/rewrite/rewrite_test.go
@@ -236,6 +236,11 @@ func TestRewrite(t *testing.T) {
expect: newRequest(t, "GET", "/foo/bar"),
},
{
+ rule: Rewrite{StripPathPrefix: "prefix"},
+ input: newRequest(t, "GET", "/prefix/foo/bar"),
+ expect: newRequest(t, "GET", "/foo/bar"),
+ },
+ {
rule: Rewrite{StripPathPrefix: "/prefix"},
input: newRequest(t, "GET", "/prefix"),
expect: newRequest(t, "GET", ""),
diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go
index 939d01e55..ccb5f2515 100644
--- a/modules/caddyhttp/routes.go
+++ b/modules/caddyhttp/routes.go
@@ -254,18 +254,13 @@ func wrapRoute(route Route) Middleware {
nextCopy := next
// route must match at least one of the matcher sets
- if !route.MatcherSets.AnyMatch(req) {
+ matches, err := route.MatcherSets.AnyMatchWithError(req)
+ if err != nil {
// allow matchers the opportunity to short circuit
// the request and trigger the error handling chain
- err, ok := GetVar(req.Context(), MatcherErrorVarKey).(error)
- if ok {
- // clear out the error from context, otherwise
- // it will cascade to the error routes (#4916)
- SetVar(req.Context(), MatcherErrorVarKey, nil)
- // return the matcher's error
- return err
- }
-
+ return err
+ }
+ if !matches {
// call the next handler, and skip this one,
// since the matcher didn't match
return nextCopy.ServeHTTP(rw, req)
@@ -341,19 +336,58 @@ func wrapMiddleware(ctx caddy.Context, mh MiddlewareHandler, metrics *Metrics) M
// MatcherSet is a set of matchers which
// must all match in order for the request
// to be matched successfully.
-type MatcherSet []RequestMatcher
+type MatcherSet []any
// Match returns true if the request matches all
// matchers in mset or if there are no matchers.
func (mset MatcherSet) Match(r *http.Request) bool {
for _, m := range mset {
- if !m.Match(r) {
- return false
+ if me, ok := m.(RequestMatcherWithError); ok {
+ match, _ := me.MatchWithError(r)
+ if !match {
+ return false
+ }
+ continue
+ }
+ if me, ok := m.(RequestMatcher); ok {
+ if !me.Match(r) {
+ return false
+ }
+ continue
}
+ return false
}
return true
}
+// MatchWithError returns true if r matches m.
+func (mset MatcherSet) MatchWithError(r *http.Request) (bool, error) {
+ for _, m := range mset {
+ if me, ok := m.(RequestMatcherWithError); ok {
+ match, err := me.MatchWithError(r)
+ if err != nil || !match {
+ return match, err
+ }
+ continue
+ }
+ if me, ok := m.(RequestMatcher); ok {
+ if !me.Match(r) {
+ // for backwards compatibility
+ err, ok := GetVar(r.Context(), MatcherErrorVarKey).(error)
+ if ok {
+ // clear out the error from context since we've consumed it
+ SetVar(r.Context(), MatcherErrorVarKey, nil)
+ return false, err
+ }
+ return false, nil
+ }
+ continue
+ }
+ return false, fmt.Errorf("matcher is not a RequestMatcher or RequestMatcherWithError: %#v", m)
+ }
+ return true, nil
+}
+
// RawMatcherSets is a group of matcher sets
// in their raw, JSON form.
type RawMatcherSets []caddy.ModuleMap
@@ -366,25 +400,50 @@ type MatcherSets []MatcherSet
// AnyMatch returns true if req matches any of the
// matcher sets in ms or if there are no matchers,
// in which case the request always matches.
+//
+// Deprecated: Use AnyMatchWithError instead.
func (ms MatcherSets) AnyMatch(req *http.Request) bool {
for _, m := range ms {
- if m.Match(req) {
- return true
+ match, err := m.MatchWithError(req)
+ if err != nil {
+ SetVar(req.Context(), MatcherErrorVarKey, err)
+ return false
+ }
+ if match {
+ return match
}
}
return len(ms) == 0
}
+// AnyMatchWithError returns true if req matches any of the
+// matcher sets in ms or if there are no matchers, in which
+// case the request always matches. If any matcher returns
+// an error, we cut short and return the error.
+func (ms MatcherSets) AnyMatchWithError(req *http.Request) (bool, error) {
+ for _, m := range ms {
+ match, err := m.MatchWithError(req)
+ if err != nil || match {
+ return match, err
+ }
+ }
+ return len(ms) == 0, nil
+}
+
// FromInterface fills ms from an 'any' value obtained from LoadModule.
func (ms *MatcherSets) FromInterface(matcherSets any) error {
for _, matcherSetIfaces := range matcherSets.([]map[string]any) {
var matcherSet MatcherSet
for _, matcher := range matcherSetIfaces {
- reqMatcher, ok := matcher.(RequestMatcher)
- if !ok {
- return fmt.Errorf("decoded module is not a RequestMatcher: %#v", matcher)
+ if m, ok := matcher.(RequestMatcherWithError); ok {
+ matcherSet = append(matcherSet, m)
+ continue
+ }
+ if m, ok := matcher.(RequestMatcher); ok {
+ matcherSet = append(matcherSet, m)
+ continue
}
- matcherSet = append(matcherSet, reqMatcher)
+ return fmt.Errorf("decoded module is not a RequestMatcher or RequestMatcherWithError: %#v", matcher)
}
*ms = append(*ms, matcherSet)
}
diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go
index 5aa7e0f63..12c032dee 100644
--- a/modules/caddyhttp/server.go
+++ b/modules/caddyhttp/server.go
@@ -61,6 +61,7 @@ type Server struct {
ReadTimeout caddy.Duration `json:"read_timeout,omitempty"`
// ReadHeaderTimeout is like ReadTimeout but for request headers.
+ // Default is 1 minute.
ReadHeaderTimeout caddy.Duration `json:"read_header_timeout,omitempty"`
// WriteTimeout is how long to allow a write to a client. Note
@@ -226,6 +227,7 @@ type Server struct {
// If set, metrics observations will be enabled.
// This setting is EXPERIMENTAL and subject to change.
+ // DEPRECATED: Use the app-level `metrics` field.
Metrics *Metrics `json:"metrics,omitempty"`
name string
@@ -614,22 +616,7 @@ func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error
// create HTTP/3 server if not done already
if s.h3server == nil {
s.h3server = &http3.Server{
- // Currently when closing a http3.Server, only listeners are closed. But caddy reuses these listeners
- // if possible, requests are still read and handled by the old handler. Close these connections manually.
- // see issue: https://github.com/caddyserver/caddy/issues/6195
- // Will interrupt ongoing requests.
- // TODO: remove the handler wrap after http3.Server.CloseGracefully is implemented, see App.Stop
- Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
- select {
- case <-s.ctx.Done():
- if quicConn, ok := request.Context().Value(quicConnCtxKey).(quic.Connection); ok {
- //nolint:errcheck
- quicConn.CloseWithError(quic.ApplicationErrorCode(http3.ErrCodeRequestRejected), "")
- }
- default:
- s.ServeHTTP(writer, request)
- }
- }),
+ Handler: s,
TLSConfig: tlsCfg,
MaxHeaderBytes: s.MaxHeaderBytes,
QUICConfig: &quic.Config{
@@ -637,9 +624,6 @@ func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error
Tracer: qlog.DefaultConnectionTracer,
},
IdleTimeout: time.Duration(s.IdleTimeout),
- ConnContext: func(ctx context.Context, c quic.Connection) context.Context {
- return context.WithValue(ctx, quicConnCtxKey, c)
- },
}
}
@@ -1099,10 +1083,6 @@ const (
// For referencing underlying net.Conn
ConnCtxKey caddy.CtxKey = "conn"
- // For referencing underlying quic.Connection
- // TODO: export if needed later
- quicConnCtxKey caddy.CtxKey = "quic_conn"
-
// For tracking whether the client is a trusted proxy
TrustedProxyVarKey string = "trusted_proxy"
diff --git a/modules/caddyhttp/server_test.go b/modules/caddyhttp/server_test.go
index 2c8033d45..53f35368f 100644
--- a/modules/caddyhttp/server_test.go
+++ b/modules/caddyhttp/server_test.go
@@ -69,12 +69,13 @@ func TestServer_LogRequest(t *testing.T) {
}`, buf.String())
}
-func TestServer_LogRequest_WithTraceID(t *testing.T) {
+func TestServer_LogRequest_WithTrace(t *testing.T) {
s := &Server{}
extra := new(ExtraLogFields)
ctx := context.WithValue(context.Background(), ExtraLogFieldsCtxKey, extra)
extra.Add(zap.String("traceID", "1234567890abcdef"))
+ extra.Add(zap.String("spanID", "12345678"))
req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx)
rec := httptest.NewRecorder()
@@ -93,7 +94,8 @@ func TestServer_LogRequest_WithTraceID(t *testing.T) {
"msg":"handled request", "level":"info", "bytes_read":0,
"duration":"50ms", "resp_headers": {}, "size":0,
"status":0, "user_id":"",
- "traceID":"1234567890abcdef"
+ "traceID":"1234567890abcdef",
+ "spanID":"12345678"
}`, buf.String())
}
@@ -144,12 +146,13 @@ func BenchmarkServer_LogRequest_NopLogger(b *testing.B) {
}
}
-func BenchmarkServer_LogRequest_WithTraceID(b *testing.B) {
+func BenchmarkServer_LogRequest_WithTrace(b *testing.B) {
s := &Server{}
extra := new(ExtraLogFields)
ctx := context.WithValue(context.Background(), ExtraLogFieldsCtxKey, extra)
extra.Add(zap.String("traceID", "1234567890abcdef"))
+ extra.Add(zap.String("spanID", "12345678"))
req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx)
rec := httptest.NewRecorder()
diff --git a/modules/caddyhttp/tracing/tracer.go b/modules/caddyhttp/tracing/tracer.go
index 89c617bf4..261952aa6 100644
--- a/modules/caddyhttp/tracing/tracer.go
+++ b/modules/caddyhttp/tracing/tracer.go
@@ -88,11 +88,15 @@ func (ot *openTelemetryWrapper) serveHTTP(w http.ResponseWriter, r *http.Request
spanCtx := trace.SpanContextFromContext(ctx)
if spanCtx.IsValid() {
traceID := spanCtx.TraceID().String()
+ spanID := spanCtx.SpanID().String()
// Add a trace_id placeholder, accessible via `{http.vars.trace_id}`.
caddyhttp.SetVar(ctx, "trace_id", traceID)
- // Add the trace id to the log fields for the request.
+ // Add a span_id placeholder, accessible via `{http.vars.span_id}`.
+ caddyhttp.SetVar(ctx, "span_id", spanID)
+ // Add the traceID and spanID to the log fields for the request.
if extra, ok := ctx.Value(caddyhttp.ExtraLogFieldsCtxKey).(*caddyhttp.ExtraLogFields); ok {
extra.Add(zap.String("traceID", traceID))
+ extra.Add(zap.String("spanID", spanID))
}
}
next := ctx.Value(nextCallCtxKey).(*nextCall)
diff --git a/modules/caddyhttp/vars.go b/modules/caddyhttp/vars.go
index 77e06e3cb..7ab891fc0 100644
--- a/modules/caddyhttp/vars.go
+++ b/modules/caddyhttp/vars.go
@@ -166,8 +166,14 @@ func (m *VarsMatcher) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Match matches a request based on variables in the context,
// or placeholders if the key is not a variable.
func (m VarsMatcher) Match(r *http.Request) bool {
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m VarsMatcher) MatchWithError(r *http.Request) (bool, error) {
if len(m) == 0 {
- return true
+ return true, nil
}
vars := r.Context().Value(VarsCtxKey).(map[string]any)
@@ -200,11 +206,11 @@ func (m VarsMatcher) Match(r *http.Request) bool {
varStr = fmt.Sprintf("%v", vv)
}
if varStr == matcherValExpanded {
- return true
+ return true, nil
}
}
}
- return false
+ return false, nil
}
// CELLibrary produces options that expose this matcher for use in CEL
@@ -219,7 +225,7 @@ func (VarsMatcher) CELLibrary(_ caddy.Context) (cel.Library, error) {
"vars",
"vars_matcher_request_map",
[]*cel.Type{CELTypeJSON},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
mapStrListStr, err := CELValueToMapStrList(data)
if err != nil {
return nil, err
@@ -294,6 +300,12 @@ func (m MatchVarsRE) Provision(ctx caddy.Context) error {
// Match returns true if r matches m.
func (m MatchVarsRE) Match(r *http.Request) bool {
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) {
vars := r.Context().Value(VarsCtxKey).(map[string]any)
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
for key, val := range m {
@@ -322,10 +334,10 @@ func (m MatchVarsRE) Match(r *http.Request) bool {
valExpanded := repl.ReplaceAll(varStr, "")
if match := val.Match(valExpanded, repl); match {
- return match
+ return match, nil
}
}
- return false
+ return false, nil
}
// CELLibrary produces options that expose this matcher for use in CEL
@@ -340,7 +352,7 @@ func (MatchVarsRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
"vars_regexp",
"vars_regexp_request_string_string",
[]*cel.Type{cel.StringType, cel.StringType},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
refStringList := reflect.TypeOf([]string{})
params, err := data.ConvertToNative(refStringList)
if err != nil {
@@ -363,7 +375,7 @@ func (MatchVarsRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
"vars_regexp",
"vars_regexp_request_string_string_string",
[]*cel.Type{cel.StringType, cel.StringType, cel.StringType},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
refStringList := reflect.TypeOf([]string{})
params, err := data.ConvertToNative(refStringList)
if err != nil {
@@ -435,8 +447,10 @@ func SetVar(ctx context.Context, key string, value any) {
// Interface guards
var (
- _ MiddlewareHandler = (*VarsMiddleware)(nil)
- _ caddyfile.Unmarshaler = (*VarsMiddleware)(nil)
- _ RequestMatcher = (*VarsMatcher)(nil)
- _ caddyfile.Unmarshaler = (*VarsMatcher)(nil)
+ _ MiddlewareHandler = (*VarsMiddleware)(nil)
+ _ caddyfile.Unmarshaler = (*VarsMiddleware)(nil)
+ _ RequestMatcherWithError = (*VarsMatcher)(nil)
+ _ caddyfile.Unmarshaler = (*VarsMatcher)(nil)
+ _ RequestMatcherWithError = (*MatchVarsRE)(nil)
+ _ caddyfile.Unmarshaler = (*MatchVarsRE)(nil)
)
diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go
index 9dfeff724..29a5954e7 100644
--- a/modules/caddytls/acmeissuer.go
+++ b/modules/caddytls/acmeissuer.go
@@ -28,7 +28,7 @@ import (
"github.com/caddyserver/certmagic"
"github.com/caddyserver/zerossl"
- "github.com/mholt/acmez/v2/acme"
+ "github.com/mholt/acmez/v3/acme"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
diff --git a/modules/caddytls/automation.go b/modules/caddytls/automation.go
index f6a535077..1bc86020d 100644
--- a/modules/caddytls/automation.go
+++ b/modules/caddytls/automation.go
@@ -25,7 +25,7 @@ import (
"strings"
"github.com/caddyserver/certmagic"
- "github.com/mholt/acmez/v2"
+ "github.com/mholt/acmez/v3"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go
index f415fffa0..d9fc6bcfe 100644
--- a/modules/caddytls/connpolicy.go
+++ b/modules/caddytls/connpolicy.go
@@ -24,10 +24,9 @@ import (
"fmt"
"io"
"os"
- "path/filepath"
"strings"
- "github.com/mholt/acmez/v2"
+ "github.com/mholt/acmez/v3"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
@@ -351,6 +350,20 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
if err := p.ClientAuthentication.ConfigureTLSConfig(cfg); err != nil {
return fmt.Errorf("configuring TLS client authentication: %v", err)
}
+
+ // Prevent privilege escalation in case multiple vhosts are configured for
+ // this TLS server; we could potentially figure out if that's the case, but
+ // that might be complex to get right every time. Actually, two proper
+ // solutions could leave tickets enabled, but I am not sure how to do them
+ // properly without significant time investment; there may be new Go
+ // APIs that alloaw this (Wrap/UnwrapSession?) but I do not know how to use
+ // them at this time. TODO: one of these is a possible future enhancement:
+ // A) Prevent resumptions across server identities (certificates): binding the ticket to the
+ // certificate we would serve in a full handshake, or even bind a ticket to the exact SNI
+ // it was issued under (though there are proposals for session resumption across hostnames).
+ // B) Prevent resumptions falsely authenticating a client: include the realm in the ticket,
+ // so that it can be validated upon resumption.
+ cfg.SessionTicketsDisabled = true
}
if p.InsecureSecretsLog != "" {
@@ -358,7 +371,7 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
if err != nil {
return err
}
- filename, err = filepath.Abs(filename)
+ filename, err = caddy.FastAbs(filename)
if err != nil {
return err
}
@@ -535,21 +548,21 @@ type ClientAuthentication struct {
CARaw json.RawMessage `json:"ca,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"`
ca CA
- // DEPRECATED: Use the `ca` field with the `tls.ca_pool.source.inline` module instead.
+ // Deprecated: Use the `ca` field with the `tls.ca_pool.source.inline` module instead.
// A list of base64 DER-encoded CA certificates
// against which to validate client certificates.
// Client certs which are not signed by any of
// these CAs will be rejected.
TrustedCACerts []string `json:"trusted_ca_certs,omitempty"`
- // DEPRECATED: Use the `ca` field with the `tls.ca_pool.source.file` module instead.
+ // Deprecated: Use the `ca` field with the `tls.ca_pool.source.file` module instead.
// TrustedCACertPEMFiles is a list of PEM file names
// from which to load certificates of trusted CAs.
// Client certificates which are not signed by any of
// these CA certificates will be rejected.
TrustedCACertPEMFiles []string `json:"trusted_ca_certs_pem_files,omitempty"`
- // DEPRECATED: This field is deprecated and will be removed in
+ // Deprecated: This field is deprecated and will be removed in
// a future version. Please use the `validators` field instead
// with the tls.client_auth.verifier.leaf module instead.
//
diff --git a/modules/caddytls/ondemand.go b/modules/caddytls/ondemand.go
index 066473cd9..0970234ce 100644
--- a/modules/caddytls/ondemand.go
+++ b/modules/caddytls/ondemand.go
@@ -42,7 +42,7 @@ func init() {
// to your application whether a particular domain is allowed
// to have a certificate issued for it.
type OnDemandConfig struct {
- // DEPRECATED. WILL BE REMOVED SOON. Use 'permission' instead with the `http` module.
+ // Deprecated. WILL BE REMOVED SOON. Use 'permission' instead with the `http` module.
Ask string `json:"ask,omitempty"`
// REQUIRED. A module that will determine whether a
diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go
index 6e660dea8..abb519eb7 100644
--- a/modules/caddytls/tls.go
+++ b/modules/caddytls/tls.go
@@ -92,6 +92,17 @@ type TLS struct {
// EXPERIMENTAL. Subject to change.
DisableStorageCheck bool `json:"disable_storage_check,omitempty"`
+ // Disables the automatic cleanup of the storage backend.
+ // This is useful when TLS is not being used to store certificates
+ // and the user wants run their server in a read-only mode.
+ //
+ // Storage cleaning creates two files: instance.uuid and last_clean.json.
+ // The instance.uuid file is used to identify the instance of Caddy
+ // in a cluster. The last_clean.json file is used to store the last
+ // time the storage was cleaned.
+ // EXPERIMENTAL. Subject to change.
+ DisableStorageClean bool `json:"disable_storage_clean,omitempty"`
+
certificateLoaders []CertificateLoader
automateNames []string
ctx caddy.Context
@@ -328,7 +339,11 @@ func (t *TLS) Start() error {
return fmt.Errorf("automate: managing %v: %v", t.automateNames, err)
}
- t.keepStorageClean()
+ if !t.DisableStorageClean {
+ // start the storage cleaner goroutine and ticker,
+ // which cleans out expired certificates and more
+ t.keepStorageClean()
+ }
return nil
}
diff --git a/modules/logging/filewriter.go b/modules/logging/filewriter.go
index 44c0feb67..62d500dca 100644
--- a/modules/logging/filewriter.go
+++ b/modules/logging/filewriter.go
@@ -20,7 +20,6 @@ import (
"io"
"math"
"os"
- "path/filepath"
"strconv"
"github.com/dustin/go-humanize"
@@ -133,7 +132,7 @@ func (fw *FileWriter) Provision(ctx caddy.Context) error {
}
func (fw FileWriter) String() string {
- fpath, err := filepath.Abs(fw.Filename)
+ fpath, err := caddy.FastAbs(fw.Filename)
if err == nil {
return fpath
}
diff --git a/sigtrap_posix.go b/sigtrap_posix.go
index 7033f1635..2c6306121 100644
--- a/sigtrap_posix.go
+++ b/sigtrap_posix.go
@@ -28,6 +28,10 @@ import (
// trapSignalsPosix captures POSIX-only signals.
func trapSignalsPosix() {
+ // Ignore all SIGPIPE signals to prevent weird issues with systemd: https://github.com/dunglas/frankenphp/issues/1020
+ // Docker/Moby has a similar hack: https://github.com/moby/moby/blob/d828b032a87606ae34267e349bf7f7ccb1f6495a/cmd/dockerd/docker.go#L87-L90
+ signal.Ignore(syscall.SIGPIPE)
+
go func() {
sigchan := make(chan os.Signal, 1)
signal.Notify(sigchan, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2)