diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 666ddef30..7142530e5 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -25,7 +25,7 @@ Other menu items:
You can have a huge impact on the project by helping with its code. To contribute code to Caddy, first submit or comment in an issue to discuss your contribution, then open a [pull request](https://github.com/caddyserver/caddy/pulls) (PR). If you're new to our community, that's okay: **we gladly welcome pull requests from anyone, regardless of your native language or coding experience.** You can get familiar with Caddy's code base by using [code search at Sourcegraph](https://sourcegraph.com/github.com/caddyserver/caddy).
-We hold contributions to a high standard for quality :bowtie:, so don't be surprised if we ask for revisions—even if it seems small or insignificant. Please don't take it personally. :blue_heart: If your change is on the right track, we can guide you to make it mergable.
+We hold contributions to a high standard for quality :bowtie:, so don't be surprised if we ask for revisions—even if it seems small or insignificant. Please don't take it personally. :blue_heart: If your change is on the right track, we can guide you to make it mergeable.
Here are some of the expectations we have of contributors:
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 309ef7935..5f3e98db6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -12,28 +12,28 @@ on:
- master
- 2.*
+env:
+ # https://github.com/actions/setup-go/issues/491
+ GOTOOLCHAIN: local
+
jobs:
test:
strategy:
# Default is true, cancels jobs for other platforms in the matrix if one fails
fail-fast: false
matrix:
- os:
+ os:
- linux
- mac
- windows
- go:
- - '1.21'
- - '1.22'
+ go:
+ - '1.24'
include:
# Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }}
- - go: '1.21'
- GO_SEMVER: '~1.21.0'
-
- - go: '1.22'
- GO_SEMVER: '~1.22.1'
+ - go: '1.24'
+ GO_SEMVER: '~1.24.1'
# Set some variables per OS, usable via ${{ matrix.VAR }}
# OS_LABEL: the VM label from GitHub Actions (see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories)
@@ -99,7 +99,13 @@ jobs:
env:
CGO_ENABLED: 0
run: |
- go build -tags nobdger -trimpath -ldflags="-w -s" -v
+ go build -tags nobadger,nomysql,nopgx -trimpath -ldflags="-w -s" -v
+
+ - name: Smoke test Caddy
+ working-directory: ./cmd/caddy
+ run: |
+ ./caddy start
+ ./caddy stop
- name: Publish Build Artifact
uses: actions/upload-artifact@v4
@@ -116,7 +122,7 @@ jobs:
# continue-on-error: true
run: |
# (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
- go test -tags nobadger -v -coverprofile="cover-profile.out" -short -race ./...
+ go test -tags nobadger,nomysql,nopgx -v -coverprofile="cover-profile.out" -short -race ./...
# echo "status=$?" >> $GITHUB_OUTPUT
# Relevant step if we reinvestigate publishing test/coverage reports
@@ -137,25 +143,48 @@ 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
uses: actions/checkout@v4
- name: Run Tests
run: |
+ set +e
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
# short sha is enough?
short_sha=$(git rev-parse --short HEAD)
+ # To shorten the following lines
+ ssh_opts="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
+ ssh_host="$CI_USER@ci-s390x.caddyserver.com"
+
# The environment is fresh, so there's no point in keeping accepting and adding the key.
- rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . "$CI_USER"@ci-s390x.caddyserver.com:/var/tmp/"$short_sha"
- ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t "$CI_USER"@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -tags nobadger -v ./..."
+ rsync -arz -e "ssh $ssh_opts" --progress --delete --exclude '.git' . "$ssh_host":/var/tmp/"$short_sha"
+ ssh $ssh_opts -t "$ssh_host" bash < 0)); do
+ CGO_ENABLED=0 go test -p 1 -tags nobadger,nomysql,nopgx -v ./...
+ exit_code=$?
+ if ((exit_code == 0)); then
+ break
+ fi
+ echo "\n\nTest failed: \$exit_code, retrying..."
+ ((retries--))
+ done
+ echo "Remote exit code: \$exit_code"
+ exit \$exit_code
+ EOF
test_result=$?
# There's no need leaving the files around
- ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$CI_USER"@ci-s390x.caddyserver.com "rm -rf /var/tmp/'$short_sha'"
+ ssh $ssh_opts "$ssh_host" "rm -rf /var/tmp/'$short_sha'"
echo "Test exit code: $test_result"
exit $test_result
@@ -165,11 +194,27 @@ 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
- - uses: goreleaser/goreleaser-action@v5
+ - uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: check
+ - name: Install Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: "~1.24"
+ check-latest: true
+ - name: Install xcaddy
+ run: |
+ go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
+ xcaddy version
+ - uses: goreleaser/goreleaser-action@v6
+ with:
+ version: latest
+ args: build --single-target --snapshot
+ env:
+ TAG: ${{ github.head_ref || github.ref_name }}
diff --git a/.github/workflows/cross-build.yml b/.github/workflows/cross-build.yml
index 676607d0e..372cc7652 100644
--- a/.github/workflows/cross-build.yml
+++ b/.github/workflows/cross-build.yml
@@ -10,32 +10,34 @@ on:
- master
- 2.*
+env:
+ # https://github.com/actions/setup-go/issues/491
+ GOTOOLCHAIN: local
+
jobs:
build:
strategy:
fail-fast: false
matrix:
- goos:
+ goos:
- 'aix'
- - 'android'
- 'linux'
- 'solaris'
- 'illumos'
- 'dragonfly'
- 'freebsd'
- 'openbsd'
- - 'plan9'
- 'windows'
- 'darwin'
- 'netbsd'
- go:
- - '1.22'
+ go:
+ - '1.24'
include:
# Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }}
- - go: '1.22'
- GO_SEMVER: '~1.22.1'
+ - go: '1.24'
+ GO_SEMVER: '~1.24.1'
runs-on: ubuntu-latest
continue-on-error: true
@@ -68,8 +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
- if [ $? -ne 0 ]; then
- echo "::warning ::$GOOS Build Failed"
- exit 0
- fi
+ GOOS=$GOOS GOARCH=$GOARCH go build -tags=nobadger,nomysql,nopgx -trimpath -o caddy-"$GOOS"-$GOARCH 2> /dev/null
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index bfb91dc66..c5c89b502 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -13,6 +13,10 @@ on:
permissions:
contents: read
+env:
+ # https://github.com/actions/setup-go/issues/491
+ GOTOOLCHAIN: local
+
jobs:
# From https://github.com/golangci/golangci-lint-action
golangci:
@@ -43,16 +47,13 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
- go-version: '~1.22.1'
+ go-version: '~1.24'
check-latest: true
- name: golangci-lint
- uses: golangci/golangci-lint-action@v4
+ uses: golangci/golangci-lint-action@v6
with:
- version: v1.55
-
- # Workaround for https://github.com/golangci/golangci-lint-action/issues/135
- skip-pkg-cache: true
+ version: latest
# Windows times out frequently after about 5m50s if we don't set a longer timeout.
args: --timeout 10m
@@ -66,5 +67,5 @@ jobs:
- name: govulncheck
uses: golang/govulncheck-action@v1
with:
- go-version-input: '~1.22.1'
+ go-version-input: '~1.24.1'
check-latest: true
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b11686626..b508ba468 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -5,6 +5,10 @@ on:
tags:
- 'v*.*.*'
+env:
+ # https://github.com/actions/setup-go/issues/491
+ GOTOOLCHAIN: local
+
jobs:
release:
name: Release
@@ -13,13 +17,13 @@ jobs:
os:
- ubuntu-latest
go:
- - '1.21'
+ - '1.24'
include:
# Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }}
- - go: '1.21'
- GO_SEMVER: '~1.21.0'
+ - go: '1.24'
+ GO_SEMVER: '~1.24.1'
runs-on: ${{ matrix.os }}
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
@@ -104,9 +108,13 @@ jobs:
uses: anchore/sbom-action/download-syft@main
- name: Syft version
run: syft version
+ - name: Install xcaddy
+ run: |
+ go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
+ xcaddy version
# GoReleaser will take care of publishing those artifacts into the release
- name: Run GoReleaser
- uses: goreleaser/goreleaser-action@v5
+ uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: release --clean --timeout 60m
diff --git a/.gitignore b/.gitignore
index 6cde9db00..381bf7403 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ _gitignore/
Caddyfile
Caddyfile.*
!caddyfile/
+!caddyfile.go
# artifacts from pprof tooling
*.prof
diff --git a/.golangci.yml b/.golangci.yml
index d144395db..aecff563e 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,7 +1,9 @@
linters-settings:
errcheck:
- ignore: fmt:.*,go.uber.org/zap/zapcore:^Add.*
- ignoretests: true
+ exclude-functions:
+ - fmt.*
+ - (go.uber.org/zap/zapcore.ObjectEncoder).AddObject
+ - (go.uber.org/zap/zapcore.ObjectEncoder).AddArray
gci:
sections:
- standard # Standard section: captures all standard packages.
@@ -33,7 +35,6 @@ linters:
- errcheck
- errname
- exhaustive
- - exportloopref
- gci
- gofmt
- goimports
@@ -130,18 +131,22 @@ linters:
run:
# default concurrency is a available CPU number.
# concurrency: 4 # explicitly omit this value to fully utilize available resources.
- deadline: 5m
+ timeout: 5m
issues-exit-code: 1
tests: false
# output configuration options
output:
- format: 'colored-line-number'
+ formats:
+ - format: 'colored-line-number'
print-issued-lines: true
print-linter-name: true
issues:
exclude-rules:
+ - text: 'G115' # TODO: Either we should fix the issues or nuke the linter if it's bad
+ linters:
+ - gosec
# we aren't calling unknown URL
- text: 'G107' # G107: Url provided to HTTP request as taint input
linters:
@@ -166,3 +171,12 @@ issues:
- path: modules/logging/filters.go
linters:
- dupl
+ - path: modules/caddyhttp/matchers.go
+ linters:
+ - dupl
+ - path: modules/caddyhttp/vars.go
+ linters:
+ - dupl
+ - path: _test\.go
+ linters:
+ - errcheck
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 0fba8e2c4..c7ed4b365 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -1,3 +1,5 @@
+version: 2
+
before:
hooks:
# The build is done in this particular way to build Caddy in a designated directory named in .gitignore.
@@ -10,6 +12,9 @@ before:
- mkdir -p caddy-build
- cp cmd/caddy/main.go caddy-build/main.go
- /bin/sh -c 'cd ./caddy-build && go mod init caddy'
+ # prepare syso files for windows embedding
+ - /bin/sh -c 'for a in amd64 arm arm64; do XCADDY_SKIP_BUILD=1 GOOS=windows GOARCH=$a xcaddy build {{.Env.TAG}}; done'
+ - /bin/sh -c 'mv /tmp/buildenv_*/*.syso caddy-build'
# GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env
# so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate
- go mod edit -require=github.com/caddyserver/caddy/v2@{{.Env.TAG}} ./caddy-build/go.mod
@@ -29,7 +34,6 @@ builds:
- env:
- CGO_ENABLED=0
- GO111MODULE=on
- main: main.go
dir: ./caddy-build
binary: caddy
goos:
@@ -79,6 +83,8 @@ builds:
- -s -w
tags:
- nobadger
+ - nomysql
+ - nopgx
signs:
- cmd: cosign
@@ -105,7 +111,7 @@ archives:
- id: default
format_overrides:
- goos: windows
- format: zip
+ formats: zip
name_template: >-
{{ .ProjectName }}_
{{- .Version }}_
@@ -186,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 e5f99efdf..4bebaafdb 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@
-
+
@@ -56,7 +56,7 @@
-## [Features](https://caddyserver.com/v2)
+## [Features](https://caddyserver.com/features)
- **Easy configuration** with the [Caddyfile](https://caddyserver.com/docs/caddyfile)
- **Powerful configuration** with its [native JSON config](https://caddyserver.com/docs/json/)
@@ -67,6 +67,7 @@
- Fully-managed local CA for internal names & IPs
- Can coordinate with other Caddy instances in a cluster
- Multi-issuer fallback
+ - Encrypted ClientHello (ECH) support
- **Stays up when other servers go down** due to TLS/OCSP/certificate-related issues
- **Production-ready** after serving trillions of requests and managing millions of TLS certificates
- **Scales to hundreds of thousands of sites** as proven in production
@@ -75,7 +76,7 @@
- **Runs anywhere** with **no external dependencies** (not even libc)
- Written in Go, a language with higher **memory safety guarantees** than other servers
- Actually **fun to use**
-- So much more to [discover](https://caddyserver.com/v2)
+- So much more to [discover](https://caddyserver.com/features)
## Install
@@ -87,7 +88,7 @@ See [our online documentation](https://caddyserver.com/docs/install) for other i
Requirements:
-- [Go 1.21 or newer](https://golang.org/dl/)
+- [Go 1.24.0 or newer](https://golang.org/dl/)
### For development
@@ -131,7 +132,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`
@@ -176,7 +177,7 @@ The docs are also open source. You can contribute to them here: https://github.c
## Getting help
-- We advise companies using Caddy to secure a support contract through [Ardan Labs](https://www.ardanlabs.com/my/contact-us?dd=caddy) before help is needed.
+- We advise companies using Caddy to secure a support contract through [Ardan Labs](https://www.ardanlabs.com) before help is needed.
- A [sponsorship](https://github.com/sponsors/mholt) goes a long way! We can offer private help to sponsors. If Caddy is benefitting your company, please consider a sponsorship. This not only helps fund full-time work to ensure the longevity of the project, it provides your company the resources, support, and discounts you need; along with being a great look for your company to your customers and potential customers!
@@ -192,8 +193,8 @@ Matthew Holt began developing Caddy in 2014 while studying computer science at B
**The name "Caddy" is trademarked.** The name of the software is "Caddy", not "Caddy Server" or "CaddyServer". Please call it "Caddy" or, if you wish to clarify, "the Caddy web server". Caddy is a registered trademark of Stack Holdings GmbH.
-- _Project on Twitter: [@caddyserver](https://twitter.com/caddyserver)_
-- _Author on Twitter: [@mholt6](https://twitter.com/mholt6)_
+- _Project on X: [@caddyserver](https://x.com/caddyserver)_
+- _Author on X: [@mholt6](https://x.com/mholt6)_
Caddy is a project of [ZeroSSL](https://zerossl.com), a Stack Holdings company.
diff --git a/admin.go b/admin.go
index 0a9bfc49b..45bd574a1 100644
--- a/admin.go
+++ b/admin.go
@@ -26,7 +26,6 @@ import (
"expvar"
"fmt"
"hash"
- "hash/fnv"
"io"
"net"
"net/http"
@@ -35,19 +34,21 @@ import (
"os"
"path"
"regexp"
+ "slices"
"strconv"
"strings"
"sync"
"time"
"github.com/caddyserver/certmagic"
+ "github.com/cespare/xxhash/v2"
"github.com/prometheus/client_golang/prometheus"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func init() {
- // The hard-coded default `DefaultAdminListen` can be overidden
+ // The hard-coded default `DefaultAdminListen` can be overridden
// by setting the `CADDY_ADMIN` environment variable.
// The environment variable may be used by packagers to change
// the default admin address to something more appropriate for
@@ -213,14 +214,15 @@ 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) adminHandler {
+func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Context) adminHandler {
muxWrap := adminHandler{mux: http.NewServeMux()}
// secure the local or remote endpoint respectively
if remote {
muxWrap.remoteControl = admin.Remote
} else {
- muxWrap.enforceHost = !addr.isWildcardInterface()
+ // see comment in allowedOrigins() as to why we disable the host check for unix/fd networks
+ muxWrap.enforceHost = !addr.isWildcardInterface() && !addr.IsUnixNetwork() && !addr.IsFdNetwork()
muxWrap.allowedOrigins = admin.allowedOrigins(addr)
muxWrap.enforceOrigin = admin.EnforceOrigin
}
@@ -269,7 +271,6 @@ func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool) admi
// register third-party module endpoints
for _, m := range GetModules("admin.api") {
router := m.New().(AdminRouter)
- handlerLabel := m.ID.Name()
for _, route := range router.Routes() {
addRoute(route.Pattern, handlerLabel, route.Handler)
}
@@ -310,47 +311,43 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
for _, o := range admin.Origins {
uniqueOrigins[o] = struct{}{}
}
- if admin.Origins == nil {
+ // RFC 2616, Section 14.26:
+ // "A client MUST include a Host header field in all HTTP/1.1 request
+ // messages. If the requested URI does not include an Internet host
+ // name for the service being requested, then the Host header field MUST
+ // be given with an empty value."
+ //
+ // UPDATE July 2023: Go broke this by patching a minor security bug in 1.20.6.
+ // Understandable, but frustrating. See:
+ // https://github.com/golang/go/issues/60374
+ // See also the discussion here:
+ // https://github.com/golang/go/issues/61431
+ //
+ // We can no longer conform to RFC 2616 Section 14.26 from either Go or curl
+ // in purity. (Curl allowed no host between 7.40 and 7.50, but now requires a
+ // bogus host; see https://superuser.com/a/925610.) If we disable Host/Origin
+ // security checks, the infosec community assures me that it is secure to do
+ // so, because:
+ //
+ // 1) Browsers do not allow access to unix sockets
+ // 2) DNS is irrelevant to unix sockets
+ //
+ // If either of those two statements ever fail to hold true, it is not the
+ // fault of Caddy.
+ //
+ // Thus, we do not fill out allowed origins and do not enforce Host
+ // requirements for unix sockets. Enforcing it leads to confusion and
+ // frustration, when UDS have their own permissions from the OS.
+ // Enforcing host requirements here is effectively security theater,
+ // and a false sense of security.
+ //
+ // See also the discussion in #6832.
+ if admin.Origins == nil && !addr.IsUnixNetwork() && !addr.IsFdNetwork() {
if addr.isLoopback() {
- if addr.IsUnixNetwork() {
- // RFC 2616, Section 14.26:
- // "A client MUST include a Host header field in all HTTP/1.1 request
- // messages. If the requested URI does not include an Internet host
- // name for the service being requested, then the Host header field MUST
- // be given with an empty value."
- //
- // UPDATE July 2023: Go broke this by patching a minor security bug in 1.20.6.
- // Understandable, but frustrating. See:
- // https://github.com/golang/go/issues/60374
- // See also the discussion here:
- // https://github.com/golang/go/issues/61431
- //
- // We can no longer conform to RFC 2616 Section 14.26 from either Go or curl
- // in purity. (Curl allowed no host between 7.40 and 7.50, but now requires a
- // bogus host; see https://superuser.com/a/925610.) If we disable Host/Origin
- // security checks, the infosec community assures me that it is secure to do
- // so, because:
- // 1) Browsers do not allow access to unix sockets
- // 2) DNS is irrelevant to unix sockets
- //
- // I am not quite ready to trust either of those external factors, so instead
- // of disabling Host/Origin checks, we now allow specific Host values when
- // accessing the admin endpoint over unix sockets. I definitely don't trust
- // DNS (e.g. I don't trust 'localhost' to always resolve to the local host),
- // and IP shouldn't even be used, but if it is for some reason, I think we can
- // at least be reasonably assured that 127.0.0.1 and ::1 route to the local
- // machine, meaning that a hypothetical browser origin would have to be on the
- // local machine as well.
- uniqueOrigins[""] = struct{}{}
- uniqueOrigins["127.0.0.1"] = struct{}{}
- uniqueOrigins["::1"] = struct{}{}
- } else {
- uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
- uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
- uniqueOrigins[net.JoinHostPort("127.0.0.1", addr.port())] = struct{}{}
- }
- }
- if !addr.IsUnixNetwork() {
+ uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
+ uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
+ uniqueOrigins[net.JoinHostPort("127.0.0.1", addr.port())] = struct{}{}
+ } else {
uniqueOrigins[addr.JoinHostPort(0)] = struct{}{}
}
}
@@ -381,7 +378,9 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
// for the admin endpoint exists in cfg, a default one is used, so
// that there is always an admin server (unless it is explicitly
// configured to be disabled).
-func replaceLocalAdminServer(cfg *Config) error {
+// Critically note that some elements and functionality of the context
+// may not be ready, e.g. storage. Tread carefully.
+func replaceLocalAdminServer(cfg *Config, ctx Context) error {
// always* be sure to close down the old admin endpoint
// as gracefully as possible, even if the new one is
// disabled -- careful to use reference to the current
@@ -423,7 +422,14 @@ func replaceLocalAdminServer(cfg *Config) error {
return err
}
- handler := cfg.Admin.newAdminHandler(addr, false)
+ handler := cfg.Admin.newAdminHandler(addr, false, ctx)
+
+ // run the provisioners for loaded modules to make sure local
+ // state is properly re-initialized in the new admin server
+ err = cfg.Admin.provisionAdminRouters(ctx)
+ if err != nil {
+ return err
+ }
ln, err := addr.Listen(context.TODO(), 0, net.ListenConfig{})
if err != nil {
@@ -474,7 +480,6 @@ func manageIdentity(ctx Context, cfg *Config) error {
// import the caddytls package -- but it works
if cfg.Admin.Identity.IssuersRaw == nil {
cfg.Admin.Identity.IssuersRaw = []json.RawMessage{
- json.RawMessage(`{"module": "zerossl"}`),
json.RawMessage(`{"module": "acme"}`),
}
}
@@ -545,7 +550,14 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
// make the HTTP handler but disable Host/Origin enforcement
// because we are using TLS authentication instead
- handler := cfg.Admin.newAdminHandler(addr, true)
+ handler := cfg.Admin.newAdminHandler(addr, true, ctx)
+
+ // run the provisioners for loaded modules to make sure local
+ // state is properly re-initialized in the new admin server
+ err = cfg.Admin.provisionAdminRouters(ctx)
+ if err != nil {
+ return err
+ }
// create client certificate pool for TLS mutual auth, and extract public keys
// so that we can enforce access controls at the application layer
@@ -676,13 +688,7 @@ func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error {
// key recognized; make sure its HTTP request is permitted
for _, accessPerm := range adminAccess.Permissions {
// verify method
- methodFound := accessPerm.Methods == nil
- for _, method := range accessPerm.Methods {
- if method == r.Method {
- methodFound = true
- break
- }
- }
+ methodFound := accessPerm.Methods == nil || slices.Contains(accessPerm.Methods, r.Method)
if !methodFound {
return APIError{
HTTPStatus: http.StatusForbidden,
@@ -878,13 +884,9 @@ func (h adminHandler) handleError(w http.ResponseWriter, r *http.Request, err er
// a trustworthy/expected value. This helps to mitigate DNS
// rebinding attacks.
func (h adminHandler) checkHost(r *http.Request) error {
- var allowed bool
- for _, allowedOrigin := range h.allowedOrigins {
- if r.Host == allowedOrigin.Host {
- allowed = true
- break
- }
- }
+ allowed := slices.ContainsFunc(h.allowedOrigins, func(u *url.URL) bool {
+ return r.Host == u.Host
+ })
if !allowed {
return APIError{
HTTPStatus: http.StatusForbidden,
@@ -946,7 +948,7 @@ func (h adminHandler) originAllowed(origin *url.URL) bool {
// etagHasher returns a the hasher we used on the config to both
// produce and verify ETags.
-func etagHasher() hash.Hash32 { return fnv.New32a() }
+func etagHasher() hash.Hash { return xxhash.New() }
// makeEtag returns an Etag header value (including quotes) for
// the given config path and hash of contents at that path.
@@ -954,17 +956,28 @@ func makeEtag(path string, hash hash.Hash) string {
return fmt.Sprintf(`"%s %x"`, path, hash.Sum(nil))
}
+// This buffer pool is used to keep buffers for
+// reading the config file during eTag header generation
+var bufferPool = sync.Pool{
+ New: func() any {
+ return new(bytes.Buffer)
+ },
+}
+
func handleConfig(w http.ResponseWriter, r *http.Request) error {
switch r.Method {
case http.MethodGet:
w.Header().Set("Content-Type", "application/json")
- // Set the ETag as a trailer header.
- // The alternative is to write the config to a buffer, and
- // then hash that.
- w.Header().Set("Trailer", "ETag")
-
hash := etagHasher()
- configWriter := io.MultiWriter(w, hash)
+
+ // Read the config into a buffer instead of writing directly to
+ // the response writer, as we want to set the ETag as the header,
+ // not the trailer.
+ buf := bufferPool.Get().(*bytes.Buffer)
+ buf.Reset()
+ defer bufferPool.Put(buf)
+
+ configWriter := io.MultiWriter(buf, hash)
err := readConfig(r.URL.Path, configWriter)
if err != nil {
return APIError{HTTPStatus: http.StatusBadRequest, Err: err}
@@ -973,6 +986,10 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
// we could consider setting up a sync.Pool for the summed
// hashes to reduce GC pressure.
w.Header().Set("Etag", makeEtag(r.URL.Path, hash))
+ _, err = w.Write(buf.Bytes())
+ if err != nil {
+ return APIError{HTTPStatus: http.StatusInternalServerError, Err: err}
+ }
return nil
@@ -1133,7 +1150,7 @@ traverseLoop:
return fmt.Errorf("[%s] invalid array index '%s': %v",
path, idxStr, err)
}
- if idx < 0 || idx >= len(arr) {
+ if idx < 0 || (method != http.MethodPut && idx >= len(arr)) || idx > len(arr) {
return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr)
}
}
diff --git a/admin_test.go b/admin_test.go
index 9137a8881..e3d235b66 100644
--- a/admin_test.go
+++ b/admin_test.go
@@ -15,12 +15,19 @@
package caddy
import (
+ "context"
+ "crypto/x509"
"encoding/json"
"fmt"
"net/http"
+ "net/http/httptest"
"reflect"
"sync"
"testing"
+
+ "github.com/caddyserver/certmagic"
+ "github.com/prometheus/client_golang/prometheus"
+ dto "github.com/prometheus/client_model/go"
)
var testCfg = []byte(`{
@@ -203,3 +210,727 @@ func BenchmarkLoad(b *testing.B) {
Load(testCfg, true)
}
}
+
+func TestAdminHandlerErrorHandling(t *testing.T) {
+ initAdminMetrics()
+
+ handler := adminHandler{
+ mux: http.NewServeMux(),
+ }
+
+ handler.mux.Handle("/error", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ err := fmt.Errorf("test error")
+ handler.handleError(w, r, err)
+ }))
+
+ req := httptest.NewRequest(http.MethodGet, "/error", nil)
+ rr := httptest.NewRecorder()
+
+ handler.ServeHTTP(rr, req)
+
+ if rr.Code == http.StatusOK {
+ t.Error("expected error response, got success")
+ }
+
+ var apiErr APIError
+ if err := json.NewDecoder(rr.Body).Decode(&apiErr); err != nil {
+ t.Fatalf("decoding response: %v", err)
+ }
+ if apiErr.Message != "test error" {
+ t.Errorf("expected error message 'test error', got '%s'", apiErr.Message)
+ }
+}
+
+func initAdminMetrics() {
+ if adminMetrics.requestErrors != nil {
+ prometheus.Unregister(adminMetrics.requestErrors)
+ }
+ if adminMetrics.requestCount != nil {
+ prometheus.Unregister(adminMetrics.requestCount)
+ }
+
+ adminMetrics.requestErrors = prometheus.NewCounterVec(prometheus.CounterOpts{
+ Namespace: "caddy",
+ Subsystem: "admin_http",
+ Name: "request_errors_total",
+ Help: "Number of errors that occurred handling admin endpoint requests",
+ }, []string{"handler", "path", "method"})
+
+ adminMetrics.requestCount = prometheus.NewCounterVec(prometheus.CounterOpts{
+ Namespace: "caddy",
+ Subsystem: "admin_http",
+ Name: "requests_total",
+ Help: "Count of requests to the admin endpoint",
+ }, []string{"handler", "path", "code", "method"}) // Added code and method labels
+
+ prometheus.MustRegister(adminMetrics.requestErrors)
+ prometheus.MustRegister(adminMetrics.requestCount)
+}
+
+func TestAdminHandlerBuiltinRouteErrors(t *testing.T) {
+ initAdminMetrics()
+
+ cfg := &Config{
+ Admin: &AdminConfig{
+ Listen: "localhost:2019",
+ },
+ }
+
+ err := replaceLocalAdminServer(cfg, Context{})
+ if err != nil {
+ t.Fatalf("setting up admin server: %v", err)
+ }
+ defer func() {
+ stopAdminServer(localAdminServer)
+ }()
+
+ tests := []struct {
+ name string
+ path string
+ method string
+ expectedStatus int
+ }{
+ {
+ name: "stop endpoint wrong method",
+ path: "/stop",
+ method: http.MethodGet,
+ expectedStatus: http.StatusMethodNotAllowed,
+ },
+ {
+ name: "config endpoint wrong content-type",
+ path: "/config/",
+ method: http.MethodPost,
+ expectedStatus: http.StatusBadRequest,
+ },
+ {
+ name: "config ID missing ID",
+ path: "/id/",
+ method: http.MethodGet,
+ expectedStatus: http.StatusBadRequest,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ req := httptest.NewRequest(test.method, fmt.Sprintf("http://localhost:2019%s", test.path), nil)
+ rr := httptest.NewRecorder()
+
+ localAdminServer.Handler.ServeHTTP(rr, req)
+
+ if rr.Code != test.expectedStatus {
+ t.Errorf("expected status %d but got %d", test.expectedStatus, rr.Code)
+ }
+
+ metricValue := testGetMetricValue(map[string]string{
+ "path": test.path,
+ "handler": "admin",
+ "method": test.method,
+ })
+ if metricValue != 1 {
+ t.Errorf("expected error metric to be incremented once, got %v", metricValue)
+ }
+ })
+ }
+}
+
+func testGetMetricValue(labels map[string]string) float64 {
+ promLabels := prometheus.Labels{}
+ for k, v := range labels {
+ promLabels[k] = v
+ }
+
+ metric, err := adminMetrics.requestErrors.GetMetricWith(promLabels)
+ if err != nil {
+ return 0
+ }
+
+ pb := &dto.Metric{}
+ metric.Write(pb)
+ return pb.GetCounter().GetValue()
+}
+
+type mockRouter struct {
+ routes []AdminRoute
+}
+
+func (m mockRouter) Routes() []AdminRoute {
+ return m.routes
+}
+
+type mockModule struct {
+ mockRouter
+}
+
+func (m *mockModule) CaddyModule() ModuleInfo {
+ return ModuleInfo{
+ ID: "admin.api.mock",
+ New: func() Module {
+ mm := &mockModule{
+ mockRouter: mockRouter{
+ routes: m.routes,
+ },
+ }
+ return mm
+ },
+ }
+}
+
+func TestNewAdminHandlerRouterRegistration(t *testing.T) {
+ originalModules := make(map[string]ModuleInfo)
+ for k, v := range modules {
+ originalModules[k] = v
+ }
+ defer func() {
+ modules = originalModules
+ }()
+
+ mockRoute := AdminRoute{
+ Pattern: "/mock",
+ Handler: AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
+ w.WriteHeader(http.StatusOK)
+ return nil
+ }),
+ }
+
+ mock := &mockModule{
+ mockRouter: mockRouter{
+ routes: []AdminRoute{mockRoute},
+ },
+ }
+ RegisterModule(mock)
+
+ addr, err := ParseNetworkAddress("localhost:2019")
+ if err != nil {
+ t.Fatalf("Failed to parse address: %v", err)
+ }
+
+ admin := &AdminConfig{
+ EnforceOrigin: false,
+ }
+ handler := admin.newAdminHandler(addr, false, Context{})
+
+ req := httptest.NewRequest("GET", "/mock", nil)
+ req.Host = "localhost:2019"
+ rr := httptest.NewRecorder()
+
+ handler.ServeHTTP(rr, req)
+
+ if rr.Code != http.StatusOK {
+ t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code)
+ t.Logf("Response body: %s", rr.Body.String())
+ }
+
+ if len(admin.routers) != 1 {
+ t.Errorf("Expected 1 router to be stored, got %d", len(admin.routers))
+ }
+}
+
+type mockProvisionableRouter struct {
+ mockRouter
+ provisionErr error
+ provisioned bool
+}
+
+func (m *mockProvisionableRouter) Provision(Context) error {
+ m.provisioned = true
+ return m.provisionErr
+}
+
+type mockProvisionableModule struct {
+ *mockProvisionableRouter
+}
+
+func (m *mockProvisionableModule) CaddyModule() ModuleInfo {
+ return ModuleInfo{
+ ID: "admin.api.mock_provision",
+ New: func() Module {
+ mm := &mockProvisionableModule{
+ mockProvisionableRouter: &mockProvisionableRouter{
+ mockRouter: m.mockRouter,
+ provisionErr: m.provisionErr,
+ },
+ }
+ return mm
+ },
+ }
+}
+
+func TestAdminRouterProvisioning(t *testing.T) {
+ tests := []struct {
+ name string
+ provisionErr error
+ wantErr bool
+ routersAfter int // expected number of routers after provisioning
+ }{
+ {
+ name: "successful provisioning",
+ provisionErr: nil,
+ wantErr: false,
+ routersAfter: 0,
+ },
+ {
+ name: "provisioning error",
+ provisionErr: fmt.Errorf("provision failed"),
+ wantErr: true,
+ routersAfter: 1,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ originalModules := make(map[string]ModuleInfo)
+ for k, v := range modules {
+ originalModules[k] = v
+ }
+ defer func() {
+ modules = originalModules
+ }()
+
+ mockRoute := AdminRoute{
+ Pattern: "/mock",
+ Handler: AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
+ return nil
+ }),
+ }
+
+ // Create provisionable module
+ mock := &mockProvisionableModule{
+ mockProvisionableRouter: &mockProvisionableRouter{
+ mockRouter: mockRouter{
+ routes: []AdminRoute{mockRoute},
+ },
+ provisionErr: test.provisionErr,
+ },
+ }
+ RegisterModule(mock)
+
+ admin := &AdminConfig{}
+ addr, err := ParseNetworkAddress("localhost:2019")
+ if err != nil {
+ t.Fatalf("Failed to parse address: %v", err)
+ }
+
+ _ = admin.newAdminHandler(addr, false, Context{})
+ err = admin.provisionAdminRouters(Context{})
+
+ if test.wantErr {
+ if err == nil {
+ t.Error("Expected error but got nil")
+ }
+ } else {
+ if err != nil {
+ t.Errorf("Expected no error but got: %v", err)
+ }
+ }
+
+ if len(admin.routers) != test.routersAfter {
+ t.Errorf("Expected %d routers after provisioning, got %d", test.routersAfter, len(admin.routers))
+ }
+ })
+ }
+}
+
+func TestAllowedOriginsUnixSocket(t *testing.T) {
+ // see comment in allowedOrigins() as to why we do not fill out allowed origins for UDS
+ tests := []struct {
+ name string
+ addr NetworkAddress
+ origins []string
+ expectOrigins []string
+ }{
+ {
+ name: "unix socket with default origins",
+ addr: NetworkAddress{
+ Network: "unix",
+ Host: "/tmp/caddy.sock",
+ },
+ origins: nil, // default origins
+ expectOrigins: []string{},
+ },
+ {
+ name: "unix socket with custom origins",
+ addr: NetworkAddress{
+ Network: "unix",
+ Host: "/tmp/caddy.sock",
+ },
+ origins: []string{"example.com"},
+ expectOrigins: []string{
+ "example.com",
+ },
+ },
+ {
+ name: "tcp socket on localhost gets all loopback addresses",
+ addr: NetworkAddress{
+ Network: "tcp",
+ Host: "localhost",
+ StartPort: 2019,
+ EndPort: 2019,
+ },
+ origins: nil,
+ expectOrigins: []string{
+ "localhost:2019",
+ "[::1]:2019",
+ "127.0.0.1:2019",
+ },
+ },
+ }
+
+ for i, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ admin := AdminConfig{
+ Origins: test.origins,
+ }
+
+ got := admin.allowedOrigins(test.addr)
+
+ var gotOrigins []string
+ for _, u := range got {
+ gotOrigins = append(gotOrigins, u.Host)
+ }
+
+ if len(gotOrigins) != len(test.expectOrigins) {
+ t.Errorf("%d: Expected %d origins but got %d", i, len(test.expectOrigins), len(gotOrigins))
+ return
+ }
+
+ expectMap := make(map[string]struct{})
+ for _, origin := range test.expectOrigins {
+ expectMap[origin] = struct{}{}
+ }
+
+ gotMap := make(map[string]struct{})
+ for _, origin := range gotOrigins {
+ gotMap[origin] = struct{}{}
+ }
+
+ if !reflect.DeepEqual(expectMap, gotMap) {
+ t.Errorf("%d: Origins mismatch.\nExpected: %v\nGot: %v", i, test.expectOrigins, gotOrigins)
+ }
+ })
+ }
+}
+
+func TestReplaceRemoteAdminServer(t *testing.T) {
+ const testCert = `MIIDCTCCAfGgAwIBAgIUXsqJ1mY8pKlHQtI3HJ23x2eZPqwwDQYJKoZIhvcNAQEL
+BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwMTAwMDAwMFoXDTI0MDEw
+MTAwMDAwMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEA4O4S6BSoYcoxvRqI+h7yPOjF6KjntjzVVm9M+uHK4lzX
+F1L3pSxJ2nDD4wZEV3FJ5yFOHVFqkG2vXG3BIczOlYG7UeNmKbQnKc5kZj3HGUrS
+VGEktA4OJbeZhhWP15gcXN5eDM2eH3g9BFXVX6AURxLiUXzhNBUEZuj/OEyH9yEF
+/qPCE+EjzVvWxvBXwgz/io4r4yok/Vq/bxJ6FlV6R7DX5oJSXyO0VEHZPi9DIyNU
+kK3F/r4U1sWiJGWOs8i3YQWZ2ejh1C0aLFZpPcCGGgMNpoF31gyYP6ZuPDUyCXsE
+g36UUw1JHNtIXYcLhnXuqj4A8TybTDpgXLqvwA9DBQIDAQABo1MwUTAdBgNVHQ4E
+FgQUc13z30pFC63rr/HGKOE7E82vjXwwHwYDVR0jBBgwFoAUc13z30pFC63rr/HG
+KOE7E82vjXwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAHO3j
+oeiUXXJ7xD4P8Wj5t9d+E8lE1Xv1Dk3Z+EdG5+dan+RcToE42JJp9zB7FIh5Qz8g
+W77LAjqh5oyqz3A2VJcyVgfE3uJP1R1mJM7JfGHf84QH4TZF2Q1RZY4SZs0VQ6+q
+5wSlIZ4NXDy4Q4XkIJBGS61wT8IzYFXYBpx4PCP1Qj0PIE4sevEGwjsBIgxK307o
+BxF8AWe6N6e4YZmQLGjQ+SeH0iwZb6vpkHyAY8Kj2hvK+cq2P7vU3VGi0t3r1F8L
+IvrXHCvO2BMNJ/1UK1M4YNX8LYJqQhg9hEsIROe1OE/m3VhxIYMJI+qZXk9yHfgJ
+vq+SH04xKhtFudVBAQ==`
+
+ tests := []struct {
+ name string
+ cfg *Config
+ wantErr bool
+ }{
+ {
+ name: "nil config",
+ cfg: nil,
+ wantErr: false,
+ },
+ {
+ name: "nil admin config",
+ cfg: &Config{
+ Admin: nil,
+ },
+ wantErr: false,
+ },
+ {
+ name: "nil remote config",
+ cfg: &Config{
+ Admin: &AdminConfig{},
+ },
+ wantErr: false,
+ },
+ {
+ name: "invalid listen address",
+ cfg: &Config{
+ Admin: &AdminConfig{
+ Remote: &RemoteAdmin{
+ Listen: "invalid:address",
+ },
+ },
+ },
+ wantErr: true,
+ },
+ {
+ name: "valid config",
+ cfg: &Config{
+ Admin: &AdminConfig{
+ Identity: &IdentityConfig{},
+ Remote: &RemoteAdmin{
+ Listen: "localhost:2021",
+ AccessControl: []*AdminAccess{
+ {
+ PublicKeys: []string{testCert},
+ Permissions: []AdminPermissions{{Methods: []string{"GET"}, Paths: []string{"/test"}}},
+ },
+ },
+ },
+ },
+ },
+ wantErr: false,
+ },
+ {
+ name: "invalid certificate",
+ cfg: &Config{
+ Admin: &AdminConfig{
+ Identity: &IdentityConfig{},
+ Remote: &RemoteAdmin{
+ Listen: "localhost:2021",
+ AccessControl: []*AdminAccess{
+ {
+ PublicKeys: []string{"invalid-cert-data"},
+ Permissions: []AdminPermissions{{Methods: []string{"GET"}, Paths: []string{"/test"}}},
+ },
+ },
+ },
+ },
+ },
+ wantErr: true,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ ctx := Context{
+ Context: context.Background(),
+ cfg: test.cfg,
+ }
+
+ if test.cfg != nil {
+ test.cfg.storage = &certmagic.FileStorage{Path: t.TempDir()}
+ }
+
+ if test.cfg != nil && test.cfg.Admin != nil && test.cfg.Admin.Identity != nil {
+ identityCertCache = certmagic.NewCache(certmagic.CacheOptions{
+ GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
+ return &certmagic.Config{}, nil
+ },
+ })
+ }
+
+ err := replaceRemoteAdminServer(ctx, test.cfg)
+
+ if test.wantErr {
+ if err == nil {
+ t.Error("Expected error but got nil")
+ }
+ } else {
+ if err != nil {
+ t.Errorf("Expected no error but got: %v", err)
+ }
+ }
+
+ // Clean up
+ if remoteAdminServer != nil {
+ _ = stopAdminServer(remoteAdminServer)
+ }
+ })
+ }
+}
+
+type mockIssuer struct {
+ configSet *certmagic.Config
+}
+
+func (m *mockIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) {
+ return &certmagic.IssuedCertificate{
+ Certificate: []byte(csr.Raw),
+ }, nil
+}
+
+func (m *mockIssuer) SetConfig(cfg *certmagic.Config) {
+ m.configSet = cfg
+}
+
+func (m *mockIssuer) IssuerKey() string {
+ return "mock"
+}
+
+type mockIssuerModule struct {
+ *mockIssuer
+}
+
+func (m *mockIssuerModule) CaddyModule() ModuleInfo {
+ return ModuleInfo{
+ ID: "tls.issuance.acme",
+ New: func() Module {
+ return &mockIssuerModule{mockIssuer: new(mockIssuer)}
+ },
+ }
+}
+
+func TestManageIdentity(t *testing.T) {
+ originalModules := make(map[string]ModuleInfo)
+ for k, v := range modules {
+ originalModules[k] = v
+ }
+ defer func() {
+ modules = originalModules
+ }()
+
+ RegisterModule(&mockIssuerModule{})
+
+ certPEM := []byte(`-----BEGIN CERTIFICATE-----
+MIIDujCCAqKgAwIBAgIIE31FZVaPXTUwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE
+BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl
+cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMTI5MTMyNzQzWhcNMTQwNTI5MDAwMDAw
+WjBpMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN
+TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEYMBYGA1UEAwwPbWFp
+bC5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3lcub2pUwkjC
+5GJQA2ZZfJJi6d1QHhEmkX9VxKYGp6gagZuRqJWy9TXP6++1ZzQQxqZLD0TkuxZ9
+8i9Nz00000CCBjCCAQQwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMGgG
+CCsGAQUFBwEBBFwwWjArBggrBgEFBQcwAoYfaHR0cDovL3BraS5nb29nbGUuY29t
+L0dJQUcyLmNydDArBggrBgEFBQcwAYYfaHR0cDovL2NsaWVudHMxLmdvb2dsZS5j
+b20vb2NzcDAdBgNVHQ4EFgQUiJxtimAuTfwb+aUtBn5UYKreKvMwDAYDVR0TAQH/
+BAIwADAfBgNVHSMEGDAWgBRK3QYWG7z2aLV29YG2u2IaulqBLzAXBgNVHREEEDAO
+ggxtYWlsLmdvb2dsZTANBgkqhkiG9w0BAQUFAAOCAQEAMP6IWgNGZE8wP9TjFjSZ
+3mmW3A1eIr0CuPwNZ2LJ5ZD1i70ojzcj4I9IdP5yPg9CAEV4hNASbM1LzfC7GmJE
+tPzW5tRmpKVWZGRgTgZI8Hp/xZXMwLh9ZmXV4kESFAGj5G5FNvJyUV7R5Eh+7OZX
+7G4jJ4ZGJh+5jzN9HdJJHQHGYNIYOzC7+HH9UMwCjX9vhQ4RjwFZJThS2Yb+y7pb
+9yxTJZoXC6J0H5JpnZb7kZEJ+Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+-----END CERTIFICATE-----`)
+
+ keyPEM := []byte(`-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP
+...
+-----END PRIVATE KEY-----`)
+
+ testStorage := certmagic.FileStorage{Path: t.TempDir()}
+ err := testStorage.Store(context.Background(), "localhost/localhost.crt", certPEM)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = testStorage.Store(context.Background(), "localhost/localhost.key", keyPEM)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ tests := []struct {
+ name string
+ cfg *Config
+ wantErr bool
+ checkState func(*testing.T, *Config)
+ }{
+ {
+ name: "nil config",
+ cfg: nil,
+ },
+ {
+ name: "nil admin config",
+ cfg: &Config{
+ Admin: nil,
+ },
+ },
+ {
+ name: "nil identity config",
+ cfg: &Config{
+ Admin: &AdminConfig{},
+ },
+ },
+ {
+ name: "default issuer when none specified",
+ cfg: &Config{
+ Admin: &AdminConfig{
+ Identity: &IdentityConfig{
+ Identifiers: []string{"localhost"},
+ },
+ },
+ storage: &testStorage,
+ },
+ checkState: func(t *testing.T, cfg *Config) {
+ if len(cfg.Admin.Identity.issuers) == 0 {
+ t.Error("Expected at least 1 issuer to be configured")
+ return
+ }
+ if _, ok := cfg.Admin.Identity.issuers[0].(*mockIssuerModule); !ok {
+ t.Error("Expected mock issuer to be configured")
+ }
+ },
+ },
+ {
+ name: "custom issuer",
+ cfg: &Config{
+ Admin: &AdminConfig{
+ Identity: &IdentityConfig{
+ Identifiers: []string{"localhost"},
+ IssuersRaw: []json.RawMessage{
+ json.RawMessage(`{"module": "acme"}`),
+ },
+ },
+ },
+ storage: &certmagic.FileStorage{Path: "testdata"},
+ },
+ checkState: func(t *testing.T, cfg *Config) {
+ if len(cfg.Admin.Identity.issuers) != 1 {
+ t.Fatalf("Expected 1 issuer, got %d", len(cfg.Admin.Identity.issuers))
+ }
+ mockIss, ok := cfg.Admin.Identity.issuers[0].(*mockIssuerModule)
+ if !ok {
+ t.Fatal("Expected mock issuer")
+ }
+ if mockIss.configSet == nil {
+ t.Error("Issuer config was not set")
+ }
+ },
+ },
+ {
+ name: "invalid issuer module",
+ cfg: &Config{
+ Admin: &AdminConfig{
+ Identity: &IdentityConfig{
+ Identifiers: []string{"localhost"},
+ IssuersRaw: []json.RawMessage{
+ json.RawMessage(`{"module": "doesnt_exist"}`),
+ },
+ },
+ },
+ },
+ wantErr: true,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ if identityCertCache != nil {
+ // Reset the cert cache before each test
+ identityCertCache.Stop()
+ identityCertCache = nil
+ }
+
+ ctx := Context{
+ Context: context.Background(),
+ cfg: test.cfg,
+ moduleInstances: make(map[string][]Module),
+ }
+
+ err := manageIdentity(ctx, test.cfg)
+
+ if test.wantErr {
+ if err == nil {
+ t.Error("Expected error but got nil")
+ }
+ return
+ }
+ if err != nil {
+ t.Fatalf("Expected no error but got: %v", err)
+ }
+
+ if test.checkState != nil {
+ test.checkState(t, test.cfg)
+ }
+ })
+ }
+}
diff --git a/caddy.go b/caddy.go
index 1e0eacd2b..1a6a11629 100644
--- a/caddy.go
+++ b/caddy.go
@@ -81,13 +81,14 @@ type Config struct {
// associated value.
AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="`
- apps map[string]App
- storage certmagic.Storage
+ apps map[string]App
+ storage certmagic.Storage
+ eventEmitter eventEmitter
cancelFunc context.CancelFunc
- // filesystems is a dict of filesystems that will later be loaded from and added to.
- filesystems FileSystems
+ // fileSystems is a dict of fileSystems that will later be loaded from and added to.
+ fileSystems FileSystems
}
// App is a thing that Caddy runs.
@@ -397,6 +398,66 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
// will want to use Run instead, which also
// updates the config's raw state.
func run(newCfg *Config, start bool) (Context, error) {
+ ctx, err := provisionContext(newCfg, start)
+ if err != nil {
+ globalMetrics.configSuccess.Set(0)
+ return ctx, err
+ }
+
+ if !start {
+ return ctx, nil
+ }
+
+ // Provision any admin routers which may need to access
+ // some of the other apps at runtime
+ err = ctx.cfg.Admin.provisionAdminRouters(ctx)
+ if err != nil {
+ globalMetrics.configSuccess.Set(0)
+ return ctx, err
+ }
+
+ // Start
+ err = func() error {
+ started := make([]string, 0, len(ctx.cfg.apps))
+ for name, a := range ctx.cfg.apps {
+ err := a.Start()
+ if err != nil {
+ // an app failed to start, so we need to stop
+ // all other apps that were already started
+ for _, otherAppName := range started {
+ err2 := ctx.cfg.apps[otherAppName].Stop()
+ if err2 != nil {
+ err = fmt.Errorf("%v; additionally, aborting app %s: %v",
+ err, otherAppName, err2)
+ }
+ }
+ return fmt.Errorf("%s app module: start: %v", name, err)
+ }
+ started = append(started, name)
+ }
+ return nil
+ }()
+ if err != nil {
+ globalMetrics.configSuccess.Set(0)
+ return ctx, err
+ }
+ globalMetrics.configSuccess.Set(1)
+ globalMetrics.configSuccessTime.SetToCurrentTime()
+
+ // TODO: This event is experimental and subject to change.
+ ctx.emitEvent("started", nil)
+
+ // now that the user's config is running, finish setting up anything else,
+ // such as remote admin endpoint, config loader, etc.
+ return ctx, finishSettingUp(ctx, ctx.cfg)
+}
+
+// provisionContext creates a new context from the given configuration and provisions
+// storage and apps.
+// If `newCfg` is nil a new empty configuration will be created.
+// If `replaceAdminServer` is true any currently active admin server will be replaced
+// with a new admin server based on the provided configuration.
+func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error) {
// because we will need to roll back any state
// modifications if this function errors, we
// keep a single error value and scope all
@@ -419,6 +480,7 @@ func run(newCfg *Config, start bool) (Context, error) {
ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg})
defer func() {
if err != nil {
+ globalMetrics.configSuccess.Set(0)
// if there were any errors during startup,
// we should cancel the new context we created
// since the associated config won't be used;
@@ -443,16 +505,8 @@ func run(newCfg *Config, start bool) (Context, error) {
return ctx, err
}
- // start the admin endpoint (and stop any prior one)
- if start {
- err = replaceLocalAdminServer(newCfg)
- if err != nil {
- return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err)
- }
- }
-
// create the new filesystem map
- newCfg.filesystems = &filesystems.FilesystemMap{}
+ newCfg.fileSystems = &filesystems.FileSystemMap{}
// prepare the new config for use
newCfg.apps = make(map[string]App)
@@ -482,6 +536,14 @@ func run(newCfg *Config, start bool) (Context, error) {
return ctx, err
}
+ // start the admin endpoint (and stop any prior one)
+ if replaceAdminServer {
+ err = replaceLocalAdminServer(newCfg, ctx)
+ if err != nil {
+ return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err)
+ }
+ }
+
// Load and Provision each app and their submodules
err = func() error {
for appName := range newCfg.AppsRaw {
@@ -491,49 +553,16 @@ func run(newCfg *Config, start bool) (Context, error) {
}
return nil
}()
- if err != nil {
- return ctx, err
- }
+ return ctx, err
+}
- if !start {
- return ctx, nil
- }
-
- // Provision any admin routers which may need to access
- // some of the other apps at runtime
- err = newCfg.Admin.provisionAdminRouters(ctx)
- if err != nil {
- return ctx, err
- }
-
- // Start
- err = func() error {
- started := make([]string, 0, len(newCfg.apps))
- for name, a := range newCfg.apps {
- err := a.Start()
- if err != nil {
- // an app failed to start, so we need to stop
- // all other apps that were already started
- for _, otherAppName := range started {
- err2 := newCfg.apps[otherAppName].Stop()
- if err2 != nil {
- err = fmt.Errorf("%v; additionally, aborting app %s: %v",
- err, otherAppName, err2)
- }
- }
- return fmt.Errorf("%s app module: start: %v", name, err)
- }
- started = append(started, name)
- }
- return nil
- }()
- if err != nil {
- return ctx, err
- }
-
- // now that the user's config is running, finish setting up anything else,
- // such as remote admin endpoint, config loader, etc.
- return ctx, finishSettingUp(ctx, newCfg)
+// ProvisionContext creates a new context from the configuration and provisions storage
+// and app modules.
+// The function is intended for testing and advanced use cases only, typically `Run` should be
+// use to ensure a fully functional caddy instance.
+// EXPERIMENTAL: While this is public the interface and implementation details of this function may change.
+func ProvisionContext(newCfg *Config) (Context, error) {
+ return provisionContext(newCfg, false)
}
// finishSettingUp should be run after all apps have successfully started.
@@ -672,6 +701,9 @@ func unsyncedStop(ctx Context) {
return
}
+ // TODO: This event is experimental and subject to change.
+ ctx.emitEvent("stopping", nil)
+
// stop each app
for name, a := range ctx.cfg.apps {
err := a.Stop()
@@ -701,8 +733,10 @@ func Validate(cfg *Config) error {
// Errors are logged along the way, and an appropriate exit
// code is emitted.
func exitProcess(ctx context.Context, logger *zap.Logger) {
- // let the rest of the program know we're quitting
- atomic.StoreInt32(exiting, 1)
+ // let the rest of the program know we're quitting; only do it once
+ if !atomic.CompareAndSwapInt32(exiting, 0, 1) {
+ return
+ }
// give the OS or service/process manager our 2 weeks' notice: we quit
if err := notify.Stopping(); err != nil {
@@ -869,7 +903,7 @@ func InstanceID() (uuid.UUID, error) {
if err != nil {
return uuid, err
}
- err = os.MkdirAll(appDataDir, 0o600)
+ err = os.MkdirAll(appDataDir, 0o700)
if err != nil {
return uuid, err
}
@@ -1012,6 +1046,92 @@ func Version() (simple, full string) {
return
}
+// Event represents something that has happened or is happening.
+// An Event value is not synchronized, so it should be copied if
+// being used in goroutines.
+//
+// EXPERIMENTAL: Events are subject to change.
+type Event struct {
+ // If non-nil, the event has been aborted, meaning
+ // propagation has stopped to other handlers and
+ // the code should stop what it was doing. Emitters
+ // may choose to use this as a signal to adjust their
+ // code path appropriately.
+ Aborted error
+
+ // The data associated with the event. Usually the
+ // original emitter will be the only one to set or
+ // change these values, but the field is exported
+ // so handlers can have full access if needed.
+ // However, this map is not synchronized, so
+ // handlers must not use this map directly in new
+ // goroutines; instead, copy the map to use it in a
+ // goroutine. Data may be nil.
+ Data map[string]any
+
+ id uuid.UUID
+ ts time.Time
+ name string
+ origin Module
+}
+
+// NewEvent creates a new event, but does not emit the event. To emit an
+// event, call Emit() on the current instance of the caddyevents app insteaad.
+//
+// EXPERIMENTAL: Subject to change.
+func NewEvent(ctx Context, name string, data map[string]any) (Event, error) {
+ id, err := uuid.NewRandom()
+ if err != nil {
+ return Event{}, fmt.Errorf("generating new event ID: %v", err)
+ }
+ name = strings.ToLower(name)
+ return Event{
+ Data: data,
+ id: id,
+ ts: time.Now(),
+ name: name,
+ origin: ctx.Module(),
+ }, nil
+}
+
+func (e Event) ID() uuid.UUID { return e.id }
+func (e Event) Timestamp() time.Time { return e.ts }
+func (e Event) Name() string { return e.name }
+func (e Event) Origin() Module { return e.origin } // Returns the module that originated the event. May be nil, usually if caddy core emits the event.
+
+// CloudEvent exports event e as a structure that, when
+// serialized as JSON, is compatible with the
+// CloudEvents spec.
+func (e Event) CloudEvent() CloudEvent {
+ dataJSON, _ := json.Marshal(e.Data)
+ return CloudEvent{
+ ID: e.id.String(),
+ Source: e.origin.CaddyModule().String(),
+ SpecVersion: "1.0",
+ Type: e.name,
+ Time: e.ts,
+ DataContentType: "application/json",
+ Data: dataJSON,
+ }
+}
+
+// CloudEvent is a JSON-serializable structure that
+// is compatible with the CloudEvents specification.
+// See https://cloudevents.io.
+// EXPERIMENTAL: Subject to change.
+type CloudEvent struct {
+ ID string `json:"id"`
+ Source string `json:"source"`
+ SpecVersion string `json:"specversion"`
+ Type string `json:"type"`
+ Time time.Time `json:"time"`
+ DataContentType string `json:"datacontenttype,omitempty"`
+ Data json.RawMessage `json:"data,omitempty"`
+}
+
+// ErrEventAborted cancels an event.
+var ErrEventAborted = errors.New("event aborted")
+
// ActiveContext returns the currently-active context.
// This function is experimental and might be changed
// or removed in the future.
diff --git a/caddyconfig/caddyfile/dispenser.go b/caddyconfig/caddyfile/dispenser.go
index 215a1641f..325bb54d3 100644
--- a/caddyconfig/caddyfile/dispenser.go
+++ b/caddyconfig/caddyfile/dispenser.go
@@ -30,6 +30,10 @@ type Dispenser struct {
tokens []Token
cursor int
nesting int
+
+ // A map of arbitrary context data that can be used
+ // to pass through some information to unmarshalers.
+ context map[string]any
}
// NewDispenser returns a Dispenser filled with the given tokens.
@@ -411,7 +415,7 @@ func (d *Dispenser) EOFErr() error {
// Err generates a custom parse-time error with a message of msg.
func (d *Dispenser) Err(msg string) error {
- return d.Errf(msg)
+ return d.WrapErr(errors.New(msg))
}
// Errf is like Err, but for formatted error messages
@@ -454,6 +458,34 @@ func (d *Dispenser) DeleteN(amount int) []Token {
return d.tokens
}
+// SetContext sets a key-value pair in the context map.
+func (d *Dispenser) SetContext(key string, value any) {
+ if d.context == nil {
+ d.context = make(map[string]any)
+ }
+ d.context[key] = value
+}
+
+// GetContext gets the value of a key in the context map.
+func (d *Dispenser) GetContext(key string) any {
+ if d.context == nil {
+ return nil
+ }
+ return d.context[key]
+}
+
+// GetContextString gets the value of a key in the context map
+// as a string, or an empty string if the key does not exist.
+func (d *Dispenser) GetContextString(key string) string {
+ if d.context == nil {
+ return ""
+ }
+ if val, ok := d.context[key].(string); ok {
+ return val
+ }
+ return ""
+}
+
// isNewLine determines whether the current token is on a different
// line (higher line number) than the previous token. It handles imported
// tokens correctly. If there isn't a previous token, it returns true.
@@ -485,3 +517,5 @@ func (d *Dispenser) isNextOnNewLine() bool {
next := d.tokens[d.cursor+1]
return isNextOnNewLine(curr, next)
}
+
+const MatcherNameCtxKey = "matcher_name"
diff --git a/caddyconfig/caddyfile/formatter.go b/caddyconfig/caddyfile/formatter.go
index d506219c4..f1d12fa51 100644
--- a/caddyconfig/caddyfile/formatter.go
+++ b/caddyconfig/caddyfile/formatter.go
@@ -17,9 +17,8 @@ package caddyfile
import (
"bytes"
"io"
+ "slices"
"unicode"
-
- "golang.org/x/exp/slices"
)
// Format formats the input Caddyfile to a standard, nice-looking
@@ -62,7 +61,8 @@ func Format(input []byte) []byte {
heredocMarker []rune
heredocClosingMarker []rune
- nesting int // indentation level
+ nesting int // indentation level
+ withinBackquote bool
)
write := func(ch rune) {
@@ -89,6 +89,9 @@ func Format(input []byte) []byte {
}
panic(err)
}
+ if ch == '`' {
+ withinBackquote = !withinBackquote
+ }
// detect whether we have the start of a heredoc
if !quoted && !(heredoc != heredocClosed || heredocEscaped) &&
@@ -237,14 +240,23 @@ func Format(input []byte) []byte {
switch {
case ch == '{':
openBrace = true
- openBraceWritten = false
openBraceSpace = spacePrior && !beginningOfLine
if openBraceSpace {
write(' ')
}
+ openBraceWritten = false
+ if withinBackquote {
+ write('{')
+ openBraceWritten = true
+ continue
+ }
continue
case ch == '}' && (spacePrior || !openBrace):
+ if withinBackquote {
+ write('}')
+ continue
+ }
if last != '\n' {
nextLine()
}
diff --git a/caddyconfig/caddyfile/formatter_test.go b/caddyconfig/caddyfile/formatter_test.go
index 6eec822fe..a64383c3c 100644
--- a/caddyconfig/caddyfile/formatter_test.go
+++ b/caddyconfig/caddyfile/formatter_test.go
@@ -434,6 +434,16 @@ block2 {
}
`,
},
+ {
+ description: "Preserve braces wrapped by backquotes",
+ input: "block {respond `All braces should remain: {{now | date \"2006\"}}`}",
+ expect: "block {respond `All braces should remain: {{now | date \"2006\"}}`}",
+ },
+ {
+ description: "Preserve braces wrapped by quotes",
+ input: "block {respond \"All braces should remain: {{now | date `2006`}}\"}",
+ expect: "block {respond \"All braces should remain: {{now | date `2006`}}\"}",
+ },
} {
// the formatter should output a trailing newline,
// even if the tests aren't written to expect that
diff --git a/caddyconfig/caddyfile/importgraph.go b/caddyconfig/caddyfile/importgraph.go
index d27f4710b..ca859299d 100644
--- a/caddyconfig/caddyfile/importgraph.go
+++ b/caddyconfig/caddyfile/importgraph.go
@@ -16,23 +16,24 @@ package caddyfile
import (
"fmt"
+ "slices"
)
type adjacency map[string][]string
type importGraph struct {
- nodes map[string]bool
+ nodes map[string]struct{}
edges adjacency
}
func (i *importGraph) addNode(name string) {
if i.nodes == nil {
- i.nodes = make(map[string]bool)
+ i.nodes = make(map[string]struct{})
}
if _, exists := i.nodes[name]; exists {
return
}
- i.nodes[name] = true
+ i.nodes[name] = struct{}{}
}
func (i *importGraph) addNodes(names []string) {
@@ -66,7 +67,7 @@ func (i *importGraph) addEdge(from, to string) error {
}
if i.nodes == nil {
- i.nodes = make(map[string]bool)
+ i.nodes = make(map[string]struct{})
}
if i.edges == nil {
i.edges = make(adjacency)
@@ -91,12 +92,7 @@ func (i *importGraph) areConnected(from, to string) bool {
if !ok {
return false
}
- for _, v := range al {
- if v == to {
- return true
- }
- }
- return false
+ return slices.Contains(al, to)
}
func (i *importGraph) willCycle(from, to string) bool {
diff --git a/caddyconfig/caddyfile/lexer.go b/caddyconfig/caddyfile/lexer.go
index 4db63749b..9b523f397 100644
--- a/caddyconfig/caddyfile/lexer.go
+++ b/caddyconfig/caddyfile/lexer.go
@@ -340,6 +340,8 @@ func (l *lexer) finalizeHeredoc(val []rune, marker string) ([]rune, error) {
return []rune(out), nil
}
+// Quoted returns true if the token was enclosed in quotes
+// (i.e. double quotes, backticks, or heredoc).
func (t Token) Quoted() bool {
return t.wasQuoted > 0
}
@@ -356,6 +358,19 @@ func (t Token) NumLineBreaks() int {
return lineBreaks
}
+// Clone returns a deep copy of the token.
+func (t Token) Clone() Token {
+ return Token{
+ File: t.File,
+ imports: append([]string{}, t.imports...),
+ Line: t.Line,
+ Text: t.Text,
+ wasQuoted: t.wasQuoted,
+ heredocMarker: t.heredocMarker,
+ snippetName: t.snippetName,
+ }
+}
+
var heredocMarkerRegexp = regexp.MustCompile("^[A-Za-z0-9_-]+$")
// isNextOnNewLine tests whether t2 is on a different line from t1
diff --git a/caddyconfig/caddyfile/parse.go b/caddyconfig/caddyfile/parse.go
index 9f79d913a..d04a1ac46 100644
--- a/caddyconfig/caddyfile/parse.go
+++ b/caddyconfig/caddyfile/parse.go
@@ -50,7 +50,7 @@ func Parse(filename string, input []byte) ([]ServerBlock, error) {
p := parser{
Dispenser: NewDispenser(tokens),
importGraph: importGraph{
- nodes: make(map[string]bool),
+ nodes: make(map[string]struct{}),
edges: make(adjacency),
},
}
@@ -214,7 +214,12 @@ func (p *parser) addresses() error {
value := p.Val()
token := p.Token()
- // special case: import directive replaces tokens during parse-time
+ // Reject request matchers if trying to define them globally
+ if strings.HasPrefix(value, "@") {
+ return p.Errf("request matchers may not be defined globally, they must be in a site block; found %s", value)
+ }
+
+ // Special case: import directive replaces tokens during parse-time
if value == "import" && p.isNewLine() {
err := p.doImport(0)
if err != nil {
@@ -259,8 +264,13 @@ func (p *parser) addresses() error {
return p.Errf("Site addresses cannot contain a comma ',': '%s' - put a space after the comma to separate site addresses", value)
}
- token.Text = value
- p.block.Keys = append(p.block.Keys, token)
+ // After the above, a comma surrounded by spaces would result
+ // in an empty token which we should ignore
+ if value != "" {
+ // Add the token as a site address
+ token.Text = value
+ p.block.Keys = append(p.block.Keys, token)
+ }
}
// Advance token and possibly break out of loop or return error
@@ -359,9 +369,45 @@ func (p *parser) doImport(nesting int) error {
// set up a replacer for non-variadic args replacement
repl := makeArgsReplacer(args)
+ // grab all the tokens (if it exists) from within a block that follows the import
+ var blockTokens []Token
+ for currentNesting := p.Nesting(); p.NextBlock(currentNesting); {
+ blockTokens = append(blockTokens, p.Token())
+ }
+ // initialize with size 1
+ blockMapping := make(map[string][]Token, 1)
+ if len(blockTokens) > 0 {
+ // use such tokens to create a new dispenser, and then use it to parse each block
+ bd := NewDispenser(blockTokens)
+ for bd.Next() {
+ // see if we can grab a key
+ var currentMappingKey string
+ if bd.Val() == "{" {
+ return p.Err("anonymous blocks are not supported")
+ }
+ currentMappingKey = bd.Val()
+ currentMappingTokens := []Token{}
+ // read all args until end of line / {
+ if bd.NextArg() {
+ currentMappingTokens = append(currentMappingTokens, bd.Token())
+ for bd.NextArg() {
+ currentMappingTokens = append(currentMappingTokens, bd.Token())
+ }
+ // TODO(elee1766): we don't enter another mapping here because it's annoying to extract the { and } properly.
+ // maybe someone can do that in the future
+ } else {
+ // attempt to enter a block and add tokens to the currentMappingTokens
+ for mappingNesting := bd.Nesting(); bd.NextBlock(mappingNesting); {
+ currentMappingTokens = append(currentMappingTokens, bd.Token())
+ }
+ }
+ blockMapping[currentMappingKey] = currentMappingTokens
+ }
+ }
+
// splice out the import directive and its arguments
// (2 tokens, plus the length of args)
- tokensBefore := p.tokens[:p.cursor-1-len(args)]
+ tokensBefore := p.tokens[:p.cursor-1-len(args)-len(blockTokens)]
tokensAfter := p.tokens[p.cursor+1:]
var importedTokens []Token
var nodes []string
@@ -377,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)
}
@@ -395,7 +441,6 @@ func (p *parser) doImport(nesting int) error {
return p.Errf("Glob pattern may only contain one wildcard (*), but has others: %s", globPattern)
}
matches, err = filepath.Glob(globPattern)
-
if err != nil {
return p.Errf("Failed to use import pattern %s: %v", importPattern, err)
}
@@ -491,6 +536,33 @@ func (p *parser) doImport(nesting int) error {
maybeSnippet = false
}
}
+ // if it is {block}, we substitute with all tokens in the block
+ // if it is {blocks.*}, we substitute with the tokens in the mapping for the *
+ var skip bool
+ var tokensToAdd []Token
+ switch {
+ case token.Text == "{block}":
+ tokensToAdd = blockTokens
+ case strings.HasPrefix(token.Text, "{blocks.") && strings.HasSuffix(token.Text, "}"):
+ // {blocks.foo.bar} will be extracted to key `foo.bar`
+ blockKey := strings.TrimPrefix(strings.TrimSuffix(token.Text, "}"), "{blocks.")
+ val, ok := blockMapping[blockKey]
+ if ok {
+ tokensToAdd = val
+ }
+ default:
+ skip = true
+ }
+ if !skip {
+ if len(tokensToAdd) == 0 {
+ // if there is no content in the snippet block, don't do any replacement
+ // this allows snippets which contained {block}/{block.*} before this change to continue functioning as normal
+ tokensCopy = append(tokensCopy, token)
+ } else {
+ tokensCopy = append(tokensCopy, tokensToAdd...)
+ }
+ continue
+ }
if maybeSnippet {
tokensCopy = append(tokensCopy, token)
@@ -512,7 +584,7 @@ func (p *parser) doImport(nesting int) error {
// splice the imported tokens in the place of the import statement
// and rewind cursor so Next() will land on first imported token
p.tokens = append(tokensBefore, append(tokensCopy, tokensAfter...)...)
- p.cursor -= len(args) + 1
+ p.cursor -= len(args) + len(blockTokens) + 1
return nil
}
@@ -550,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/caddyfile/parse_test.go b/caddyconfig/caddyfile/parse_test.go
index 6daded1cf..d3fada4e0 100644
--- a/caddyconfig/caddyfile/parse_test.go
+++ b/caddyconfig/caddyfile/parse_test.go
@@ -555,6 +555,10 @@ func TestParseAll(t *testing.T) {
{"localhost:1234", "http://host2"},
}},
+ {`foo.example.com , example.com`, false, [][]string{
+ {"foo.example.com", "example.com"},
+ }},
+
{`localhost:1234, http://host2,`, true, [][]string{}},
{`http://host1.com, http://host2.com {
@@ -614,8 +618,8 @@ func TestParseAll(t *testing.T) {
}
for j, block := range blocks {
if len(block.Keys) != len(test.keys[j]) {
- t.Errorf("Test %d: Expected %d keys in block %d, got %d",
- i, len(test.keys[j]), j, len(block.Keys))
+ t.Errorf("Test %d: Expected %d keys in block %d, got %d: %v",
+ i, len(test.keys[j]), j, len(block.Keys), block.Keys)
continue
}
for k, addr := range block.GetKeysText() {
@@ -857,6 +861,29 @@ func TestSnippetAcrossMultipleFiles(t *testing.T) {
}
}
+func TestRejectsGlobalMatcher(t *testing.T) {
+ p := testParser(`
+ @rejected path /foo
+
+ (common) {
+ gzip foo
+ errors stderr
+ }
+
+ http://example.com {
+ import common
+ }
+ `)
+ _, err := p.parseAll()
+ if err == nil {
+ t.Fatal("Expected an error, but got nil")
+ }
+ expected := "request matchers may not be defined globally, they must be in a site block; found @rejected, at Testfile:2"
+ if err.Error() != expected {
+ t.Errorf("Expected error to be '%s' but got '%v'", expected, err)
+ }
+}
+
func testParser(input string) parser {
return parser{Dispenser: NewTestDispenser(input)}
}
diff --git a/caddyconfig/httpcaddyfile/addresses.go b/caddyconfig/httpcaddyfile/addresses.go
index da51fe9b0..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
@@ -77,10 +77,15 @@ import (
// repetition may be undesirable, so call consolidateAddrMappings() to map
// multiple addresses to the same lists of server blocks (a many:many mapping).
// (Doing this is essentially a map-reduce technique.)
-func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock,
+func (st *ServerType) mapAddressToProtocolToServerBlocks(originalServerBlocks []serverBlock,
options map[string]any,
-) (map[string][]serverBlock, error) {
- sbmap := make(map[string][]serverBlock)
+) (map[string]map[string][]serverBlock, error) {
+ addrToProtocolToServerBlocks := map[string]map[string][]serverBlock{}
+
+ type keyWithParsedKey struct {
+ key caddyfile.Token
+ parsedKey Address
+ }
for i, sblock := range originalServerBlocks {
// within a server block, we need to map all the listener addresses
@@ -88,27 +93,48 @@ func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBloc
// will be served by them; this has the effect of treating each
// key of a server block as its own, but without having to repeat its
// contents in cases where multiple keys really can be served together
- addrToKeys := make(map[string][]caddyfile.Token)
+ addrToProtocolToKeyWithParsedKeys := map[string]map[string][]keyWithParsedKey{}
for j, key := range sblock.block.Keys {
+ parsedKey, err := ParseAddress(key.Text)
+ if err != nil {
+ return nil, fmt.Errorf("parsing key: %v", err)
+ }
+ parsedKey = parsedKey.Normalize()
+
// a key can have multiple listener addresses if there are multiple
// arguments to the 'bind' directive (although they will all have
// the same port, since the port is defined by the key or is implicit
// through automatic HTTPS)
- addrs, err := st.listenerAddrsForServerBlockKey(sblock, key.Text, options)
+ listeners, err := st.listenersForServerBlockAddress(sblock, parsedKey, options)
if err != nil {
return nil, fmt.Errorf("server block %d, key %d (%s): determining listener address: %v", i, j, key.Text, err)
}
- // associate this key with each listener address it is served on
- for _, addr := range addrs {
- addrToKeys[addr] = append(addrToKeys[addr], key)
+ // associate this key with its protocols and each listener address served with them
+ kwpk := keyWithParsedKey{key, parsedKey}
+ for addr, protocols := range listeners {
+ protocolToKeyWithParsedKeys, ok := addrToProtocolToKeyWithParsedKeys[addr]
+ if !ok {
+ protocolToKeyWithParsedKeys = map[string][]keyWithParsedKey{}
+ addrToProtocolToKeyWithParsedKeys[addr] = protocolToKeyWithParsedKeys
+ }
+
+ // an empty protocol indicates the default, a nil or empty value in the ListenProtocols array
+ if len(protocols) == 0 {
+ protocols[""] = struct{}{}
+ }
+ for prot := range protocols {
+ protocolToKeyWithParsedKeys[prot] = append(
+ protocolToKeyWithParsedKeys[prot],
+ kwpk)
+ }
}
}
// make a slice of the map keys so we can iterate in sorted order
- addrs := make([]string, 0, len(addrToKeys))
- for k := range addrToKeys {
- addrs = append(addrs, k)
+ addrs := make([]string, 0, len(addrToProtocolToKeyWithParsedKeys))
+ for addr := range addrToProtocolToKeyWithParsedKeys {
+ addrs = append(addrs, addr)
}
sort.Strings(addrs)
@@ -118,85 +144,132 @@ func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBloc
// server block are only the ones which use the address; but
// the contents (tokens) are of course the same
for _, addr := range addrs {
- keys := addrToKeys[addr]
- // parse keys so that we only have to do it once
- parsedKeys := make([]Address, 0, len(keys))
- for _, key := range keys {
- addr, err := ParseAddress(key.Text)
- if err != nil {
- return nil, fmt.Errorf("parsing key '%s': %v", key.Text, err)
- }
- parsedKeys = append(parsedKeys, addr.Normalize())
+ protocolToKeyWithParsedKeys := addrToProtocolToKeyWithParsedKeys[addr]
+
+ prots := make([]string, 0, len(protocolToKeyWithParsedKeys))
+ for prot := range protocolToKeyWithParsedKeys {
+ prots = append(prots, prot)
}
- sbmap[addr] = append(sbmap[addr], serverBlock{
- block: caddyfile.ServerBlock{
- Keys: keys,
- Segments: sblock.block.Segments,
- },
- pile: sblock.pile,
- keys: parsedKeys,
+ sort.Strings(prots)
+
+ protocolToServerBlocks, ok := addrToProtocolToServerBlocks[addr]
+ if !ok {
+ protocolToServerBlocks = map[string][]serverBlock{}
+ addrToProtocolToServerBlocks[addr] = protocolToServerBlocks
+ }
+
+ for _, prot := range prots {
+ keyWithParsedKeys := protocolToKeyWithParsedKeys[prot]
+
+ keys := make([]caddyfile.Token, len(keyWithParsedKeys))
+ parsedKeys := make([]Address, len(keyWithParsedKeys))
+
+ for k, keyWithParsedKey := range keyWithParsedKeys {
+ keys[k] = keyWithParsedKey.key
+ parsedKeys[k] = keyWithParsedKey.parsedKey
+ }
+
+ protocolToServerBlocks[prot] = append(protocolToServerBlocks[prot], serverBlock{
+ block: caddyfile.ServerBlock{
+ Keys: keys,
+ Segments: sblock.block.Segments,
+ },
+ pile: sblock.pile,
+ parsedKeys: parsedKeys,
+ })
+ }
+ }
+ }
+
+ return addrToProtocolToServerBlocks, nil
+}
+
+// consolidateAddrMappings eliminates repetition of identical server blocks in a mapping of
+// single listener addresses to protocols to lists of server blocks. Since multiple addresses
+// may serve multiple protocols to identical sites (server block contents), this function turns
+// a 1:many mapping into a many:many mapping. Server block contents (tokens) must be
+// exactly identical so that reflect.DeepEqual returns true in order for the addresses to be combined.
+// Identical entries are deleted from the addrToServerBlocks map. Essentially, each pairing (each
+// association from multiple addresses to multiple server blocks; i.e. each element of
+// the returned slice) becomes a server definition in the output JSON.
+func (st *ServerType) consolidateAddrMappings(addrToProtocolToServerBlocks map[string]map[string][]serverBlock) []sbAddrAssociation {
+ sbaddrs := make([]sbAddrAssociation, 0, len(addrToProtocolToServerBlocks))
+
+ addrs := make([]string, 0, len(addrToProtocolToServerBlocks))
+ for addr := range addrToProtocolToServerBlocks {
+ addrs = append(addrs, addr)
+ }
+ sort.Strings(addrs)
+
+ for _, addr := range addrs {
+ protocolToServerBlocks := addrToProtocolToServerBlocks[addr]
+
+ prots := make([]string, 0, len(protocolToServerBlocks))
+ for prot := range protocolToServerBlocks {
+ prots = append(prots, prot)
+ }
+ sort.Strings(prots)
+
+ for _, prot := range prots {
+ serverBlocks := protocolToServerBlocks[prot]
+
+ // now find other addresses that map to identical
+ // server blocks and add them to our map of listener
+ // addresses and protocols, while removing them from
+ // the original map
+ listeners := map[string]map[string]struct{}{}
+
+ for otherAddr, otherProtocolToServerBlocks := range addrToProtocolToServerBlocks {
+ for otherProt, otherServerBlocks := range otherProtocolToServerBlocks {
+ if addr == otherAddr && prot == otherProt || reflect.DeepEqual(serverBlocks, otherServerBlocks) {
+ listener, ok := listeners[otherAddr]
+ if !ok {
+ listener = map[string]struct{}{}
+ listeners[otherAddr] = listener
+ }
+ listener[otherProt] = struct{}{}
+ delete(otherProtocolToServerBlocks, otherProt)
+ }
+ }
+ }
+
+ addresses := make([]string, 0, len(listeners))
+ for lnAddr := range listeners {
+ addresses = append(addresses, lnAddr)
+ }
+ sort.Strings(addresses)
+
+ addressesWithProtocols := make([]addressWithProtocols, 0, len(listeners))
+
+ for _, lnAddr := range addresses {
+ lnProts := listeners[lnAddr]
+ prots := make([]string, 0, len(lnProts))
+ for prot := range lnProts {
+ prots = append(prots, prot)
+ }
+ sort.Strings(prots)
+
+ addressesWithProtocols = append(addressesWithProtocols, addressWithProtocols{
+ address: lnAddr,
+ protocols: prots,
+ })
+ }
+
+ sbaddrs = append(sbaddrs, sbAddrAssociation{
+ addressesWithProtocols: addressesWithProtocols,
+ serverBlocks: serverBlocks,
})
}
}
- return sbmap, nil
-}
-
-// consolidateAddrMappings eliminates repetition of identical server blocks in a mapping of
-// single listener addresses to lists of server blocks. Since multiple addresses may serve
-// identical sites (server block contents), this function turns a 1:many mapping into a
-// many:many mapping. Server block contents (tokens) must be exactly identical so that
-// reflect.DeepEqual returns true in order for the addresses to be combined. Identical
-// entries are deleted from the addrToServerBlocks map. Essentially, each pairing (each
-// association from multiple addresses to multiple server blocks; i.e. each element of
-// the returned slice) becomes a server definition in the output JSON.
-func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]serverBlock) []sbAddrAssociation {
- sbaddrs := make([]sbAddrAssociation, 0, len(addrToServerBlocks))
- for addr, sblocks := range addrToServerBlocks {
- // we start with knowing that at least this address
- // maps to these server blocks
- a := sbAddrAssociation{
- addresses: []string{addr},
- serverBlocks: sblocks,
- }
-
- // now find other addresses that map to identical
- // server blocks and add them to our list of
- // addresses, while removing them from the map
- for otherAddr, otherSblocks := range addrToServerBlocks {
- if addr == otherAddr {
- continue
- }
- if reflect.DeepEqual(sblocks, otherSblocks) {
- a.addresses = append(a.addresses, otherAddr)
- delete(addrToServerBlocks, otherAddr)
- }
- }
- sort.Strings(a.addresses)
-
- sbaddrs = append(sbaddrs, a)
- }
-
- // sort them by their first address (we know there will always be at least one)
- // to avoid problems with non-deterministic ordering (makes tests flaky)
- sort.Slice(sbaddrs, func(i, j int) bool {
- return sbaddrs[i].addresses[0] < sbaddrs[j].addresses[0]
- })
-
return sbaddrs
}
-// listenerAddrsForServerBlockKey essentially converts the Caddyfile
-// site addresses to Caddy listener addresses for each server block.
-func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string,
+// listenersForServerBlockAddress essentially converts the Caddyfile site addresses to a map from
+// Caddy listener addresses and the protocols to serve them with to the parsed address for each server block.
+func (st *ServerType) listenersForServerBlockAddress(sblock serverBlock, addr Address,
options map[string]any,
-) ([]string, error) {
- addr, err := ParseAddress(key)
- if err != nil {
- return nil, fmt.Errorf("parsing key: %v", err)
- }
- addr = addr.Normalize()
-
+) (map[string]map[string]struct{}, error) {
switch addr.Scheme {
case "wss":
return nil, fmt.Errorf("the scheme wss:// is only supported in browsers; use https:// instead")
@@ -230,55 +303,58 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
// error if scheme and port combination violate convention
if (addr.Scheme == "http" && lnPort == httpsPort) || (addr.Scheme == "https" && lnPort == httpPort) {
- return nil, fmt.Errorf("[%s] scheme and port violate convention", key)
+ return nil, fmt.Errorf("[%s] scheme and port violate convention", addr.String())
}
- // the bind directive specifies hosts (and potentially network), but is optional
- lnHosts := make([]string, 0, len(sblock.pile["bind"]))
+ // the bind directive specifies hosts (and potentially network), and the protocols to serve them with, but is optional
+ lnCfgVals := make([]addressesWithProtocols, 0, len(sblock.pile["bind"]))
for _, cfgVal := range sblock.pile["bind"] {
- lnHosts = append(lnHosts, cfgVal.Value.([]string)...)
+ if val, ok := cfgVal.Value.(addressesWithProtocols); ok {
+ lnCfgVals = append(lnCfgVals, val)
+ }
}
- if len(lnHosts) == 0 {
- if defaultBind, ok := options["default_bind"].([]string); ok {
- lnHosts = defaultBind
+ if len(lnCfgVals) == 0 {
+ if defaultBindValues, ok := options["default_bind"].([]ConfigValue); ok {
+ for _, defaultBindValue := range defaultBindValues {
+ lnCfgVals = append(lnCfgVals, defaultBindValue.Value.(addressesWithProtocols))
+ }
} else {
- lnHosts = []string{""}
+ lnCfgVals = []addressesWithProtocols{{
+ addresses: []string{""},
+ protocols: nil,
+ }}
}
}
// use a map to prevent duplication
- listeners := make(map[string]struct{})
- for _, lnHost := range lnHosts {
- // normally we would simply append the port,
- // but if lnHost is IPv6, we need to ensure it
- // is enclosed in [ ]; net.JoinHostPort does
- // this for us, but lnHost might also have a
- // network type in front (e.g. "tcp/") leading
- // to "[tcp/::1]" which causes parsing failures
- // later; what we need is "tcp/[::1]", so we have
- // to split the network and host, then re-combine
- network, host, ok := strings.Cut(lnHost, "/")
- if !ok {
- host = network
- network = ""
+ listeners := map[string]map[string]struct{}{}
+ for _, lnCfgVal := range lnCfgVals {
+ for _, 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)
+ }
+ if _, ok := listeners[addr.String()]; !ok {
+ listeners[networkAddr.String()] = map[string]struct{}{}
+ }
+ for _, protocol := range lnCfgVal.protocols {
+ listeners[networkAddr.String()][protocol] = struct{}{}
+ }
}
- host = strings.Trim(host, "[]") // IPv6
- networkAddr := caddy.JoinNetworkAddress(network, host, lnPort)
- addr, err := caddy.ParseNetworkAddress(networkAddr)
- if err != nil {
- return nil, fmt.Errorf("parsing network address: %v", err)
- }
- listeners[addr.String()] = struct{}{}
}
- // now turn map into list
- listenersList := make([]string, 0, len(listeners))
- for lnStr := range listeners {
- listenersList = append(listenersList, lnStr)
- }
- sort.Strings(listenersList)
+ return listeners, nil
+}
- return listenersList, nil
+// addressesWithProtocols associates a list of listen addresses
+// with a list of protocols to serve them with
+type addressesWithProtocols struct {
+ addresses []string
+ protocols []string
}
// Address represents a site address. It contains
diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go
index 505885d2d..3fc08b2c8 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/acme"
+ "github.com/mholt/acmez/v3/acme"
"go.uber.org/zap/zapcore"
"github.com/caddyserver/caddy/v2"
@@ -51,26 +51,46 @@ func init() {
RegisterDirective("log", parseLog)
RegisterHandlerDirective("skip_log", parseLogSkip)
RegisterHandlerDirective("log_skip", parseLogSkip)
+ RegisterHandlerDirective("log_name", parseLogName)
}
// parseBind parses the bind directive. Syntax:
//
-// bind
+// bind [{
+// protocols [h1|h2|h2c|h3] [...]
+// }]
func parseBind(h Helper) ([]ConfigValue, error) {
h.Next() // consume directive name
- return []ConfigValue{{Class: "bind", Value: h.RemainingArgs()}}, nil
+ var addresses, protocols []string
+ addresses = h.RemainingArgs()
+
+ for h.NextBlock(0) {
+ switch h.Val() {
+ case "protocols":
+ protocols = h.RemainingArgs()
+ if len(protocols) == 0 {
+ return nil, h.Errf("protocols requires one or more arguments")
+ }
+ default:
+ return nil, h.Errf("unknown subdirective: %s", h.Val())
+ }
+ }
+
+ return []ConfigValue{{Class: "bind", Value: addressesWithProtocols{
+ addresses: addresses,
+ protocols: protocols,
+ }}}, nil
}
// parseTLS parses the tls directive. Syntax:
//
-// tls [|internal]|[ ] {
+// tls [|internal|force_automate]|[ ] {
// protocols []
// ciphers
// curves
// client_auth {
// mode [request|require|verify_if_given|require_and_verify]
-// trusted_ca_cert
-// trusted_ca_cert_file
+// trust_pool [...]
// trusted_leaf_cert
// trusted_leaf_cert_file
// }
@@ -79,7 +99,7 @@ func parseBind(h Helper) ([]ConfigValue, error) {
// ca
// ca_root
// key_type [ed25519|p256|p384|rsa2048|rsa4096]
-// dns [...]
+// dns [ [...]] (required, though, if DNS is not configured as global option)
// propagation_delay
// propagation_timeout
// resolvers
@@ -87,6 +107,7 @@ func parseBind(h Helper) ([]ConfigValue, error) {
// dns_challenge_override_domain
// on_demand
// reuse_private_keys
+// force_automate
// eab
// issuer [...]
// get_certificate [...]
@@ -106,24 +127,26 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
var certManagers []certmagic.Manager
var onDemand bool
var reusePrivateKeys bool
+ var forceAutomate bool
- // file certificate loader
firstLine := h.RemainingArgs()
switch len(firstLine) {
case 0:
case 1:
if firstLine[0] == "internal" {
internalIssuer = new(caddytls.InternalIssuer)
+ } else if firstLine[0] == "force_automate" {
+ forceAutomate = true
} else if !strings.Contains(firstLine[0], "@") {
- return nil, h.Err("single argument must either be 'internal' or an email address")
+ return nil, h.Err("single argument must either be 'internal', 'force_automate', or an email address")
} else {
- if acmeIssuer == nil {
- acmeIssuer = new(caddytls.ACMEIssuer)
+ acmeIssuer = &caddytls.ACMEIssuer{
+ Email: firstLine[0],
}
- acmeIssuer.Email = firstLine[0]
}
case 2:
+ // file certificate loader
certFilename := firstLine[0]
keyFilename := firstLine[1]
@@ -289,10 +312,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
certManagers = append(certManagers, certManager)
case "dns":
- if !h.NextArg() {
- return nil, h.ArgErr()
- }
- provName := h.Val()
if acmeIssuer == nil {
acmeIssuer = new(caddytls.ACMEIssuer)
}
@@ -302,12 +321,19 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
if acmeIssuer.Challenges.DNS == nil {
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
}
- modID := "dns.providers." + provName
- unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
- if err != nil {
- return nil, err
+ // DNS provider configuration optional, since it may be configured globally via the TLS app with global options
+ if h.NextArg() {
+ provName := h.Val()
+ modID := "dns.providers." + provName
+ unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
+ if err != nil {
+ return nil, err
+ }
+ acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, h.warnings)
+ } else if h.Option("dns") == nil {
+ // if DNS is omitted locally, it needs to be configured globally
+ return nil, h.ArgErr()
}
- acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, h.warnings)
case "resolvers":
args := h.RemainingArgs()
@@ -488,19 +514,24 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
case acmeIssuer != nil:
// implicit ACME issuers (from various subdirectives) - use defaults; there might be more than one
- defaultIssuers := caddytls.DefaultIssuers()
+ defaultIssuers := caddytls.DefaultIssuers(acmeIssuer.Email)
- // if a CA endpoint was set, override multiple implicit issuers since it's a specific one
+ // if an ACME CA endpoint was set, the user expects to use that specific one,
+ // not any others that may be defaults, so replace all defaults with that ACME CA
if acmeIssuer.CA != "" {
defaultIssuers = []certmagic.Issuer{acmeIssuer}
}
for _, issuer := range defaultIssuers {
- switch iss := issuer.(type) {
- case *caddytls.ACMEIssuer:
- issuer = acmeIssuer
- case *caddytls.ZeroSSLIssuer:
- iss.ACMEIssuer = acmeIssuer
+ // apply settings from the implicitly-configured ACMEIssuer to any
+ // default ACMEIssuers, but preserve each default issuer's CA endpoint,
+ // because, for example, if you configure the DNS challenge, it should
+ // apply to any of the default ACMEIssuers, but you don't want to trample
+ // out their unique CA endpoints
+ if iss, ok := issuer.(*caddytls.ACMEIssuer); ok && iss != nil {
+ acmeCopy := *acmeIssuer
+ acmeCopy.CA = iss.CA
+ issuer = &acmeCopy
}
configVals = append(configVals, ConfigValue{
Class: "tls.cert_issuer",
@@ -545,6 +576,15 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
})
}
+ // if enabled, the names in the site addresses will be
+ // added to the automation policies
+ if forceAutomate {
+ configVals = append(configVals, ConfigValue{
+ Class: "tls.force_automate",
+ Value: true,
+ })
+ }
+
// custom certificate selection
if len(certSelector.AnyTag) > 0 {
cp.CertSelection = &certSelector
@@ -845,6 +885,7 @@ func parseInvoke(h Helper) (caddyhttp.MiddlewareHandler, error) {
// log {
// hostnames
// output ...
+// core ...
// format ...
// level
// }
@@ -911,7 +952,7 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
// this is useful for setting up loggers per subdomain in a site block
// with a wildcard domain
customHostnames := []string{}
-
+ noHostname := false
for h.NextBlock(0) {
switch h.Val() {
case "hostnames":
@@ -956,6 +997,66 @@ 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()
+ }
+ moduleName := h.Val()
+ moduleID := "caddy.logging.cores." + moduleName
+ unm, err := caddyfile.UnmarshalModule(h.Dispenser, moduleID)
+ if err != nil {
+ return nil, err
+ }
+ core, ok := unm.(zapcore.Core)
+ if !ok {
+ return nil, h.Errf("module %s (%T) is not a zapcore.Core", moduleID, unm)
+ }
+ cl.CoreRaw = caddyconfig.JSONModuleObject(core, "module", moduleName, h.warnings)
+
case "format":
if !h.NextArg() {
return nil, h.ArgErr()
@@ -997,6 +1098,12 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
cl.Exclude = append(cl.Exclude, h.Val())
}
+ case "no_hostname":
+ if h.NextArg() {
+ return nil, h.ArgErr()
+ }
+ noHostname = true
+
default:
return nil, h.Errf("unrecognized subdirective: %s", h.Val())
}
@@ -1004,7 +1111,7 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
var val namedCustomLog
val.hostnames = customHostnames
-
+ val.noHostname = noHostname
isEmptyConfig := reflect.DeepEqual(cl, new(caddy.CustomLog))
// Skip handling of empty logging configs
@@ -1055,3 +1162,13 @@ func parseLogSkip(h Helper) (caddyhttp.MiddlewareHandler, error) {
}
return caddyhttp.VarsMiddleware{"log_skip": true}, nil
}
+
+// parseLogName parses the log_name directive. Syntax:
+//
+// log_name
+func parseLogName(h Helper) (caddyhttp.MiddlewareHandler, error) {
+ h.Next() // consume directive name
+ return caddyhttp.VarsMiddleware{
+ caddyhttp.AccessLoggerNameVarKey: h.RemainingArgs(),
+ }, nil
+}
diff --git a/caddyconfig/httpcaddyfile/builtins_test.go b/caddyconfig/httpcaddyfile/builtins_test.go
index 70f347dd9..c23531f22 100644
--- a/caddyconfig/httpcaddyfile/builtins_test.go
+++ b/caddyconfig/httpcaddyfile/builtins_test.go
@@ -25,11 +25,12 @@ func TestLogDirectiveSyntax(t *testing.T) {
{
input: `:8080 {
log {
+ core mock
output file foo.log
}
}
`,
- output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"writer":{"filename":"foo.log","output":"file"},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
+ output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"writer":{"filename":"foo.log","output":"file"},"core":{"module":"mock"},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
expectError: false,
},
{
@@ -53,11 +54,26 @@ func TestLogDirectiveSyntax(t *testing.T) {
{
input: `:8080 {
log name-override {
+ core mock
output file foo.log
}
}
`,
- output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.name-override"]},"name-override":{"writer":{"filename":"foo.log","output":"file"},"include":["http.log.access.name-override"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"name-override"}}}}}}`,
+ 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,
},
} {
diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go
index 172594218..f0687a7e9 100644
--- a/caddyconfig/httpcaddyfile/directives.go
+++ b/caddyconfig/httpcaddyfile/directives.go
@@ -17,6 +17,7 @@ package httpcaddyfile
import (
"encoding/json"
"net"
+ "slices"
"sort"
"strconv"
"strings"
@@ -53,6 +54,7 @@ var defaultDirectiveOrder = []string{
"log_append",
"skip_log", // TODO: deprecated, renamed to log_skip
"log_skip",
+ "log_name",
"header",
"copy_response_headers", // only in reverse_proxy's handle_response
@@ -73,6 +75,7 @@ var defaultDirectiveOrder = []string{
"request_header",
"encode",
"push",
+ "intercept",
"templates",
// special routing & dispatching directives
@@ -98,17 +101,6 @@ var defaultDirectiveOrder = []string{
// plugins or by the user via the "order" global option.
var directiveOrder = defaultDirectiveOrder
-// directiveIsOrdered returns true if dir is
-// a known, ordered (sorted) directive.
-func directiveIsOrdered(dir string) bool {
- for _, d := range directiveOrder {
- if d == dir {
- return true
- }
- }
- return false
-}
-
// RegisterDirective registers a unique directive dir with an
// associated unmarshaling (setup) function. When directive dir
// is encountered in a Caddyfile, setupFunc will be called to
@@ -159,7 +151,7 @@ func RegisterHandlerDirective(dir string, setupFunc UnmarshalHandlerFunc) {
// EXPERIMENTAL: This API may change or be removed.
func RegisterDirectiveOrder(dir string, position Positional, standardDir string) {
// check if directive was already ordered
- if directiveIsOrdered(dir) {
+ if slices.Contains(directiveOrder, dir) {
panic("directive '" + dir + "' already ordered")
}
@@ -170,12 +162,7 @@ func RegisterDirectiveOrder(dir string, position Positional, standardDir string)
// check if directive exists in standard distribution, since
// we can't allow plugins to depend on one another; we can't
// guarantee the order that plugins are loaded in.
- foundStandardDir := false
- for _, d := range defaultDirectiveOrder {
- if d == standardDir {
- foundStandardDir = true
- }
- }
+ foundStandardDir := slices.Contains(defaultDirectiveOrder, standardDir)
if !foundStandardDir {
panic("the 3rd argument '" + standardDir + "' must be a directive that exists in the standard distribution of Caddy")
}
@@ -529,9 +516,9 @@ func sortRoutes(routes []ConfigValue) {
// a "pile" of config values, keyed by class name,
// as well as its parsed keys for convenience.
type serverBlock struct {
- block caddyfile.ServerBlock
- pile map[string][]ConfigValue // config values obtained from directives
- keys []Address
+ block caddyfile.ServerBlock
+ pile map[string][]ConfigValue // config values obtained from directives
+ parsedKeys []Address
}
// hostsFromKeys returns a list of all the non-empty hostnames found in
@@ -548,7 +535,7 @@ type serverBlock struct {
func (sb serverBlock) hostsFromKeys(loggerMode bool) []string {
// ensure each entry in our list is unique
hostMap := make(map[string]struct{})
- for _, addr := range sb.keys {
+ for _, addr := range sb.parsedKeys {
if addr.Host == "" {
if !loggerMode {
// server block contains a key like ":443", i.e. the host portion
@@ -580,7 +567,7 @@ func (sb serverBlock) hostsFromKeys(loggerMode bool) []string {
func (sb serverBlock) hostsFromKeysNotHTTP(httpPort string) []string {
// ensure each entry in our list is unique
hostMap := make(map[string]struct{})
- for _, addr := range sb.keys {
+ for _, addr := range sb.parsedKeys {
if addr.Host == "" {
continue
}
@@ -601,23 +588,17 @@ func (sb serverBlock) hostsFromKeysNotHTTP(httpPort string) []string {
// hasHostCatchAllKey returns true if sb has a key that
// omits a host portion, i.e. it "catches all" hosts.
func (sb serverBlock) hasHostCatchAllKey() bool {
- for _, addr := range sb.keys {
- if addr.Host == "" {
- return true
- }
- }
- return false
+ return slices.ContainsFunc(sb.parsedKeys, func(addr Address) bool {
+ return addr.Host == ""
+ })
}
// isAllHTTP returns true if all sb keys explicitly specify
// the http:// scheme
func (sb serverBlock) isAllHTTP() bool {
- for _, addr := range sb.keys {
- if addr.Scheme != "http" {
- return false
- }
- }
- return true
+ return !slices.ContainsFunc(sb.parsedKeys, func(addr Address) bool {
+ return addr.Scheme != "http"
+ })
}
// Positional are the supported modes for ordering directives.
diff --git a/caddyconfig/httpcaddyfile/directives_test.go b/caddyconfig/httpcaddyfile/directives_test.go
index db0282299..2b4d3e6c3 100644
--- a/caddyconfig/httpcaddyfile/directives_test.go
+++ b/caddyconfig/httpcaddyfile/directives_test.go
@@ -78,7 +78,7 @@ func TestHostsFromKeys(t *testing.T) {
[]string{"example.com:2015"},
},
} {
- sb := serverBlock{keys: tc.keys}
+ sb := serverBlock{parsedKeys: tc.keys}
// test in normal mode
actual := sb.hostsFromKeys(false)
diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go
index da5557aa8..3dcd3ea5b 100644
--- a/caddyconfig/httpcaddyfile/httptype.go
+++ b/caddyconfig/httpcaddyfile/httptype.go
@@ -15,16 +15,17 @@
package httpcaddyfile
import (
+ "cmp"
"encoding/json"
"fmt"
"net"
"reflect"
+ "slices"
"sort"
"strconv"
"strings"
"go.uber.org/zap"
- "golang.org/x/exp/slices"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
@@ -171,7 +172,7 @@ func (st ServerType) Setup(
}
// map
- sbmap, err := st.mapAddressToServerBlocks(originalServerBlocks, options)
+ sbmap, err := st.mapAddressToProtocolToServerBlocks(originalServerBlocks, options)
if err != nil {
return nil, warnings, err
}
@@ -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(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,
}
@@ -336,7 +350,7 @@ func (st ServerType) Setup(
// avoid duplicates by sorting + compacting
sort.Strings(defaultLog.Exclude)
- defaultLog.Exclude = slices.Compact[[]string, string](defaultLog.Exclude)
+ defaultLog.Exclude = slices.Compact(defaultLog.Exclude)
}
}
// we may have not actually added anything, so remove if empty
@@ -402,6 +416,20 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
options[opt] = append(existingOpts, logOpts...)
continue
}
+ // Also fold multiple "default_bind" options together into an
+ // array so that server blocks can have multiple binds by default.
+ if opt == "default_bind" {
+ existingOpts, ok := options[opt].([]ConfigValue)
+ if !ok {
+ existingOpts = []ConfigValue{}
+ }
+ defaultBindOpts, ok := val.([]ConfigValue)
+ if !ok {
+ return nil, fmt.Errorf("unexpected type from 'default_bind' global options: %T", val)
+ }
+ options[opt] = append(existingOpts, defaultBindOpts...)
+ continue
+ }
options[opt] = val
}
@@ -520,8 +548,8 @@ func (st *ServerType) serversFromPairings(
if hsp, ok := options["https_port"].(int); ok {
httpsPort = strconv.Itoa(hsp)
}
- autoHTTPS := "on"
- if ah, ok := options["auto_https"].(string); ok {
+ autoHTTPS := []string{}
+ if ah, ok := options["auto_https"].([]string); ok {
autoHTTPS = ah
}
@@ -536,28 +564,74 @@ func (st *ServerType) serversFromPairings(
if k == j {
continue
}
- if sliceContains(sblock2.block.GetKeysText(), key) {
+ if slices.Contains(sblock2.block.GetKeysText(), key) {
return nil, fmt.Errorf("ambiguous site definition: %s", key)
}
}
}
}
+ var (
+ addresses []string
+ protocols [][]string
+ )
+
+ for _, addressWithProtocols := range p.addressesWithProtocols {
+ addresses = append(addresses, addressWithProtocols.address)
+ protocols = append(protocols, addressWithProtocols.protocols)
+ }
+
srv := &caddyhttp.Server{
- Listen: p.addresses,
+ Listen: addresses,
+ ListenProtocols: protocols,
+ }
+
+ // remove srv.ListenProtocols[j] if it only contains the default protocols
+ for j, lnProtocols := range srv.ListenProtocols {
+ srv.ListenProtocols[j] = nil
+ for _, lnProtocol := range lnProtocols {
+ if lnProtocol != "" {
+ srv.ListenProtocols[j] = lnProtocols
+ break
+ }
+ }
+ }
+
+ // remove srv.ListenProtocols if it only contains the default protocols for all listen addresses
+ listenProtocols := srv.ListenProtocols
+ srv.ListenProtocols = nil
+ for _, lnProtocols := range listenProtocols {
+ if lnProtocols != nil {
+ srv.ListenProtocols = listenProtocols
+ break
+ }
}
// handle the auto_https global option
- if autoHTTPS != "on" {
- srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
- switch autoHTTPS {
+ for _, val := range autoHTTPS {
+ switch val {
case "off":
+ if srv.AutoHTTPS == nil {
+ srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
+ }
srv.AutoHTTPS.Disabled = true
+
case "disable_redirects":
+ if srv.AutoHTTPS == nil {
+ srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
+ }
srv.AutoHTTPS.DisableRedir = true
+
case "disable_certs":
+ if srv.AutoHTTPS == nil {
+ srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
+ }
srv.AutoHTTPS.DisableCerts = true
+
case "ignore_loaded_certs":
+ if srv.AutoHTTPS == nil {
+ srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
+ }
srv.AutoHTTPS.IgnoreLoadedCerts = true
}
}
@@ -566,7 +640,7 @@ func (st *ServerType) serversFromPairings(
// See ParseAddress() where parsing should later reject paths
// See https://github.com/caddyserver/caddy/pull/4728 for a full explanation
for _, sblock := range p.serverBlocks {
- for _, addr := range sblock.keys {
+ for _, addr := range sblock.parsedKeys {
if addr.Path != "" {
caddy.Log().Named("caddyfile").Warn("Using a path in a site address is deprecated; please use the 'handle' directive instead", zap.String("address", addr.String()))
}
@@ -584,7 +658,7 @@ func (st *ServerType) serversFromPairings(
var iLongestPath, jLongestPath string
var iLongestHost, jLongestHost string
var iWildcardHost, jWildcardHost bool
- for _, addr := range p.serverBlocks[i].keys {
+ for _, addr := range p.serverBlocks[i].parsedKeys {
if strings.Contains(addr.Host, "*") || addr.Host == "" {
iWildcardHost = true
}
@@ -595,7 +669,7 @@ func (st *ServerType) serversFromPairings(
iLongestPath = addr.Path
}
}
- for _, addr := range p.serverBlocks[j].keys {
+ for _, addr := range p.serverBlocks[j].parsedKeys {
if strings.Contains(addr.Host, "*") || addr.Host == "" {
jWildcardHost = true
}
@@ -627,7 +701,7 @@ func (st *ServerType) serversFromPairings(
})
var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
- autoHTTPSWillAddConnPolicy := autoHTTPS != "off"
+ autoHTTPSWillAddConnPolicy := srv.AutoHTTPS == nil || !srv.AutoHTTPS.Disabled
// if needed, the ServerLogConfig is initialized beforehand so
// that all server blocks can populate it with data, even when not
@@ -673,6 +747,14 @@ func (st *ServerType) serversFromPairings(
}
}
+ // collect hosts that are forced to be automated
+ forceAutomatedNames := make(map[string]struct{})
+ if _, ok := sblock.pile["tls.force_automate"]; ok {
+ for _, host := range hosts {
+ forceAutomatedNames[host] = struct{}{}
+ }
+ }
+
// tls: connection policies
if cpVals, ok := sblock.pile["tls.connection_policy"]; ok {
// tls connection policies
@@ -703,15 +785,21 @@ func (st *ServerType) serversFromPairings(
cp.FallbackSNI = fallbackSNI
}
- // only append this policy if it actually changes something
- if !cp.SettingsEmpty() {
+ // only append this policy if it actually changes something,
+ // or if the configuration explicitly automates certs for
+ // these names (this is necessary to hoist a connection policy
+ // above one that may manually load a wildcard cert that would
+ // otherwise clobber the automated one; the code that appends
+ // policies that manually load certs comes later, so they're
+ // lower in the list)
+ if !cp.SettingsEmpty() || mapContains(forceAutomatedNames, hosts) {
srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp)
hasCatchAllTLSConnPolicy = len(hosts) == 0
}
}
}
- for _, addr := range sblock.keys {
+ for _, addr := range sblock.parsedKeys {
// if server only uses HTTP port, auto-HTTPS will not apply
if listenersUseAnyPortOtherThan(srv.Listen, httpPort) {
// exclude any hosts that were defined explicitly with "http://"
@@ -720,7 +808,7 @@ func (st *ServerType) serversFromPairings(
if srv.AutoHTTPS == nil {
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
}
- if !sliceContains(srv.AutoHTTPS.Skip, addr.Host) {
+ if !slices.Contains(srv.AutoHTTPS.Skip, addr.Host) {
srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, addr.Host)
}
}
@@ -734,7 +822,7 @@ func (st *ServerType) serversFromPairings(
// https://caddy.community/t/making-sense-of-auto-https-and-why-disabling-it-still-serves-https-instead-of-http/9761
createdTLSConnPolicies, ok := sblock.pile["tls.connection_policy"]
hasTLSEnabled := (ok && len(createdTLSConnPolicies) > 0) ||
- (addr.Host != "" && srv.AutoHTTPS != nil && !sliceContains(srv.AutoHTTPS.Skip, addr.Host))
+ (addr.Host != "" && srv.AutoHTTPS != nil && !slices.Contains(srv.AutoHTTPS.Skip, addr.Host))
// we'll need to remember if the address qualifies for auto-HTTPS, so we
// can add a TLS conn policy if necessary
@@ -742,6 +830,7 @@ func (st *ServerType) serversFromPairings(
(addr.Scheme != "http" && addr.Port != httpPort && hasTLSEnabled) {
addressQualifiesForTLS = true
}
+
// 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 &&
@@ -797,6 +886,15 @@ func (st *ServerType) serversFromPairings(
sblockLogHosts := sblock.hostsFromKeys(true)
for _, cval := range sblock.pile["custom_log"] {
ncl := cval.Value.(namedCustomLog)
+
+ // if `no_hostname` is set, then this logger will not
+ // be associated with any of the site block's hostnames,
+ // and only be usable via the `log_name` directive
+ // or the `access_logger_names` variable
+ if ncl.noHostname {
+ continue
+ }
+
if sblock.hasHostCatchAllKey() && len(ncl.hostnames) == 0 {
// all requests for hosts not able to be listed should use
// this log because it's a catch-all-hosts server block
@@ -805,22 +903,22 @@ func (st *ServerType) serversFromPairings(
// if the logger overrides the hostnames, map that to the logger name
for _, h := range ncl.hostnames {
if srv.Logs.LoggerNames == nil {
- srv.Logs.LoggerNames = make(map[string]string)
+ srv.Logs.LoggerNames = make(map[string]caddyhttp.StringArray)
}
- srv.Logs.LoggerNames[h] = ncl.name
+ srv.Logs.LoggerNames[h] = append(srv.Logs.LoggerNames[h], ncl.name)
}
} else {
// otherwise, map each host to the logger name
for _, h := range sblockLogHosts {
- if srv.Logs.LoggerNames == nil {
- srv.Logs.LoggerNames = make(map[string]string)
- }
// strip the port from the host, if any
host, _, err := net.SplitHostPort(h)
if err != nil {
host = h
}
- srv.Logs.LoggerNames[host] = ncl.name
+ if srv.Logs.LoggerNames == nil {
+ srv.Logs.LoggerNames = make(map[string]caddyhttp.StringArray)
+ }
+ srv.Logs.LoggerNames[host] = append(srv.Logs.LoggerNames[host], ncl.name)
}
}
}
@@ -864,7 +962,10 @@ func (st *ServerType) serversFromPairings(
if addressQualifiesForTLS &&
!hasCatchAllTLSConnPolicy &&
(len(srv.TLSConnPolicies) > 0 || !autoHTTPSWillAddConnPolicy || defaultSNI != "" || fallbackSNI != "") {
- srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{DefaultSNI: defaultSNI, FallbackSNI: fallbackSNI})
+ srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{
+ DefaultSNI: defaultSNI,
+ FallbackSNI: fallbackSNI,
+ })
}
// tidy things up a bit
@@ -877,8 +978,7 @@ func (st *ServerType) serversFromPairings(
servers[fmt.Sprintf("srv%d", i)] = srv
}
- err := applyServerOptions(servers, options, warnings)
- if err != nil {
+ if err := applyServerOptions(servers, options, warnings); err != nil {
return nil, fmt.Errorf("applying global server options: %v", err)
}
@@ -923,7 +1023,7 @@ func detectConflictingSchemes(srv *caddyhttp.Server, serverBlocks []serverBlock,
}
for _, sblock := range serverBlocks {
- for _, addr := range sblock.keys {
+ for _, addr := range sblock.parsedKeys {
if addr.Scheme == "http" || addr.Port == httpPort {
if err := checkAndSetHTTP(addr); err != nil {
return err
@@ -961,11 +1061,40 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti
// if they're exactly equal in every way, just keep one of them
if reflect.DeepEqual(cps[i], cps[j]) {
- cps = append(cps[:j], cps[j+1:]...)
+ cps = slices.Delete(cps, j, j+1)
i--
break
}
+ // as a special case, if there are adjacent TLS conn policies that are identical except
+ // by their matchers, and the matchers are specifically just ServerName ("sni") matchers
+ // (by far the most common), we can combine them into a single policy
+ if i == j-1 && len(cps[i].MatchersRaw) == 1 && len(cps[j].MatchersRaw) == 1 {
+ if iSNIMatcherJSON, ok := cps[i].MatchersRaw["sni"]; ok {
+ if jSNIMatcherJSON, ok := cps[j].MatchersRaw["sni"]; ok {
+ // position of policies and the matcher criteria check out; if settings are
+ // the same, then we can combine the policies; we have to unmarshal and
+ // remarshal the matchers though
+ if cps[i].SettingsEqual(*cps[j]) {
+ var iSNIMatcher caddytls.MatchServerName
+ if err := json.Unmarshal(iSNIMatcherJSON, &iSNIMatcher); err == nil {
+ var jSNIMatcher caddytls.MatchServerName
+ if err := json.Unmarshal(jSNIMatcherJSON, &jSNIMatcher); err == nil {
+ iSNIMatcher = append(iSNIMatcher, jSNIMatcher...)
+ cps[i].MatchersRaw["sni"], err = json.Marshal(iSNIMatcher)
+ if err != nil {
+ return nil, fmt.Errorf("recombining SNI matchers: %v", err)
+ }
+ cps = slices.Delete(cps, j, j+1)
+ i--
+ break
+ }
+ }
+ }
+ }
+ }
+ }
+
// if they have the same matcher, try to reconcile each field: either they must
// be identical, or we have to be able to combine them safely
if reflect.DeepEqual(cps[i].MatchersRaw, cps[j].MatchersRaw) {
@@ -999,6 +1128,12 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti
return nil, fmt.Errorf("two policies with same match criteria have conflicting default SNI: %s vs. %s",
cps[i].DefaultSNI, cps[j].DefaultSNI)
}
+ if cps[i].FallbackSNI != "" &&
+ cps[j].FallbackSNI != "" &&
+ cps[i].FallbackSNI != cps[j].FallbackSNI {
+ return nil, fmt.Errorf("two policies with same match criteria have conflicting fallback SNI: %s vs. %s",
+ cps[i].FallbackSNI, cps[j].FallbackSNI)
+ }
if cps[i].ProtocolMin != "" &&
cps[j].ProtocolMin != "" &&
cps[i].ProtocolMin != cps[j].ProtocolMin {
@@ -1039,6 +1174,9 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti
if cps[i].DefaultSNI == "" && cps[j].DefaultSNI != "" {
cps[i].DefaultSNI = cps[j].DefaultSNI
}
+ if cps[i].FallbackSNI == "" && cps[j].FallbackSNI != "" {
+ cps[i].FallbackSNI = cps[j].FallbackSNI
+ }
if cps[i].ProtocolMin == "" && cps[j].ProtocolMin != "" {
cps[i].ProtocolMin = cps[j].ProtocolMin
}
@@ -1052,18 +1190,19 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti
} else if cps[i].CertSelection != nil && cps[j].CertSelection != nil {
// if both have one, then combine AnyTag
for _, tag := range cps[j].CertSelection.AnyTag {
- if !sliceContains(cps[i].CertSelection.AnyTag, tag) {
+ if !slices.Contains(cps[i].CertSelection.AnyTag, tag) {
cps[i].CertSelection.AnyTag = append(cps[i].CertSelection.AnyTag, tag)
}
}
}
- cps = append(cps[:j], cps[j+1:]...)
+ cps = slices.Delete(cps, j, j+1)
i--
break
}
}
}
+
return cps, nil
}
@@ -1135,7 +1274,7 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
func buildSubroute(routes []ConfigValue, groupCounter counter, needsSorting bool) (*caddyhttp.Subroute, error) {
if needsSorting {
for _, val := range routes {
- if !directiveIsOrdered(val.directive) {
+ if !slices.Contains(directiveOrder, val.directive) {
return nil, fmt.Errorf("directive '%s' is not an ordered HTTP handler, so it cannot be used here - try placing within a route block or using the order global option", val.directive)
}
}
@@ -1282,19 +1421,24 @@ func matcherSetFromMatcherToken(
if tkn.Text == "*" {
// match all requests == no matchers, so nothing to do
return nil, true, nil
- } else if strings.HasPrefix(tkn.Text, "/") {
- // convenient way to specify a single path match
+ }
+
+ // convenient way to specify a single path match
+ if strings.HasPrefix(tkn.Text, "/") {
return caddy.ModuleMap{
"path": caddyconfig.JSON(caddyhttp.MatchPath{tkn.Text}, warnings),
}, true, nil
- } else if strings.HasPrefix(tkn.Text, matcherPrefix) {
- // pre-defined matcher
+ }
+
+ // pre-defined matcher
+ if strings.HasPrefix(tkn.Text, matcherPrefix) {
m, ok := matcherDefs[tkn.Text]
if !ok {
return nil, false, fmt.Errorf("unrecognized matcher name: %+v", tkn.Text)
}
return m, true, nil
}
+
return nil, false, nil
}
@@ -1308,7 +1452,7 @@ func (st *ServerType) compileEncodedMatcherSets(sblock serverBlock) ([]caddy.Mod
var matcherPairs []*hostPathPair
var catchAllHosts bool
- for _, addr := range sblock.keys {
+ for _, addr := range sblock.parsedKeys {
// choose a matcher pair that should be shared by this
// server block; if none exists yet, create one
var chosenMatcherPair *hostPathPair
@@ -1340,25 +1484,16 @@ func (st *ServerType) compileEncodedMatcherSets(sblock serverBlock) ([]caddy.Mod
// add this server block's keys to the matcher
// pair if it doesn't already exist
- if addr.Host != "" {
- var found bool
- for _, h := range chosenMatcherPair.hostm {
- if h == addr.Host {
- found = true
- break
- }
- }
- if !found {
- chosenMatcherPair.hostm = append(chosenMatcherPair.hostm, addr.Host)
- }
+ if addr.Host != "" && !slices.Contains(chosenMatcherPair.hostm, addr.Host) {
+ chosenMatcherPair.hostm = append(chosenMatcherPair.hostm, addr.Host)
}
}
// 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
}
@@ -1397,6 +1532,14 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.M
// given a matcher name and the tokens following it, parse
// the tokens as a matcher module and record it
makeMatcher := func(matcherName string, tokens []caddyfile.Token) error {
+ // create a new dispenser from the tokens
+ dispenser := caddyfile.NewDispenser(tokens)
+
+ // set the matcher name (without @) in the dispenser context so
+ // that matcher modules can access it to use it as their name
+ // (e.g. regexp matchers which use the name for capture groups)
+ dispenser.SetContext(caddyfile.MatcherNameCtxKey, definitionName[1:])
+
mod, err := caddy.GetModule("http.matchers." + matcherName)
if err != nil {
return fmt.Errorf("getting matcher module '%s': %v", matcherName, err)
@@ -1405,16 +1548,21 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.M
if !ok {
return fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName)
}
- err = unm.UnmarshalCaddyfile(caddyfile.NewDispenser(tokens))
+ err = unm.UnmarshalCaddyfile(dispenser)
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
@@ -1422,11 +1570,13 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.M
if d.NextArg() {
if d.Token().Quoted() {
// since it was missing the matcher name, we insert a token
- // in front of the expression token itself
- err := makeMatcher("expression", []caddyfile.Token{
- {Text: "expression", File: d.File(), Line: d.Line()},
- d.Token(),
- })
+ // in front of the expression token itself; we use Clone() to
+ // make the new token to keep the same the import location as
+ // the next token, if this is within a snippet or imported file.
+ // see https://github.com/caddyserver/caddy/issues/6287
+ expressionToken := d.Token().Clone()
+ expressionToken.Text = "expression"
+ err := makeMatcher("expression", []caddyfile.Token{expressionToken, d.Token()})
if err != nil {
return err
}
@@ -1456,7 +1606,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)
@@ -1516,16 +1666,6 @@ func tryDuration(val any, warnings *[]caddyconfig.Warning) caddy.Duration {
return durationVal
}
-// sliceContains returns true if needle is in haystack.
-func sliceContains(haystack []string, needle string) bool {
- for _, s := range haystack {
- if s == needle {
- return true
- }
- }
- return false
-}
-
// listenersUseAnyPortOtherThan returns true if there are any
// listeners in addresses that use a port which is not otherPort.
// Mostly borrowed from unexported method in caddyhttp package.
@@ -1546,6 +1686,18 @@ func listenersUseAnyPortOtherThan(addresses []string, otherPort string) bool {
return false
}
+func mapContains[K comparable, V any](m map[K]V, keys []K) bool {
+ if len(m) == 0 || len(keys) == 0 {
+ return false
+ }
+ for _, key := range keys {
+ if _, ok := m[key]; ok {
+ return true
+ }
+ }
+ return false
+}
+
// specificity returns len(s) minus any wildcards (*) and
// placeholders ({...}). Basically, it's a length count
// that penalizes the use of wildcards and placeholders.
@@ -1583,17 +1735,25 @@ func (c counter) nextGroup() string {
}
type namedCustomLog struct {
- name string
- hostnames []string
- log *caddy.CustomLog
+ name string
+ hostnames []string
+ log *caddy.CustomLog
+ noHostname bool
+}
+
+// addressWithProtocols associates a listen address with
+// the protocols to serve it with
+type addressWithProtocols struct {
+ address string
+ protocols []string
}
// sbAddrAssociation is a mapping from a list of
-// addresses to a list of server blocks that are
-// served on those addresses.
+// addresses with protocols, and a list of server
+// blocks that are served on those addresses.
type sbAddrAssociation struct {
- addresses []string
- serverBlocks []serverBlock
+ addressesWithProtocols []addressWithProtocols
+ serverBlocks []serverBlock
}
const (
diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go
index 70d475d6b..e48a52577 100644
--- a/caddyconfig/httpcaddyfile/options.go
+++ b/caddyconfig/httpcaddyfile/options.go
@@ -15,14 +15,17 @@
package httpcaddyfile
import (
+ "slices"
"strconv"
"github.com/caddyserver/certmagic"
- "github.com/mholt/acmez/acme"
+ "github.com/libdns/libdns"
+ "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"
)
@@ -30,19 +33,20 @@ func init() {
RegisterGlobalOption("debug", parseOptTrue)
RegisterGlobalOption("http_port", parseOptHTTPPort)
RegisterGlobalOption("https_port", parseOptHTTPSPort)
- RegisterGlobalOption("default_bind", parseOptStringList)
+ RegisterGlobalOption("default_bind", parseOptDefaultBind)
RegisterGlobalOption("grace_period", parseOptDuration)
RegisterGlobalOption("shutdown_delay", parseOptDuration)
RegisterGlobalOption("default_sni", parseOptSingleString)
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)
RegisterGlobalOption("acme_ca_root", parseOptSingleString)
- RegisterGlobalOption("acme_dns", parseOptACMEDNS)
+ RegisterGlobalOption("acme_dns", parseOptDNS)
RegisterGlobalOption("acme_eab", parseOptACMEEAB)
RegisterGlobalOption("cert_issuer", parseOptCertIssuer)
RegisterGlobalOption("skip_install_trust", parseOptTrue)
@@ -52,11 +56,15 @@ 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)
RegisterGlobalOption("log", parseLogOptions)
RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
RegisterGlobalOption("persist_config", parseOptPersistConfig)
+ RegisterGlobalOption("dns", parseOptDNS)
+ RegisterGlobalOption("ech", parseOptECH)
}
func parseOptTrue(d *caddyfile.Dispenser, _ any) (any, error) { return true, nil }
@@ -109,17 +117,12 @@ func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
}
pos := Positional(d.Val())
- newOrder := directiveOrder
+ // if directive already had an order, drop it
+ newOrder := slices.DeleteFunc(directiveOrder, func(d string) bool {
+ return d == dirName
+ })
- // if directive exists, first remove it
- for i, d := range newOrder {
- if d == dirName {
- newOrder = append(newOrder[:i], newOrder[i+1:]...)
- break
- }
- }
-
- // act on the positional
+ // act on the positional; if it's First or Last, we're done right away
switch pos {
case First:
newOrder = append([]string{dirName}, newOrder...)
@@ -128,6 +131,7 @@ func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
}
directiveOrder = newOrder
return newOrder, nil
+
case Last:
newOrder = append(newOrder, dirName)
if d.NextArg() {
@@ -135,8 +139,11 @@ func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
}
directiveOrder = newOrder
return newOrder, nil
+
+ // if it's Before or After, continue
case Before:
case After:
+
default:
return nil, d.Errf("unknown positional '%s'", pos)
}
@@ -150,17 +157,17 @@ func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) {
return nil, d.ArgErr()
}
- // insert directive into proper position
- for i, d := range newOrder {
- if d == otherDir {
- if pos == Before {
- newOrder = append(newOrder[:i], append([]string{dirName}, newOrder[i:]...)...)
- } else if pos == After {
- newOrder = append(newOrder[:i+1], append([]string{dirName}, newOrder[i+1:]...)...)
- }
- break
- }
+ // get the position of the target directive
+ targetIndex := slices.Index(newOrder, otherDir)
+ if targetIndex == -1 {
+ return nil, d.Errf("directive '%s' not found", otherDir)
}
+ // if we're inserting after, we need to increment the index to go after
+ if pos == After {
+ targetIndex++
+ }
+ // insert the directive into the new order
+ newOrder = slices.Insert(newOrder, targetIndex, dirName)
directiveOrder = newOrder
@@ -186,6 +193,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()
@@ -200,25 +241,6 @@ func parseOptDuration(d *caddyfile.Dispenser, _ any) (any, error) {
return caddy.Duration(dur), nil
}
-func parseOptACMEDNS(d *caddyfile.Dispenser, _ any) (any, error) {
- if !d.Next() { // consume option name
- return nil, d.ArgErr()
- }
- if !d.Next() { // get DNS module name
- return nil, d.ArgErr()
- }
- modID := "dns.providers." + d.Val()
- unm, err := caddyfile.UnmarshalModule(d, modID)
- if err != nil {
- return nil, err
- }
- prov, ok := unm.(certmagic.ACMEDNSProvider)
- if !ok {
- return nil, d.Errf("module %s (%T) is not a certmagic.ACMEDNSProvider", modID, unm)
- }
- return prov, nil
-}
-
func parseOptACMEEAB(d *caddyfile.Dispenser, _ any) (any, error) {
eab := new(acme.EAB)
d.Next() // consume option name
@@ -283,13 +305,32 @@ func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) {
return val, nil
}
-func parseOptStringList(d *caddyfile.Dispenser, _ any) (any, error) {
+func parseOptDefaultBind(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
- val := d.RemainingArgs()
- if len(val) == 0 {
- return "", d.ArgErr()
+
+ var addresses, protocols []string
+ addresses = d.RemainingArgs()
+
+ if len(addresses) == 0 {
+ addresses = append(addresses, "")
}
- return val, nil
+
+ for d.NextBlock(0) {
+ switch d.Val() {
+ case "protocols":
+ protocols = d.RemainingArgs()
+ if len(protocols) == 0 {
+ return nil, d.Errf("protocols requires one or more arguments")
+ }
+ default:
+ return nil, d.Errf("unknown subdirective: %s", d.Val())
+ }
+ }
+
+ return []ConfigValue{{Class: "bind", Value: addressesWithProtocols{
+ addresses: addresses,
+ protocols: protocols,
+ }}}, nil
}
func parseOptAdmin(d *caddyfile.Dispenser, _ any) (any, error) {
@@ -345,40 +386,39 @@ func parseOptOnDemand(d *caddyfile.Dispenser, _ any) (any, error) {
if ond == nil {
ond = new(caddytls.OnDemandConfig)
}
+ if ond.PermissionRaw != nil {
+ return nil, d.Err("on-demand TLS permission module (or 'ask') already specified")
+ }
perm := caddytls.PermissionByHTTP{Endpoint: d.Val()}
ond.PermissionRaw = caddyconfig.JSONModuleObject(perm, "module", "http", nil)
- case "interval":
+ case "permission":
if !d.NextArg() {
return nil, d.ArgErr()
}
- dur, err := caddy.ParseDuration(d.Val())
- if err != nil {
- return nil, err
- }
if ond == nil {
ond = new(caddytls.OnDemandConfig)
}
- if ond.RateLimit == nil {
- ond.RateLimit = new(caddytls.RateLimit)
+ if ond.PermissionRaw != nil {
+ return nil, d.Err("on-demand TLS permission module (or 'ask') already specified")
}
- ond.RateLimit.Interval = caddy.Duration(dur)
+ modName := d.Val()
+ modID := "tls.permission." + modName
+ unm, err := caddyfile.UnmarshalModule(d, modID)
+ if err != nil {
+ return nil, err
+ }
+ perm, ok := unm.(caddytls.OnDemandPermission)
+ if !ok {
+ return nil, d.Errf("module %s (%T) is not an on-demand TLS permission module", modID, unm)
+ }
+ ond.PermissionRaw = caddyconfig.JSONModuleObject(perm, "module", modName, nil)
+
+ case "interval":
+ return nil, d.Errf("the on_demand_tls 'interval' option is no longer supported, remove it from your config")
case "burst":
- if !d.NextArg() {
- return nil, d.ArgErr()
- }
- burst, err := strconv.Atoi(d.Val())
- if err != nil {
- return nil, err
- }
- if ond == nil {
- ond = new(caddytls.OnDemandConfig)
- }
- if ond.RateLimit == nil {
- ond.RateLimit = new(caddytls.RateLimit)
- }
- ond.RateLimit.Burst = burst
+ return nil, d.Errf("the on_demand_tls 'burst' option is no longer supported, remove it from your config")
default:
return nil, d.Errf("unrecognized parameter '%s'", d.Val())
@@ -407,19 +447,44 @@ func parseOptPersistConfig(d *caddyfile.Dispenser, _ any) (any, error) {
func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
- if !d.Next() {
+ val := d.RemainingArgs()
+ if len(val) == 0 {
return "", d.ArgErr()
}
- val := d.Val()
- if d.Next() {
- return "", d.ArgErr()
- }
- if val != "off" && val != "disable_redirects" && val != "disable_certs" && val != "ignore_loaded_certs" {
- return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', or 'ignore_loaded_certs'")
+ for _, v := range val {
+ switch v {
+ case "off":
+ case "disable_redirects":
+ case "disable_certs":
+ case "ignore_loaded_certs":
+ case "prefer_wildcard":
+ break
+
+ default:
+ return "", d.Errf("auto_https must be one of 'off', 'disable_redirects', 'disable_certs', 'ignore_loaded_certs', or 'prefer_wildcard'")
+ }
}
return val, nil
}
+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)
}
@@ -489,3 +554,68 @@ func parseOptPreferredChains(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next()
return caddytls.ParseCaddyfilePreferredChainsOptions(d)
}
+
+func parseOptDNS(d *caddyfile.Dispenser, _ any) (any, error) {
+ d.Next() // consume option name
+
+ if !d.Next() { // get DNS module name
+ return nil, d.ArgErr()
+ }
+ modID := "dns.providers." + d.Val()
+ unm, err := caddyfile.UnmarshalModule(d, modID)
+ if err != nil {
+ return nil, err
+ }
+ switch unm.(type) {
+ case libdns.RecordGetter,
+ libdns.RecordSetter,
+ libdns.RecordAppender,
+ libdns.RecordDeleter:
+ default:
+ return nil, d.Errf("module %s (%T) is not a libdns provider", modID, unm)
+ }
+ return unm, nil
+}
+
+func parseOptECH(d *caddyfile.Dispenser, _ any) (any, error) {
+ d.Next() // consume option name
+
+ ech := new(caddytls.ECH)
+
+ publicNames := d.RemainingArgs()
+ for _, publicName := range publicNames {
+ ech.Configs = append(ech.Configs, caddytls.ECHConfiguration{
+ PublicName: publicName,
+ })
+ }
+ if len(ech.Configs) == 0 {
+ return nil, d.ArgErr()
+ }
+
+ for nesting := d.Nesting(); d.NextBlock(nesting); {
+ switch d.Val() {
+ case "dns":
+ if !d.Next() {
+ return nil, d.ArgErr()
+ }
+ providerName := d.Val()
+ modID := "dns.providers." + providerName
+ unm, err := caddyfile.UnmarshalModule(d, modID)
+ if err != nil {
+ return nil, err
+ }
+ ech.Publication = append(ech.Publication, &caddytls.ECHPublication{
+ Configs: publicNames,
+ PublishersRaw: caddy.ModuleMap{
+ "dns": caddyconfig.JSON(caddytls.ECHDNSPublisher{
+ ProviderRaw: caddyconfig.JSONModuleObject(unm, "name", providerName, nil),
+ }, nil),
+ },
+ })
+ default:
+ return nil, d.Errf("ech: unrecognized subdirective '%s'", d.Val())
+ }
+ }
+
+ return ech, nil
+}
diff --git a/caddyconfig/httpcaddyfile/serveroptions.go b/caddyconfig/httpcaddyfile/serveroptions.go
index 62902b964..d60ce51a9 100644
--- a/caddyconfig/httpcaddyfile/serveroptions.go
+++ b/caddyconfig/httpcaddyfile/serveroptions.go
@@ -17,6 +17,7 @@ package httpcaddyfile
import (
"encoding/json"
"fmt"
+ "slices"
"github.com/dustin/go-humanize"
@@ -50,6 +51,7 @@ type serverOptions struct {
ClientIPHeaders []string
ShouldLogCredentials bool
Metrics *caddyhttp.Metrics
+ Trace bool // TODO: EXPERIMENTAL
}
func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
@@ -179,7 +181,7 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
if proto != "h1" && proto != "h2" && proto != "h2c" && proto != "h3" {
return nil, d.Errf("unknown protocol '%s': expected h1, h2, h2c, or h3", proto)
}
- if sliceContains(serverOpts.Protocols, proto) {
+ if slices.Contains(serverOpts.Protocols, proto) {
return nil, d.Errf("protocol %s specified more than once", proto)
}
serverOpts.Protocols = append(serverOpts.Protocols, proto)
@@ -228,7 +230,7 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
case "client_ip_headers":
headers := d.RemainingArgs()
for _, header := range headers {
- if sliceContains(serverOpts.ClientIPHeaders, header) {
+ if slices.Contains(serverOpts.ClientIPHeaders, header) {
return nil, d.Errf("client IP header %s specified more than once", header)
}
serverOpts.ClientIPHeaders = append(serverOpts.ClientIPHeaders, header)
@@ -238,47 +240,22 @@ 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() {
+ case "per_host":
+ serverOpts.Metrics.PerHost = true
+ default:
+ return nil, d.Errf("unrecognized metrics option '%s'", d.Val())
+ }
+ }
+
+ case "trace":
if d.NextArg() {
return nil, d.ArgErr()
}
- if nesting := d.Nesting(); d.NextBlock(nesting) {
- return nil, d.ArgErr()
- }
- serverOpts.Metrics = new(caddyhttp.Metrics)
-
- // TODO: DEPRECATED. (August 2022)
- case "protocol":
- caddy.Log().Named("caddyfile").Warn("DEPRECATED: protocol sub-option will be removed soon")
-
- for nesting := d.Nesting(); d.NextBlock(nesting); {
- switch d.Val() {
- case "allow_h2c":
- caddy.Log().Named("caddyfile").Warn("DEPRECATED: allow_h2c will be removed soon; use protocols option instead")
-
- if d.NextArg() {
- return nil, d.ArgErr()
- }
- if sliceContains(serverOpts.Protocols, "h2c") {
- return nil, d.Errf("protocol h2c already specified")
- }
- serverOpts.Protocols = append(serverOpts.Protocols, "h2c")
-
- case "strict_sni_host":
- caddy.Log().Named("caddyfile").Warn("DEPRECATED: protocol > strict_sni_host in this position will be removed soon; move up to the servers block instead")
-
- if d.NextArg() && d.Val() != "insecure_off" && d.Val() != "on" {
- return nil, d.Errf("strict_sni_host only supports 'on' or 'insecure_off', got '%s'", d.Val())
- }
- boolVal := true
- if d.Val() == "insecure_off" {
- boolVal = false
- }
- serverOpts.StrictSNIHost = &boolVal
-
- default:
- return nil, d.Errf("unrecognized protocol option '%s'", d.Val())
- }
- }
+ serverOpts.Trace = true
default:
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
@@ -291,7 +268,7 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
func applyServerOptions(
servers map[string]*caddyhttp.Server,
options map[string]any,
- warnings *[]caddyconfig.Warning,
+ _ *[]caddyconfig.Warning,
) error {
serverOpts, ok := options["servers"].([]serverOptions)
if !ok {
@@ -315,24 +292,15 @@ func applyServerOptions(
for key, server := range servers {
// find the options that apply to this server
- opts := func() *serverOptions {
- for _, entry := range serverOpts {
- if entry.ListenerAddress == "" {
- return &entry
- }
- for _, listener := range server.Listen {
- if entry.ListenerAddress == listener {
- return &entry
- }
- }
- }
- return nil
- }()
+ optsIndex := slices.IndexFunc(serverOpts, func(s serverOptions) bool {
+ return s.ListenerAddress == "" || slices.Contains(server.Listen, s.ListenerAddress)
+ })
// if none apply, then move to the next server
- if opts == nil {
+ if optsIndex == -1 {
continue
}
+ opts := serverOpts[optsIndex]
// set all the options
server.ListenerWrappersRaw = opts.ListenerWrappersRaw
@@ -351,10 +319,17 @@ func applyServerOptions(
server.Metrics = opts.Metrics
if opts.ShouldLogCredentials {
if server.Logs == nil {
- server.Logs = &caddyhttp.ServerLogConfig{}
+ server.Logs = new(caddyhttp.ServerLogConfig)
}
server.Logs.ShouldLogCredentials = opts.ShouldLogCredentials
}
+ if opts.Trace {
+ // TODO: THIS IS EXPERIMENTAL (MAY 2024)
+ if server.Logs == nil {
+ server.Logs = new(caddyhttp.ServerLogConfig)
+ }
+ server.Logs.Trace = opts.Trace
+ }
if opts.Name != "" {
nameReplacements[key] = opts.Name
diff --git a/caddyconfig/httpcaddyfile/shorthands.go b/caddyconfig/httpcaddyfile/shorthands.go
index 5855d127c..ca6e4f92c 100644
--- a/caddyconfig/httpcaddyfile/shorthands.go
+++ b/caddyconfig/httpcaddyfile/shorthands.go
@@ -36,6 +36,7 @@ func NewShorthandReplacer() ShorthandReplacer {
{regexp.MustCompile(`{re\.([\w-\.]*)}`), "{http.regexp.$1}"},
{regexp.MustCompile(`{vars\.([\w-]*)}`), "{http.vars.$1}"},
{regexp.MustCompile(`{rp\.([\w-\.]*)}`), "{http.reverse_proxy.$1}"},
+ {regexp.MustCompile(`{resp\.([\w-\.]*)}`), "{http.intercept.$1}"},
{regexp.MustCompile(`{err\.([\w-\.]*)}`), "{http.error.$1}"},
{regexp.MustCompile(`{file_match\.([\w-]*)}`), "{http.matchers.file.$1}"},
}
@@ -51,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 1adb2b6e0..a0899dd8d 100644
--- a/caddyconfig/httpcaddyfile/tlsapp.go
+++ b/caddyconfig/httpcaddyfile/tlsapp.go
@@ -19,12 +19,13 @@ import (
"encoding/json"
"fmt"
"reflect"
+ "slices"
"sort"
"strconv"
"strings"
"github.com/caddyserver/certmagic"
- "github.com/mholt/acmez/acme"
+ "github.com/mholt/acmez/v3/acme"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
@@ -44,8 +45,8 @@ func (st ServerType) buildTLSApp(
if hp, ok := options["http_port"].(int); ok {
httpPort = strconv.Itoa(hp)
}
- autoHTTPS := "on"
- if ah, ok := options["auto_https"].(string); ok {
+ autoHTTPS := []string{}
+ if ah, ok := options["auto_https"].([]string); ok {
autoHTTPS = ah
}
@@ -53,23 +54,25 @@ func (st ServerType) buildTLSApp(
// key, so that they don't get forgotten/omitted by auto-HTTPS
// (since they won't appear in route matchers)
httpsHostsSharedWithHostlessKey := make(map[string]struct{})
- if autoHTTPS != "off" {
+ if !slices.Contains(autoHTTPS, "off") {
for _, pair := range pairings {
for _, sb := range pair.serverBlocks {
- for _, addr := range sb.keys {
- if addr.Host == "" {
- // this server block has a hostless key, now
- // go through and add all the hosts to the set
- for _, otherAddr := range sb.keys {
- if otherAddr.Original == addr.Original {
- continue
- }
- if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
- httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
- }
- }
- break
+ for _, addr := range sb.parsedKeys {
+ if addr.Host != "" {
+ continue
}
+
+ // this server block has a hostless key, now
+ // go through and add all the hosts to the set
+ for _, otherAddr := range sb.parsedKeys {
+ if otherAddr.Original == addr.Original {
+ continue
+ }
+ if otherAddr.Host != "" && otherAddr.Scheme != "http" && otherAddr.Port != httpPort {
+ httpsHostsSharedWithHostlessKey[otherAddr.Host] = struct{}{}
+ }
+ }
+ break
}
}
}
@@ -89,9 +92,33 @@ func (st ServerType) buildTLSApp(
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP)
}
+ var wildcardHosts []string // collect all hosts that have a wildcard in them, and aren't HTTP
+ forcedAutomatedNames := make(map[string]struct{}) // explicitly configured to be automated, even if covered by a wildcard
+
+ 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
- if !listenersUseAnyPortOtherThan(p.addresses, httpPort) {
+ var addresses []string
+ for _, addressWithProtocols := range p.addressesWithProtocols {
+ addresses = append(addresses, addressWithProtocols.address)
+ }
+ if !listenersUseAnyPortOtherThan(addresses, httpPort) {
continue
}
@@ -108,6 +135,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
@@ -118,6 +151,13 @@ func (st ServerType) buildTLSApp(
ap.OnDemand = true
}
+ // collect hosts that are forced to have certs automated for their specific name
+ if _, ok := sblock.pile["tls.force_automate"]; ok {
+ for _, host := range sblockHosts {
+ forcedAutomatedNames[host] = struct{}{}
+ }
+ }
+
// reuse private keys tls
if _, ok := sblock.pile["tls.reuse_private_keys"]; ok {
ap.ReusePrivateKeys = true
@@ -181,8 +221,8 @@ func (st ServerType) buildTLSApp(
if acmeIssuer.Challenges.BindHost == "" {
// only binding to one host is supported
var bindHost string
- if bindHosts, ok := cfgVal.Value.([]string); ok && len(bindHosts) > 0 {
- bindHost = bindHosts[0]
+ if asserted, ok := cfgVal.Value.(addressesWithProtocols); ok && len(asserted.addresses) > 0 {
+ bindHost = asserted.addresses[0]
}
acmeIssuer.Challenges.BindHost = bindHost
}
@@ -210,9 +250,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
@@ -224,7 +276,7 @@ func (st ServerType) buildTLSApp(
var internal, external []string
for _, s := range ap.SubjectsRaw {
// do not create Issuers for Tailscale domains; they will be given a Manager instead
- if strings.HasSuffix(strings.ToLower(s), ".ts.net") {
+ if isTailscaleDomain(s) {
continue
}
if !certmagic.SubjectQualifiesForCert(s) {
@@ -251,6 +303,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)
}
@@ -285,7 +338,7 @@ func (st ServerType) buildTLSApp(
combined = reflect.New(reflect.TypeOf(cl)).Elem()
}
clVal := reflect.ValueOf(cl)
- for i := 0; i < clVal.Len(); i++ {
+ for i := range clVal.Len() {
combined = reflect.Append(combined, clVal.Index(i))
}
loadersByName[name] = combined.Interface().(caddytls.CertificateLoader)
@@ -304,6 +357,42 @@ func (st ServerType) buildTLSApp(
tlsApp.Automation.OnDemand = onDemand
}
+ // set up "global" (to the TLS app) DNS provider config
+ if globalDNS, ok := options["dns"]; ok && globalDNS != nil {
+ tlsApp.DNSRaw = caddyconfig.JSONModuleObject(globalDNS, "name", globalDNS.(caddy.Module).CaddyModule().ID.Name(), nil)
+ }
+
+ // set up ECH from Caddyfile options
+ if ech, ok := options["ech"].(*caddytls.ECH); ok {
+ tlsApp.EncryptedClientHello = ech
+
+ // outer server names will need certificates, so make sure they're included
+ // in an automation policy for them that applies any global options
+ ap, err := newBaseAutomationPolicy(options, warnings, true)
+ if err != nil {
+ return nil, warnings, err
+ }
+ for _, cfg := range ech.Configs {
+ if cfg.PublicName != "" {
+ ap.SubjectsRaw = append(ap.SubjectsRaw, cfg.PublicName)
+ }
+ }
+ if tlsApp.Automation == nil {
+ tlsApp.Automation = new(caddytls.AutomationConfig)
+ }
+ tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap)
+ }
+
+ // 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 {
@@ -344,7 +433,7 @@ func (st ServerType) buildTLSApp(
internalAP := &caddytls.AutomationPolicy{
IssuersRaw: []json.RawMessage{json.RawMessage(`{"module":"internal"}`)},
}
- if autoHTTPS != "off" {
+ if !slices.Contains(autoHTTPS, "off") && !slices.Contains(autoHTTPS, "disable_certs") {
for h := range httpsHostsSharedWithHostlessKey {
al = append(al, h)
if !certmagic.SubjectQualifiesForPublicCert(h) {
@@ -352,6 +441,13 @@ func (st ServerType) buildTLSApp(
}
}
}
+ for name := range forcedAutomatedNames {
+ if slices.Contains(al, name) {
+ continue
+ }
+ al = append(al, name)
+ }
+ slices.Sort(al) // to stabilize the adapt output
if len(al) > 0 {
tlsApp.CertificatesRaw["automate"] = caddyconfig.JSON(al, &warnings)
}
@@ -373,20 +469,17 @@ func (st ServerType) buildTLSApp(
globalPreferredChains := options["preferred_chains"]
hasGlobalACMEDefaults := globalEmail != nil || globalACMECA != nil || globalACMECARoot != nil || globalACMEDNS != nil || globalACMEEAB != nil || globalPreferredChains != nil
if hasGlobalACMEDefaults {
- for i := 0; i < len(tlsApp.Automation.Policies); i++ {
+ for i := range tlsApp.Automation.Policies {
ap := tlsApp.Automation.Policies[i]
if len(ap.Issuers) == 0 && automationPolicyHasAllPublicNames(ap) {
// for public names, create default issuers which will later be filled in with configured global defaults
// (internal names will implicitly use the internal issuer at auto-https time)
- ap.Issuers = caddytls.DefaultIssuers()
+ emailStr, _ := globalEmail.(string)
+ ap.Issuers = caddytls.DefaultIssuers(emailStr)
// if a specific endpoint is configured, can't use multiple default issuers
if globalACMECA != nil {
- if strings.Contains(globalACMECA.(string), "zerossl") {
- ap.Issuers = []certmagic.Issuer{&caddytls.ZeroSSLIssuer{ACMEIssuer: new(caddytls.ACMEIssuer)}}
- } else {
- ap.Issuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)}
- }
+ ap.Issuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)}
}
}
}
@@ -459,6 +552,8 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
globalACMEDNS := options["acme_dns"]
globalACMEEAB := options["acme_eab"]
globalPreferredChains := options["preferred_chains"]
+ globalCertLifetime := options["cert_lifetime"]
+ globalHTTPPort, globalHTTPSPort := options["http_port"], options["https_port"]
if globalEmail != nil && acmeIssuer.Email == "" {
acmeIssuer.Email = globalEmail.(string)
@@ -466,7 +561,7 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
if globalACMECA != nil && acmeIssuer.CA == "" {
acmeIssuer.CA = globalACMECA.(string)
}
- if globalACMECARoot != nil && !sliceContains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) {
+ if globalACMECARoot != nil && !slices.Contains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) {
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string))
}
if globalACMEDNS != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) {
@@ -482,6 +577,28 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
if globalPreferredChains != nil && acmeIssuer.PreferredChains == nil {
acmeIssuer.PreferredChains = globalPreferredChains.(*caddytls.ChainPreference)
}
+ // only configure alt HTTP and TLS-ALPN ports if the DNS challenge is not enabled (wouldn't hurt, but isn't necessary since the DNS challenge is exclusive of others)
+ if globalHTTPPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.HTTP == nil || acmeIssuer.Challenges.HTTP.AlternatePort == 0) {
+ if acmeIssuer.Challenges == nil {
+ acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
+ }
+ if acmeIssuer.Challenges.HTTP == nil {
+ acmeIssuer.Challenges.HTTP = new(caddytls.HTTPChallengeConfig)
+ }
+ acmeIssuer.Challenges.HTTP.AlternatePort = globalHTTPPort.(int)
+ }
+ if globalHTTPSPort != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.TLSALPN == nil || acmeIssuer.Challenges.TLSALPN.AlternatePort == 0) {
+ if acmeIssuer.Challenges == nil {
+ acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
+ }
+ if acmeIssuer.Challenges.TLSALPN == nil {
+ acmeIssuer.Challenges.TLSALPN = new(caddytls.TLSALPNChallengeConfig)
+ }
+ acmeIssuer.Challenges.TLSALPN.AlternatePort = globalHTTPSPort.(int)
+ }
+ if globalCertLifetime != nil && acmeIssuer.CertificateLifetime == 0 {
+ acmeIssuer.CertificateLifetime = globalCertLifetime.(caddy.Duration)
+ }
return nil
}
@@ -490,7 +607,11 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
// for any other automation policies. A nil policy (and no error) will be
// returned if there are no default/global options. However, if always is
// true, a non-nil value will always be returned (unless there is an error).
-func newBaseAutomationPolicy(options map[string]any, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, error) {
+func newBaseAutomationPolicy(
+ options map[string]any,
+ _ []caddyconfig.Warning,
+ always bool,
+) (*caddytls.AutomationPolicy, error) {
issuers, hasIssuers := options["cert_issuer"]
_, hasLocalCerts := options["local_certs"]
keyType, hasKeyType := options["key_type"]
@@ -556,7 +677,7 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
if !automationPolicyHasAllPublicNames(aps[i]) {
// if this automation policy has internal names, we might as well remove it
// so auto-https can implicitly use the internal issuer
- aps = append(aps[:i], aps[i+1:]...)
+ aps = slices.Delete(aps, i, i+1)
i--
}
}
@@ -573,7 +694,7 @@ outer:
for j := i + 1; j < len(aps); j++ {
// if they're exactly equal in every way, just keep one of them
if reflect.DeepEqual(aps[i], aps[j]) {
- aps = append(aps[:j], aps[j+1:]...)
+ aps = slices.Delete(aps, j, j+1)
// must re-evaluate current i against next j; can't skip it!
// even if i decrements to -1, will be incremented to 0 immediately
i--
@@ -603,18 +724,18 @@ outer:
// cause example.com to be served by the less specific policy for
// '*.com', which might be different (yes we've seen this happen)
if automationPolicyShadows(i, aps) >= j {
- aps = append(aps[:i], aps[i+1:]...)
+ aps = slices.Delete(aps, i, i+1)
i--
continue outer
}
} else {
// avoid repeated subjects
for _, subj := range aps[j].SubjectsRaw {
- if !sliceContains(aps[i].SubjectsRaw, subj) {
+ if !slices.Contains(aps[i].SubjectsRaw, subj) {
aps[i].SubjectsRaw = append(aps[i].SubjectsRaw, subj)
}
}
- aps = append(aps[:j], aps[j+1:]...)
+ aps = slices.Delete(aps, j, j+1)
j--
}
}
@@ -634,13 +755,9 @@ func automationPolicyIsSubset(a, b *caddytls.AutomationPolicy) bool {
return false
}
for _, aSubj := range a.SubjectsRaw {
- var inSuperset bool
- for _, bSubj := range b.SubjectsRaw {
- if certmagic.MatchWildcard(aSubj, bSubj) {
- inSuperset = true
- break
- }
- }
+ inSuperset := slices.ContainsFunc(b.SubjectsRaw, func(bSubj string) bool {
+ return certmagic.MatchWildcard(aSubj, bSubj)
+ })
if !inSuperset {
return false
}
@@ -666,17 +783,47 @@ func automationPolicyShadows(i int, aps []*caddytls.AutomationPolicy) int {
// subjectQualifiesForPublicCert is like certmagic.SubjectQualifiesForPublicCert() except
// that this allows domains with multiple wildcard levels like '*.*.example.com' to qualify
// if the automation policy has OnDemand enabled (i.e. this function is more lenient).
+//
+// IP subjects are considered as non-qualifying for public certs. Technically, there are
+// now public ACME CAs as well as non-ACME CAs that issue IP certificates. But this function
+// is used solely for implicit automation (defaults), where it gets really complicated to
+// keep track of which issuers support IP certificates in which circumstances. Currently,
+// issuers that support IP certificates are very few, and all require some sort of config
+// from the user anyway (such as an account credential). Since we cannot implicitly and
+// automatically get public IP certs without configuration from the user, we treat IPs as
+// not qualifying for public certificates. Users should expressly configure an issuer
+// that supports IP certs for that purpose.
func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) bool {
return !certmagic.SubjectIsIP(subj) &&
!certmagic.SubjectIsInternal(subj) &&
(strings.Count(subj, "*.") < 2 || ap.OnDemand)
}
+// automationPolicyHasAllPublicNames returns true if all the names on the policy
+// do NOT qualify for public certs OR are tailscale domains.
func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
- for _, subj := range ap.SubjectsRaw {
- if !subjectQualifiesForPublicCert(ap, subj) {
- return false
+ return !slices.ContainsFunc(ap.SubjectsRaw, func(i string) bool {
+ return !subjectQualifiesForPublicCert(ap, i) || isTailscaleDomain(i)
+ })
+}
+
+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 true
+ return false
}
diff --git a/caddyconfig/httploader.go b/caddyconfig/httploader.go
index e0ce4ebf7..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 {
@@ -181,19 +181,16 @@ func (hl HTTPLoader) makeClient(ctx caddy.Context) (*http.Client, error) {
if err != nil {
return nil, fmt.Errorf("getting server identity credentials: %v", err)
}
- if tlsConfig == nil {
- tlsConfig = new(tls.Config)
- }
- tlsConfig.Certificates = certs
+ // See https://github.com/securego/gosec/issues/1054#issuecomment-2072235199
+ //nolint:gosec
+ tlsConfig = &tls.Config{Certificates: certs}
} else if hl.TLS.ClientCertificateFile != "" && hl.TLS.ClientCertificateKeyFile != "" {
cert, err := tls.LoadX509KeyPair(hl.TLS.ClientCertificateFile, hl.TLS.ClientCertificateKeyFile)
if err != nil {
return nil, err
}
- if tlsConfig == nil {
- tlsConfig = new(tls.Config)
- }
- tlsConfig.Certificates = []tls.Certificate{cert}
+ //nolint:gosec
+ tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
}
// trusted server certs
diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go
index deb567b3b..623c45e5e 100644
--- a/caddytest/caddytest.go
+++ b/caddytest/caddytest.go
@@ -31,12 +31,12 @@ import (
_ "github.com/caddyserver/caddy/v2/modules/standard"
)
-// Defaults store any configuration required to make the tests run
-type Defaults struct {
+// Config store any configuration required to make the tests run
+type Config struct {
// Port we expect caddy to listening on
AdminPort int
// Certificates we expect to be loaded before attempting to run the tests
- Certifcates []string
+ Certificates []string
// TestRequestTimeout is the time to wait for a http request to
TestRequestTimeout time.Duration
// LoadRequestTimeout is the time to wait for the config to be loaded against the caddy server
@@ -44,9 +44,9 @@ type Defaults struct {
}
// Default testing values
-var Default = Defaults{
+var Default = Config{
AdminPort: 2999, // different from what a real server also running on a developer's machine might be
- Certifcates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"},
+ Certificates: []string{"/caddy.localhost.crt", "/caddy.localhost.key"},
TestRequestTimeout: 5 * time.Second,
LoadRequestTimeout: 5 * time.Second,
}
@@ -61,6 +61,7 @@ type Tester struct {
Client *http.Client
configLoaded bool
t testing.TB
+ config Config
}
// NewTester will create a new testing client with an attached cookie jar
@@ -78,9 +79,29 @@ func NewTester(t testing.TB) *Tester {
},
configLoaded: false,
t: t,
+ config: Default,
}
}
+// WithDefaultOverrides this will override the default test configuration with the provided values.
+func (tc *Tester) WithDefaultOverrides(overrides Config) *Tester {
+ if overrides.AdminPort != 0 {
+ tc.config.AdminPort = overrides.AdminPort
+ }
+ if len(overrides.Certificates) > 0 {
+ tc.config.Certificates = overrides.Certificates
+ }
+ if overrides.TestRequestTimeout != 0 {
+ tc.config.TestRequestTimeout = overrides.TestRequestTimeout
+ tc.Client.Timeout = overrides.TestRequestTimeout
+ }
+ if overrides.LoadRequestTimeout != 0 {
+ tc.config.LoadRequestTimeout = overrides.LoadRequestTimeout
+ }
+
+ return tc
+}
+
type configLoadError struct {
Response string
}
@@ -113,7 +134,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
return nil
}
- err := validateTestPrerequisites(tc.t)
+ err := validateTestPrerequisites(tc)
if err != nil {
tc.t.Skipf("skipping tests as failed integration prerequisites. %s", err)
return nil
@@ -121,7 +142,7 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
tc.t.Cleanup(func() {
if tc.t.Failed() && tc.configLoaded {
- res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
+ res, err := http.Get(fmt.Sprintf("http://localhost:%d/config/", tc.config.AdminPort))
if err != nil {
tc.t.Log("unable to read the current config")
return
@@ -136,11 +157,25 @@ func (tc *Tester) initServer(rawConfig string, configType string) error {
})
rawConfig = prependCaddyFilePath(rawConfig)
+ // normalize JSON config
+ if configType == "json" {
+ tc.t.Logf("Before: %s", rawConfig)
+ var conf any
+ if err := json.Unmarshal([]byte(rawConfig), &conf); err != nil {
+ return err
+ }
+ c, err := json.Marshal(conf)
+ if err != nil {
+ return err
+ }
+ rawConfig = string(c)
+ tc.t.Logf("After: %s", rawConfig)
+ }
client := &http.Client{
- Timeout: Default.LoadRequestTimeout,
+ Timeout: tc.config.LoadRequestTimeout,
}
start := time.Now()
- req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", Default.AdminPort), strings.NewReader(rawConfig))
+ req, err := http.NewRequest("POST", fmt.Sprintf("http://localhost:%d/load", tc.config.AdminPort), strings.NewReader(rawConfig))
if err != nil {
tc.t.Errorf("failed to create request. %s", err)
return err
@@ -191,11 +226,11 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
}
client := &http.Client{
- Timeout: Default.LoadRequestTimeout,
+ Timeout: tc.config.LoadRequestTimeout,
}
fetchConfig := func(client *http.Client) any {
- resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
+ resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.config.AdminPort))
if err != nil {
return nil
}
@@ -223,30 +258,30 @@ func (tc *Tester) ensureConfigRunning(rawConfig string, configType string) error
}
const initConfig = `{
- admin localhost:2999
+ admin localhost:%d
}
`
// validateTestPrerequisites ensures the certificates are available in the
// designated path and Caddy sub-process is running.
-func validateTestPrerequisites(t testing.TB) error {
+func validateTestPrerequisites(tc *Tester) error {
// check certificates are found
- for _, certName := range Default.Certifcates {
+ for _, certName := range tc.config.Certificates {
if _, err := os.Stat(getIntegrationDir() + certName); errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("caddy integration test certificates (%s) not found", certName)
}
}
- if isCaddyAdminRunning() != nil {
+ if isCaddyAdminRunning(tc) != nil {
// setup the init config file, and set the cleanup afterwards
f, err := os.CreateTemp("", "")
if err != nil {
return err
}
- t.Cleanup(func() {
+ tc.t.Cleanup(func() {
os.Remove(f.Name())
})
- if _, err := f.WriteString(initConfig); err != nil {
+ if _, err := f.WriteString(fmt.Sprintf(initConfig, tc.config.AdminPort)); err != nil {
return err
}
@@ -257,23 +292,23 @@ func validateTestPrerequisites(t testing.TB) error {
}()
// wait for caddy to start serving the initial config
- for retries := 10; retries > 0 && isCaddyAdminRunning() != nil; retries-- {
+ for retries := 10; retries > 0 && isCaddyAdminRunning(tc) != nil; retries-- {
time.Sleep(1 * time.Second)
}
}
// one more time to return the error
- return isCaddyAdminRunning()
+ return isCaddyAdminRunning(tc)
}
-func isCaddyAdminRunning() error {
+func isCaddyAdminRunning(tc *Tester) error {
// assert that caddy is running
client := &http.Client{
- Timeout: Default.LoadRequestTimeout,
+ Timeout: tc.config.LoadRequestTimeout,
}
- resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", Default.AdminPort))
+ resp, err := client.Get(fmt.Sprintf("http://localhost:%d/config/", tc.config.AdminPort))
if err != nil {
- return fmt.Errorf("caddy integration test caddy server not running. Expected to be listening on localhost:%d", Default.AdminPort)
+ return fmt.Errorf("caddy integration test caddy server not running. Expected to be listening on localhost:%d", tc.config.AdminPort)
}
resp.Body.Close()
diff --git a/caddytest/caddytest_test.go b/caddytest/caddytest_test.go
index a46867ca8..a9d5da936 100644
--- a/caddytest/caddytest_test.go
+++ b/caddytest/caddytest_test.go
@@ -1,6 +1,7 @@
package caddytest
import (
+ "net/http"
"strings"
"testing"
)
@@ -31,3 +32,97 @@ func TestReplaceCertificatePaths(t *testing.T) {
t.Error("expected redirect uri to be unchanged")
}
}
+
+func TestLoadUnorderedJSON(t *testing.T) {
+ tester := NewTester(t)
+ tester.InitServer(`
+ {
+ "logging": {
+ "logs": {
+ "default": {
+ "level": "DEBUG",
+ "writer": {
+ "output": "stdout"
+ }
+ },
+ "sStdOutLogs": {
+ "level": "DEBUG",
+ "writer": {
+ "output": "stdout"
+ },
+ "include": [
+ "http.*",
+ "admin.*"
+ ]
+ },
+ "sFileLogs": {
+ "level": "DEBUG",
+ "writer": {
+ "output": "stdout"
+ },
+ "include": [
+ "http.*",
+ "admin.*"
+ ]
+ }
+ }
+ },
+ "admin": {
+ "listen": "localhost:2999"
+ },
+ "apps": {
+ "pki": {
+ "certificate_authorities" : {
+ "local" : {
+ "install_trust": false
+ }
+ }
+ },
+ "http": {
+ "http_port": 9080,
+ "https_port": 9443,
+ "servers": {
+ "s_server": {
+ "listen": [
+ ":9080"
+ ],
+ "routes": [
+ {
+ "handle": [
+ {
+ "handler": "static_response",
+ "body": "Hello"
+ }
+ ]
+ },
+ {
+ "match": [
+ {
+ "host": [
+ "localhost",
+ "127.0.0.1"
+ ]
+ }
+ ]
+ }
+ ],
+ "logs": {
+ "default_logger_name": "sStdOutLogs",
+ "logger_names": {
+ "localhost": "sStdOutLogs",
+ "127.0.0.1": "sFileLogs"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ `, "json")
+ req, err := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
+ if err != nil {
+ t.Fail()
+ return
+ }
+ tester.AssertResponseCode(req, 200)
+}
diff --git a/caddytest/integration/acme_test.go b/caddytest/integration/acme_test.go
index 840af023f..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"
- "github.com/mholt/acmez/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},
@@ -77,7 +79,7 @@ func TestACMEServerWithDefaults(t *testing.T) {
return
}
- certs, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"localhost"})
+ certs, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"localhost"})
if err != nil {
t.Errorf("obtaining certificate: %v", err)
return
@@ -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},
@@ -146,7 +148,7 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) {
return
}
- certs, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"localhost"})
+ certs, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"localhost"})
if len(certs) > 0 {
t.Errorf("expected '0' certificates, but received '%d'", len(certs))
}
diff --git a/caddytest/integration/acmeserver_test.go b/caddytest/integration/acmeserver_test.go
index 435bfc7b4..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"
- "github.com/mholt/acmez/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},
@@ -105,12 +107,7 @@ func TestACMEServerAllowPolicy(t *testing.T) {
return
}
{
- certs, err := client.ObtainCertificate(
- ctx,
- account,
- certPrivateKey,
- []string{"localhost"},
- )
+ certs, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"localhost"})
if err != nil {
t.Errorf("obtaining certificate for allowed domain: %v", err)
return
@@ -126,7 +123,7 @@ func TestACMEServerAllowPolicy(t *testing.T) {
}
}
{
- _, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"not-matching.localhost"})
+ _, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"not-matching.localhost"})
if err == nil {
t.Errorf("obtaining certificate for 'not-matching.localhost' domain")
} else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
@@ -170,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},
@@ -199,7 +196,7 @@ func TestACMEServerDenyPolicy(t *testing.T) {
return
}
{
- _, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"deny.localhost"})
+ _, err := client.ObtainCertificateForSANs(ctx, account, certPrivateKey, []string{"deny.localhost"})
if err == nil {
t.Errorf("obtaining certificate for 'deny.localhost' domain")
} else if err != nil && !strings.Contains(err.Error(), "urn:ietf:params:acme:error:rejectedIdentifier") {
diff --git a/caddytest/integration/caddyfile_adapt/acme_server_policy-allow.caddyfiletest b/caddytest/integration/caddyfile_adapt/acme_server_policy-allow.caddyfiletest
new file mode 100644
index 000000000..5d1d8a3bc
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/acme_server_policy-allow.caddyfiletest
@@ -0,0 +1,72 @@
+{
+ pki {
+ ca custom-ca {
+ name "Custom CA"
+ }
+ }
+}
+
+acme.example.com {
+ acme_server {
+ ca custom-ca
+ allow {
+ domains host-1.internal.example.com host-2.internal.example.com
+ }
+ }
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":443"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "acme.example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "ca": "custom-ca",
+ "handler": "acme_server",
+ "policy": {
+ "allow": {
+ "domains": [
+ "host-1.internal.example.com",
+ "host-2.internal.example.com"
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ]
+ }
+ }
+ },
+ "pki": {
+ "certificate_authorities": {
+ "custom-ca": {
+ "name": "Custom CA"
+ }
+ }
+ }
+ }
+}
diff --git a/caddytest/integration/caddyfile_adapt/acme_server_policy-both.caddyfiletest b/caddytest/integration/caddyfile_adapt/acme_server_policy-both.caddyfiletest
new file mode 100644
index 000000000..15cdbba90
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/acme_server_policy-both.caddyfiletest
@@ -0,0 +1,80 @@
+{
+ pki {
+ ca custom-ca {
+ name "Custom CA"
+ }
+ }
+}
+
+acme.example.com {
+ acme_server {
+ ca custom-ca
+ allow {
+ domains host-1.internal.example.com host-2.internal.example.com
+ }
+ deny {
+ domains dc.internal.example.com
+ }
+ }
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":443"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "acme.example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "ca": "custom-ca",
+ "handler": "acme_server",
+ "policy": {
+ "allow": {
+ "domains": [
+ "host-1.internal.example.com",
+ "host-2.internal.example.com"
+ ]
+ },
+ "deny": {
+ "domains": [
+ "dc.internal.example.com"
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ]
+ }
+ }
+ },
+ "pki": {
+ "certificate_authorities": {
+ "custom-ca": {
+ "name": "Custom CA"
+ }
+ }
+ }
+ }
+}
diff --git a/caddytest/integration/caddyfile_adapt/acme_server_policy-deny.caddyfiletest b/caddytest/integration/caddyfile_adapt/acme_server_policy-deny.caddyfiletest
new file mode 100644
index 000000000..0478088c9
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/acme_server_policy-deny.caddyfiletest
@@ -0,0 +1,71 @@
+{
+ pki {
+ ca custom-ca {
+ name "Custom CA"
+ }
+ }
+}
+
+acme.example.com {
+ acme_server {
+ ca custom-ca
+ deny {
+ domains dc.internal.example.com
+ }
+ }
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":443"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "acme.example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "ca": "custom-ca",
+ "handler": "acme_server",
+ "policy": {
+ "deny": {
+ "domains": [
+ "dc.internal.example.com"
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ]
+ }
+ }
+ },
+ "pki": {
+ "certificate_authorities": {
+ "custom-ca": {
+ "name": "Custom CA"
+ }
+ }
+ }
+ }
+}
diff --git a/caddytest/integration/caddyfile_adapt/acme_server_sign_with_root.caddyfiletest b/caddytest/integration/caddyfile_adapt/acme_server_sign_with_root.caddyfiletest
new file mode 100644
index 000000000..5b5040107
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/acme_server_sign_with_root.caddyfiletest
@@ -0,0 +1,67 @@
+{
+ pki {
+ ca internal {
+ name "Internal"
+ root_cn "Internal Root Cert"
+ intermediate_cn "Internal Intermediate Cert"
+ }
+ }
+}
+acme.example.com {
+ acme_server {
+ ca internal
+ sign_with_root
+ }
+}
+
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":443"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "acme.example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "ca": "internal",
+ "handler": "acme_server",
+ "sign_with_root": true
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ]
+ }
+ }
+ },
+ "pki": {
+ "certificate_authorities": {
+ "internal": {
+ "name": "Internal",
+ "root_common_name": "Internal Root Cert",
+ "intermediate_common_name": "Internal Intermediate Cert"
+ }
+ }
+ }
+ }
+}
diff --git a/caddytest/integration/caddyfile_adapt/bind_fd_fdgram_h123.caddyfiletest b/caddytest/integration/caddyfile_adapt/bind_fd_fdgram_h123.caddyfiletest
new file mode 100644
index 000000000..08f30d18a
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/bind_fd_fdgram_h123.caddyfiletest
@@ -0,0 +1,142 @@
+{
+ auto_https disable_redirects
+ admin off
+}
+
+http://localhost {
+ bind fd/{env.CADDY_HTTP_FD} {
+ protocols h1
+ }
+ log
+ respond "Hello, HTTP!"
+}
+
+https://localhost {
+ bind fd/{env.CADDY_HTTPS_FD} {
+ protocols h1 h2
+ }
+ bind fdgram/{env.CADDY_HTTP3_FD} {
+ protocols h3
+ }
+ log
+ respond "Hello, HTTPS!"
+}
+----------
+{
+ "admin": {
+ "disabled": true
+ },
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ "fd/{env.CADDY_HTTPS_FD}",
+ "fdgram/{env.CADDY_HTTP3_FD}"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "localhost"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Hello, HTTPS!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ],
+ "automatic_https": {
+ "disable_redirects": true
+ },
+ "logs": {
+ "logger_names": {
+ "localhost": [
+ ""
+ ]
+ }
+ },
+ "listen_protocols": [
+ [
+ "h1",
+ "h2"
+ ],
+ [
+ "h3"
+ ]
+ ]
+ },
+ "srv1": {
+ "automatic_https": {
+ "disable_redirects": true
+ }
+ },
+ "srv2": {
+ "listen": [
+ "fd/{env.CADDY_HTTP_FD}"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "localhost"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Hello, HTTP!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ],
+ "automatic_https": {
+ "disable_redirects": true,
+ "skip": [
+ "localhost"
+ ]
+ },
+ "logs": {
+ "logger_names": {
+ "localhost": [
+ ""
+ ]
+ }
+ },
+ "listen_protocols": [
+ [
+ "h1"
+ ]
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/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/expression_quotes.caddyfiletest b/caddytest/integration/caddyfile_adapt/expression_quotes.caddyfiletest
index f5f8983eb..4bc47a3dc 100644
--- a/caddytest/integration/caddyfile_adapt/expression_quotes.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/expression_quotes.caddyfiletest
@@ -1,3 +1,7 @@
+(snippet) {
+ @g `{http.error.status_code} == 404`
+}
+
example.com
@a expression {http.error.status_code} == 400
@@ -14,6 +18,12 @@ abort @d
@e expression `{http.error.status_code} == 404`
abort @e
+
+@f `{http.error.status_code} == 404`
+abort @f
+
+import snippet
+abort @g
----------
{
"apps": {
@@ -84,7 +94,10 @@ abort @e
],
"match": [
{
- "expression": "{http.error.status_code} == 403"
+ "expression": {
+ "expr": "{http.error.status_code} == 403",
+ "name": "d"
+ }
}
]
},
@@ -97,7 +110,42 @@ abort @e
],
"match": [
{
- "expression": "{http.error.status_code} == 404"
+ "expression": {
+ "expr": "{http.error.status_code} == 404",
+ "name": "e"
+ }
+ }
+ ]
+ },
+ {
+ "handle": [
+ {
+ "abort": true,
+ "handler": "static_response"
+ }
+ ],
+ "match": [
+ {
+ "expression": {
+ "expr": "{http.error.status_code} == 404",
+ "name": "f"
+ }
+ }
+ ]
+ },
+ {
+ "handle": [
+ {
+ "abort": true,
+ "handler": "static_response"
+ }
+ ],
+ "match": [
+ {
+ "expression": {
+ "expr": "{http.error.status_code} == 404",
+ "name": "g"
+ }
}
]
}
diff --git a/caddytest/integration/caddyfile_adapt/file_server_etag_file_extensions.caddyfiletest b/caddytest/integration/caddyfile_adapt/file_server_etag_file_extensions.caddyfiletest
new file mode 100644
index 000000000..d0dc79216
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/file_server_etag_file_extensions.caddyfiletest
@@ -0,0 +1,40 @@
+:8080 {
+ root * ./
+ file_server {
+ etag_file_extensions .b3sum .sha256
+ }
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":8080"
+ ],
+ "routes": [
+ {
+ "handle": [
+ {
+ "handler": "vars",
+ "root": "./"
+ },
+ {
+ "etag_file_extensions": [
+ ".b3sum",
+ ".sha256"
+ ],
+ "handler": "file_server",
+ "hide": [
+ "./Caddyfile"
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
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/file_server_sort.caddyfiletest b/caddytest/integration/caddyfile_adapt/file_server_sort.caddyfiletest
new file mode 100644
index 000000000..7f07cba80
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/file_server_sort.caddyfiletest
@@ -0,0 +1,39 @@
+:80
+
+file_server {
+ browse {
+ sort size desc
+ }
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":80"
+ ],
+ "routes": [
+ {
+ "handle": [
+ {
+ "browse": {
+ "sort": [
+ "size",
+ "desc"
+ ]
+ },
+ "handler": "file_server",
+ "hide": [
+ "./Caddyfile"
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
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
}
@@ -39,6 +39,13 @@ app.example.com {
]
},
"routes": [
+ {
+ "handle": [
+ {
+ "handler": "vars"
+ }
+ ]
+ },
{
"handle": [
{
@@ -47,19 +54,104 @@ app.example.com {
"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
@@ -28,6 +28,13 @@ forward_auth localhost:9000 {
]
},
"routes": [
+ {
+ "handle": [
+ {
+ "handler": "vars"
+ }
+ ]
+ },
{
"handle": [
{
@@ -36,22 +43,131 @@ forward_auth localhost:9000 {
"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 88729c512..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
@@ -17,8 +19,6 @@
admin off
on_demand_tls {
ask https://example.com
- interval 30s
- burst 20
}
local_certs
key_type ed25519
@@ -72,14 +72,12 @@
"permission": {
"endpoint": "https://example.com",
"module": "http"
- },
- "rate_limit": {
- "interval": 30000000000,
- "burst": 20
}
}
},
- "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_acme.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options_acme.caddyfiletest
index f51779253..004a3a32e 100644
--- a/caddytest/integration/caddyfile_adapt/global_options_acme.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/global_options_acme.caddyfiletest
@@ -17,8 +17,6 @@
admin off
on_demand_tls {
ask https://example.com
- interval 30s
- burst 20
}
storage_clean_interval 7d
renew_interval 1d
@@ -63,6 +61,14 @@
"issuers": [
{
"ca": "https://example.com",
+ "challenges": {
+ "http": {
+ "alternate_port": 8080
+ },
+ "tls-alpn": {
+ "alternate_port": 8443
+ }
+ },
"email": "test@example.com",
"external_account": {
"key_id": "4K2scIVbBpNd-78scadB2g",
@@ -81,10 +87,6 @@
"permission": {
"endpoint": "https://example.com",
"module": "http"
- },
- "rate_limit": {
- "interval": 30000000000,
- "burst": 20
}
},
"ocsp_interval": 172800000000000,
diff --git a/caddytest/integration/caddyfile_adapt/global_options_admin.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options_admin.caddyfiletest
index cfc578826..be309eaa3 100644
--- a/caddytest/integration/caddyfile_adapt/global_options_admin.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/global_options_admin.caddyfiletest
@@ -16,8 +16,6 @@
}
on_demand_tls {
ask https://example.com
- interval 30s
- burst 20
}
local_certs
key_type ed25519
@@ -74,10 +72,6 @@
"permission": {
"endpoint": "https://example.com",
"module": "http"
- },
- "rate_limit": {
- "interval": 30000000000,
- "burst": 20
}
}
}
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/global_options_preferred_chains.caddyfiletest b/caddytest/integration/caddyfile_adapt/global_options_preferred_chains.caddyfiletest
index 9173b26bf..1f5d0093e 100644
--- a/caddytest/integration/caddyfile_adapt/global_options_preferred_chains.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/global_options_preferred_chains.caddyfiletest
@@ -40,12 +40,6 @@ example.com
"preferred_chains": {
"smallest": true
}
- },
- {
- "module": "zerossl",
- "preferred_chains": {
- "smallest": true
- }
}
]
}
diff --git a/caddytest/integration/caddyfile_adapt/header.caddyfiletest b/caddytest/integration/caddyfile_adapt/header.caddyfiletest
index ec2a842a3..a0af24ff6 100644
--- a/caddytest/integration/caddyfile_adapt/header.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/header.caddyfiletest
@@ -12,10 +12,14 @@
@images path /images/*
header @images {
Cache-Control "public, max-age=3600, stale-while-revalidate=86400"
+ match {
+ status 200
+ }
}
header {
+Link "Foo"
+Link "Bar"
+ match status 200
}
header >Set Defer
header >Replace Deferred Replacement
@@ -42,6 +46,11 @@
{
"handler": "headers",
"response": {
+ "require": {
+ "status_code": [
+ 200
+ ]
+ },
"set": {
"Cache-Control": [
"public, max-age=3600, stale-while-revalidate=86400"
@@ -136,6 +145,11 @@
"Foo",
"Bar"
]
+ },
+ "require": {
+ "status_code": [
+ 200
+ ]
}
}
},
diff --git a/caddytest/integration/caddyfile_adapt/heredoc.caddyfiletest b/caddytest/integration/caddyfile_adapt/heredoc.caddyfiletest
index cc1174d6d..f50d2b7f9 100644
--- a/caddytest/integration/caddyfile_adapt/heredoc.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/heredoc.caddyfiletest
@@ -1,11 +1,12 @@
example.com {
- respond <
Foo
Foo
EOF 200
}
+
----------
{
"apps": {
diff --git a/caddytest/integration/caddyfile_adapt/import_args_snippet.caddyfiletest b/caddytest/integration/caddyfile_adapt/import_args_snippet.caddyfiletest
index 10d9f4cfc..9d5c25352 100644
--- a/caddytest/integration/caddyfile_adapt/import_args_snippet.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/import_args_snippet.caddyfiletest
@@ -72,8 +72,12 @@ b.example.com {
],
"logs": {
"logger_names": {
- "a.example.com": "log0",
- "b.example.com": "log1"
+ "a.example.com": [
+ "log0"
+ ],
+ "b.example.com": [
+ "log1"
+ ]
}
}
}
diff --git a/caddytest/integration/caddyfile_adapt/import_block_snippet.caddyfiletest b/caddytest/integration/caddyfile_adapt/import_block_snippet.caddyfiletest
new file mode 100644
index 000000000..a60c238ce
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/import_block_snippet.caddyfiletest
@@ -0,0 +1,58 @@
+(snippet) {
+ header {
+ {block}
+ }
+}
+
+example.com {
+ import snippet {
+ foo bar
+ }
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":443"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "handler": "headers",
+ "response": {
+ "set": {
+ "Foo": [
+ "bar"
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/caddytest/integration/caddyfile_adapt/import_block_snippet_args.caddyfiletest b/caddytest/integration/caddyfile_adapt/import_block_snippet_args.caddyfiletest
new file mode 100644
index 000000000..7f2e68b79
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/import_block_snippet_args.caddyfiletest
@@ -0,0 +1,56 @@
+(snippet) {
+ {block}
+}
+
+example.com {
+ import snippet {
+ header foo bar
+ }
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":443"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "handler": "headers",
+ "response": {
+ "set": {
+ "Foo": [
+ "bar"
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/caddytest/integration/caddyfile_adapt/import_blocks_snippet.caddyfiletest b/caddytest/integration/caddyfile_adapt/import_blocks_snippet.caddyfiletest
new file mode 100644
index 000000000..4098f90b2
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/import_blocks_snippet.caddyfiletest
@@ -0,0 +1,76 @@
+(snippet) {
+ header {
+ {blocks.foo}
+ }
+ header {
+ {blocks.bar}
+ }
+}
+
+example.com {
+ import snippet {
+ foo {
+ foo a
+ }
+ bar {
+ bar b
+ }
+ }
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":443"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "handler": "headers",
+ "response": {
+ "set": {
+ "Foo": [
+ "a"
+ ]
+ }
+ }
+ },
+ {
+ "handler": "headers",
+ "response": {
+ "set": {
+ "Bar": [
+ "b"
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/caddytest/integration/caddyfile_adapt/import_blocks_snippet_nested.caddyfiletest b/caddytest/integration/caddyfile_adapt/import_blocks_snippet_nested.caddyfiletest
new file mode 100644
index 000000000..ac1c5226c
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/import_blocks_snippet_nested.caddyfiletest
@@ -0,0 +1,82 @@
+(snippet) {
+ header {
+ {blocks.bar}
+ }
+ import sub_snippet {
+ bar {
+ {blocks.foo}
+ }
+ }
+}
+(sub_snippet) {
+ header {
+ {blocks.bar}
+ }
+}
+example.com {
+ import snippet {
+ foo {
+ foo a
+ }
+ bar {
+ bar b
+ }
+ }
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":443"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "handler": "headers",
+ "response": {
+ "set": {
+ "Bar": [
+ "b"
+ ]
+ }
+ }
+ },
+ {
+ "handler": "headers",
+ "response": {
+ "set": {
+ "Foo": [
+ "a"
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/caddytest/integration/caddyfile_adapt/intercept_response.caddyfiletest b/caddytest/integration/caddyfile_adapt/intercept_response.caddyfiletest
new file mode 100644
index 000000000..c92b76fe5
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/intercept_response.caddyfiletest
@@ -0,0 +1,230 @@
+localhost
+
+respond "To intercept"
+
+intercept {
+ @500 status 500
+ replace_status @500 400
+
+ @all status 2xx 3xx 4xx 5xx
+ replace_status @all {http.error.status_code}
+
+ replace_status {http.error.status_code}
+
+ @accel header X-Accel-Redirect *
+ handle_response @accel {
+ respond "Header X-Accel-Redirect!"
+ }
+
+ @another {
+ header X-Another *
+ }
+ handle_response @another {
+ respond "Header X-Another!"
+ }
+
+ @401 status 401
+ handle_response @401 {
+ respond "Status 401!"
+ }
+
+ handle_response {
+ respond "Any! This should be last in the JSON!"
+ }
+
+ @403 {
+ status 403
+ }
+ handle_response @403 {
+ respond "Status 403!"
+ }
+
+ @multi {
+ status 401 403
+ status 404
+ header Foo *
+ header Bar *
+ }
+ handle_response @multi {
+ respond "Headers Foo, Bar AND statuses 401, 403 and 404!"
+ }
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":443"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "localhost"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "handle_response": [
+ {
+ "match": {
+ "status_code": [
+ 500
+ ]
+ },
+ "status_code": 400
+ },
+ {
+ "match": {
+ "status_code": [
+ 2,
+ 3,
+ 4,
+ 5
+ ]
+ },
+ "status_code": "{http.error.status_code}"
+ },
+ {
+ "match": {
+ "headers": {
+ "X-Accel-Redirect": [
+ "*"
+ ]
+ }
+ },
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Header X-Accel-Redirect!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "match": {
+ "headers": {
+ "X-Another": [
+ "*"
+ ]
+ }
+ },
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Header X-Another!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "match": {
+ "status_code": [
+ 401
+ ]
+ },
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Status 401!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "match": {
+ "status_code": [
+ 403
+ ]
+ },
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Status 403!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "match": {
+ "headers": {
+ "Bar": [
+ "*"
+ ],
+ "Foo": [
+ "*"
+ ]
+ },
+ "status_code": [
+ 401,
+ 403,
+ 404
+ ]
+ },
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Headers Foo, Bar AND statuses 401, 403 and 404!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "status_code": "{http.error.status_code}"
+ },
+ {
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Any! This should be last in the JSON!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "handler": "intercept"
+ },
+ {
+ "body": "To intercept",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/caddytest/integration/caddyfile_adapt/log_except_catchall_blocks.caddyfiletest b/caddytest/integration/caddyfile_adapt/log_except_catchall_blocks.caddyfiletest
index e1362f8fb..b2a7f2afc 100644
--- a/caddytest/integration/caddyfile_adapt/log_except_catchall_blocks.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/log_except_catchall_blocks.caddyfiletest
@@ -99,7 +99,9 @@ http://localhost:2020 {
},
"logs": {
"logger_names": {
- "localhost": ""
+ "localhost": [
+ ""
+ ]
},
"skip_unmapped_hosts": true
}
diff --git a/caddytest/integration/caddyfile_adapt/log_filter_with_header.txt b/caddytest/integration/caddyfile_adapt/log_filter_with_header.txt
new file mode 100644
index 000000000..3ab6d6246
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/log_filter_with_header.txt
@@ -0,0 +1,151 @@
+localhost {
+ log {
+ output file ./caddy.access.log
+ }
+ log health_check_log {
+ output file ./caddy.access.health.log
+ no_hostname
+ }
+ log general_log {
+ output file ./caddy.access.general.log
+ no_hostname
+ }
+ @healthCheck `header_regexp('User-Agent', '^some-regexp$') || path('/healthz*')`
+ handle @healthCheck {
+ log_name health_check_log general_log
+ respond "Healthy"
+ }
+
+ handle {
+ respond "Hello World"
+ }
+}
+----------
+{
+ "logging": {
+ "logs": {
+ "default": {
+ "exclude": [
+ "http.log.access.general_log",
+ "http.log.access.health_check_log",
+ "http.log.access.log0"
+ ]
+ },
+ "general_log": {
+ "writer": {
+ "filename": "./caddy.access.general.log",
+ "output": "file"
+ },
+ "include": [
+ "http.log.access.general_log"
+ ]
+ },
+ "health_check_log": {
+ "writer": {
+ "filename": "./caddy.access.health.log",
+ "output": "file"
+ },
+ "include": [
+ "http.log.access.health_check_log"
+ ]
+ },
+ "log0": {
+ "writer": {
+ "filename": "./caddy.access.log",
+ "output": "file"
+ },
+ "include": [
+ "http.log.access.log0"
+ ]
+ }
+ }
+ },
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":443"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "localhost"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "group": "group2",
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "access_logger_names": [
+ "health_check_log",
+ "general_log"
+ ],
+ "handler": "vars"
+ },
+ {
+ "body": "Healthy",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "match": [
+ {
+ "expression": {
+ "expr": "header_regexp('User-Agent', '^some-regexp$') || path('/healthz*')",
+ "name": "healthCheck"
+ }
+ }
+ ]
+ },
+ {
+ "group": "group2",
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Hello World",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ],
+ "logs": {
+ "logger_names": {
+ "localhost": [
+ "log0"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/caddytest/integration/caddyfile_adapt/log_multi_logger_name.caddyfiletest b/caddytest/integration/caddyfile_adapt/log_multi_logger_name.caddyfiletest
new file mode 100644
index 000000000..be9ec1885
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/log_multi_logger_name.caddyfiletest
@@ -0,0 +1,117 @@
+(log-both) {
+ log {args[0]}-json {
+ hostnames {args[0]}
+ output file /var/log/{args[0]}.log
+ format json
+ }
+ log {args[0]}-console {
+ hostnames {args[0]}
+ output file /var/log/{args[0]}.json
+ format console
+ }
+}
+
+*.example.com {
+ # Subdomains log to multiple files at once, with
+ # different output files and formats.
+ import log-both foo.example.com
+ import log-both bar.example.com
+}
+----------
+{
+ "logging": {
+ "logs": {
+ "bar.example.com-console": {
+ "writer": {
+ "filename": "/var/log/bar.example.com.json",
+ "output": "file"
+ },
+ "encoder": {
+ "format": "console"
+ },
+ "include": [
+ "http.log.access.bar.example.com-console"
+ ]
+ },
+ "bar.example.com-json": {
+ "writer": {
+ "filename": "/var/log/bar.example.com.log",
+ "output": "file"
+ },
+ "encoder": {
+ "format": "json"
+ },
+ "include": [
+ "http.log.access.bar.example.com-json"
+ ]
+ },
+ "default": {
+ "exclude": [
+ "http.log.access.bar.example.com-console",
+ "http.log.access.bar.example.com-json",
+ "http.log.access.foo.example.com-console",
+ "http.log.access.foo.example.com-json"
+ ]
+ },
+ "foo.example.com-console": {
+ "writer": {
+ "filename": "/var/log/foo.example.com.json",
+ "output": "file"
+ },
+ "encoder": {
+ "format": "console"
+ },
+ "include": [
+ "http.log.access.foo.example.com-console"
+ ]
+ },
+ "foo.example.com-json": {
+ "writer": {
+ "filename": "/var/log/foo.example.com.log",
+ "output": "file"
+ },
+ "encoder": {
+ "format": "json"
+ },
+ "include": [
+ "http.log.access.foo.example.com-json"
+ ]
+ }
+ }
+ },
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":443"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "*.example.com"
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ],
+ "logs": {
+ "logger_names": {
+ "bar.example.com": [
+ "bar.example.com-json",
+ "bar.example.com-console"
+ ],
+ "foo.example.com": [
+ "foo.example.com-json",
+ "foo.example.com-console"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/caddytest/integration/caddyfile_adapt/log_override_hostname.caddyfiletest b/caddytest/integration/caddyfile_adapt/log_override_hostname.caddyfiletest
index c36ba23f8..b9213e65a 100644
--- a/caddytest/integration/caddyfile_adapt/log_override_hostname.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/log_override_hostname.caddyfiletest
@@ -75,9 +75,15 @@ example.com:8443 {
],
"logs": {
"logger_names": {
- "bar.example.com": "log0",
- "baz.example.com": "log1",
- "foo.example.com": "log0"
+ "bar.example.com": [
+ "log0"
+ ],
+ "baz.example.com": [
+ "log1"
+ ],
+ "foo.example.com": [
+ "log0"
+ ]
}
}
},
@@ -99,7 +105,9 @@ example.com:8443 {
],
"logs": {
"logger_names": {
- "example.com": "log2"
+ "example.com": [
+ "log2"
+ ]
}
}
}
diff --git a/caddytest/integration/caddyfile_adapt/log_override_name_multiaccess.caddyfiletest b/caddytest/integration/caddyfile_adapt/log_override_name_multiaccess.caddyfiletest
index d48857a2d..2708503e5 100644
--- a/caddytest/integration/caddyfile_adapt/log_override_name_multiaccess.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/log_override_name_multiaccess.caddyfiletest
@@ -76,7 +76,9 @@ http://localhost:8881 {
},
"logs": {
"logger_names": {
- "localhost": "foo"
+ "localhost": [
+ "foo"
+ ]
}
}
}
diff --git a/caddytest/integration/caddyfile_adapt/log_override_name_multiaccess_debug.caddyfiletest b/caddytest/integration/caddyfile_adapt/log_override_name_multiaccess_debug.caddyfiletest
index d024dc386..7ea659789 100644
--- a/caddytest/integration/caddyfile_adapt/log_override_name_multiaccess_debug.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/log_override_name_multiaccess_debug.caddyfiletest
@@ -81,7 +81,9 @@ http://localhost:8881 {
},
"logs": {
"logger_names": {
- "localhost": "foo"
+ "localhost": [
+ "foo"
+ ]
}
}
}
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/log_skip_hosts.caddyfiletest b/caddytest/integration/caddyfile_adapt/log_skip_hosts.caddyfiletest
index 8fdd57156..c10610c2e 100644
--- a/caddytest/integration/caddyfile_adapt/log_skip_hosts.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/log_skip_hosts.caddyfiletest
@@ -63,7 +63,9 @@ example.com {
],
"logs": {
"logger_names": {
- "one.example.com": ""
+ "one.example.com": [
+ ""
+ ]
},
"skip_hosts": [
"example.com",
diff --git a/caddytest/integration/caddyfile_adapt/map_and_vars_with_raw_types.caddyfiletest b/caddytest/integration/caddyfile_adapt/map_and_vars_with_raw_types.caddyfiletest
index cc7563015..8b872635d 100644
--- a/caddytest/integration/caddyfile_adapt/map_and_vars_with_raw_types.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/map_and_vars_with_raw_types.caddyfiletest
@@ -1,23 +1,23 @@
example.com
-map {host} {my_placeholder} {magic_number} {
+map {host} {my_placeholder} {magic_number} {
# Should output boolean "true" and an integer
- example.com true 3
+ example.com true 3
# Should output a string and null
- foo.example.com "string value"
+ foo.example.com "string value"
# Should output two strings (quoted int)
- (.*)\.example.com "${1} subdomain" "5"
+ (.*)\.example.com "${1} subdomain" "5"
# Should output null and a string (quoted int)
- ~.*\.net$ - `7`
+ ~.*\.net$ - `7`
# Should output a float and the string "false"
- ~.*\.xyz$ 123.456 "false"
+ ~.*\.xyz$ 123.456 "false"
# Should output two strings, second being escaped quote
- default "unknown domain" \"""
+ default "unknown domain" \"""
}
vars foo bar
@@ -27,6 +27,7 @@ vars {
ghi 2.3
jkl "mn op"
}
+
----------
{
"apps": {
diff --git a/caddytest/integration/caddyfile_adapt/matcher_syntax.caddyfiletest b/caddytest/integration/caddyfile_adapt/matcher_syntax.caddyfiletest
index ffab2c70d..efb66cf2c 100644
--- a/caddytest/integration/caddyfile_adapt/matcher_syntax.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/matcher_syntax.caddyfiletest
@@ -46,6 +46,18 @@
@matcher12 client_ip private_ranges
respond @matcher12 "client_ip matcher with private ranges"
+
+ @matcher13 {
+ remote_ip 1.1.1.1
+ remote_ip 2.2.2.2
+ }
+ respond @matcher13 "remote_ip merged"
+
+ @matcher14 {
+ client_ip 1.1.1.1
+ client_ip 2.2.2.2
+ }
+ respond @matcher14 "client_ip merged"
}
----------
{
@@ -146,6 +158,7 @@
{
"vars_regexp": {
"{http.request.uri}": {
+ "name": "matcher6",
"pattern": "\\.([a-f0-9]{6})\\.(css|js)$"
}
}
@@ -161,7 +174,10 @@
{
"match": [
{
- "expression": "path('/foo*') \u0026\u0026 method('GET')"
+ "expression": {
+ "expr": "path('/foo*') \u0026\u0026 method('GET')",
+ "name": "matcher7"
+ }
}
],
"handle": [
@@ -275,6 +291,42 @@
"handler": "static_response"
}
]
+ },
+ {
+ "match": [
+ {
+ "remote_ip": {
+ "ranges": [
+ "1.1.1.1",
+ "2.2.2.2"
+ ]
+ }
+ }
+ ],
+ "handle": [
+ {
+ "body": "remote_ip merged",
+ "handler": "static_response"
+ }
+ ]
+ },
+ {
+ "match": [
+ {
+ "client_ip": {
+ "ranges": [
+ "1.1.1.1",
+ "2.2.2.2"
+ ]
+ }
+ }
+ ],
+ "handle": [
+ {
+ "body": "client_ip merged",
+ "handler": "static_response"
+ }
+ ]
}
]
}
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
new file mode 100644
index 000000000..e362cecc9
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/metrics_perhost.caddyfiletest
@@ -0,0 +1,37 @@
+{
+ 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
+ }
+ }
+ }
+}
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/caddyfile_adapt/reverse_proxy_dynamic_upstreams_grace_period.caddyfiletest b/caddytest/integration/caddyfile_adapt/reverse_proxy_dynamic_upstreams_grace_period.caddyfiletest
new file mode 100644
index 000000000..0389b2f12
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_dynamic_upstreams_grace_period.caddyfiletest
@@ -0,0 +1,38 @@
+:8884 {
+ reverse_proxy {
+ dynamic srv {
+ name foo
+ refresh 5m
+ grace_period 5s
+ }
+ }
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":8884"
+ ],
+ "routes": [
+ {
+ "handle": [
+ {
+ "dynamic_upstreams": {
+ "grace_period": 5000000000,
+ "name": "foo",
+ "refresh": 300000000000,
+ "source": "srv"
+ },
+ "handler": "reverse_proxy"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_health_method.caddyfiletest b/caddytest/integration/caddyfile_adapt/reverse_proxy_health_method.caddyfiletest
new file mode 100644
index 000000000..920702c10
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_health_method.caddyfiletest
@@ -0,0 +1,40 @@
+:8884
+
+reverse_proxy 127.0.0.1:65535 {
+ health_uri /health
+ health_method HEAD
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":8884"
+ ],
+ "routes": [
+ {
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "health_checks": {
+ "active": {
+ "method": "HEAD",
+ "uri": "/health"
+ }
+ },
+ "upstreams": [
+ {
+ "dial": "127.0.0.1:65535"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_health_reqbody.caddyfiletest b/caddytest/integration/caddyfile_adapt/reverse_proxy_health_reqbody.caddyfiletest
new file mode 100644
index 000000000..ae5a6791e
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_health_reqbody.caddyfiletest
@@ -0,0 +1,40 @@
+:8884
+
+reverse_proxy 127.0.0.1:65535 {
+ health_uri /health
+ health_request_body "test body"
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":8884"
+ ],
+ "routes": [
+ {
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "health_checks": {
+ "active": {
+ "body": "test body",
+ "uri": "/health"
+ }
+ },
+ "upstreams": [
+ {
+ "dial": "127.0.0.1:65535"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_forward_proxy_url.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_forward_proxy_url.txt
new file mode 100644
index 000000000..9fc445283
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_forward_proxy_url.txt
@@ -0,0 +1,41 @@
+:8884
+reverse_proxy 127.0.0.1:65535 {
+ transport http {
+ forward_proxy_url http://localhost:8080
+ }
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":8884"
+ ],
+ "routes": [
+ {
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "transport": {
+ "network_proxy": {
+ "from": "url",
+ "url": "http://localhost:8080"
+ },
+ "protocol": "http"
+ },
+ "upstreams": [
+ {
+ "dial": "127.0.0.1:65535"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_none_proxy.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_none_proxy.txt
new file mode 100644
index 000000000..3805448d9
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_none_proxy.txt
@@ -0,0 +1,40 @@
+:8884
+reverse_proxy 127.0.0.1:65535 {
+ transport http {
+ network_proxy none
+ }
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":8884"
+ ],
+ "routes": [
+ {
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "transport": {
+ "network_proxy": {
+ "from": "none"
+ },
+ "protocol": "http"
+ },
+ "upstreams": [
+ {
+ "dial": "127.0.0.1:65535"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_tls_file_cert.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_tls_file_cert.txt
new file mode 100644
index 000000000..d43aa1175
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_tls_file_cert.txt
@@ -0,0 +1,47 @@
+:8884
+reverse_proxy 127.0.0.1:65535 {
+ transport http {
+ tls_trust_pool file {
+ pem_file ../caddy.ca.cer
+ }
+ }
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":8884"
+ ],
+ "routes": [
+ {
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "transport": {
+ "protocol": "http",
+ "tls": {
+ "ca": {
+ "pem_files": [
+ "../caddy.ca.cer"
+ ],
+ "provider": "file"
+ }
+ }
+ },
+ "upstreams": [
+ {
+ "dial": "127.0.0.1:65535"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_tls_inline_cert.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_tls_inline_cert.txt
new file mode 100644
index 000000000..ef9e8243a
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_tls_inline_cert.txt
@@ -0,0 +1,47 @@
+:8884
+reverse_proxy 127.0.0.1:65535 {
+ transport http {
+ tls_trust_pool inline {
+ trust_der MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==
+ }
+ }
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":8884"
+ ],
+ "routes": [
+ {
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "transport": {
+ "protocol": "http",
+ "tls": {
+ "ca": {
+ "provider": "inline",
+ "trusted_ca_certs": [
+ "MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ=="
+ ]
+ }
+ }
+ },
+ "upstreams": [
+ {
+ "dial": "127.0.0.1:65535"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_url_proxy.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_url_proxy.txt
new file mode 100644
index 000000000..9397458e9
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_url_proxy.txt
@@ -0,0 +1,41 @@
+:8884
+reverse_proxy 127.0.0.1:65535 {
+ transport http {
+ network_proxy url http://localhost:8080
+ }
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":8884"
+ ],
+ "routes": [
+ {
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "transport": {
+ "network_proxy": {
+ "from": "url",
+ "url": "http://localhost:8080"
+ },
+ "protocol": "http"
+ },
+ "upstreams": [
+ {
+ "dial": "127.0.0.1:65535"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_localaddr.caddyfiletest b/caddytest/integration/caddyfile_adapt/reverse_proxy_localaddr.caddyfiletest
new file mode 100644
index 000000000..d734c9ce0
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_localaddr.caddyfiletest
@@ -0,0 +1,57 @@
+https://example.com {
+ reverse_proxy http://localhost:54321 {
+ transport http {
+ local_address 192.168.0.1
+ }
+ }
+}
+
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":443"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "transport": {
+ "local_address": "192.168.0.1",
+ "protocol": "http"
+ },
+ "upstreams": [
+ {
+ "dial": "localhost:54321"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/caddytest/integration/caddyfile_adapt/shorthand_parameterized_placeholders.caddyfiletest b/caddytest/integration/caddyfile_adapt/shorthand_parameterized_placeholders.caddyfiletest
index 30bc2c128..ef8d2330b 100644
--- a/caddytest/integration/caddyfile_adapt/shorthand_parameterized_placeholders.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/shorthand_parameterized_placeholders.caddyfiletest
@@ -36,6 +36,7 @@ respond @match "{re.1}"
"match": [
{
"path_regexp": {
+ "name": "match",
"pattern": "^/foo(.*)$"
}
}
diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_3.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_automation_policies_3.caddyfiletest
index da5824a36..9daaf436d 100644
--- a/caddytest/integration/caddyfile_adapt/tls_automation_policies_3.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/tls_automation_policies_3.caddyfiletest
@@ -70,8 +70,9 @@ c.example.com {
"module": "acme"
},
{
+ "ca": "https://acme.zerossl.com/v2/DV90",
"email": "abc@example.com",
- "module": "zerossl"
+ "module": "acme"
}
]
},
diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_4.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_automation_policies_4.caddyfiletest
index d8f2164de..a4385a8f3 100644
--- a/caddytest/integration/caddyfile_adapt/tls_automation_policies_4.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/tls_automation_policies_4.caddyfiletest
@@ -131,8 +131,9 @@ abc.de {
"module": "acme"
},
{
+ "ca": "https://acme.zerossl.com/v2/DV90",
"email": "my.email@example.com",
- "module": "zerossl"
+ "module": "acme"
}
]
}
diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_8.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_automation_policies_8.caddyfiletest
index 1703178eb..bd1bbf221 100644
--- a/caddytest/integration/caddyfile_adapt/tls_automation_policies_8.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/tls_automation_policies_8.caddyfiletest
@@ -86,8 +86,9 @@ http://localhost:8081 {
"module": "acme"
},
{
+ "ca": "https://acme.zerossl.com/v2/DV90",
"email": "abc@example.com",
- "module": "zerossl"
+ "module": "acme"
}
]
}
diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_global_email_localhost.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_automation_policies_global_email_localhost.caddyfiletest
index e8ef3a7e9..50fbf51aa 100644
--- a/caddytest/integration/caddyfile_adapt/tls_automation_policies_global_email_localhost.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/tls_automation_policies_global_email_localhost.caddyfiletest
@@ -54,8 +54,9 @@ example.com {
"module": "acme"
},
{
+ "ca": "https://acme.zerossl.com/v2/DV90",
"email": "foo@bar",
- "module": "zerossl"
+ "module": "acme"
}
]
}
diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_force_automate.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_force_automate.caddyfiletest
new file mode 100644
index 000000000..623bafd70
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_force_automate.caddyfiletest
@@ -0,0 +1,174 @@
+automated1.example.com {
+ tls force_automate
+ respond "Automated!"
+}
+
+automated2.example.com {
+ tls force_automate
+ respond "Automated!"
+}
+
+shadowed.example.com {
+ respond "Shadowed!"
+}
+
+*.example.com {
+ tls cert.pem key.pem
+ respond "Wildcard!"
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":443"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "automated1.example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Automated!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ },
+ {
+ "match": [
+ {
+ "host": [
+ "automated2.example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Automated!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ },
+ {
+ "match": [
+ {
+ "host": [
+ "shadowed.example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Shadowed!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ },
+ {
+ "match": [
+ {
+ "host": [
+ "*.example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Wildcard!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ],
+ "tls_connection_policies": [
+ {
+ "match": {
+ "sni": [
+ "automated1.example.com",
+ "automated2.example.com"
+ ]
+ }
+ },
+ {
+ "match": {
+ "sni": [
+ "*.example.com"
+ ]
+ },
+ "certificate_selection": {
+ "any_tag": [
+ "cert0"
+ ]
+ }
+ },
+ {}
+ ]
+ }
+ }
+ },
+ "tls": {
+ "certificates": {
+ "automate": [
+ "automated1.example.com",
+ "automated2.example.com"
+ ],
+ "load_files": [
+ {
+ "certificate": "cert.pem",
+ "key": "key.pem",
+ "tags": [
+ "cert0"
+ ]
+ }
+ ]
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_shadowing.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_shadowing.caddyfiletest
new file mode 100644
index 000000000..2be543779
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/tls_automation_wildcard_shadowing.caddyfiletest
@@ -0,0 +1,102 @@
+subdomain.example.com {
+ respond "Subdomain!"
+}
+
+*.example.com {
+ tls cert.pem key.pem
+ respond "Wildcard!"
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":443"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "subdomain.example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Subdomain!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ },
+ {
+ "match": [
+ {
+ "host": [
+ "*.example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Wildcard!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ],
+ "tls_connection_policies": [
+ {
+ "match": {
+ "sni": [
+ "*.example.com"
+ ]
+ },
+ "certificate_selection": {
+ "any_tag": [
+ "cert0"
+ ]
+ }
+ },
+ {}
+ ]
+ }
+ }
+ },
+ "tls": {
+ "certificates": {
+ "load_files": [
+ {
+ "certificate": "cert.pem",
+ "key": "key.pem",
+ "tags": [
+ "cert0"
+ ]
+ }
+ ]
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/caddytest/integration/caddyfile_adapt/tls_dns_ttl.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_dns_ttl.caddyfiletest
index 02e46763d..c452bf79f 100644
--- a/caddytest/integration/caddyfile_adapt/tls_dns_ttl.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/tls_dns_ttl.caddyfiletest
@@ -58,14 +58,6 @@ tls {
}
},
"module": "acme"
- },
- {
- "challenges": {
- "dns": {
- "ttl": 310000000000
- }
- },
- "module": "zerossl"
}
]
}
diff --git a/caddytest/integration/caddyfile_adapt/tls_explicit_issuer_dns_ttl.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_explicit_issuer_dns_ttl.caddyfiletest
index 53629e3a1..d552599ff 100644
--- a/caddytest/integration/caddyfile_adapt/tls_explicit_issuer_dns_ttl.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/tls_explicit_issuer_dns_ttl.caddyfiletest
@@ -5,7 +5,7 @@ tls {
issuer acme {
dns_ttl 5m10s
}
- issuer zerossl {
+ issuer zerossl api_key {
dns_ttl 10m20s
}
}
@@ -65,10 +65,9 @@ tls {
"module": "acme"
},
{
- "challenges": {
- "dns": {
- "ttl": 620000000000
- }
+ "api_key": "api_key",
+ "cname_validation": {
+ "ttl": 620000000000
},
"module": "zerossl"
}
diff --git a/caddytest/integration/caddyfile_adapt/tls_explicit_issuer_propagation_options.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_explicit_issuer_propagation_options.caddyfiletest
index 032f9284f..206d59ca5 100644
--- a/caddytest/integration/caddyfile_adapt/tls_explicit_issuer_propagation_options.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/tls_explicit_issuer_propagation_options.caddyfiletest
@@ -6,7 +6,7 @@ tls {
propagation_delay 5m10s
propagation_timeout 10m20s
}
- issuer zerossl {
+ issuer zerossl api_key {
propagation_delay 5m30s
propagation_timeout -1
}
@@ -68,11 +68,10 @@ tls {
"module": "acme"
},
{
- "challenges": {
- "dns": {
- "propagation_delay": 330000000000,
- "propagation_timeout": -1
- }
+ "api_key": "api_key",
+ "cname_validation": {
+ "propagation_delay": 330000000000,
+ "propagation_timeout": -1
},
"module": "zerossl"
}
diff --git a/caddytest/integration/caddyfile_adapt/tls_propagation_options.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_propagation_options.caddyfiletest
index ee4666b66..43ec9774b 100644
--- a/caddytest/integration/caddyfile_adapt/tls_propagation_options.caddyfiletest
+++ b/caddytest/integration/caddyfile_adapt/tls_propagation_options.caddyfiletest
@@ -60,15 +60,6 @@ tls {
}
},
"module": "acme"
- },
- {
- "challenges": {
- "dns": {
- "propagation_delay": 310000000000,
- "propagation_timeout": 620000000000
- }
- },
- "module": "zerossl"
}
]
}
diff --git a/caddytest/integration/caddyfile_adapt/wildcard_pattern.caddyfiletest b/caddytest/integration/caddyfile_adapt/wildcard_pattern.caddyfiletest
new file mode 100644
index 000000000..1a9ccea74
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/wildcard_pattern.caddyfiletest
@@ -0,0 +1,157 @@
+*.example.com {
+ tls foo@example.com {
+ dns mock
+ }
+
+ @foo host foo.example.com
+ handle @foo {
+ respond "Foo!"
+ }
+
+ @bar host bar.example.com
+ handle @bar {
+ respond "Bar!"
+ }
+
+ # Fallback for otherwise unhandled domains
+ handle {
+ abort
+ }
+}
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":443"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "*.example.com"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "group": "group3",
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Foo!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "match": [
+ {
+ "host": [
+ "foo.example.com"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "group3",
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "body": "Bar!",
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "match": [
+ {
+ "host": [
+ "bar.example.com"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "group3",
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "abort": true,
+ "handler": "static_response"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ]
+ }
+ }
+ },
+ "tls": {
+ "automation": {
+ "policies": [
+ {
+ "subjects": [
+ "*.example.com"
+ ],
+ "issuers": [
+ {
+ "challenges": {
+ "dns": {
+ "provider": {
+ "name": "mock"
+ }
+ }
+ },
+ "email": "foo@example.com",
+ "module": "acme"
+ },
+ {
+ "ca": "https://acme.zerossl.com/v2/DV90",
+ "challenges": {
+ "dns": {
+ "provider": {
+ "name": "mock"
+ }
+ }
+ },
+ "email": "foo@example.com",
+ "module": "acme"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/caddytest/integration/caddyfile_test.go b/caddytest/integration/caddyfile_test.go
index 628363a52..11ffc08ae 100644
--- a/caddytest/integration/caddyfile_test.go
+++ b/caddytest/integration/caddyfile_test.go
@@ -517,6 +517,25 @@ func TestUriOps(t *testing.T) {
tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar0&baz=buz&taz=nottest&changethis=val", 200, "changed=val&foo=bar0&foo=bar&key%3Dvalue=example&taz=test")
}
+// Tests the `http.request.local.port` placeholder.
+// We don't test the very similar `http.request.local.host` placeholder,
+// because depending on the host the test is running on, localhost might
+// refer to 127.0.0.1 or ::1.
+// TODO: Test each http version separately (especially http/3)
+func TestHttpRequestLocalPortPlaceholder(t *testing.T) {
+ tester := caddytest.NewTester(t)
+
+ tester.InitServer(`
+ {
+ admin localhost:2999
+ http_port 9080
+ }
+ :9080
+ respond "{http.request.local.port}"`, "caddyfile")
+
+ tester.AssertGetResponse("http://localhost:9080/", 200, "9080")
+}
+
func TestSetThenAddQueryParams(t *testing.T) {
tester := caddytest.NewTester(t)
diff --git a/caddytest/integration/handler_test.go b/caddytest/integration/handler_test.go
index 6db119eae..afc700b02 100644
--- a/caddytest/integration/handler_test.go
+++ b/caddytest/integration/handler_test.go
@@ -1,6 +1,7 @@
package integration
import (
+ "bytes"
"net/http"
"testing"
@@ -29,3 +30,30 @@ func TestBrowse(t *testing.T) {
}
tester.AssertResponseCode(req, 200)
}
+
+func TestRespondWithJSON(t *testing.T) {
+ tester := caddytest.NewTester(t)
+ tester.InitServer(`
+ {
+ skip_install_trust
+ admin localhost:2999
+ http_port 9080
+ https_port 9443
+ grace_period 1ns
+ }
+ localhost {
+ respond {http.request.body}
+ }
+ `, "caddyfile")
+
+ res, _ := tester.AssertPostResponseBody("https://localhost:9443/",
+ nil,
+ bytes.NewBufferString(`{
+ "greeting": "Hello, world!"
+ }`), 200, `{
+ "greeting": "Hello, world!"
+ }`)
+ if res.Header.Get("Content-Type") != "application/json" {
+ t.Errorf("expected Content-Type to be application/json, but was %s", res.Header.Get("Content-Type"))
+ }
+}
diff --git a/caddytest/integration/intercept_test.go b/caddytest/integration/intercept_test.go
new file mode 100644
index 000000000..6f8ffc929
--- /dev/null
+++ b/caddytest/integration/intercept_test.go
@@ -0,0 +1,40 @@
+package integration
+
+import (
+ "testing"
+
+ "github.com/caddyserver/caddy/v2/caddytest"
+)
+
+func TestIntercept(t *testing.T) {
+ tester := caddytest.NewTester(t)
+ tester.InitServer(`{
+ skip_install_trust
+ admin localhost:2999
+ http_port 9080
+ https_port 9443
+ grace_period 1ns
+ }
+
+ localhost:9080 {
+ respond /intercept "I'm a teapot" 408
+ header /intercept To-Intercept ok
+ respond /no-intercept "I'm not a teapot"
+
+ intercept {
+ @teapot status 408
+ handle_response @teapot {
+ header /intercept intercepted {resp.header.To-Intercept}
+ respond /intercept "I'm a combined coffee/tea pot that is temporarily out of coffee" 503
+ }
+ }
+ }
+ `, "caddyfile")
+
+ r, _ := tester.AssertGetResponse("http://localhost:9080/intercept", 503, "I'm a combined coffee/tea pot that is temporarily out of coffee")
+ if r.Header.Get("intercepted") != "ok" {
+ t.Fatalf(`header "intercepted" value is not "ok": %s`, r.Header.Get("intercepted"))
+ }
+
+ tester.AssertGetResponse("http://localhost:9080/no-intercept", 200, "I'm not a teapot")
+}
diff --git a/caddytest/integration/mockdns_test.go b/caddytest/integration/mockdns_test.go
new file mode 100644
index 000000000..615116a3a
--- /dev/null
+++ b/caddytest/integration/mockdns_test.go
@@ -0,0 +1,61 @@
+package integration
+
+import (
+ "context"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+ "github.com/caddyserver/certmagic"
+ "github.com/libdns/libdns"
+)
+
+func init() {
+ caddy.RegisterModule(MockDNSProvider{})
+}
+
+// MockDNSProvider is a mock DNS provider, for testing config with DNS modules.
+type MockDNSProvider struct{}
+
+// CaddyModule returns the Caddy module information.
+func (MockDNSProvider) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ ID: "dns.providers.mock",
+ New: func() caddy.Module { return new(MockDNSProvider) },
+ }
+}
+
+// Provision sets up the module.
+func (MockDNSProvider) Provision(ctx caddy.Context) error {
+ return nil
+}
+
+// UnmarshalCaddyfile sets up the module from Caddyfile tokens.
+func (MockDNSProvider) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
+ return nil
+}
+
+// AppendRecords appends DNS records to the zone.
+func (MockDNSProvider) AppendRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
+ return nil, nil
+}
+
+// DeleteRecords deletes DNS records from the zone.
+func (MockDNSProvider) DeleteRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
+ return nil, nil
+}
+
+// GetRecords gets DNS records from the zone.
+func (MockDNSProvider) GetRecords(ctx context.Context, zone string) ([]libdns.Record, error) {
+ return nil, nil
+}
+
+// SetRecords sets DNS records in the zone.
+func (MockDNSProvider) SetRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
+ return nil, nil
+}
+
+// Interface guard
+var _ caddyfile.Unmarshaler = (*MockDNSProvider)(nil)
+var _ certmagic.DNSProvider = (*MockDNSProvider)(nil)
+var _ caddy.Provisioner = (*MockDNSProvider)(nil)
+var _ caddy.Module = (*MockDNSProvider)(nil)
diff --git a/caddytest/integration/stream_test.go b/caddytest/integration/stream_test.go
index ef3ea498d..d2f2fd79b 100644
--- a/caddytest/integration/stream_test.go
+++ b/caddytest/integration/stream_test.go
@@ -334,7 +334,7 @@ func TestH2ToH1ChunkedResponse(t *testing.T) {
ProtoMinor: 0,
Header: make(http.Header),
}
- // underlying transport will automaticlly add gzip
+ // underlying transport will automatically add gzip
// req.Header.Set("Accept-Encoding", "gzip")
go func() {
fmt.Fprint(w, expectedBody)
diff --git a/caddytest/integration/testdata/foo.txt b/caddytest/integration/testdata/foo.txt
new file mode 100644
index 000000000..191028156
--- /dev/null
+++ b/caddytest/integration/testdata/foo.txt
@@ -0,0 +1 @@
+foo
\ No newline at end of file
diff --git a/caddytest/integration/testdata/foo_with_multiple_trailing_newlines.txt b/caddytest/integration/testdata/foo_with_multiple_trailing_newlines.txt
new file mode 100644
index 000000000..75d7bfb87
--- /dev/null
+++ b/caddytest/integration/testdata/foo_with_multiple_trailing_newlines.txt
@@ -0,0 +1,2 @@
+foo
+
diff --git a/caddytest/integration/testdata/foo_with_trailing_newline.txt b/caddytest/integration/testdata/foo_with_trailing_newline.txt
new file mode 100644
index 000000000..257cc5642
--- /dev/null
+++ b/caddytest/integration/testdata/foo_with_trailing_newline.txt
@@ -0,0 +1 @@
+foo
diff --git a/cmd/caddy/setcap.sh b/cmd/caddy/setcap.sh
index ac5335c56..39aea2d60 100755
--- a/cmd/caddy/setcap.sh
+++ b/cmd/caddy/setcap.sh
@@ -1,13 +1,16 @@
#!/bin/sh
-# USAGE: go run -exec ./setcap.sh main.go
+# USAGE:
+# go run -exec ./setcap.sh main.go
#
# (Example: `go run -exec ./setcap.sh main.go run --config caddy.json`)
#
# For some reason this does not work on my Arch system, so if you find that's
-# the case, you can instead do: go build && ./setcap.sh ./caddy
-# but this will leave the ./caddy binary laying around.
+# the case, you can instead do:
#
+# go build && ./setcap.sh ./caddy
+#
+# but this will leave the ./caddy binary laying around.
#
sudo setcap cap_net_bind_service=+ep "$1"
diff --git a/cmd/cobra.go b/cmd/cobra.go
index 1a2509206..9ecb389e2 100644
--- a/cmd/cobra.go
+++ b/cmd/cobra.go
@@ -8,9 +8,10 @@ import (
"github.com/caddyserver/caddy/v2"
)
-var rootCmd = &cobra.Command{
- Use: "caddy",
- Long: `Caddy is an extensible server platform written in Go.
+var defaultFactory = newRootCommandFactory(func() *cobra.Command {
+ return &cobra.Command{
+ Use: "caddy",
+ Long: `Caddy is an extensible server platform written in Go.
At its core, Caddy merely manages configuration. Modules are plugged
in statically at compile-time to provide useful functionality. Caddy's
@@ -91,23 +92,26 @@ package installers: https://caddyserver.com/docs/install
Instructions for running Caddy in production are also available:
https://caddyserver.com/docs/running
`,
- Example: ` $ caddy run
+ Example: ` $ caddy run
$ caddy run --config caddy.json
$ caddy reload --config caddy.json
$ caddy stop`,
- // kind of annoying to have all the help text printed out if
- // caddy has an error provisioning its modules, for instance...
- SilenceUsage: true,
- Version: onlyVersionText(),
-}
+ // kind of annoying to have all the help text printed out if
+ // caddy has an error provisioning its modules, for instance...
+ SilenceUsage: true,
+ Version: onlyVersionText(),
+ }
+})
const fullDocsFooter = `Full documentation is available at:
https://caddyserver.com/docs/command-line`
func init() {
- rootCmd.SetVersionTemplate("{{.Version}}\n")
- rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n")
+ defaultFactory.Use(func(rootCmd *cobra.Command) {
+ rootCmd.SetVersionTemplate("{{.Version}}\n")
+ rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n")
+ })
}
func onlyVersionText() string {
diff --git a/cmd/commandfactory.go b/cmd/commandfactory.go
new file mode 100644
index 000000000..ac571a21a
--- /dev/null
+++ b/cmd/commandfactory.go
@@ -0,0 +1,28 @@
+package caddycmd
+
+import (
+ "github.com/spf13/cobra"
+)
+
+type rootCommandFactory struct {
+ constructor func() *cobra.Command
+ options []func(*cobra.Command)
+}
+
+func newRootCommandFactory(fn func() *cobra.Command) *rootCommandFactory {
+ return &rootCommandFactory{
+ constructor: fn,
+ }
+}
+
+func (f *rootCommandFactory) Use(fn func(cmd *cobra.Command)) {
+ f.options = append(f.options, fn)
+}
+
+func (f *rootCommandFactory) Build() *cobra.Command {
+ o := f.constructor()
+ for _, v := range f.options {
+ v(o)
+ }
+ return o
+}
diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go
index 746cf3da6..5127c0f90 100644
--- a/cmd/commandfuncs.go
+++ b/cmd/commandfuncs.go
@@ -74,6 +74,10 @@ func cmdStart(fl Flags) (int, error) {
// sure by giving it some random bytes and having it echo
// them back to us)
cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String())
+ // we should be able to run caddy in relative paths
+ if errors.Is(cmd.Err, exec.ErrDot) {
+ cmd.Err = nil
+ }
if configFlag != "" {
cmd.Args = append(cmd.Args, "--config", configFlag)
}
@@ -167,6 +171,10 @@ func cmdStart(fl Flags) (int, error) {
func cmdRun(fl Flags) (int, error) {
caddy.TrapSignals()
+ logger := caddy.Log()
+ undoMaxProcs := setResourceLimits(logger)
+ defer undoMaxProcs()
+
configFlag := fl.String("config")
configAdapterFlag := fl.String("adapter")
resumeFlag := fl.Bool("resume")
@@ -192,18 +200,18 @@ func cmdRun(fl Flags) (int, error) {
config, err = os.ReadFile(caddy.ConfigAutosavePath)
if errors.Is(err, fs.ErrNotExist) {
// not a bad error; just can't resume if autosave file doesn't exist
- caddy.Log().Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath))
+ logger.Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath))
resumeFlag = false
} else if err != nil {
return caddy.ExitCodeFailedStartup, err
} else {
if configFlag == "" {
- caddy.Log().Info("resuming from last configuration",
+ logger.Info("resuming from last configuration",
zap.String("autosave_file", caddy.ConfigAutosavePath))
} else {
// if they also specified a config file, user should be aware that we're not
// using it (doing so could lead to data/config loss by overwriting!)
- caddy.Log().Warn("--config and --resume flags were used together; ignoring --config and resuming from last configuration",
+ logger.Warn("--config and --resume flags were used together; ignoring --config and resuming from last configuration",
zap.String("autosave_file", caddy.ConfigAutosavePath))
}
}
@@ -221,7 +229,7 @@ func cmdRun(fl Flags) (int, error) {
if pidfileFlag != "" {
err := caddy.PIDFile(pidfileFlag)
if err != nil {
- caddy.Log().Error("unable to write PID file",
+ logger.Error("unable to write PID file",
zap.String("pidfile", pidfileFlag),
zap.Error(err))
}
@@ -232,7 +240,7 @@ func cmdRun(fl Flags) (int, error) {
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("loading initial config: %v", err)
}
- caddy.Log().Info("serving initial configuration")
+ logger.Info("serving initial configuration")
// if we are to report to another process the successful start
// of the server, do so now by echoing back contents of stdin
@@ -268,15 +276,15 @@ func cmdRun(fl Flags) (int, error) {
switch runtime.GOOS {
case "windows":
if os.Getenv("HOME") == "" && os.Getenv("USERPROFILE") == "" && !hasXDG {
- caddy.Log().Warn("neither HOME nor USERPROFILE environment variables are set - please fix; some assets might be stored in ./caddy")
+ logger.Warn("neither HOME nor USERPROFILE environment variables are set - please fix; some assets might be stored in ./caddy")
}
case "plan9":
if os.Getenv("home") == "" && !hasXDG {
- caddy.Log().Warn("$home environment variable is empty - please fix; some assets might be stored in ./caddy")
+ logger.Warn("$home environment variable is empty - please fix; some assets might be stored in ./caddy")
}
default:
if os.Getenv("HOME") == "" && !hasXDG {
- caddy.Log().Warn("$HOME environment variable is empty - please fix; some assets might be stored in ./caddy")
+ logger.Warn("$HOME environment variable is empty - please fix; some assets might be stored in ./caddy")
}
}
@@ -556,10 +564,15 @@ func cmdValidateConfig(fl Flags) (int, error) {
func cmdFmt(fl Flags) (int, error) {
configFile := fl.Arg(0)
- if configFile == "" {
- configFile = "Caddyfile"
+ 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)
@@ -656,6 +669,8 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
return nil, err
}
parsedAddr.Host = addr
+ } else if parsedAddr.IsFdNetwork() {
+ origin = "http://127.0.0.1"
}
// form the request
@@ -663,13 +678,13 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
if err != nil {
return nil, fmt.Errorf("making request: %v", err)
}
- if parsedAddr.IsUnixNetwork() {
+ if parsedAddr.IsUnixNetwork() || parsedAddr.IsFdNetwork() {
// We used to conform to RFC 2616 Section 14.26 which requires
// an empty host header when there is no host, as is the case
- // with unix sockets. However, Go required a Host value so we
- // used a hack of a space character as the host (it would see
- // the Host was non-empty, then trim the space later). As of
- // Go 1.20.6 (July 2023), this hack no longer works. See:
+ // with unix sockets and socket fds. However, Go required a
+ // Host value so we used a hack of a space character as the host
+ // (it would see the Host was non-empty, then trim the space later).
+ // As of Go 1.20.6 (July 2023), this hack no longer works. See:
// https://github.com/golang/go/issues/60374
// See also the discussion here:
// https://github.com/golang/go/issues/61431
@@ -710,7 +725,7 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
// if it didn't work, let the user know
if resp.StatusCode >= 400 {
- respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1024*10))
+ respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024*2))
if err != nil {
return nil, fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
}
diff --git a/cmd/commands.go b/cmd/commands.go
index e5e1265e4..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: "",
+ Usage: "",
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")
@@ -438,43 +440,44 @@ EXPERIMENTAL: May be changed or removed.
},
})
- RegisterCommand(Command{
- Name: "manpage",
- Usage: "--directory ",
- Short: "Generates the manual pages for Caddy commands",
- Long: `
+ defaultFactory.Use(func(rootCmd *cobra.Command) {
+ rootCmd.AddCommand(caddyCmdToCobra(Command{
+ Name: "manpage",
+ Usage: "--directory ",
+ Short: "Generates the manual pages for Caddy commands",
+ Long: `
Generates the manual pages for Caddy commands into the designated directory
tagged into section 8 (System Administration).
The manual page files are generated into the directory specified by the
argument of --directory. If the directory does not exist, it will be created.
`,
- CobraFunc: func(cmd *cobra.Command) {
- cmd.Flags().StringP("directory", "o", "", "The output directory where the manpages are generated")
- cmd.RunE = WrapCommandFuncForCobra(func(fl Flags) (int, error) {
- dir := strings.TrimSpace(fl.String("directory"))
- if dir == "" {
- return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required")
- }
- if err := os.MkdirAll(dir, 0o755); err != nil {
- return caddy.ExitCodeFailedQuit, err
- }
- if err := doc.GenManTree(rootCmd, &doc.GenManHeader{
- Title: "Caddy",
- Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections
- }, dir); err != nil {
- return caddy.ExitCodeFailedQuit, err
- }
- return caddy.ExitCodeSuccess, nil
- })
- },
- })
+ CobraFunc: func(cmd *cobra.Command) {
+ cmd.Flags().StringP("directory", "o", "", "The output directory where the manpages are generated")
+ cmd.RunE = WrapCommandFuncForCobra(func(fl Flags) (int, error) {
+ dir := strings.TrimSpace(fl.String("directory"))
+ if dir == "" {
+ return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required")
+ }
+ if err := os.MkdirAll(dir, 0o755); err != nil {
+ return caddy.ExitCodeFailedQuit, err
+ }
+ if err := doc.GenManTree(rootCmd, &doc.GenManHeader{
+ Title: "Caddy",
+ Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections
+ }, dir); err != nil {
+ return caddy.ExitCodeFailedQuit, err
+ }
+ return caddy.ExitCodeSuccess, nil
+ })
+ },
+ }))
- // source: https://github.com/spf13/cobra/blob/main/shell_completions.md
- rootCmd.AddCommand(&cobra.Command{
- Use: "completion [bash|zsh|fish|powershell]",
- Short: "Generate completion script",
- Long: fmt.Sprintf(`To load completions:
+ // source: https://github.com/spf13/cobra/blob/main/shell_completions.md
+ rootCmd.AddCommand(&cobra.Command{
+ Use: "completion [bash|zsh|fish|powershell]",
+ Short: "Generate completion script",
+ Long: fmt.Sprintf(`To load completions:
Bash:
@@ -513,23 +516,24 @@ argument of --directory. If the directory does not exist, it will be created.
PS> %[1]s completion powershell > %[1]s.ps1
# and source this file from your PowerShell profile.
`, rootCmd.Root().Name()),
- DisableFlagsInUseLine: true,
- ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
- Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
- RunE: func(cmd *cobra.Command, args []string) error {
- switch args[0] {
- case "bash":
- return cmd.Root().GenBashCompletion(os.Stdout)
- case "zsh":
- return cmd.Root().GenZshCompletion(os.Stdout)
- case "fish":
- return cmd.Root().GenFishCompletion(os.Stdout, true)
- case "powershell":
- return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
- default:
- return fmt.Errorf("unrecognized shell: %s", args[0])
- }
- },
+ DisableFlagsInUseLine: true,
+ ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
+ Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ switch args[0] {
+ case "bash":
+ return cmd.Root().GenBashCompletion(os.Stdout)
+ case "zsh":
+ return cmd.Root().GenZshCompletion(os.Stdout)
+ case "fish":
+ return cmd.Root().GenFishCompletion(os.Stdout, true)
+ case "powershell":
+ return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
+ default:
+ return fmt.Errorf("unrecognized shell: %s", args[0])
+ }
+ },
+ })
})
}
@@ -563,7 +567,9 @@ func RegisterCommand(cmd Command) {
if !commandNameRegex.MatchString(cmd.Name) {
panic("invalid command name")
}
- rootCmd.AddCommand(caddyCmdToCobra(cmd))
+ defaultFactory.Use(func(rootCmd *cobra.Command) {
+ rootCmd.AddCommand(caddyCmdToCobra(cmd))
+ })
}
var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`)
diff --git a/cmd/main.go b/cmd/main.go
index d832cbc5e..87fa9fb95 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -24,6 +24,7 @@ import (
"io"
"io/fs"
"log"
+ "log/slog"
"net"
"os"
"path/filepath"
@@ -33,10 +34,12 @@ import (
"strings"
"time"
+ "github.com/KimMachineGun/automemlimit/memlimit"
"github.com/caddyserver/certmagic"
"github.com/spf13/pflag"
"go.uber.org/automaxprocs/maxprocs"
"go.uber.org/zap"
+ "go.uber.org/zap/exp/zapslog"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
@@ -66,13 +69,7 @@ func Main() {
os.Exit(caddy.ExitCodeFailedStartup)
}
- undo, err := maxprocs.Set()
- defer undo()
- if err != nil {
- caddy.Log().Warn("failed to set GOMAXPROCS", zap.Error(err))
- }
-
- if err := rootCmd.Execute(); err != nil {
+ if err := defaultFactory.Build().Execute(); err != nil {
var exitError *exitError
if errors.As(err, &exitError) {
os.Exit(exitError.ExitCode)
@@ -107,6 +104,40 @@ func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
return loadConfigWithLogger(caddy.Log(), configFile, adapterName)
}
+func isCaddyfile(configFile, adapterName string) (bool, error) {
+ if adapterName == "caddyfile" {
+ return true, nil
+ }
+
+ // as a special case, if a config file starts with "caddyfile" or
+ // has a ".caddyfile" extension, and no adapter is specified, and
+ // no adapter module name matches the extension, assume
+ // caddyfile adapter for convenience
+ baseConfig := strings.ToLower(filepath.Base(configFile))
+ baseConfigExt := filepath.Ext(baseConfig)
+ startsOrEndsInCaddyfile := strings.HasPrefix(baseConfig, "caddyfile") || strings.HasSuffix(baseConfig, ".caddyfile")
+
+ if baseConfigExt == ".json" {
+ return false, nil
+ }
+
+ // If the adapter is not specified,
+ // the config file starts with "caddyfile",
+ // the config file has an extension,
+ // and isn't a JSON file (e.g. Caddyfile.yaml),
+ // then we don't know what the config format is.
+ if adapterName == "" && startsOrEndsInCaddyfile {
+ return true, nil
+ }
+
+ // adapter is not empty,
+ // adapter is not "caddyfile",
+ // extension is not ".json",
+ // extension is not ".caddyfile"
+ // file does not start with "Caddyfile"
+ return false, nil
+}
+
func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([]byte, string, error) {
// if no logger is provided, use a nop logger
// just so we don't have to check for nil
@@ -157,13 +188,10 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
}
}
- // as a special case, if a config file called "Caddyfile" was
- // specified, and no adapter is specified, assume caddyfile adapter
- // for convenience
- if strings.HasPrefix(filepath.Base(configFile), "Caddyfile") &&
- filepath.Ext(configFile) != ".json" &&
- adapterName == "" {
+ if yes, err := isCaddyfile(configFile, adapterName); yes {
adapterName = "caddyfile"
+ } else if err != nil {
+ return nil, "", err
}
// load config adapter
@@ -194,7 +222,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
zap.Int("line", warn.Line))
}
config = adaptedConfig
- } else {
+ } else if len(config) != 0 {
// validate that the config is at least valid JSON
err = json.Unmarshal(config, new(any))
if err != nil {
@@ -436,6 +464,31 @@ func printEnvironment() {
}
}
+func setResourceLimits(logger *zap.Logger) func() {
+ // Configure the maximum number of CPUs to use to match the Linux container quota (if any)
+ // See https://pkg.go.dev/runtime#GOMAXPROCS
+ undo, err := maxprocs.Set(maxprocs.Logger(logger.Sugar().Infof))
+ if err != nil {
+ logger.Warn("failed to set GOMAXPROCS", zap.Error(err))
+ }
+
+ // Configure the maximum memory to use to match the Linux container quota (if any) or system memory
+ // See https://pkg.go.dev/runtime/debug#SetMemoryLimit
+ _, _ = memlimit.SetGoMemLimitWithOpts(
+ memlimit.WithLogger(
+ slog.New(zapslog.NewHandler(logger.Core())),
+ ),
+ memlimit.WithProvider(
+ memlimit.ApplyFallback(
+ memlimit.FromCgroup,
+ memlimit.FromSystem,
+ ),
+ ),
+ )
+
+ return undo
+}
+
// StringSlice is a flag.Value that enables repeated use of a string flag.
type StringSlice []string
diff --git a/cmd/main_test.go b/cmd/main_test.go
index 90e8194f6..3b2412c57 100644
--- a/cmd/main_test.go
+++ b/cmd/main_test.go
@@ -168,3 +168,113 @@ here"
}
}
}
+
+func Test_isCaddyfile(t *testing.T) {
+ type args struct {
+ configFile string
+ adapterName string
+ }
+ tests := []struct {
+ name string
+ args args
+ want bool
+ wantErr bool
+ }{
+ {
+ name: "bare Caddyfile without adapter",
+ args: args{
+ configFile: "Caddyfile",
+ adapterName: "",
+ },
+ want: true,
+ wantErr: false,
+ },
+ {
+ name: "local Caddyfile without adapter",
+ args: args{
+ configFile: "./Caddyfile",
+ adapterName: "",
+ },
+ want: true,
+ wantErr: false,
+ },
+ {
+ name: "local caddyfile with adapter",
+ args: args{
+ configFile: "./Caddyfile",
+ adapterName: "caddyfile",
+ },
+ want: true,
+ wantErr: false,
+ },
+ {
+ name: "ends with .caddyfile with adapter",
+ args: args{
+ configFile: "./conf.caddyfile",
+ adapterName: "caddyfile",
+ },
+ want: true,
+ wantErr: false,
+ },
+ {
+ name: "ends with .caddyfile without adapter",
+ args: args{
+ configFile: "./conf.caddyfile",
+ adapterName: "",
+ },
+ want: true,
+ wantErr: false,
+ },
+ {
+ name: "config is Caddyfile.yaml with adapter",
+ args: args{
+ configFile: "./Caddyfile.yaml",
+ adapterName: "yaml",
+ },
+ want: false,
+ wantErr: false,
+ },
+ {
+
+ name: "json is not caddyfile but not error",
+ args: args{
+ configFile: "./Caddyfile.json",
+ adapterName: "",
+ },
+ want: false,
+ wantErr: false,
+ },
+ {
+
+ name: "prefix of Caddyfile and ./ with any extension is Caddyfile",
+ args: args{
+ configFile: "./Caddyfile.prd",
+ adapterName: "",
+ },
+ want: true,
+ wantErr: false,
+ },
+ {
+
+ name: "prefix of Caddyfile without ./ with any extension is Caddyfile",
+ args: args{
+ configFile: "Caddyfile.prd",
+ adapterName: "",
+ },
+ want: true,
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := isCaddyfile(tt.args.configFile, tt.args.adapterName)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("isCaddyfile() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if got != tt.want {
+ t.Errorf("isCaddyfile() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
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/cmd/x509rootsfallback.go b/cmd/x509rootsfallback.go
new file mode 100644
index 000000000..935a48ec1
--- /dev/null
+++ b/cmd/x509rootsfallback.go
@@ -0,0 +1,33 @@
+// 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 caddycmd
+
+import (
+ // For running in minimal environments, this can ease
+ // headaches related to establishing TLS connections.
+ // "Package fallback embeds a set of fallback X.509 trusted
+ // roots in the application by automatically invoking
+ // x509.SetFallbackRoots. This allows the application to
+ // work correctly even if the operating system does not
+ // provide a verifier or system roots pool. ... It's
+ // recommended that only binaries, and not libraries,
+ // import this package. This package must be kept up to
+ // date for security and compatibility reasons."
+ //
+ // This is in its own file only because of conflicts
+ // between gci and goimports when in main.go.
+ // See https://github.com/daixiang0/gci/issues/76
+ _ "golang.org/x/crypto/x509roots/fallback"
+)
diff --git a/context.go b/context.go
index d73af7702..eb0979f3a 100644
--- a/context.go
+++ b/context.go
@@ -23,6 +23,8 @@ import (
"reflect"
"github.com/caddyserver/certmagic"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/collectors"
"go.uber.org/zap"
"go.uber.org/zap/exp/zapslog"
@@ -47,6 +49,7 @@ type Context struct {
ancestry []Module
cleanupFuncs []func() // invoked at every config unload
exitFuncs []func(context.Context) // invoked at config unload ONLY IF the process is exiting (EXPERIMENTAL)
+ metricsRegistry *prometheus.Registry
}
// NewContext provides a new context derived from the given
@@ -58,7 +61,7 @@ type Context struct {
// modules which are loaded will be properly unloaded.
// See standard library context package's documentation.
func NewContext(ctx Context) (Context, context.CancelFunc) {
- newCtx := Context{moduleInstances: make(map[string][]Module), cfg: ctx.cfg}
+ newCtx := Context{moduleInstances: make(map[string][]Module), cfg: ctx.cfg, metricsRegistry: prometheus.NewPedanticRegistry()}
c, cancel := context.WithCancel(ctx.Context)
wrappedCancel := func() {
cancel()
@@ -79,6 +82,7 @@ func NewContext(ctx Context) (Context, context.CancelFunc) {
}
}
newCtx.Context = c
+ newCtx.initMetrics()
return newCtx, wrappedCancel
}
@@ -87,14 +91,32 @@ func (ctx *Context) OnCancel(f func()) {
ctx.cleanupFuncs = append(ctx.cleanupFuncs, f)
}
-// Filesystems returns a ref to the FilesystemMap.
+// FileSystems returns a ref to the FilesystemMap.
// EXPERIMENTAL: This API is subject to change.
-func (ctx *Context) Filesystems() FileSystems {
+func (ctx *Context) FileSystems() FileSystems {
// if no config is loaded, we use a default filesystemmap, which includes the osfs
if ctx.cfg == nil {
- return &filesystems.FilesystemMap{}
+ return &filesystems.FileSystemMap{}
}
- return ctx.cfg.filesystems
+ return ctx.cfg.fileSystems
+}
+
+// Returns the active metrics registry for the context
+// EXPERIMENTAL: This API is subject to change.
+func (ctx *Context) GetMetricsRegistry() *prometheus.Registry {
+ return ctx.metricsRegistry
+}
+
+func (ctx *Context) initMetrics() {
+ ctx.metricsRegistry.MustRegister(
+ collectors.NewBuildInfoCollector(),
+ collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
+ collectors.NewGoCollector(),
+ adminMetrics.requestCount,
+ adminMetrics.requestErrors,
+ globalMetrics.configSuccess,
+ globalMetrics.configSuccessTime,
+ )
}
// OnExit executes f when the process exits gracefully.
@@ -255,6 +277,14 @@ func (ctx Context) LoadModule(structPointer any, fieldName string) (any, error)
return result, nil
}
+// emitEvent is a small convenience method so the caddy core can emit events, if the event app is configured.
+func (ctx Context) emitEvent(name string, data map[string]any) Event {
+ if ctx.cfg == nil || ctx.cfg.eventEmitter == nil {
+ return Event{}
+ }
+ return ctx.cfg.eventEmitter.Emit(ctx, name, data)
+}
+
// loadModulesFromSomeMap loads modules from val, which must be a type of map[string]any.
// Depending on inlineModuleKey, it will be interpreted as either a ModuleMap (key is the module
// name) or as a regular map (key is not the module name, and module name is defined inline).
@@ -363,6 +393,17 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
return nil, fmt.Errorf("module value cannot be null")
}
+ // if this is an app module, keep a reference to it,
+ // since submodules may need to reference it during
+ // provisioning (even though the parent app module
+ // may not be fully provisioned yet; this is the case
+ // with the tls app's automation policies, which may
+ // refer to the tls app to check if a global DNS
+ // module has been configured for DNS challenges)
+ if appModule, ok := val.(App); ok {
+ ctx.cfg.apps[id] = appModule
+ }
+
ctx.ancestry = append(ctx.ancestry, val)
if prov, ok := val.(Provisioner); ok {
@@ -396,6 +437,14 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
ctx.moduleInstances[id] = append(ctx.moduleInstances[id], val)
+ // if the loaded module happens to be an app that can emit events, store it so the
+ // core can have access to emit events without an import cycle
+ if ee, ok := val.(eventEmitter); ok {
+ if _, ok := ee.(App); ok {
+ ctx.cfg.eventEmitter = ee
+ }
+ }
+
return val, nil
}
@@ -449,29 +498,32 @@ func (ctx Context) App(name string) (any, error) {
if appRaw != nil {
ctx.cfg.AppsRaw[name] = nil // allow GC to deallocate
}
- ctx.cfg.apps[name] = modVal.(App)
return modVal, nil
}
-// AppIfConfigured returns an app by its name if it has been
-// configured. Can be called instead of App() to avoid
-// instantiating an empty app when that's not desirable. If
-// the app has not been loaded, nil is returned.
-//
-// We return any type instead of the App type because it is not
-// intended for the caller of this method to be the one to start
-// or stop App modules. The caller is expected to assert to the
-// concrete type.
-func (ctx Context) AppIfConfigured(name string) any {
+// AppIfConfigured is like App, but it returns an error if the
+// app has not been configured. This is useful when the app is
+// required and its absence is a configuration error; or when
+// the app is optional and you don't want to instantiate a
+// new one that hasn't been explicitly configured. If the app
+// is not in the configuration, the error wraps ErrNotConfigured.
+func (ctx Context) AppIfConfigured(name string) (any, error) {
if ctx.cfg == nil {
- // this can happen if the currently-active context
- // is being accessed, but no config has successfully
- // been loaded yet
- return nil
+ return nil, fmt.Errorf("app module %s: %w", name, ErrNotConfigured)
}
- return ctx.cfg.apps[name]
+ if app, ok := ctx.cfg.apps[name]; ok {
+ return app, nil
+ }
+ appRaw := ctx.cfg.AppsRaw[name]
+ if appRaw == nil {
+ return nil, fmt.Errorf("app module %s: %w", name, ErrNotConfigured)
+ }
+ return ctx.App(name)
}
+// ErrNotConfigured indicates a module is not configured.
+var ErrNotConfigured = fmt.Errorf("module not configured")
+
// Storage returns the configured Caddy storage implementation.
func (ctx Context) Storage() certmagic.Storage {
return ctx.cfg.storage
@@ -525,18 +577,14 @@ func (ctx Context) Slogger() *slog.Logger {
if err != nil {
panic("config missing, unable to create dev logger: " + err.Error())
}
- return slog.New(zapslog.NewHandler(l.Core(), nil))
+ return slog.New(zapslog.NewHandler(l.Core()))
}
mod := ctx.Module()
if mod == nil {
- return slog.New(zapslog.NewHandler(Log().Core(), nil))
+ return slog.New(zapslog.NewHandler(Log().Core()))
}
-
- 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)),
))
}
@@ -556,3 +604,23 @@ func (ctx Context) Module() Module {
}
return ctx.ancestry[len(ctx.ancestry)-1]
}
+
+// WithValue returns a new context with the given key-value pair.
+func (ctx *Context) WithValue(key, value any) Context {
+ return Context{
+ Context: context.WithValue(ctx.Context, key, value),
+ moduleInstances: ctx.moduleInstances,
+ cfg: ctx.cfg,
+ ancestry: ctx.ancestry,
+ cleanupFuncs: ctx.cleanupFuncs,
+ exitFuncs: ctx.exitFuncs,
+ }
+}
+
+// eventEmitter is a small interface that inverts dependencies for
+// the caddyevents package, so the core can emit events without an
+// import cycle (i.e. the caddy package doesn't have to import
+// the caddyevents package, which imports the caddy package).
+type eventEmitter interface {
+ Emit(ctx Context, eventName string, data map[string]any) Event
+}
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 160c4f0bb..e160b0626 100644
--- a/go.mod
+++ b/go.mod
@@ -1,108 +1,114 @@
module github.com/caddyserver/caddy/v2
-go 1.21
-
-toolchain go1.21.4
+go 1.24
require (
- github.com/BurntSushi/toml v1.3.2
- github.com/Masterminds/sprig/v3 v3.2.3
- github.com/alecthomas/chroma/v2 v2.13.0
+ github.com/BurntSushi/toml v1.4.0
+ github.com/KimMachineGun/automemlimit v0.7.1
+ github.com/Masterminds/sprig/v3 v3.3.0
+ github.com/alecthomas/chroma/v2 v2.15.0
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
- github.com/caddyserver/certmagic v0.20.0
+ github.com/caddyserver/certmagic v0.23.0
+ github.com/caddyserver/zerossl v0.1.3
+ github.com/cloudflare/circl v1.6.0
github.com/dustin/go-humanize v1.0.1
- github.com/go-chi/chi/v5 v5.0.10
- github.com/google/cel-go v0.20.0
- github.com/google/uuid v1.3.1
- github.com/klauspost/compress v1.17.0
- github.com/klauspost/cpuid/v2 v2.2.5
- github.com/mholt/acmez v1.2.0
- github.com/prometheus/client_golang v1.18.0
- github.com/quic-go/quic-go v0.42.0
- github.com/smallstep/certificates v0.25.0
- github.com/smallstep/nosql v0.6.0
- github.com/smallstep/truststore v0.12.1
- github.com/spf13/cobra v1.8.0
- github.com/spf13/pflag v1.0.5
- github.com/stretchr/testify v1.8.4
- github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046
- github.com/yuin/goldmark v1.5.6
+ github.com/go-chi/chi/v5 v5.2.1
+ github.com/google/cel-go v0.24.1
+ github.com/google/uuid v1.6.0
+ github.com/klauspost/compress v1.18.0
+ github.com/klauspost/cpuid/v2 v2.2.10
+ github.com/mholt/acmez/v3 v3.1.2
+ github.com/prometheus/client_golang v1.19.1
+ github.com/quic-go/quic-go v0.51.0
+ github.com/smallstep/certificates v0.26.1
+ github.com/smallstep/nosql v0.6.1
+ github.com/smallstep/truststore v0.13.0
+ github.com/spf13/cobra v1.9.1
+ github.com/spf13/pflag v1.0.6
+ github.com/stretchr/testify v1.10.0
+ github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53
+ 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.46.1
+ 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.21.0
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0
- go.opentelemetry.io/otel/sdk v1.21.0
- go.uber.org/automaxprocs v1.5.3
- go.uber.org/zap v1.26.0
- go.uber.org/zap/exp v0.2.0
- golang.org/x/crypto v0.20.0
- golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611
- golang.org/x/net v0.21.0
- golang.org/x/sync v0.5.0
- golang.org/x/term v0.17.0
- golang.org/x/time v0.5.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.3.0
+ golang.org/x/crypto v0.36.0
+ golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810
+ golang.org/x/net v0.38.0
+ golang.org/x/sync v0.12.0
+ golang.org/x/term v0.30.0
+ golang.org/x/time v0.11.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
)
require (
- cloud.google.com/go/iam v1.1.2 // indirect
+ cel.dev/expr v0.19.1 // indirect
+ dario.cat/mergo v1.0.1 // indirect
github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
- github.com/fxamacker/cbor/v2 v2.5.0 // indirect
- github.com/golang/glog v1.1.2 // indirect
+ github.com/francoispqt/gojay v1.2.13 // indirect
+ github.com/fxamacker/cbor/v2 v2.6.0 // indirect
+ github.com/go-jose/go-jose/v3 v3.0.4 // indirect
+ github.com/go-kit/log v0.2.1 // 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/hashicorp/golang-lru/v2 v2.0.2 // indirect
- github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/onsi/ginkgo/v2 v2.13.2 // indirect
+ github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
- github.com/quic-go/qpack v0.4.0 // indirect
- github.com/smallstep/go-attestation v0.4.4-0.20230627102604-cf579e53cbd2 // indirect
+ github.com/quic-go/qpack v0.5.1 // indirect
+ github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 // indirect
+ github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 // indirect
+ github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
- github.com/zeebo/blake3 v0.2.3 // indirect
+ github.com/zeebo/blake3 v0.2.4 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.17.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.17.0 // indirect
- go.uber.org/mock v0.4.0 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect
+ go.uber.org/mock v0.5.0 // indirect
+ golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // 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 (
- filippo.io/edwards25519 v1.0.0 // indirect
+ filippo.io/edwards25519 v1.1.0 // indirect
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
- github.com/Masterminds/semver/v3 v3.2.0 // 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 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0
github.com/chzyer/readline v1.5.1 // indirect
- github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
+ github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/dgraph-io/badger v1.6.2 // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
- github.com/dgraph-io/ristretto v0.1.0 // indirect
+ github.com/dgraph-io/ristretto v0.2.0 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
- github.com/dlclark/regexp2 v1.11.0 // indirect
+ github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
- github.com/go-kit/kit v0.10.0 // indirect
- github.com/go-logfmt/logfmt v0.5.1 // indirect
- github.com/go-logr/logr v1.3.0 // 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.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
- github.com/golang/protobuf v1.5.3 // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
- github.com/huandu/xstrings v1.3.3 // indirect
- github.com/imdario/mergo v0.3.12 // indirect
+ github.com/huandu/xstrings v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.3 // indirect
@@ -111,48 +117,45 @@ require (
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect
- github.com/jackc/pgx/v4 v4.18.2 // indirect
- github.com/libdns/libdns v0.2.1 // indirect
+ github.com/jackc/pgx/v4 v4.18.3 // indirect
+ github.com/libdns/libdns v1.0.0-beta.1
github.com/manifoldco/promptui v0.9.0 // indirect
- github.com/mattn/go-colorable v0.1.8 // indirect
- github.com/mattn/go-isatty v0.0.16 // indirect
+ github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
- github.com/micromdm/scep/v2 v2.1.0 // indirect
- github.com/miekg/dns v1.1.55 // indirect
+ github.com/miekg/dns v1.1.63 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
- github.com/pires/go-proxyproto v0.7.0
+ github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964
github.com/pkg/errors v0.9.1 // indirect
- github.com/prometheus/client_model v0.5.0 // indirect
- github.com/prometheus/common v0.45.0 // indirect
+ github.com/prometheus/client_model v0.5.0
+ github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/rosedblabs/wal v1.3.6
github.com/rs/xid v1.5.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
- github.com/shopspring/decimal v1.2.0 // indirect
+ github.com/shopspring/decimal v1.4.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/slackhq/nebula v1.6.1 // indirect
- github.com/spf13/cast v1.4.1 // indirect
+ github.com/spf13/cast v1.7.0 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/urfave/cli v1.22.14 // indirect
- go.etcd.io/bbolt v1.3.8 // indirect
- go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
- go.opentelemetry.io/otel/metric v1.21.0 // indirect
- go.opentelemetry.io/otel/trace v1.21.0
- go.opentelemetry.io/proto/otlp v1.0.0 // indirect
- go.step.sm/cli-utils v0.8.0 // indirect
- go.step.sm/crypto v0.35.1
+ go.etcd.io/bbolt v1.3.9 // 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.14.0 // indirect
- golang.org/x/sys v0.17.0
- golang.org/x/text v0.14.0 // indirect
- golang.org/x/tools v0.16.1 // indirect
- google.golang.org/grpc v1.60.1 // indirect
- google.golang.org/protobuf v1.33.0 // indirect
- gopkg.in/square/go-jose.v2 v2.6.0 // indirect
+ golang.org/x/mod v0.24.0 // indirect
+ golang.org/x/sys v0.31.0
+ golang.org/x/text v0.23.0 // indirect
+ golang.org/x/tools v0.31.0 // 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 0368fb5a8..03ca8ca10 100644
--- a/go.sum
+++ b/go.sum
@@ -1,83 +1,108 @@
+cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4=
+cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o=
+cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
+cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
+cloud.google.com/go/auth v0.4.1 h1:Z7YNIhlWRtrnKlZke7z3GMqzvuYzdc2z98F9D1NV5Hg=
+cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro=
+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.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
-cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
-cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
-cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
-cloud.google.com/go/iam v1.1.2 h1:gacbrBdWcoVmGLozRuStX45YKvJtzIjJdAolzUs1sm4=
-cloud.google.com/go/iam v1.1.2/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU=
-cloud.google.com/go/kms v1.15.2 h1:lh6qra6oC4AyWe5fUUUBe/S27k12OHAleOOOw6KakdE=
-cloud.google.com/go/kms v1.15.2/go.mod h1:3hopT4+7ooWRCjc2DxgnpESFxhIraaI2IpAVUEhbT/w=
-filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
-filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
+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=
+cloud.google.com/go/kms v1.16.0/go.mod h1:olQUXy2Xud+1GzYfiBO9N0RhjsJk5IJLU6n/ethLXVc=
+cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU=
+cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
+dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
+dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
+dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
+dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
+dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
-github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
+github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
+github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/KimMachineGun/automemlimit v0.7.1 h1:QcG/0iCOLChjfUweIMC3YL5Xy9C3VBeNmCZHrZfJMBw=
+github.com/KimMachineGun/automemlimit v0.7.1/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
-github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
-github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
-github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
-github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
+github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
+github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
+github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
-github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
-github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
-github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
-github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
-github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
+github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
+github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
-github.com/alecthomas/chroma/v2 v2.13.0 h1:VP72+99Fb2zEcYM0MeaWJmV+xQvz5v5cxRHd+ooU1lI=
-github.com/alecthomas/chroma/v2 v2.13.0/go.mod h1:BUGjjsD+ndS6eX37YgTchSEG+Jg9Jv1GiZs9sqPqztk=
+github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc=
+github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
-github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
-github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
-github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
-github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
-github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
-github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
-github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
-github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
-github.com/aws/aws-sdk-go v1.46.4 h1:48tKgtm9VMPkb6y7HuYlsfhQmoIRAsTEXTsWLVlty4M=
-github.com/aws/aws-sdk-go v1.46.4/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
-github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
+github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA=
+github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
+github.com/aws/aws-sdk-go-v2/config v1.27.13 h1:WbKW8hOzrWoOA/+35S5okqO/2Ap8hkkFUzoW8Hzq24A=
+github.com/aws/aws-sdk-go-v2/config v1.27.13/go.mod h1:XLiyiTMnguytjRER7u5RIkhIqS8Nyz41SwAWb4xEjxs=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.13 h1:XDCJDzk/u5cN7Aple7D/MiAhx1Rjo/0nueJ0La8mRuE=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.13/go.mod h1:FMNcjQrmuBYvOTZDtOLCIu0esmxjF7RuA/89iSXWzQI=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk=
+github.com/aws/aws-sdk-go-v2/service/kms v1.31.1 h1:5wtyAwuUiJiM3DHYeGZmP5iMonM7DFBWAEaaVPHYZA0=
+github.com/aws/aws-sdk-go-v2/service/kms v1.31.1/go.mod h1:2snWQJQUKsbN66vAawJuOGX7dr37pfOq9hb0tZDGIqQ=
+github.com/aws/aws-sdk-go-v2/service/sso v1.20.6 h1:o5cTaeunSpfXiLTIBx5xo2enQmiChtu1IBbzXnfU9Hs=
+github.com/aws/aws-sdk-go-v2/service/sso v1.20.6/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0 h1:Qe0r0lVURDDeBQJ4yP+BOrJkvkiCo/3FH/t+wY11dmw=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.0/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak=
+github.com/aws/aws-sdk-go-v2/service/sts v1.28.7 h1:et3Ta53gotFR4ERLXXHIHl/Uuk1qYpP5uU7cvNql8ns=
+github.com/aws/aws-sdk-go-v2/service/sts v1.28.7/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw=
+github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
+github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
-github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
-github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
-github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc=
-github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg=
-github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
-github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
-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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+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.23.0 h1:CfpZ/50jMfG4+1J/u2LV6piJq4HOfO6ppOnOf7DkFEU=
+github.com/caddyserver/certmagic v0.23.0/go.mod h1:9mEZIWqqWoI+Gf+4Trh04MOVPD0tGSxtqsxg87hAIH4=
+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.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=
@@ -87,24 +112,21 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
-github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
+github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
-github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
-github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
-github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
-github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
-github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -115,170 +137,123 @@ github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdw
github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk=
github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
-github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
-github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
-github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=
+github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
-github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
-github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
-github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
+github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
-github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
-github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
-github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
-github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
-github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
-github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
-github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
-github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
+github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
+github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE=
-github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
+github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
+github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
-github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
+github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
+github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
+github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
+github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-kit/kit v0.4.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo=
-github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
+github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU=
+github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
-github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
-github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
+github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
-github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
+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.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
-github.com/go-logr/logr v1.3.0/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.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
-github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo=
-github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
-github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
-github.com/google/cel-go v0.20.0 h1:h4n6DOCppEMpWERzllyNkntl7JrDyxoE543KWS6BLpc=
-github.com/google/cel-go v0.20.0/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
+github.com/google/cel-go v0.24.1 h1:jsBCtxG8mM5wiUJDSGUqU0K7Mtr3w7Eyv00rw4DiZxI=
+github.com/google/cel-go v0.24.1/go.mod h1:Hdf9TqOaTNSFQA1ybQaRqATVoK7m/zcf7IMhGXP5zI8=
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
+github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk=
github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU=
-github.com/google/go-tpm-tools v0.4.1 h1:gYU6iwRo0tY3V6NDnS6m+XYog+b3g6YFhHQl3sYaUL4=
-github.com/google/go-tpm-tools v0.4.1/go.mod h1:w03m0jynhTo7puXTYoyfpNOMqyQ9SB7sixnKWsS/1L0=
+github.com/google/go-tpm-tools v0.4.4 h1:oiQfAIkc6xTy9Fl5NKTeTJkBTlXdHsxAofmQyxBKY98=
+github.com/google/go-tpm-tools v0.4.4/go.mod h1:T8jXkp2s+eltnCDIsXR84/MTcVU9Ja7bh3Mit0pa4AY=
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
-github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20231212022811-ec68065c825e h1:bwOy7hAFd0C91URzMIEBfr6BAz29yk7Qj0cy6S7DJlU=
github.com/google/pprof v0.0.0-20231212022811-ec68065c825e/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
-github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
-github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
-github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
-github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
+github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU=
+github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
+github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
+github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg=
+github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
-github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
-github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
-github.com/gorilla/mux v1.4.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
-github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
-github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
-github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
-github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda/go.mod h1:MyndkAZd5rUMdNogn35MWXBX1UiBigrU8eTj8DoAC2c=
-github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
-github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
-github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 h1:RtRsiaGvWxcwd8y3BiRZxsylPT8hLWZ5SPcfI+3IDNk=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0/go.mod h1:TzP6duP4Py2pHLVPPQp42aoYI92+PCrVotyR5e8Vqlk=
-github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
-github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
-github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
-github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
-github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
-github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
-github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
-github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
-github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
-github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
-github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
-github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+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.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
github.com/hashicorp/golang-lru/v2 v2.0.2 h1:Dwmkdr5Nc/oBiXgJS3CDHNhJtIHkuZ3DZF5twqnfBdU=
github.com/hashicorp/golang-lru/v2 v2.0.2/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
-github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
-github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
-github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
-github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
-github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
-github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
-github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
-github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
-github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
+github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
@@ -321,37 +296,29 @@ github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
-github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU=
-github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
+github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA=
+github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
-github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
-github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
-github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
-github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
-github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
-github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+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.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
-github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
-github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
-github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
-github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
+github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
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/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -362,140 +329,81 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
-github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
-github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
-github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
-github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
-github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
+github.com/libdns/libdns v1.0.0-beta.1 h1:KIf4wLfsrEpXpZ3vmc/poM8zCATXT2klbdPe6hyOBjQ=
+github.com/libdns/libdns v1.0.0-beta.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
+github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
-github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
-github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
-github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
+github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
-github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
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 v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
-github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE=
-github.com/micromdm/scep/v2 v2.1.0 h1:2fS9Rla7qRR266hvUoEauBJ7J6FhgssEiq2OkSKXmaU=
-github.com/micromdm/scep/v2 v2.1.0/go.mod h1:BkF7TkPPhmgJAMtHfP+sFTKXmgzNJgLQlvvGoOExBcc=
-github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
-github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
-github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
-github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
-github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
+github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc=
+github.com/mholt/acmez/v3 v3.1.2/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.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
+github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
-github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
-github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
-github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
-github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
-github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
-github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
-github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
-github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
-github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
-github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
-github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
-github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
-github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
-github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
-github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
+github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs=
github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM=
-github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
-github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
-github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
-github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
-github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
-github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
-github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
-github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
-github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
-github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
-github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
-github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
-github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
+github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
+github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
+github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
-github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
-github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
-github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
-github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
-github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
-github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964 h1:ct/vxNBgHpASQ4sT8NaBX9LtsEtluZqaUJydLG50U3E=
+github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
-github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
-github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
-github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
-github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
-github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
+github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
+github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
-github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
-github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
-github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
-github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
+github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
+github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
+github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
-github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
-github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
-github.com/quic-go/quic-go v0.42.0 h1:uSfdap0eveIl8KXnipv9K7nlwZ5IqLlYOpJ58u5utpM=
-github.com/quic-go/quic-go v0.42.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
-github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+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.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc=
+github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
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/rosedblabs/wal v1.3.6 h1:oxZYTPX/u4JuGDW98wQ1YamWqerlrlSUFKhgP6Gd/Ao=
github.com/rosedblabs/wal v1.3.6/go.mod h1:wdq54KJUyVTOv1uddMc6Cdh2d/YCIo8yjcwJAb1RCEM=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
@@ -504,21 +412,40 @@ github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
-github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
-github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E=
github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg=
-github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
-github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
+github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
+github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
+github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
+github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
+github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
+github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
+github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
+github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
+github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
+github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
+github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
+github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
+github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
+github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
+github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
+github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
+github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
+github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
+github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
+github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
+github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
-github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
+github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
@@ -528,41 +455,37 @@ github.com/slackhq/nebula v1.6.1 h1:/OCTR3abj0Sbf2nGoLUrdDXImrCv0ZVFpVPP5qa0DsM=
github.com/slackhq/nebula v1.6.1/go.mod h1:UmkqnXe4O53QwToSl/gG7sM4BroQwAB7dd4hUaT6MlI=
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
-github.com/smallstep/certificates v0.25.0 h1:WWihtjQ7SprnRxDV44mBp8t5SMsNO5EWsQaEwy1rgFg=
-github.com/smallstep/certificates v0.25.0/go.mod h1:thJmekMKUplKYip+la99Lk4IwQej/oVH/zS9PVMagEE=
-github.com/smallstep/go-attestation v0.4.4-0.20230627102604-cf579e53cbd2 h1:UIAS8DTWkeclraEGH2aiJPyNPu16VbT41w4JoBlyFfU=
-github.com/smallstep/go-attestation v0.4.4-0.20230627102604-cf579e53cbd2/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
-github.com/smallstep/nosql v0.6.0 h1:ur7ysI8s9st0cMXnTvB8tA3+x5Eifmkb6hl4uqNV5jc=
-github.com/smallstep/nosql v0.6.0/go.mod h1:jOXwLtockXORUPPZ2MCUcIkGR6w0cN1QGZniY9DITQA=
-github.com/smallstep/truststore v0.12.1 h1:guLUKkc1UlsXeS3t6BuVMa4leOOpdiv02PCRTiy1WdY=
-github.com/smallstep/truststore v0.12.1/go.mod h1:M4mebeNy28KusGX3lJxpLARIktLcyqBOrj3ZiZ46pqw=
-github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
-github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
-github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
-github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
+github.com/smallstep/certificates v0.26.1 h1:FIUliEBcExSfJJDhRFA/s8aZgMIFuorexnRSKQd884o=
+github.com/smallstep/certificates v0.26.1/go.mod h1:OQMrW39IrGKDViKSHrKcgSQArMZ8c7EcjhYKK7mYqis=
+github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935 h1:kjYvkvS/Wdy0PVRDUAA0gGJIVSEZYhiAJtfwYgOYoGA=
+github.com/smallstep/go-attestation v0.4.4-0.20240109183208-413678f90935/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
+github.com/smallstep/nosql v0.6.1 h1:X8IBZFTRIp1gmuf23ne/jlD/BWKJtDQbtatxEn7Et1Y=
+github.com/smallstep/nosql v0.6.1/go.mod h1:vrN+CftYYNnDM+DQqd863ATynvYFm/6FuY9D4TeAm2Y=
+github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 h1:B6cED3iLJTgxpdh4tuqByDjRRKan2EvtnOfHr2zHJVg=
+github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81/go.mod h1:SoUAr/4M46rZ3WaLstHxGhLEgoYIDRqxQEXLOmOEB0Y=
+github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d h1:06LUHn4Ia2X6syjIaCMNaXXDNdU+1N/oOHynJbWgpXw=
+github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d/go.mod h1:4d0ub42ut1mMtvGyMensjuHYEUpRrASvkzLEJvoRQcU=
+github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4=
+github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A=
+github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
+github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
-github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
+github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
-github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
-github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
+github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
+github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
-github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
-github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
-github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
-github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
-github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
@@ -576,51 +499,44 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046 h1:8rUlviSVOEe7TMk7W0gIPrW8MqEzYfZHpsNWSf8s2vg=
-github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
-github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ=
+github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
+github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
-github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
-github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk=
github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
+github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
-github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
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.5.6 h1:COmQAWTCcGetChm3Ig7G/t8AFAN00t+o8Mt4cf7JpwA=
-github.com/yuin/goldmark v1.5.6/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+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=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
-github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
-github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
+github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
+github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
-go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
-go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
-go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
-go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
-go.mozilla.org/pkcs7 v0.0.0-20210730143726-725912489c62/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
-go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak=
-go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
-go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
-go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
-go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
+go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
+go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
+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.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=
@@ -631,36 +547,36 @@ 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.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
-go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
-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.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
-go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
-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.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
-go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
-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.step.sm/cli-utils v0.8.0 h1:b/Tc1/m3YuQq+u3ghTFP7Dz5zUekZj6GUmd5pCvkEXQ=
-go.step.sm/cli-utils v0.8.0/go.mod h1:S77aISrC0pKuflqiDfxxJlUbiXcAanyJ4POOnzFSxD4=
-go.step.sm/crypto v0.35.1 h1:QAZZ7Q8xaM4TdungGSAYw/zxpyH4fMYTkfaXVV9H7pY=
-go.step.sm/crypto v0.35.1/go.mod h1:vn8Vkx/Mbqgoe7AG8btC0qZ995Udm3e+JySuDS1LCJA=
+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=
+go.step.sm/crypto v0.45.0/go.mod h1:6IYlT0L2jfj81nVyCPpvA5cORy0EVHPhieSgQyuwHIY=
go.step.sm/linkedca v0.20.1 h1:bHDn1+UG1NgRrERkWbbCiAIvv4lD5NOFaswPDTyO5vU=
go.step.sm/linkedca v0.20.1/go.mod h1:Vaq4+Umtjh7DLFI1KuIxeo598vfBzgSYZUjgVJ7Syxw=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
-go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
-go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
+go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
+go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
-go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
-go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
+go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
+go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
@@ -670,17 +586,18 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
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.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
-go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
-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=
-golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+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.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=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -688,82 +605,75 @@ golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWP
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
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.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
-golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
-golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
+golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810 h1:V5+zy0jmgNYmK1uW/sPpBw8ioFvalrhaUrYWmu1Fpe4=
+golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810/go.mod h1:lxN5T34bK4Z/i6cMaU7frUU57VkDXFD4Kamfl/cp9oU=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4=
-golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
+golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
+golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
+golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
-golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
-golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/net v0.0.0-20170726083632-f5079bd7f6f7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
+golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
-golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+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.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
+golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
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.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
-golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
+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=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
-golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.0.0-20170728174421-0f826bdd13b5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
+golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -771,39 +681,43 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
+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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
-golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
+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.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
+golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+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.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
-golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
-golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
+golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -811,81 +725,65 @@ golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
-golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
+golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
-google.golang.org/api v0.154.0 h1:X7QkVKZBskztmpPKWQXgjJRPA2dJYrL6r+sYPRLj050=
-google.golang.org/api v0.154.0/go.mod h1:qhSMkM85hgqiokIYsrRyKxrjfBeIhgl4Z2JmeRkYylc=
+google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
+google.golang.org/api v0.180.0 h1:M2D87Yo0rGBPWpo1orwfCLehUUL6E7/TYe5gvMQWDh4=
+google.golang.org/api v0.180.0/go.mod h1:51AiyoEg1MJPSZ9zvklA8VnRILPXxn1iVen9v25XHAE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
-google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f h1:Vn+VyHU5guc9KjB5KrjI2q0wCOWEOIh0OEsleqakHJg=
-google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f/go.mod h1:nWSwAFPb+qfNJXsoeO3Io7zf4tMSfN8EA8RlDA04GhY=
-google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f h1:2yNACc1O40tTnrsbk9Cv6oxiW8pxI/pXj0wRtdlYmgY=
-google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 h1:DC7wcm+i+P1rN3Ff07vL+OndGg5OhNddHyTA+ocPqYE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM=
+google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
+google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw=
+google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw=
+google.golang.org/genproto/googleapis/api v0.0.0-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.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
-google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
-google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
-google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
-google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+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=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
-gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
-gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
-gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
-gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
-sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
-sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
+sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
+sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
diff --git a/internal/filesystems/map.go b/internal/filesystems/map.go
index e795ed1fe..3ecb34e40 100644
--- a/internal/filesystems/map.go
+++ b/internal/filesystems/map.go
@@ -7,10 +7,10 @@ import (
)
const (
- DefaultFilesystemKey = "default"
+ DefaultFileSystemKey = "default"
)
-var DefaultFilesystem = &wrapperFs{key: DefaultFilesystemKey, FS: OsFS{}}
+var DefaultFileSystem = &wrapperFs{key: DefaultFileSystemKey, FS: OsFS{}}
// wrapperFs exists so can easily add to wrapperFs down the line
type wrapperFs struct {
@@ -18,24 +18,24 @@ type wrapperFs struct {
fs.FS
}
-// FilesystemMap stores a map of filesystems
+// FileSystemMap stores a map of filesystems
// the empty key will be overwritten to be the default key
// it includes a default filesystem, based off the os fs
-type FilesystemMap struct {
+type FileSystemMap struct {
m sync.Map
}
// note that the first invocation of key cannot be called in a racy context.
-func (f *FilesystemMap) key(k string) string {
+func (f *FileSystemMap) key(k string) string {
if k == "" {
- k = DefaultFilesystemKey
+ k = DefaultFileSystemKey
}
return k
}
// Register will add the filesystem with key to later be retrieved
// A call with a nil fs will call unregister, ensuring that a call to Default() will never be nil
-func (f *FilesystemMap) Register(k string, v fs.FS) {
+func (f *FileSystemMap) Register(k string, v fs.FS) {
k = f.key(k)
if v == nil {
f.Unregister(k)
@@ -47,23 +47,23 @@ func (f *FilesystemMap) Register(k string, v fs.FS) {
// Unregister will remove the filesystem with key from the filesystem map
// if the key is the default key, it will set the default to the osFS instead of deleting it
// modules should call this on cleanup to be safe
-func (f *FilesystemMap) Unregister(k string) {
+func (f *FileSystemMap) Unregister(k string) {
k = f.key(k)
- if k == DefaultFilesystemKey {
- f.m.Store(k, DefaultFilesystem)
+ if k == DefaultFileSystemKey {
+ f.m.Store(k, DefaultFileSystem)
} else {
f.m.Delete(k)
}
}
// Get will get a filesystem with a given key
-func (f *FilesystemMap) Get(k string) (v fs.FS, ok bool) {
+func (f *FileSystemMap) Get(k string) (v fs.FS, ok bool) {
k = f.key(k)
c, ok := f.m.Load(strings.TrimSpace(k))
if !ok {
- if k == DefaultFilesystemKey {
- f.m.Store(k, DefaultFilesystem)
- return DefaultFilesystem, true
+ if k == DefaultFileSystemKey {
+ f.m.Store(k, DefaultFileSystem)
+ return DefaultFileSystem, true
}
return nil, ok
}
@@ -71,7 +71,7 @@ func (f *FilesystemMap) Get(k string) (v fs.FS, ok bool) {
}
// Default will get the default filesystem in the filesystem map
-func (f *FilesystemMap) Default() fs.FS {
- val, _ := f.Get(DefaultFilesystemKey)
+func (f *FileSystemMap) Default() fs.FS {
+ val, _ := f.Get(DefaultFileSystemKey)
return val
}
diff --git a/internal/logs.go b/internal/logs.go
new file mode 100644
index 000000000..4ed4a572e
--- /dev/null
+++ b/internal/logs.go
@@ -0,0 +1,22 @@
+package internal
+
+import "fmt"
+
+// MaxSizeSubjectsListForLog returns the keys in the map as a slice of maximum length
+// maxToDisplay. It is useful for logging domains being managed, for example, since a
+// map is typically needed for quick lookup, but a slice is needed for logging, and this
+// can be quite a doozy since there may be a huge amount (hundreds of thousands).
+func MaxSizeSubjectsListForLog(subjects map[string]struct{}, maxToDisplay int) []string {
+ numberOfNamesToDisplay := min(len(subjects), maxToDisplay)
+ domainsToDisplay := make([]string, 0, numberOfNamesToDisplay)
+ for domain := range subjects {
+ domainsToDisplay = append(domainsToDisplay, domain)
+ if len(domainsToDisplay) >= numberOfNamesToDisplay {
+ break
+ }
+ }
+ if len(subjects) > maxToDisplay {
+ domainsToDisplay = append(domainsToDisplay, fmt.Sprintf("(and %d more...)", len(subjects)-maxToDisplay))
+ }
+ return domainsToDisplay
+}
diff --git a/internal/ranges.go b/internal/ranges.go
new file mode 100644
index 000000000..e9429e263
--- /dev/null
+++ b/internal/ranges.go
@@ -0,0 +1,14 @@
+package internal
+
+// PrivateRangesCIDR returns a list of private CIDR range
+// strings, which can be used as a configuration shortcut.
+func PrivateRangesCIDR() []string {
+ return []string{
+ "192.168.0.0/16",
+ "172.16.0.0/12",
+ "10.0.0.0/8",
+ "127.0.0.1/8",
+ "fd00::/8",
+ "::1",
+ }
+}
diff --git a/listen.go b/listen.go
index 34812b54f..1a7051bbf 100644
--- a/listen.go
+++ b/listen.go
@@ -18,7 +18,11 @@ package caddy
import (
"context"
+ "fmt"
"net"
+ "os"
+ "slices"
+ "strconv"
"sync"
"sync/atomic"
"time"
@@ -26,15 +30,54 @@ import (
"go.uber.org/zap"
)
-func reuseUnixSocket(network, addr string) (any, error) {
+func reuseUnixSocket(_, _ string) (any, error) {
return nil, nil
}
func listenReusable(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (any, error) {
- switch network {
- case "udp", "udp4", "udp6", "unixgram":
+ var socketFile *os.File
+
+ fd := slices.Contains([]string{"fd", "fdgram"}, network)
+ if fd {
+ socketFd, err := strconv.ParseUint(address, 0, strconv.IntSize)
+ if err != nil {
+ return nil, fmt.Errorf("invalid file descriptor: %v", err)
+ }
+
+ func() {
+ socketFilesMu.Lock()
+ defer socketFilesMu.Unlock()
+
+ socketFdWide := uintptr(socketFd)
+ var ok bool
+
+ socketFile, ok = socketFiles[socketFdWide]
+
+ if !ok {
+ socketFile = os.NewFile(socketFdWide, lnKey)
+ if socketFile != nil {
+ socketFiles[socketFdWide] = socketFile
+ }
+ }
+ }()
+
+ if socketFile == nil {
+ return nil, fmt.Errorf("invalid socket file descriptor: %d", socketFd)
+ }
+ }
+
+ datagram := slices.Contains([]string{"udp", "udp4", "udp6", "unixgram", "fdgram"}, network)
+ if datagram {
sharedPc, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
- pc, err := config.ListenPacket(ctx, network, address)
+ var (
+ pc net.PacketConn
+ err error
+ )
+ if fd {
+ pc, err = net.FilePacketConn(socketFile)
+ } else {
+ pc, err = config.ListenPacket(ctx, network, address)
+ }
if err != nil {
return nil, err
}
@@ -44,20 +87,27 @@ func listenReusable(ctx context.Context, lnKey string, network, address string,
return nil, err
}
return &fakeClosePacketConn{sharedPacketConn: sharedPc.(*sharedPacketConn)}, nil
+ }
- default:
- sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
- ln, err := config.Listen(ctx, network, address)
- if err != nil {
- return nil, err
- }
- return &sharedListener{Listener: ln, key: lnKey}, nil
- })
+ sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
+ var (
+ ln net.Listener
+ err error
+ )
+ if fd {
+ ln, err = net.FileListener(socketFile)
+ } else {
+ ln, err = config.Listen(ctx, network, address)
+ }
if err != nil {
return nil, err
}
- return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: config.KeepAlive}, nil
+ return &sharedListener{Listener: ln, key: lnKey}, nil
+ })
+ if err != nil {
+ return nil, err
}
+ return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: config.KeepAlive}, nil
}
// fakeCloseListener is a private wrapper over a listener that
@@ -260,3 +310,9 @@ var (
Unwrap() net.PacketConn
}) = (*fakeClosePacketConn)(nil)
)
+
+// socketFiles is a fd -> *os.File map used to make a FileListener/FilePacketConn from a socket file descriptor.
+var socketFiles = map[uintptr]*os.File{}
+
+// socketFilesMu synchronizes socketFiles insertions
+var socketFilesMu sync.Mutex
diff --git a/listen_unix.go b/listen_unix.go
index 9ec65c399..d6ae0cb8e 100644
--- a/listen_unix.go
+++ b/listen_unix.go
@@ -22,10 +22,14 @@ package caddy
import (
"context"
"errors"
+ "fmt"
"io"
"io/fs"
"net"
"os"
+ "slices"
+ "strconv"
+ "sync"
"sync/atomic"
"syscall"
@@ -34,12 +38,9 @@ import (
)
// reuseUnixSocket copies and reuses the unix domain socket (UDS) if we already
-// have it open; if not, unlink it so we can have it. No-op if not a unix network.
+// have it open; if not, unlink it so we can have it.
+// No-op if not a unix network.
func reuseUnixSocket(network, addr string) (any, error) {
- if !IsUnixNetwork(network) {
- return nil, nil
- }
-
socketKey := listenerKey(network, addr)
socket, exists := unixSockets[socketKey]
@@ -71,7 +72,7 @@ func reuseUnixSocket(network, addr string) (any, error) {
return nil, err
}
atomic.AddInt32(unixSocket.count, 1)
- unixSockets[socketKey] = &unixConn{pc.(*net.UnixConn), addr, socketKey, unixSocket.count}
+ unixSockets[socketKey] = &unixConn{pc.(*net.UnixConn), socketKey, unixSocket.count}
}
return unixSockets[socketKey], nil
@@ -89,56 +90,107 @@ func reuseUnixSocket(network, addr string) (any, error) {
return nil, nil
}
+// listenReusable creates a new listener for the given network and address, and adds it to listenerPool.
func listenReusable(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (any, error) {
- // wrap any Control function set by the user so we can also add our reusePort control without clobbering theirs
- oldControl := config.Control
- config.Control = func(network, address string, c syscall.RawConn) error {
- if oldControl != nil {
- if err := oldControl(network, address, c); err != nil {
- return err
- }
- }
- return reusePort(network, address, c)
- }
-
// even though SO_REUSEPORT lets us bind the socket multiple times,
// we still put it in the listenerPool so we can count how many
// configs are using this socket; necessary to ensure we can know
// whether to enforce shutdown delays, for example (see #5393).
- var ln io.Closer
- var err error
- switch network {
- case "udp", "udp4", "udp6", "unixgram":
- ln, err = config.ListenPacket(ctx, network, address)
- default:
- ln, err = config.Listen(ctx, network, address)
+ var (
+ ln io.Closer
+ err error
+ socketFile *os.File
+ )
+
+ fd := slices.Contains([]string{"fd", "fdgram"}, network)
+ if fd {
+ socketFd, err := strconv.ParseUint(address, 0, strconv.IntSize)
+ if err != nil {
+ return nil, fmt.Errorf("invalid file descriptor: %v", err)
+ }
+
+ func() {
+ socketFilesMu.Lock()
+ defer socketFilesMu.Unlock()
+
+ socketFdWide := uintptr(socketFd)
+ var ok bool
+
+ socketFile, ok = socketFiles[socketFdWide]
+
+ if !ok {
+ socketFile = os.NewFile(socketFdWide, lnKey)
+ if socketFile != nil {
+ socketFiles[socketFdWide] = socketFile
+ }
+ }
+ }()
+
+ if socketFile == nil {
+ return nil, fmt.Errorf("invalid socket file descriptor: %d", socketFd)
+ }
+ } else {
+ // wrap any Control function set by the user so we can also add our reusePort control without clobbering theirs
+ oldControl := config.Control
+ config.Control = func(network, address string, c syscall.RawConn) error {
+ if oldControl != nil {
+ if err := oldControl(network, address, c); err != nil {
+ return err
+ }
+ }
+ return reusePort(network, address, c)
+ }
}
+
+ datagram := slices.Contains([]string{"udp", "udp4", "udp6", "unixgram", "fdgram"}, network)
+ if datagram {
+ if fd {
+ ln, err = net.FilePacketConn(socketFile)
+ } else {
+ ln, err = config.ListenPacket(ctx, network, address)
+ }
+ } else {
+ if fd {
+ ln, err = net.FileListener(socketFile)
+ } else {
+ ln, err = config.Listen(ctx, network, address)
+ }
+ }
+
if err == nil {
listenerPool.LoadOrStore(lnKey, nil)
}
- // if new listener is a unix socket, make sure we can reuse it later
- // (we do our own "unlink on close" -- not required, but more tidy)
- one := int32(1)
- if unix, ok := ln.(*net.UnixListener); ok {
- unix.SetUnlinkOnClose(false)
- ln = &unixListener{unix, lnKey, &one}
- unixSockets[lnKey] = ln.(*unixListener)
- }
-
- // TODO: Not 100% sure this is necessary, but we do this for net.UnixListener in listen_unix.go, so...
- if unix, ok := ln.(*net.UnixConn); ok {
- ln = &unixConn{unix, address, lnKey, &one}
- unixSockets[lnKey] = ln.(*unixConn)
- }
-
- // lightly wrap the listener so that when it is closed,
- // we can decrement the usage pool counter
- switch specificLn := ln.(type) {
- case net.Listener:
- return deleteListener{specificLn, lnKey}, err
- case net.PacketConn:
- return deletePacketConn{specificLn, lnKey}, err
+ if datagram {
+ if !fd {
+ // TODO: Not 100% sure this is necessary, but we do this for net.UnixListener, so...
+ if unix, ok := ln.(*net.UnixConn); ok {
+ one := int32(1)
+ ln = &unixConn{unix, lnKey, &one}
+ unixSockets[lnKey] = ln.(*unixConn)
+ }
+ }
+ // lightly wrap the connection so that when it is closed,
+ // we can decrement the usage pool counter
+ if specificLn, ok := ln.(net.PacketConn); ok {
+ ln = deletePacketConn{specificLn, lnKey}
+ }
+ } else {
+ if !fd {
+ // if new listener is a unix socket, make sure we can reuse it later
+ // (we do our own "unlink on close" -- not required, but more tidy)
+ if unix, ok := ln.(*net.UnixListener); ok {
+ unix.SetUnlinkOnClose(false)
+ one := int32(1)
+ ln = &unixListener{unix, lnKey, &one}
+ unixSockets[lnKey] = ln.(*unixListener)
+ }
+ }
+ // lightly wrap the listener so that when it is closed,
+ // we can decrement the usage pool counter
+ if specificLn, ok := ln.(net.Listener); ok {
+ ln = deleteListener{specificLn, lnKey}
+ }
}
// other types, I guess we just return them directly
@@ -170,12 +222,18 @@ type unixListener struct {
func (uln *unixListener) Close() error {
newCount := atomic.AddInt32(uln.count, -1)
if newCount == 0 {
+ file, err := uln.File()
+ var name string
+ if err == nil {
+ name = file.Name()
+ }
defer func() {
- addr := uln.Addr().String()
unixSocketsMu.Lock()
delete(unixSockets, uln.mapKey)
unixSocketsMu.Unlock()
- _ = syscall.Unlink(addr)
+ if err == nil {
+ _ = syscall.Unlink(name)
+ }
}()
}
return uln.UnixListener.Close()
@@ -183,19 +241,25 @@ func (uln *unixListener) Close() error {
type unixConn struct {
*net.UnixConn
- filename string
- mapKey string
- count *int32 // accessed atomically
+ mapKey string
+ count *int32 // accessed atomically
}
func (uc *unixConn) Close() error {
newCount := atomic.AddInt32(uc.count, -1)
if newCount == 0 {
+ file, err := uc.File()
+ var name string
+ if err == nil {
+ name = file.Name()
+ }
defer func() {
unixSocketsMu.Lock()
delete(unixSockets, uc.mapKey)
unixSocketsMu.Unlock()
- _ = syscall.Unlink(uc.filename)
+ if err == nil {
+ _ = syscall.Unlink(name)
+ }
}()
}
return uc.UnixConn.Close()
@@ -211,6 +275,12 @@ var unixSockets = make(map[string]interface {
File() (*os.File, error)
})
+// socketFiles is a fd -> *os.File map used to make a FileListener/FilePacketConn from a socket file descriptor.
+var socketFiles = map[uintptr]*os.File{}
+
+// socketFilesMu synchronizes socketFiles insertions
+var socketFilesMu sync.Mutex
+
// deleteListener is a type that simply deletes itself
// from the listenerPool when it closes. It is used
// solely for the purpose of reference counting (i.e.
diff --git a/listeners.go b/listeners.go
index dd02844dd..9e0057678 100644
--- a/listeners.go
+++ b/listeners.go
@@ -31,6 +31,7 @@ import (
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
+ "github.com/quic-go/quic-go/qlog"
"go.uber.org/zap"
"golang.org/x/time/rate"
@@ -57,11 +58,9 @@ type NetworkAddress struct {
EndPort uint
}
-// ListenAll calls Listen() for all addresses represented by this struct, i.e. all ports in the range.
+// ListenAll calls Listen for all addresses represented by this struct, i.e. all ports in the range.
// (If the address doesn't use ports or has 1 port only, then only 1 listener will be created.)
// It returns an error if any listener failed to bind, and closes any listeners opened up to that point.
-//
-// TODO: Experimental API: subject to change or removal.
func (na NetworkAddress) ListenAll(ctx context.Context, config net.ListenConfig) ([]any, error) {
var listeners []any
var err error
@@ -107,7 +106,8 @@ func (na NetworkAddress) ListenAll(ctx context.Context, config net.ListenConfig)
// portOffset to the start port. (For network types that do not use ports, the
// portOffset is ignored.)
//
-// The provided ListenConfig is used to create the listener. Its Control function,
+// First Listen checks if a plugin can provide a listener from this address. Otherwise,
+// the provided ListenConfig is used to create the listener. Its Control function,
// if set, may be wrapped by an internally-used Control function. The provided
// context may be used to cancel long operations early. The context is not used
// to close the listener after it has been created.
@@ -130,8 +130,8 @@ func (na NetworkAddress) ListenAll(ctx context.Context, config net.ListenConfig)
// Unix sockets will be unlinked before being created, to ensure we can bind to
// it even if the previous program using it exited uncleanly; it will also be
// unlinked upon a graceful exit (or when a new config does not use that socket).
-//
-// TODO: Experimental API: subject to change or removal.
+// Listen synchronizes binds to unix domain sockets to avoid race conditions
+// while an existing socket is unlinked.
func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
if na.IsUnixNetwork() {
unixSocketsMu.Lock()
@@ -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
}
@@ -149,54 +149,53 @@ func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net
func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
var (
- ln any
- err error
- address string
- unixFileMode fs.FileMode
- isAbtractUnixSocket bool
+ ln any
+ err error
+ address string
+ unixFileMode fs.FileMode
)
// split unix socket addr early so lnKey
// is independent of permissions bits
if na.IsUnixNetwork() {
- var err error
address, unixFileMode, err = internal.SplitUnixSocketPermissionsBits(na.Host)
if err != nil {
return nil, err
}
- isAbtractUnixSocket = strings.HasPrefix(address, "@")
+ } else if na.IsFdNetwork() {
+ address = na.Host
} else {
address = na.JoinHostPort(portOffset)
}
- // if this is a unix socket, see if we already have it open,
- // force socket permissions on it and return early
- if socket, err := reuseUnixSocket(na.Network, address); socket != nil || err != nil {
- if !isAbtractUnixSocket {
- if err := os.Chmod(address, unixFileMode); err != nil {
- return nil, fmt.Errorf("unable to set permissions (%s) on %s: %v", unixFileMode, address, err)
- }
- }
- return socket, err
- }
-
- lnKey := listenerKey(na.Network, address)
-
if strings.HasPrefix(na.Network, "ip") {
ln, err = config.ListenPacket(ctx, na.Network, address)
} else {
- ln, err = listenReusable(ctx, lnKey, na.Network, address, config)
+ if na.IsUnixNetwork() {
+ // if this is a unix socket, see if we already have it open
+ ln, err = reuseUnixSocket(na.Network, address)
+ }
+
+ if ln == nil && err == nil {
+ // otherwise, create a new listener
+ lnKey := listenerKey(na.Network, address)
+ ln, err = listenReusable(ctx, lnKey, na.Network, address, config)
+ }
}
+
if err != nil {
return nil, err
}
+
if ln == nil {
return nil, fmt.Errorf("unsupported network type: %s", na.Network)
}
if IsUnixNetwork(na.Network) {
- if !isAbtractUnixSocket {
- if err := os.Chmod(address, unixFileMode); err != nil {
+ isAbstractUnixSocket := strings.HasPrefix(address, "@")
+ if !isAbstractUnixSocket {
+ err = os.Chmod(address, unixFileMode)
+ if err != nil {
return nil, fmt.Errorf("unable to set permissions (%s) on %s: %v", unixFileMode, address, err)
}
}
@@ -211,18 +210,22 @@ func (na NetworkAddress) IsUnixNetwork() bool {
return IsUnixNetwork(na.Network)
}
+// IsFdNetwork returns true if na.Network is
+// fd or fdgram.
+func (na NetworkAddress) IsFdNetwork() bool {
+ return IsFdNetwork(na.Network)
+}
+
// JoinHostPort is like net.JoinHostPort, but where the port
// is StartPort + offset.
func (na NetworkAddress) JoinHostPort(offset uint) string {
- if na.IsUnixNetwork() {
+ if na.IsUnixNetwork() || na.IsFdNetwork() {
return na.Host
}
- return net.JoinHostPort(na.Host, strconv.Itoa(int(na.StartPort+offset)))
+ return net.JoinHostPort(na.Host, strconv.FormatUint(uint64(na.StartPort+offset), 10))
}
// Expand returns one NetworkAddress for each port in the port range.
-//
-// This is EXPERIMENTAL and subject to change or removal.
func (na NetworkAddress) Expand() []NetworkAddress {
size := na.PortRangeSize()
addrs := make([]NetworkAddress, size)
@@ -253,7 +256,7 @@ func (na NetworkAddress) PortRangeSize() uint {
}
func (na NetworkAddress) isLoopback() bool {
- if na.IsUnixNetwork() {
+ if na.IsUnixNetwork() || na.IsFdNetwork() {
return true
}
if na.Host == "localhost" {
@@ -297,6 +300,11 @@ func IsUnixNetwork(netw string) bool {
return strings.HasPrefix(netw, "unix")
}
+// IsFdNetwork returns true if the netw is a fd network.
+func IsFdNetwork(netw string) bool {
+ return strings.HasPrefix(netw, "fd")
+}
+
// 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
@@ -327,6 +335,12 @@ func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort ui
Host: host,
}, err
}
+ if IsFdNetwork(network) {
+ return NetworkAddress{
+ Network: network,
+ Host: host,
+ }, nil
+ }
var start, end uint64
if port == "" {
start = uint64(defaultPort)
@@ -366,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) {
- 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
}
@@ -398,7 +415,7 @@ func JoinNetworkAddress(network, host, port string) string {
if network != "" {
a = network + "/"
}
- if (host != "" && port == "") || IsUnixNetwork(network) {
+ if (host != "" && port == "") || IsUnixNetwork(network) || IsFdNetwork(network) {
a += host
} else if port != "" {
a += net.JoinHostPort(host, port)
@@ -406,9 +423,11 @@ func JoinNetworkAddress(network, host, port string) string {
return a
}
-// ListenQUIC returns a quic.EarlyListener suitable for use in a Caddy module.
-// The network will be transformed into a QUIC-compatible type (if unix, then
-// unixgram will be used; otherwise, udp will be used).
+// ListenQUIC returns a http3.QUICEarlyListener suitable for use in a Caddy module.
+//
+// The network will be transformed into a QUIC-compatible type if the same address can be used with
+// different networks. Currently this just means that for tcp, udp will be used with the same
+// address instead.
//
// NOTE: This API is EXPERIMENTAL and may be changed or removed.
func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config net.ListenConfig, tlsConf *tls.Config) (http3.QUICEarlyListener, error) {
@@ -443,7 +462,13 @@ func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config
Conn: h3ln,
VerifySourceAddress: func(addr net.Addr) bool { return !limiter.Allow() },
}
- earlyLn, err := tr.ListenEarly(http3.ConfigureTLSConfig(quicTlsConfig), &quic.Config{Allow0RTT: true})
+ earlyLn, err := tr.ListenEarly(
+ http3.ConfigureTLSConfig(quicTlsConfig),
+ &quic.Config{
+ Allow0RTT: true,
+ Tracer: qlog.DefaultConnectionTracer,
+ },
+ )
if err != nil {
return nil, err
}
@@ -616,7 +641,8 @@ func RegisterNetwork(network string, getListener ListenerFunc) {
if network == "tcp" || network == "tcp4" || network == "tcp6" ||
network == "udp" || network == "udp4" || network == "udp6" ||
network == "unix" || network == "unixpacket" || network == "unixgram" ||
- strings.HasPrefix("ip:", network) || strings.HasPrefix("ip4:", network) || strings.HasPrefix("ip6:", network) {
+ strings.HasPrefix(network, "ip:") || strings.HasPrefix(network, "ip4:") || strings.HasPrefix(network, "ip6:") ||
+ network == "fd" || network == "fdgram" {
panic("network type " + network + " is reserved")
}
@@ -632,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
@@ -650,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/logging.go b/logging.go
index a076f062c..1a7b0ce29 100644
--- a/logging.go
+++ b/logging.go
@@ -20,6 +20,7 @@ import (
"io"
"log"
"os"
+ "slices"
"strings"
"sync"
"time"
@@ -161,7 +162,9 @@ func (logging *Logging) setupNewDefault(ctx Context) error {
if err != nil {
return fmt.Errorf("setting up default log: %v", err)
}
- newDefault.logger = zap.New(newDefault.CustomLog.core, options...)
+
+ filteringCore := &filteringCore{newDefault.CustomLog.core, newDefault.CustomLog}
+ newDefault.logger = zap.New(filteringCore, options...)
// redirect the default caddy logs
defaultLoggerMu.Lock()
@@ -292,6 +295,10 @@ type BaseLog struct {
// The encoder is how the log entries are formatted or encoded.
EncoderRaw json.RawMessage `json:"encoder,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"`
+ // Tees entries through a zap.Core module which can extract
+ // log entry metadata and fields for further processing.
+ CoreRaw json.RawMessage `json:"core,omitempty" caddy:"namespace=caddy.logging.cores inline_key=module"`
+
// Level is the minimum level to emit, and is inclusive.
// Possible levels: DEBUG, INFO, WARN, ERROR, PANIC, and FATAL
Level string `json:"level,omitempty"`
@@ -366,13 +373,21 @@ func (cl *BaseLog) provisionCommon(ctx Context, logging *Logging) error {
cl.encoder = newDefaultProductionLogEncoder(cl.writerOpener)
}
cl.buildCore()
+ if cl.CoreRaw != nil {
+ mod, err := ctx.LoadModule(cl, "CoreRaw")
+ if err != nil {
+ return fmt.Errorf("loading log core module: %v", err)
+ }
+ core := mod.(zapcore.Core)
+ cl.core = zapcore.NewTee(cl.core, core)
+ }
return nil
}
func (cl *BaseLog) buildCore() {
// logs which only discard their output don't need
// to perform encoding or any other processing steps
- // at all, so just shorcut to a nop core instead
+ // at all, so just shortcut to a nop core instead
if _, ok := cl.writerOpener.(*DiscardWriter); ok {
cl.core = zapcore.NewNopCore()
return
@@ -478,10 +493,8 @@ func (cl *CustomLog) provision(ctx Context, logging *Logging) error {
if len(cl.Include) > 0 && len(cl.Exclude) > 0 {
// prevent intersections
for _, allow := range cl.Include {
- for _, deny := range cl.Exclude {
- if allow == deny {
- return fmt.Errorf("include and exclude must not intersect, but found %s in both lists", allow)
- }
+ if slices.Contains(cl.Exclude, allow) {
+ return fmt.Errorf("include and exclude must not intersect, but found %s in both lists", allow)
}
}
diff --git a/logging_test.go b/logging_test.go
new file mode 100644
index 000000000..293591fbb
--- /dev/null
+++ b/logging_test.go
@@ -0,0 +1,106 @@
+// 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 "testing"
+
+func TestCustomLog_loggerAllowed(t *testing.T) {
+ type fields struct {
+ BaseLog BaseLog
+ Include []string
+ Exclude []string
+ }
+ type args struct {
+ name string
+ isModule bool
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ want bool
+ }{
+ {
+ name: "include",
+ fields: fields{
+ Include: []string{"foo"},
+ },
+ args: args{
+ name: "foo",
+ isModule: true,
+ },
+ want: true,
+ },
+ {
+ name: "exclude",
+ fields: fields{
+ Exclude: []string{"foo"},
+ },
+ args: args{
+ name: "foo",
+ isModule: true,
+ },
+ want: false,
+ },
+ {
+ name: "include and exclude",
+ fields: fields{
+ Include: []string{"foo"},
+ Exclude: []string{"foo"},
+ },
+ args: args{
+ name: "foo",
+ isModule: true,
+ },
+ want: false,
+ },
+ {
+ name: "include and exclude (longer namespace)",
+ fields: fields{
+ Include: []string{"foo.bar"},
+ Exclude: []string{"foo"},
+ },
+ args: args{
+ name: "foo.bar",
+ isModule: true,
+ },
+ want: true,
+ },
+ {
+ name: "excluded module is not printed",
+ fields: fields{
+ Include: []string{"admin.api.load"},
+ Exclude: []string{"admin.api"},
+ },
+ args: args{
+ name: "admin.api",
+ isModule: false,
+ },
+ want: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ cl := &CustomLog{
+ BaseLog: tt.fields.BaseLog,
+ Include: tt.fields.Include,
+ Exclude: tt.fields.Exclude,
+ }
+ if got := cl.loggerAllowed(tt.args.name, tt.args.isModule); got != tt.want {
+ t.Errorf("CustomLog.loggerAllowed() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/metrics.go b/metrics.go
index 0f8ea03cb..0ee3853eb 100644
--- a/metrics.go
+++ b/metrics.go
@@ -4,30 +4,33 @@ import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
- "github.com/prometheus/client_golang/prometheus/collectors"
- "github.com/prometheus/client_golang/prometheus/promauto"
"github.com/caddyserver/caddy/v2/internal/metrics"
)
// define and register the metrics used in this package.
func init() {
- prometheus.MustRegister(collectors.NewBuildInfoCollector())
-
const ns, sub = "caddy", "admin"
-
- adminMetrics.requestCount = promauto.NewCounterVec(prometheus.CounterOpts{
+ adminMetrics.requestCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: ns,
Subsystem: sub,
Name: "http_requests_total",
Help: "Counter of requests made to the Admin API's HTTP endpoints.",
}, []string{"handler", "path", "code", "method"})
- adminMetrics.requestErrors = promauto.NewCounterVec(prometheus.CounterOpts{
+ adminMetrics.requestErrors = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: ns,
Subsystem: sub,
Name: "http_request_errors_total",
Help: "Number of requests resulting in middleware errors.",
}, []string{"handler", "path", "method"})
+ globalMetrics.configSuccess = prometheus.NewGauge(prometheus.GaugeOpts{
+ Name: "caddy_config_last_reload_successful",
+ Help: "Whether the last configuration reload attempt was successful.",
+ })
+ globalMetrics.configSuccessTime = prometheus.NewGauge(prometheus.GaugeOpts{
+ Name: "caddy_config_last_reload_success_timestamp_seconds",
+ Help: "Timestamp of the last successful configuration reload.",
+ })
}
// adminMetrics is a collection of metrics that can be tracked for the admin API.
@@ -36,6 +39,12 @@ var adminMetrics = struct {
requestErrors *prometheus.CounterVec
}{}
+// globalMetrics is a collection of metrics that can be tracked for Caddy global state
+var globalMetrics = struct {
+ configSuccess prometheus.Gauge
+ configSuccessTime prometheus.Gauge
+}{}
+
// Similar to promhttp.InstrumentHandlerCounter, but upper-cases method names
// instead of lower-casing them.
//
diff --git a/modules.go b/modules.go
index 470c25e37..37b56a988 100644
--- a/modules.go
+++ b/modules.go
@@ -18,6 +18,8 @@ import (
"bytes"
"encoding/json"
"fmt"
+ "net/http"
+ "net/url"
"reflect"
"sort"
"strings"
@@ -360,6 +362,14 @@ func isModuleMapType(typ reflect.Type) bool {
isJSONRawMessage(typ.Elem())
}
+// ProxyFuncProducer is implemented by modules which produce a
+// function that returns a URL to use as network proxy. Modules
+// in the namespace `caddy.network_proxy` must implement this
+// interface.
+type ProxyFuncProducer interface {
+ ProxyFunc() func(*http.Request) (*url.URL, error)
+}
+
var (
modules = make(map[string]ModuleInfo)
modulesMu sync.RWMutex
diff --git a/modules/caddyevents/app.go b/modules/caddyevents/app.go
index 1684cfd2a..9fc8fa8ed 100644
--- a/modules/caddyevents/app.go
+++ b/modules/caddyevents/app.go
@@ -20,9 +20,7 @@ import (
"errors"
"fmt"
"strings"
- "time"
- "github.com/google/uuid"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
@@ -124,18 +122,19 @@ func (app *App) Provision(ctx caddy.Context) error {
app.subscriptions = make(map[string]map[caddy.ModuleID][]Handler)
for _, sub := range app.Subscriptions {
- if sub.HandlersRaw != nil {
- handlersIface, err := ctx.LoadModule(sub, "HandlersRaw")
- if err != nil {
- return fmt.Errorf("loading event subscriber modules: %v", err)
- }
- for _, h := range handlersIface.([]any) {
- sub.Handlers = append(sub.Handlers, h.(Handler))
- }
- if len(sub.Handlers) == 0 {
- // pointless to bind without any handlers
- return fmt.Errorf("no handlers defined")
- }
+ if sub.HandlersRaw == nil {
+ continue
+ }
+ handlersIface, err := ctx.LoadModule(sub, "HandlersRaw")
+ if err != nil {
+ return fmt.Errorf("loading event subscriber modules: %v", err)
+ }
+ for _, h := range handlersIface.([]any) {
+ sub.Handlers = append(sub.Handlers, h.(Handler))
+ }
+ if len(sub.Handlers) == 0 {
+ // pointless to bind without any handlers
+ return fmt.Errorf("no handlers defined")
}
}
@@ -205,27 +204,26 @@ func (app *App) On(eventName string, handler Handler) error {
//
// Note that the data map is not copied, for efficiency. After Emit() is called, the
// data passed in should not be changed in other goroutines.
-func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) Event {
+func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) caddy.Event {
logger := app.logger.With(zap.String("name", eventName))
- id, err := uuid.NewRandom()
+ e, err := caddy.NewEvent(ctx, eventName, data)
if err != nil {
- logger.Error("failed generating new event ID", zap.Error(err))
+ logger.Error("failed to create event", zap.Error(err))
}
- eventName = strings.ToLower(eventName)
-
- e := Event{
- Data: data,
- id: id,
- ts: time.Now(),
- name: eventName,
- origin: ctx.Module(),
+ var originModule caddy.ModuleInfo
+ var originModuleID caddy.ModuleID
+ var originModuleName string
+ if origin := e.Origin(); origin != nil {
+ originModule = origin.CaddyModule()
+ originModuleID = originModule.ID
+ originModuleName = originModule.String()
}
logger = logger.With(
- zap.String("id", e.id.String()),
- zap.String("origin", e.origin.CaddyModule().String()))
+ zap.String("id", e.ID().String()),
+ zap.String("origin", originModuleName))
// add event info to replacer, make sure it's in the context
repl, ok := ctx.Context.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
@@ -238,15 +236,15 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E
case "event":
return e, true
case "event.id":
- return e.id, true
+ return e.ID(), true
case "event.name":
- return e.name, true
+ return e.Name(), true
case "event.time":
- return e.ts, true
+ return e.Timestamp(), true
case "event.time_unix":
- return e.ts.UnixMilli(), true
+ return e.Timestamp().UnixMilli(), true
case "event.module":
- return e.origin.CaddyModule().ID, true
+ return originModuleID, true
case "event.data":
return e.Data, true
}
@@ -261,12 +259,14 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E
return nil, false
})
- logger.Debug("event", zap.Any("data", e.Data))
+ logger = logger.WithLazy(zap.Any("data", e.Data))
+
+ logger.Debug("event")
// invoke handlers bound to the event by name and also all events; this for loop
// iterates twice at most: once for the event name, once for "" (all events)
for {
- moduleID := e.origin.CaddyModule().ID
+ moduleID := originModuleID
// implement propagation up the module tree (i.e. start with "a.b.c" then "a.b" then "a" then "")
for {
@@ -282,8 +282,14 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E
default:
}
+ // this log can be a useful sanity check to ensure your handlers are in fact being invoked
+ // (see https://github.com/mholt/caddy-events-exec/issues/6)
+ logger.Debug("invoking subscribed handler",
+ zap.String("subscribed_to", eventName),
+ zap.Any("handler", handler))
+
if err := handler.Handle(ctx, e); err != nil {
- aborted := errors.Is(err, ErrAborted)
+ aborted := errors.Is(err, caddy.ErrEventAborted)
logger.Error("handler error",
zap.Error(err),
@@ -317,71 +323,9 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E
return e
}
-// Event represents something that has happened or is happening.
-// An Event value is not synchronized, so it should be copied if
-// being used in goroutines.
-//
-// EXPERIMENTAL: As with the rest of this package, events are
-// subject to change.
-type Event struct {
- // If non-nil, the event has been aborted, meaning
- // propagation has stopped to other handlers and
- // the code should stop what it was doing. Emitters
- // may choose to use this as a signal to adjust their
- // code path appropriately.
- Aborted error
-
- // The data associated with the event. Usually the
- // original emitter will be the only one to set or
- // change these values, but the field is exported
- // so handlers can have full access if needed.
- // However, this map is not synchronized, so
- // handlers must not use this map directly in new
- // goroutines; instead, copy the map to use it in a
- // goroutine.
- Data map[string]any
-
- id uuid.UUID
- ts time.Time
- name string
- origin caddy.Module
-}
-
-// CloudEvent exports event e as a structure that, when
-// serialized as JSON, is compatible with the
-// CloudEvents spec.
-func (e Event) CloudEvent() CloudEvent {
- dataJSON, _ := json.Marshal(e.Data)
- return CloudEvent{
- ID: e.id.String(),
- Source: e.origin.CaddyModule().String(),
- SpecVersion: "1.0",
- Type: e.name,
- Time: e.ts,
- DataContentType: "application/json",
- Data: dataJSON,
- }
-}
-
-// CloudEvent is a JSON-serializable structure that
-// is compatible with the CloudEvents specification.
-// See https://cloudevents.io.
-type CloudEvent struct {
- ID string `json:"id"`
- Source string `json:"source"`
- SpecVersion string `json:"specversion"`
- Type string `json:"type"`
- Time time.Time `json:"time"`
- DataContentType string `json:"datacontenttype,omitempty"`
- Data json.RawMessage `json:"data,omitempty"`
-}
-
-// ErrAborted cancels an event.
-var ErrAborted = errors.New("event aborted")
-
// Handler is a type that can handle events.
type Handler interface {
- Handle(context.Context, Event) error
+ Handle(context.Context, caddy.Event) error
}
// Interface guards
diff --git a/modules/caddyfs/filesystem.go b/modules/caddyfs/filesystem.go
index b3361df20..2ec43079a 100644
--- a/modules/caddyfs/filesystem.go
+++ b/modules/caddyfs/filesystem.go
@@ -69,11 +69,11 @@ func (xs *Filesystems) Provision(ctx caddy.Context) error {
}
// register that module
ctx.Logger().Debug("registering fs", zap.String("fs", f.Key))
- ctx.Filesystems().Register(f.Key, f.fileSystem)
+ ctx.FileSystems().Register(f.Key, f.fileSystem)
// remember to unregister the module when we are done
xs.defers = append(xs.defers, func() {
- ctx.Logger().Debug("registering fs", zap.String("fs", f.Key))
- ctx.Filesystems().Unregister(f.Key)
+ ctx.Logger().Debug("unregistering fs", zap.String("fs", f.Key))
+ ctx.FileSystems().Unregister(f.Key)
})
}
return nil
diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go
index cdb368c4f..b550904e2 100644
--- a/modules/caddyhttp/app.go
+++ b/modules/caddyhttp/app.go
@@ -15,9 +15,11 @@
package caddyhttp
import (
+ "cmp"
"context"
"crypto/tls"
"fmt"
+ "maps"
"net"
"net/http"
"strconv"
@@ -68,7 +70,10 @@ func init() {
// `{http.request.orig_uri.query}` | The request's original query string (without `?`)
// `{http.request.port}` | The port part of the request's Host header
// `{http.request.proto}` | The protocol of the request
-// `{http.request.remote.host}` | The host (IP) part of the remote client's address
+// `{http.request.local.host}` | The host (IP) part of the local address the connection arrived on
+// `{http.request.local.port}` | The port part of the local address the connection arrived on
+// `{http.request.local}` | The local address the connection arrived on
+// `{http.request.remote.host}` | The host (IP) part of the remote client's address, if available (not known with HTTP/3 early data)
// `{http.request.remote.port}` | The port part of the remote client's address
// `{http.request.remote}` | The address of the remote client
// `{http.request.scheme}` | The request scheme, typically `http` or `https`
@@ -138,12 +143,16 @@ 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
// used temporarily between phases 1 and 2 of auto HTTPS
- allCertDomains []string
+ allCertDomains map[string]struct{}
}
// CaddyModule returns the Caddy module information.
@@ -180,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 {
@@ -192,15 +205,21 @@ 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(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")
- }
-
- // the Go standard library does not let us serve only HTTP/2 using
- // http.Server; we would probably need to write our own server
- if !srv.protocol("h1") && (srv.protocol("h2") || srv.protocol("h2c")) {
- return fmt.Errorf("server %s: cannot enable HTTP/2 or H2C without enabling HTTP/1.1; add h1 to protocols or remove h2/h2c", srvName)
+ if srv.Logs.Trace {
+ srv.traceLogger = app.logger.Named("log.trace")
+ }
}
// if no protocols configured explicitly, enable all except h2c
@@ -208,6 +227,70 @@ func (app *App) Provision(ctx caddy.Context) error {
srv.Protocols = []string{"h1", "h2", "h3"}
}
+ srvProtocolsUnique := map[string]struct{}{}
+ for _, srvProtocol := range srv.Protocols {
+ srvProtocolsUnique[srvProtocol] = struct{}{}
+ }
+ _, h1ok := srvProtocolsUnique["h1"]
+ _, h2ok := srvProtocolsUnique["h2"]
+ _, h2cok := srvProtocolsUnique["h2c"]
+
+ // the Go standard library does not let us serve only HTTP/2 using
+ // http.Server; we would probably need to write our own server
+ if !h1ok && (h2ok || h2cok) {
+ return fmt.Errorf("server %s: cannot enable HTTP/2 or H2C without enabling HTTP/1.1; add h1 to protocols or remove h2/h2c", srvName)
+ }
+
+ if srv.ListenProtocols != nil {
+ if len(srv.ListenProtocols) != len(srv.Listen) {
+ return fmt.Errorf("server %s: listener protocols count does not match address count: %d != %d",
+ srvName, len(srv.ListenProtocols), len(srv.Listen))
+ }
+
+ for i, lnProtocols := range srv.ListenProtocols {
+ if lnProtocols != nil {
+ // populate empty listen protocols with server protocols
+ lnProtocolsDefault := false
+ var lnProtocolsInclude []string
+ srvProtocolsInclude := maps.Clone(srvProtocolsUnique)
+
+ // keep existing listener protocols unless they are empty
+ for _, lnProtocol := range lnProtocols {
+ if lnProtocol == "" {
+ lnProtocolsDefault = true
+ } else {
+ lnProtocolsInclude = append(lnProtocolsInclude, lnProtocol)
+ delete(srvProtocolsInclude, lnProtocol)
+ }
+ }
+
+ // append server protocols to listener protocols if any listener protocols were empty
+ if lnProtocolsDefault {
+ for _, srvProtocol := range srv.Protocols {
+ if _, ok := srvProtocolsInclude[srvProtocol]; ok {
+ lnProtocolsInclude = append(lnProtocolsInclude, srvProtocol)
+ }
+ }
+ }
+
+ lnProtocolsIncludeUnique := map[string]struct{}{}
+ for _, lnProtocol := range lnProtocolsInclude {
+ lnProtocolsIncludeUnique[lnProtocol] = struct{}{}
+ }
+ _, h1ok := lnProtocolsIncludeUnique["h1"]
+ _, h2ok := lnProtocolsIncludeUnique["h2"]
+ _, h2cok := lnProtocolsIncludeUnique["h2c"]
+
+ // check if any listener protocols contain h2 or h2c without h1
+ if !h1ok && (h2ok || h2cok) {
+ return fmt.Errorf("server %s, listener %d: cannot enable HTTP/2 or H2C without enabling HTTP/1.1; add h1 to protocols or remove h2/h2c", srvName, i)
+ }
+
+ srv.ListenProtocols[i] = lnProtocolsInclude
+ }
+ }
+ }
+
// if not explicitly configured by the user, disallow TLS
// client auth bypass (domain fronting) which could
// otherwise be exploited by sending an unprotected SNI
@@ -277,12 +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 {
- 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)
}
@@ -301,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)
}
@@ -319,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
@@ -326,9 +411,10 @@ func (app *App) Provision(ctx caddy.Context) error {
// Validate ensures the app's configuration is valid.
func (app *App) Validate() error {
- // each server must use distinct listener addresses
lnAddrs := make(map[string]string)
+
for srvName, srv := range app.Servers {
+ // each server must use distinct listener addresses
for _, addr := range srv.Listen {
listenAddr, err := caddy.ParseNetworkAddress(addr)
if err != nil {
@@ -337,13 +423,22 @@ func (app *App) Validate() error {
// check that every address in the port range is unique to this server;
// we do not use <= here because PortRangeSize() adds 1 to EndPort for us
for i := uint(0); i < listenAddr.PortRangeSize(); i++ {
- addr := caddy.JoinNetworkAddress(listenAddr.Network, listenAddr.Host, strconv.Itoa(int(listenAddr.StartPort+i)))
+ addr := caddy.JoinNetworkAddress(listenAddr.Network, listenAddr.Host, strconv.FormatUint(uint64(listenAddr.StartPort+i), 10))
if sn, ok := lnAddrs[addr]; ok {
return fmt.Errorf("server %s: listener address repeated: %s (already claimed by server '%s')", srvName, addr, sn)
}
lnAddrs[addr] = srvName
}
}
+
+ // logger names must not have ports
+ if srv.Logs != nil {
+ for host := range srv.Logs.LoggerNames {
+ if _, _, err := net.SplitHostPort(host); err == nil {
+ return fmt.Errorf("server %s: logger name must not have a port: %s", srvName, host)
+ }
+ }
+ }
}
return nil
}
@@ -406,99 +501,130 @@ func (app *App) Start() error {
srv.server.Handler = h2c.NewHandler(srv, h2server)
}
- for _, lnAddr := range srv.Listen {
+ for lnIndex, lnAddr := range srv.Listen {
listenAddr, err := caddy.ParseNetworkAddress(lnAddr)
if err != nil {
return fmt.Errorf("%s: parsing listen address '%s': %v", srvName, lnAddr, err)
}
+
srv.addresses = append(srv.addresses, listenAddr)
- for portOffset := uint(0); portOffset < listenAddr.PortRangeSize(); portOffset++ {
- // create the listener for this socket
- hostport := listenAddr.JoinHostPort(portOffset)
- lnAny, err := listenAddr.Listen(app.ctx, portOffset, net.ListenConfig{KeepAlive: time.Duration(srv.KeepAliveInterval)})
- if err != nil {
- return fmt.Errorf("listening on %s: %v", listenAddr.At(portOffset), err)
- }
- ln := lnAny.(net.Listener)
+ protocols := srv.Protocols
+ if srv.ListenProtocols != nil && srv.ListenProtocols[lnIndex] != nil {
+ protocols = srv.ListenProtocols[lnIndex]
+ }
- // wrap listener before TLS (up to the TLS placeholder wrapper)
- var lnWrapperIdx int
- for i, lnWrapper := range srv.listenerWrappers {
- if _, ok := lnWrapper.(*tlsPlaceholderWrapper); ok {
- lnWrapperIdx = i + 1 // mark the next wrapper's spot
- break
- }
- ln = lnWrapper.WrapListener(ln)
- }
+ protocolsUnique := map[string]struct{}{}
+ for _, protocol := range protocols {
+ protocolsUnique[protocol] = struct{}{}
+ }
+ _, h1ok := protocolsUnique["h1"]
+ _, h2ok := protocolsUnique["h2"]
+ _, h2cok := protocolsUnique["h2c"]
+ _, h3ok := protocolsUnique["h3"]
+
+ for portOffset := uint(0); portOffset < listenAddr.PortRangeSize(); portOffset++ {
+ hostport := listenAddr.JoinHostPort(portOffset)
// enable TLS if there is a policy and if this is not the HTTP port
useTLS := len(srv.TLSConnPolicies) > 0 && int(listenAddr.StartPort+portOffset) != app.httpPort()
- if useTLS {
- // create TLS listener - this enables and terminates TLS
- ln = tls.NewListener(ln, tlsCfg)
- // enable HTTP/3 if configured
- if srv.protocol("h3") {
- // Can't serve HTTP/3 on the same socket as HTTP/1 and 2 because it uses
- // a different transport mechanism... which is fine, but the OS doesn't
- // differentiate between a SOCK_STREAM file and a SOCK_DGRAM file; they
- // are still one file on the system. So even though "unixpacket" and
- // "unixgram" are different network types just as "tcp" and "udp" are,
- // the OS will not let us use the same file as both STREAM and DGRAM.
- if len(srv.Protocols) > 1 && listenAddr.IsUnixNetwork() {
- app.logger.Warn("HTTP/3 disabled because Unix can't multiplex STREAM and DGRAM on same socket",
- zap.String("file", hostport))
- for i := range srv.Protocols {
- if srv.Protocols[i] == "h3" {
- srv.Protocols = append(srv.Protocols[:i], srv.Protocols[i+1:]...)
- break
- }
- }
- } else {
- app.logger.Info("enabling HTTP/3 listener", zap.String("addr", hostport))
- if err := srv.serveHTTP3(listenAddr.At(portOffset), tlsCfg); err != nil {
- return err
- }
+ if h1ok || h2ok && useTLS || h2cok {
+ // create the listener for this socket
+ lnAny, err := listenAddr.Listen(app.ctx, portOffset, net.ListenConfig{KeepAlive: time.Duration(srv.KeepAliveInterval)})
+ if err != nil {
+ return fmt.Errorf("listening on %s: %v", listenAddr.At(portOffset), err)
+ }
+ ln, ok := lnAny.(net.Listener)
+ if !ok {
+ return fmt.Errorf("network '%s' cannot handle HTTP/1 or HTTP/2 connections", listenAddr.Network)
+ }
+
+ // wrap listener before TLS (up to the TLS placeholder wrapper)
+ var lnWrapperIdx int
+ for i, lnWrapper := range srv.listenerWrappers {
+ if _, ok := lnWrapper.(*tlsPlaceholderWrapper); ok {
+ lnWrapperIdx = i + 1 // mark the next wrapper's spot
+ break
}
+ ln = lnWrapper.WrapListener(ln)
+ }
+
+ if useTLS {
+ // create TLS listener - this enables and terminates TLS
+ ln = tls.NewListener(ln, tlsCfg)
+ }
+
+ // finish wrapping listener where we left off before TLS
+ for i := lnWrapperIdx; i < len(srv.listenerWrappers); i++ {
+ ln = srv.listenerWrappers[i].WrapListener(ln)
+ }
+
+ // handle http2 if use tls listener wrapper
+ if h2ok {
+ http2lnWrapper := &http2Listener{
+ Listener: ln,
+ server: srv.server,
+ h2server: h2server,
+ }
+ srv.h2listeners = append(srv.h2listeners, http2lnWrapper)
+ ln = http2lnWrapper
+ }
+
+ // if binding to port 0, the OS chooses a port for us;
+ // but the user won't know the port unless we print it
+ if !listenAddr.IsUnixNetwork() && !listenAddr.IsFdNetwork() && listenAddr.StartPort == 0 && listenAddr.EndPort == 0 {
+ app.logger.Info("port 0 listener",
+ zap.String("input_address", lnAddr),
+ zap.String("actual_address", ln.Addr().String()))
+ }
+
+ app.logger.Debug("starting server loop",
+ zap.String("address", ln.Addr().String()),
+ zap.Bool("tls", useTLS),
+ zap.Bool("http3", srv.h3server != nil))
+
+ srv.listeners = append(srv.listeners, ln)
+
+ // enable HTTP/1 if configured
+ if h1ok {
+ //nolint:errcheck
+ go srv.server.Serve(ln)
}
}
- // finish wrapping listener where we left off before TLS
- for i := lnWrapperIdx; i < len(srv.listenerWrappers); i++ {
- ln = srv.listenerWrappers[i].WrapListener(ln)
+ if h2ok && !useTLS {
+ // Can only serve h2 with TLS enabled
+ app.logger.Warn("HTTP/2 skipped because it requires TLS",
+ zap.String("network", listenAddr.Network),
+ zap.String("addr", hostport))
}
- // handle http2 if use tls listener wrapper
- if useTLS {
- http2lnWrapper := &http2Listener{
- Listener: ln,
- server: srv.server,
- h2server: h2server,
+ if h3ok {
+ // Can't serve HTTP/3 on the same socket as HTTP/1 and 2 because it uses
+ // a different transport mechanism... which is fine, but the OS doesn't
+ // differentiate between a SOCK_STREAM file and a SOCK_DGRAM file; they
+ // are still one file on the system. So even though "unixpacket" and
+ // "unixgram" are different network types just as "tcp" and "udp" are,
+ // the OS will not let us use the same file as both STREAM and DGRAM.
+ if listenAddr.IsUnixNetwork() {
+ app.logger.Warn("HTTP/3 disabled because Unix can't multiplex STREAM and DGRAM on same socket",
+ zap.String("file", hostport))
+ continue
}
- srv.h2listeners = append(srv.h2listeners, http2lnWrapper)
- ln = http2lnWrapper
- }
- // if binding to port 0, the OS chooses a port for us;
- // but the user won't know the port unless we print it
- if !listenAddr.IsUnixNetwork() && listenAddr.StartPort == 0 && listenAddr.EndPort == 0 {
- app.logger.Info("port 0 listener",
- zap.String("input_address", lnAddr),
- zap.String("actual_address", ln.Addr().String()))
- }
-
- app.logger.Debug("starting server loop",
- zap.String("address", ln.Addr().String()),
- zap.Bool("tls", useTLS),
- zap.Bool("http3", srv.h3server != nil))
-
- srv.listeners = append(srv.listeners, ln)
-
- // enable HTTP/1 if configured
- if srv.protocol("h1") {
- //nolint:errcheck
- go srv.server.Serve(ln)
+ if useTLS {
+ // enable HTTP/3 if configured
+ app.logger.Info("enabling HTTP/3 listener", zap.String("addr", hostport))
+ if err := srv.serveHTTP3(listenAddr.At(portOffset), tlsCfg); err != nil {
+ return err
+ }
+ } else {
+ // Can only serve h3 with TLS enabled
+ app.logger.Warn("HTTP/3 skipped because it requires TLS",
+ zap.String("network", listenAddr.Network),
+ zap.String("addr", hostport))
+ }
}
}
}
@@ -523,7 +649,7 @@ func (app *App) Stop() error {
ctx := context.Background()
// see if any listeners in our config will be closing or if they are continuing
- // hrough a reload; because if any are closing, we will enforce shutdown delay
+ // through a reload; because if any are closing, we will enforce shutdown delay
var delay bool
scheduledTime := time.Now().Add(time.Duration(app.ShutdownDelay))
if app.ShutdownDelay > 0 {
@@ -591,16 +717,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))
@@ -668,11 +785,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/autohttps.go b/modules/caddyhttp/autohttps.go
index aec43c7c7..ce8e6f8df 100644
--- a/modules/caddyhttp/autohttps.go
+++ b/modules/caddyhttp/autohttps.go
@@ -17,6 +17,7 @@ package caddyhttp
import (
"fmt"
"net/http"
+ "slices"
"strconv"
"strings"
@@ -24,6 +25,7 @@ import (
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/internal"
"github.com/caddyserver/caddy/v2/modules/caddytls"
)
@@ -66,17 +68,6 @@ type AutoHTTPSConfig struct {
IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"`
}
-// Skipped returns true if name is in skipSlice, which
-// should be either the Skip or SkipCerts field on ahc.
-func (ahc AutoHTTPSConfig) Skipped(name string, skipSlice []string) bool {
- for _, n := range skipSlice {
- if name == n {
- return true
- }
- }
- return false
-}
-
// automaticHTTPSPhase1 provisions all route matchers, determines
// which domain names found in the routes qualify for automatic
// HTTPS, and sets up HTTP->HTTPS redirects. This phase must occur
@@ -117,7 +108,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
srv.AutoHTTPS = new(AutoHTTPSConfig)
}
if srv.AutoHTTPS.Disabled {
- logger.Warn("automatic HTTPS is completely disabled for server", zap.String("server_name", srvName))
+ logger.Info("automatic HTTPS is completely disabled for server", zap.String("server_name", srvName))
continue
}
@@ -158,7 +149,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
return fmt.Errorf("%s: route %d, matcher set %d, matcher %d, host matcher %d: %v",
srvName, routeIdx, matcherSetIdx, matcherIdx, hostMatcherIdx, err)
}
- if !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.Skip) {
+ if !slices.Contains(srv.AutoHTTPS.Skip, d) {
serverDomainSet[d] = struct{}{}
}
}
@@ -167,6 +158,14 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
}
}
+ // build the list of domains that could be used with ECH (if enabled)
+ // so the TLS app can know to publish ECH configs for them
+ echDomains := make([]string, 0, len(serverDomainSet))
+ for d := range serverDomainSet {
+ echDomains = append(echDomains, d)
+ }
+ app.tlsApp.RegisterServerNames(echDomains)
+
// nothing more to do here if there are no domains that qualify for
// automatic HTTPS and there are no explicit TLS connection policies:
// if there is at least one domain but no TLS conn policy (F&&T), we'll
@@ -193,7 +192,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
} else {
for d := range serverDomainSet {
if certmagic.SubjectQualifiesForCert(d) &&
- !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) {
+ !slices.Contains(srv.AutoHTTPS.SkipCerts, d) {
// if a certificate for this name is already loaded,
// don't obtain another one for it, unless we are
// supposed to ignore loaded certificates
@@ -225,7 +224,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
// nothing left to do if auto redirects are disabled
if srv.AutoHTTPS.DisableRedir {
- logger.Warn("automatic HTTP->HTTPS redirects are disabled", zap.String("server_name", srvName))
+ logger.Info("automatic HTTP->HTTPS redirects are disabled", zap.String("server_name", srvName))
continue
}
@@ -260,25 +259,16 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
// port, we'll have to choose one, so prefer the HTTPS port
if _, ok := redirDomains[d]; !ok ||
addr.StartPort == uint(app.httpsPort()) {
- redirDomains[d] = []caddy.NetworkAddress{addr}
+ redirDomains[d] = append(redirDomains[d], addr)
}
}
}
}
- // we now have a list of all the unique names for which we need certs;
- // turn the set into a slice so that phase 2 can use it
- app.allCertDomains = make([]string, 0, len(uniqueDomainsForCerts))
+ // we now have a list of all the unique names for which we need certs
var internal, tailscale []string
uniqueDomainsLoop:
for d := range uniqueDomainsForCerts {
- if !isTailscaleDomain(d) {
- // whether or not there is already an automation policy for this
- // name, we should add it to the list to manage a cert for it,
- // unless it's a Tailscale domain, because we don't manage those
- app.allCertDomains = append(app.allCertDomains, d)
- }
-
// some names we've found might already have automation policies
// explicitly specified for them; we should exclude those from
// our hidden/implicit policy, since applying a name to more than
@@ -287,17 +277,38 @@ uniqueDomainsLoop:
for _, ap := range app.tlsApp.Automation.Policies {
for _, apHost := range ap.Subjects() {
if apHost == d {
+ // if the automation policy has all internal subjects but no issuers,
+ // it will default to CertMagic's issuers which are public CAs; use
+ // our internal issuer instead
+ if len(ap.Issuers) == 0 && ap.AllInternalSubjects() {
+ iss := new(caddytls.InternalIssuer)
+ if err := iss.Provision(ctx); err != nil {
+ return err
+ }
+ ap.Issuers = append(ap.Issuers, iss)
+ }
continue uniqueDomainsLoop
}
}
}
}
- // if no automation policy exists for the name yet, we
- // will associate it with an implicit one
+ // if no automation policy exists for the name yet, we will associate it with an implicit one;
+ // we handle tailscale domains specially, and we also separate out identifiers that need the
+ // internal issuer (self-signed certs); certmagic does not consider public IP addresses to be
+ // disqualified for public certs, because there are public CAs that will issue certs for IPs.
+ // However, with auto-HTTPS, many times there is no issuer explicitly defined, and the default
+ // issuers do not (currently, as of 2024) issue IP certificates; so assign all IP subjects to
+ // the internal issuer when there are no explicit automation policies
+ shouldUseInternal := func(ident string) bool {
+ usingDefaultIssuersAndIsIP := certmagic.SubjectIsIP(ident) &&
+ (app.tlsApp == nil || app.tlsApp.Automation == nil || len(app.tlsApp.Automation.Policies) == 0)
+ return !certmagic.SubjectQualifiesForPublicCert(d) || usingDefaultIssuersAndIsIP
+ }
if isTailscaleDomain(d) {
tailscale = append(tailscale, d)
- } else if !certmagic.SubjectQualifiesForPublicCert(d) {
+ delete(uniqueDomainsForCerts, d) // not managed by us; handled separately
+ } else if shouldUseInternal(d) {
internal = append(internal, d)
}
}
@@ -426,6 +437,9 @@ redirServersLoop:
}
}
+ // persist the domains/IPs we're managing certs for through provisioning/startup
+ app.allCertDomains = uniqueDomainsForCerts
+
logger.Debug("adjusted config",
zap.Reflect("tls", app.tlsApp),
zap.Reflect("http", app))
@@ -728,11 +742,11 @@ func (app *App) automaticHTTPSPhase2() error {
return nil
}
app.logger.Info("enabling automatic TLS certificate management",
- zap.Strings("domains", app.allCertDomains),
+ zap.Strings("domains", internal.MaxSizeSubjectsListForLog(app.allCertDomains, 1000)),
)
err := app.tlsApp.Manage(app.allCertDomains)
if err != nil {
- return fmt.Errorf("managing certificates for %v: %s", app.allCertDomains, err)
+ return fmt.Errorf("managing certificates for %d domains: %s", len(app.allCertDomains), err)
}
app.allCertDomains = nil // no longer needed; allow GC to deallocate
return nil
diff --git a/modules/caddyhttp/caddyauth/caddyauth.go b/modules/caddyhttp/caddyauth/caddyauth.go
index c60de880b..69db62a5c 100644
--- a/modules/caddyhttp/caddyauth/caddyauth.go
+++ b/modules/caddyhttp/caddyauth/caddyauth.go
@@ -19,6 +19,7 @@ import (
"net/http"
"go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
@@ -36,6 +37,10 @@ func init() {
// `{http.auth.user.*}` placeholders may be set for any authentication
// modules that provide user metadata.
//
+// In case of an error, the placeholder `{http.auth..error}`
+// will be set to the error message returned by the authentication
+// provider.
+//
// Its API is still experimental and may be subject to change.
type Authentication struct {
// A set of authentication providers. If none are specified,
@@ -70,15 +75,19 @@ func (a *Authentication) Provision(ctx caddy.Context) error {
}
func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
+ repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
var user User
var authed bool
var err error
for provName, prov := range a.Providers {
user, authed, err = prov.Authenticate(w, r)
if err != nil {
- a.logger.Error("auth provider returned error",
- zap.String("provider", provName),
- zap.Error(err))
+ if c := a.logger.Check(zapcore.ErrorLevel, "auth provider returned error"); c != nil {
+ c.Write(zap.String("provider", provName), zap.Error(err))
+ }
+ // Set the error from the authentication provider in a placeholder,
+ // so it can be used in the handle_errors directive.
+ repl.Set("http.auth."+provName+".error", err.Error())
continue
}
if authed {
@@ -89,7 +98,6 @@ func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
return caddyhttp.Error(http.StatusUnauthorized, fmt.Errorf("not authenticated"))
}
- repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
repl.Set("http.auth.user.id", user.ID)
for k, v := range user.Metadata {
repl.Set("http.auth.user."+k, v)
diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go
index f15aec5ed..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
@@ -76,7 +92,10 @@ type MiddlewareHandler interface {
}
// emptyHandler is used as a no-op handler.
-var emptyHandler Handler = HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil })
+var emptyHandler Handler = HandlerFunc(func(_ http.ResponseWriter, req *http.Request) error {
+ SetVar(req.Context(), "unhandled", true)
+ return nil
+})
// An implicit suffix middleware that, if reached, sets the StatusCode to the
// error stored in the ErrorCtxKey. This is to prevent situations where the
@@ -120,7 +139,7 @@ type ResponseHandler struct {
Routes RouteList `json:"routes,omitempty"`
}
-// Provision sets up the routse in rh.
+// Provision sets up the routes in rh.
func (rh *ResponseHandler) Provision(ctx caddy.Context) error {
if rh.Routes != nil {
err := rh.Routes.Provision(ctx)
@@ -226,13 +245,22 @@ func StatusCodeMatches(actual, configured int) bool {
// in the implementation of http.Dir. The root is assumed to
// be a trusted path, but reqPath is not; and the output will
// never be outside of root. The resulting path can be used
-// with the local file system.
+// with the local file system. If root is empty, the current
+// directory is assumed. If the cleaned request path is deemed
+// not local according to lexical processing (i.e. ignoring links),
+// it will be rejected as unsafe and only the root will be returned.
func SanitizedPathJoin(root, reqPath string) string {
if root == "" {
root = "."
}
- path := filepath.Join(root, path.Clean("/"+reqPath))
+ relPath := path.Clean("/" + reqPath)[1:] // clean path and trim the leading /
+ if relPath != "" && !filepath.IsLocal(relPath) {
+ // path is unsafe (see https://github.com/golang/go/issues/56336#issuecomment-1416214885)
+ return root
+ }
+
+ path := filepath.Join(root, filepath.FromSlash(relPath))
// filepath.Join also cleans the path, and cleaning strips
// the trailing slash, so we need to re-add it afterwards.
diff --git a/modules/caddyhttp/caddyhttp_test.go b/modules/caddyhttp/caddyhttp_test.go
index 84c0271f1..aeed0135c 100644
--- a/modules/caddyhttp/caddyhttp_test.go
+++ b/modules/caddyhttp/caddyhttp_test.go
@@ -3,6 +3,7 @@ package caddyhttp
import (
"net/url"
"path/filepath"
+ "runtime"
"testing"
)
@@ -12,9 +13,10 @@ func TestSanitizedPathJoin(t *testing.T) {
// %2f = /
// %5c = \
for i, tc := range []struct {
- inputRoot string
- inputPath string
- expect string
+ inputRoot string
+ inputPath string
+ expect string
+ expectWindows string
}{
{
inputPath: "",
@@ -24,22 +26,28 @@ func TestSanitizedPathJoin(t *testing.T) {
inputPath: "/",
expect: ".",
},
+ {
+ // fileserver.MatchFile passes an inputPath of "//" for some try_files values.
+ // See https://github.com/caddyserver/caddy/issues/6352
+ inputPath: "//",
+ expect: filepath.FromSlash("./"),
+ },
{
inputPath: "/foo",
expect: "foo",
},
{
inputPath: "/foo/",
- expect: "foo" + separator,
+ expect: filepath.FromSlash("foo/"),
},
{
inputPath: "/foo/bar",
- expect: filepath.Join("foo", "bar"),
+ expect: filepath.FromSlash("foo/bar"),
},
{
inputRoot: "/a",
inputPath: "/foo/bar",
- expect: filepath.Join("/", "a", "foo", "bar"),
+ expect: filepath.FromSlash("/a/foo/bar"),
},
{
inputPath: "/foo/../bar",
@@ -48,32 +56,34 @@ func TestSanitizedPathJoin(t *testing.T) {
{
inputRoot: "/a/b",
inputPath: "/foo/../bar",
- expect: filepath.Join("/", "a", "b", "bar"),
+ expect: filepath.FromSlash("/a/b/bar"),
},
{
inputRoot: "/a/b",
inputPath: "/..%2fbar",
- expect: filepath.Join("/", "a", "b", "bar"),
+ expect: filepath.FromSlash("/a/b/bar"),
},
{
inputRoot: "/a/b",
inputPath: "/%2e%2e%2fbar",
- expect: filepath.Join("/", "a", "b", "bar"),
+ expect: filepath.FromSlash("/a/b/bar"),
},
{
+ // inputPath fails the IsLocal test so only the root is returned,
+ // but with a trailing slash since one was included in inputPath
inputRoot: "/a/b",
inputPath: "/%2e%2e%2f%2e%2e%2f",
- expect: filepath.Join("/", "a", "b") + separator,
+ expect: filepath.FromSlash("/a/b/"),
},
{
inputRoot: "/a/b",
inputPath: "/foo%2fbar",
- expect: filepath.Join("/", "a", "b", "foo", "bar"),
+ expect: filepath.FromSlash("/a/b/foo/bar"),
},
{
inputRoot: "/a/b",
inputPath: "/foo%252fbar",
- expect: filepath.Join("/", "a", "b", "foo%2fbar"),
+ expect: filepath.FromSlash("/a/b/foo%2fbar"),
},
{
inputRoot: "C:\\www",
@@ -81,9 +91,40 @@ func TestSanitizedPathJoin(t *testing.T) {
expect: filepath.Join("C:\\www", "foo", "bar"),
},
{
- inputRoot: "C:\\www",
- inputPath: "/D:\\foo\\bar",
- expect: filepath.Join("C:\\www", "D:\\foo\\bar"),
+ inputRoot: "C:\\www",
+ inputPath: "/D:\\foo\\bar",
+ expect: filepath.Join("C:\\www", "D:\\foo\\bar"),
+ expectWindows: "C:\\www", // inputPath fails IsLocal on Windows
+ },
+ {
+ inputRoot: `C:\www`,
+ inputPath: `/..\windows\win.ini`,
+ expect: `C:\www/..\windows\win.ini`,
+ expectWindows: `C:\www`,
+ },
+ {
+ inputRoot: `C:\www`,
+ inputPath: `/..\..\..\..\..\..\..\..\..\..\windows\win.ini`,
+ expect: `C:\www/..\..\..\..\..\..\..\..\..\..\windows\win.ini`,
+ expectWindows: `C:\www`,
+ },
+ {
+ inputRoot: `C:\www`,
+ inputPath: `/..%5cwindows%5cwin.ini`,
+ expect: `C:\www/..\windows\win.ini`,
+ expectWindows: `C:\www`,
+ },
+ {
+ inputRoot: `C:\www`,
+ inputPath: `/..%5c..%5c..%5c..%5c..%5c..%5c..%5c..%5c..%5c..%5cwindows%5cwin.ini`,
+ expect: `C:\www/..\..\..\..\..\..\..\..\..\..\windows\win.ini`,
+ expectWindows: `C:\www`,
+ },
+ {
+ // https://github.com/golang/go/issues/56336#issuecomment-1416214885
+ inputRoot: "root",
+ inputPath: "/a/b/../../c",
+ expect: filepath.FromSlash("root/c"),
},
} {
// we don't *need* to use an actual parsed URL, but it
@@ -96,6 +137,9 @@ func TestSanitizedPathJoin(t *testing.T) {
t.Fatalf("Test %d: invalid URL: %v", i, err)
}
actual := SanitizedPathJoin(tc.inputRoot, u.Path)
+ if runtime.GOOS == "windows" && tc.expectWindows != "" {
+ tc.expect = tc.expectWindows
+ }
if actual != tc.expect {
t.Errorf("Test %d: SanitizedPathJoin('%s', '%s') => '%s' (expected '%s')",
i, tc.inputRoot, tc.inputPath, actual, tc.expect)
diff --git a/modules/caddyhttp/celmatcher.go b/modules/caddyhttp/celmatcher.go
index e1f15e869..3d118ea79 100644
--- a/modules/caddyhttp/celmatcher.go
+++ b/modules/caddyhttp/celmatcher.go
@@ -62,7 +62,12 @@ type MatchExpression struct {
// The CEL expression to evaluate. Any Caddy placeholders
// will be expanded and situated into proper CEL function
// calls before evaluating.
- Expr string
+ Expr string `json:"expr,omitempty"`
+
+ // Name is an optional name for this matcher.
+ // This is used to populate the name for regexp
+ // matchers that appear in the expression.
+ Name string `json:"name,omitempty"`
expandedExpr string
prg cel.Program
@@ -81,12 +86,36 @@ func (MatchExpression) CaddyModule() caddy.ModuleInfo {
// MarshalJSON marshals m's expression.
func (m MatchExpression) MarshalJSON() ([]byte, error) {
- return json.Marshal(m.Expr)
+ // if the name is empty, then we can marshal just the expression string
+ if m.Name == "" {
+ return json.Marshal(m.Expr)
+ }
+ // otherwise, we need to marshal the full object, using an
+ // anonymous struct to avoid infinite recursion
+ return json.Marshal(struct {
+ Expr string `json:"expr"`
+ Name string `json:"name"`
+ }{
+ Expr: m.Expr,
+ Name: m.Name,
+ })
}
// UnmarshalJSON unmarshals m's expression.
func (m *MatchExpression) UnmarshalJSON(data []byte) error {
- return json.Unmarshal(data, &m.Expr)
+ // if the data is a string, then it's just the expression
+ if data[0] == '"' {
+ return json.Unmarshal(data, &m.Expr)
+ }
+ // otherwise, it's a full object, so unmarshal it,
+ // using an temp map to avoid infinite recursion
+ var tmpJson map[string]any
+ err := json.Unmarshal(data, &tmpJson)
+ *m = MatchExpression{
+ Expr: tmpJson["expr"].(string),
+ Name: tmpJson["name"].(string),
+ }
+ return err
}
// Provision sets ups m.
@@ -97,6 +126,10 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
// light (and possibly naïve) syntactic sugar
m.expandedExpr = placeholderRegexp.ReplaceAllString(m.Expr, placeholderExpansion)
+ // as a second pass, we'll strip the escape character from an escaped
+ // placeholder, so that it can be used as an input to other CEL functions
+ m.expandedExpr = escapedPlaceholderRegexp.ReplaceAllString(m.expandedExpr, escapedPlaceholderExpansion)
+
// our type adapter expands CEL's standard type support
m.ta = celTypeAdapter{}
@@ -109,6 +142,11 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
matcherLibProducers = append(matcherLibProducers, p)
}
}
+
+ // add the matcher name to the context so that the matcher name
+ // can be used by regexp matchers being provisioned
+ ctx = ctx.WithValue(MatcherNameCtxKey, m.Name)
+
// Assemble the compilation and program options from the different library
// producers into a single cel.Library implementation.
matcherEnvOpts := []cel.EnvOption{}
@@ -125,14 +163,17 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
// create the CEL environment
env, err := cel.NewEnv(
- cel.Function(placeholderFuncName, cel.SingletonBinaryBinding(m.caddyPlaceholderFunc), cel.Overload(
- placeholderFuncName+"_httpRequest_string",
+ cel.Function(CELPlaceholderFuncName, cel.SingletonBinaryBinding(m.caddyPlaceholderFunc), cel.Overload(
+ CELPlaceholderFuncName+"_httpRequest_string",
[]*cel.Type{httpRequestObjectType, cel.StringType},
cel.AnyType,
)),
- cel.Variable("request", httpRequestObjectType),
+ cel.Variable(CELRequestVarName, httpRequestObjectType),
cel.CustomTypeAdapter(m.ta),
ext.Strings(),
+ ext.Bindings(),
+ ext.Lists(),
+ ext.Math(),
matcherLib,
)
if err != nil {
@@ -161,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.
@@ -197,6 +246,11 @@ func (m *MatchExpression) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// quoted string; commonly quotes are used in Caddyfile to
// define the expression
m.Expr = d.Val()
+
+ // use the named matcher's name, to fill regexp
+ // matchers names by default
+ m.Name = d.GetContextString(caddyfile.MatcherNameCtxKey)
+
return nil
}
@@ -208,7 +262,7 @@ func (m MatchExpression) caddyPlaceholderFunc(lhs, rhs ref.Val) ref.Val {
return types.NewErr(
"invalid request of type '%v' to %s(request, placeholderVarName)",
lhs.Type(),
- placeholderFuncName,
+ CELPlaceholderFuncName,
)
}
phStr, ok := rhs.(types.String)
@@ -216,7 +270,7 @@ func (m MatchExpression) caddyPlaceholderFunc(lhs, rhs ref.Val) ref.Val {
return types.NewErr(
"invalid placeholder variable name of type '%v' to %s(request, placeholderVarName)",
rhs.Type(),
- placeholderFuncName,
+ CELPlaceholderFuncName,
)
}
@@ -236,7 +290,7 @@ var httpRequestCELType = cel.ObjectType("http.Request", traits.ReceiverType)
type celHTTPRequest struct{ *http.Request }
func (cr celHTTPRequest) ResolveName(name string) (any, bool) {
- if name == "request" {
+ if name == CELRequestVarName {
return cr, true
}
return nil, false
@@ -301,7 +355,7 @@ func (celTypeAdapter) NativeToValue(value any) ref.Val {
case time.Time:
return types.Timestamp{Time: v}
case error:
- types.NewErr(v.Error())
+ return types.WrapErr(v)
}
return types.DefaultTypeAdapter.NativeToValue(value)
}
@@ -334,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) {
@@ -378,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 {
@@ -406,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 {
@@ -418,15 +476,15 @@ func CELMatcherDecorator(funcName string, fac CELMatcherFactory) interpreter.Int
callArgs := call.Args()
reqAttr, ok := callArgs[0].(interpreter.InterpretableAttribute)
if !ok {
- return nil, errors.New("missing 'request' argument")
+ return nil, errors.New("missing 'req' argument")
}
nsAttr, ok := reqAttr.Attr().(interpreter.NamespacedAttribute)
if !ok {
- return nil, errors.New("missing 'request' argument")
+ return nil, errors.New("missing 'req' argument")
}
varNames := nsAttr.CandidateVariableNames()
- if len(varNames) != 1 || len(varNames) == 1 && varNames[0] != "request" {
- return nil, errors.New("missing 'request' argument")
+ if len(varNames) != 1 || len(varNames) == 1 && varNames[0] != CELRequestVarName {
+ return nil, errors.New("missing 'req' argument")
}
matcherData, ok := callArgs[1].(interpreter.InterpretableConst)
if !ok {
@@ -435,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.NewErr(err.Error())
+ 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)
}
- httpReq := celReq.Value().(celHTTPRequest)
- return types.Bool(matcher.Match(httpReq.Request))
+ 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))
+ }
+ return types.NewErr("CELMatcherRuntimeFunction invalid matcher factory: %T", fac)
}
}
@@ -485,7 +600,7 @@ func celMatcherStringListMacroExpander(funcName string) cel.MacroFactory {
return nil, eh.NewError(arg.ID(), "matcher arguments must be string constants")
}
}
- return eh.NewCall(funcName, eh.NewIdent("request"), eh.NewList(matchArgs...)), nil
+ return eh.NewCall(funcName, eh.NewIdent(CELRequestVarName), eh.NewList(matchArgs...)), nil
}
}
@@ -499,7 +614,7 @@ func celMatcherStringMacroExpander(funcName string) parser.MacroExpander {
return nil, eh.NewError(0, "matcher requires one argument")
}
if isCELStringExpr(args[0]) {
- return eh.NewCall(funcName, eh.NewIdent("request"), args[0]), nil
+ return eh.NewCall(funcName, eh.NewIdent(CELRequestVarName), args[0]), nil
}
return nil, eh.NewError(args[0].ID(), "matcher argument must be a string literal")
}
@@ -533,7 +648,7 @@ func celMatcherJSONMacroExpander(funcName string) parser.MacroExpander {
return nil, eh.NewError(entry.AsMapEntry().Value().ID(), "matcher map values must be string or list literals")
}
}
- return eh.NewCall(funcName, eh.NewIdent("request"), arg), nil
+ return eh.NewCall(funcName, eh.NewIdent(CELRequestVarName), arg), nil
case ast.UnspecifiedExprKind, ast.CallKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.SelectKind:
// appeasing the linter :)
}
@@ -607,7 +722,7 @@ func isCELCaddyPlaceholderCall(e ast.Expr) bool {
switch e.Kind() {
case ast.CallKind:
call := e.AsCall()
- if call.FunctionName() == "caddyPlaceholder" {
+ if call.FunctionName() == CELPlaceholderFuncName {
return true
}
case ast.UnspecifiedExprKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.MapKind, ast.SelectKind, ast.StructKind:
@@ -662,8 +777,15 @@ func isCELStringListLiteral(e ast.Expr) bool {
// expressions with a proper CEL function call; this is
// just for syntactic sugar.
var (
- placeholderRegexp = regexp.MustCompile(`{([a-zA-Z][\w.-]+)}`)
- placeholderExpansion = `caddyPlaceholder(request, "${1}")`
+ // The placeholder may not be preceded by a backslash; the expansion
+ // will include the preceding character if it is not a backslash.
+ placeholderRegexp = regexp.MustCompile(`([^\\]|^){([a-zA-Z][\w.-]+)}`)
+ placeholderExpansion = `${1}ph(req, "${2}")`
+
+ // As a second pass, we need to strip the escape character in front of
+ // the placeholder, if it exists.
+ escapedPlaceholderRegexp = regexp.MustCompile(`\\{([a-zA-Z][\w.-]+)}`)
+ escapedPlaceholderExpansion = `{${1}}`
CELTypeJSON = cel.MapType(cel.StringType, cel.DynType)
)
@@ -671,13 +793,18 @@ var (
var httpRequestObjectType = cel.ObjectType("http.Request")
// The name of the CEL function which accesses Replacer values.
-const placeholderFuncName = "caddyPlaceholder"
+const CELPlaceholderFuncName = "ph"
+
+// The name of the CEL request variable.
+const CELRequestVarName = "req"
+
+const MatcherNameCtxKey = "matcher_name"
// 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 e67b87c77..a7e91529c 100644
--- a/modules/caddyhttp/celmatcher_test.go
+++ b/modules/caddyhttp/celmatcher_test.go
@@ -70,13 +70,36 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
wantResult: true,
},
{
- name: "header error (MatchHeader)",
+ name: "header matches an escaped placeholder value (MatchHeader)",
+ expression: &MatchExpression{
+ Expr: `header({'Field': '\\\{foobar}'})`,
+ },
+ urlTarget: "https://example.com/foo",
+ httpHeader: &http.Header{"Field": []string{"{foobar}"}},
+ wantResult: true,
+ },
+ {
+ name: "header matches an placeholder replaced during the header matcher (MatchHeader)",
+ expression: &MatchExpression{
+ Expr: `header({'Field': '\{http.request.uri.path}'})`,
+ },
+ urlTarget: "https://example.com/foo",
+ httpHeader: &http.Header{"Field": []string{"/foo"}},
+ wantResult: true,
+ },
+ {
+ name: "header error, invalid escape sequence (MatchHeader)",
+ expression: &MatchExpression{
+ Expr: `header({'Field': '\\{foobar}'})`,
+ },
+ wantErr: true,
+ },
+ {
+ name: "header error, needs to be JSON syntax with field as key (MatchHeader)",
expression: &MatchExpression{
Expr: `header('foo')`,
},
- urlTarget: "https://example.com/foo",
- httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
- wantErr: true,
+ wantErr: true,
},
{
name: "header_regexp matches (MatchHeaderRE)",
@@ -110,9 +133,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{
Expr: `header_regexp('foo')`,
},
- urlTarget: "https://example.com/foo",
- httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
- wantErr: true,
+ wantErr: true,
},
{
name: "host matches localhost (MatchHost)",
@@ -143,8 +164,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{
Expr: `host(80)`,
},
- urlTarget: "http://localhost:80",
- wantErr: true,
+ wantErr: true,
},
{
name: "method does not match (MatchMethod)",
@@ -169,9 +189,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{
Expr: `method()`,
},
- urlTarget: "https://foo.example.com",
- httpMethod: "PUT",
- wantErr: true,
+ wantErr: true,
},
{
name: "path matches substring (MatchPath)",
@@ -266,24 +284,21 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{
Expr: `protocol()`,
},
- urlTarget: "https://example.com",
- wantErr: true,
+ wantErr: true,
},
{
name: "protocol invocation error too many args (MatchProtocol)",
expression: &MatchExpression{
Expr: `protocol('grpc', 'https')`,
},
- urlTarget: "https://example.com",
- wantErr: true,
+ wantErr: true,
},
{
name: "protocol invocation error wrong arg type (MatchProtocol)",
expression: &MatchExpression{
Expr: `protocol(true)`,
},
- urlTarget: "https://example.com",
- wantErr: true,
+ wantErr: true,
},
{
name: "query does not match against a specific value (MatchQuery)",
@@ -330,40 +345,35 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{
Expr: `query({1: "1"})`,
},
- urlTarget: "https://example.com/foo",
- wantErr: true,
+ wantErr: true,
},
{
name: "query error typed struct instead of map (MatchQuery)",
expression: &MatchExpression{
Expr: `query(Message{field: "1"})`,
},
- urlTarget: "https://example.com/foo",
- wantErr: true,
+ wantErr: true,
},
{
name: "query error bad map value type (MatchQuery)",
expression: &MatchExpression{
Expr: `query({"debug": 1})`,
},
- urlTarget: "https://example.com/foo/?debug=1",
- wantErr: true,
+ wantErr: true,
},
{
name: "query error no args (MatchQuery)",
expression: &MatchExpression{
Expr: `query()`,
},
- urlTarget: "https://example.com/foo/?debug=1",
- wantErr: true,
+ wantErr: true,
},
{
name: "remote_ip error no args (MatchRemoteIP)",
expression: &MatchExpression{
Expr: `remote_ip()`,
},
- urlTarget: "https://example.com/foo",
- wantErr: true,
+ wantErr: true,
},
{
name: "remote_ip single IP match (MatchRemoteIP)",
@@ -373,6 +383,67 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
urlTarget: "https://example.com/foo",
wantResult: true,
},
+ {
+ name: "vars value (VarsMatcher)",
+ expression: &MatchExpression{
+ Expr: `vars({'foo': 'bar'})`,
+ },
+ urlTarget: "https://example.com/foo",
+ wantResult: true,
+ },
+ {
+ name: "vars matches placeholder, needs escape (VarsMatcher)",
+ expression: &MatchExpression{
+ Expr: `vars({'\{http.request.uri.path}': '/foo'})`,
+ },
+ urlTarget: "https://example.com/foo",
+ wantResult: true,
+ },
+ {
+ name: "vars error wrong syntax (VarsMatcher)",
+ expression: &MatchExpression{
+ Expr: `vars('foo', 'bar')`,
+ },
+ wantErr: true,
+ },
+ {
+ name: "vars error no args (VarsMatcher)",
+ expression: &MatchExpression{
+ Expr: `vars()`,
+ },
+ wantErr: true,
+ },
+ {
+ name: "vars_regexp value (MatchVarsRE)",
+ expression: &MatchExpression{
+ Expr: `vars_regexp('foo', 'ba?r')`,
+ },
+ urlTarget: "https://example.com/foo",
+ wantResult: true,
+ },
+ {
+ name: "vars_regexp value with name (MatchVarsRE)",
+ expression: &MatchExpression{
+ Expr: `vars_regexp('name', 'foo', 'ba?r')`,
+ },
+ urlTarget: "https://example.com/foo",
+ wantResult: true,
+ },
+ {
+ name: "vars_regexp matches placeholder, needs escape (MatchVarsRE)",
+ expression: &MatchExpression{
+ Expr: `vars_regexp('\{http.request.uri.path}', '/fo?o')`,
+ },
+ urlTarget: "https://example.com/foo",
+ wantResult: true,
+ },
+ {
+ name: "vars_regexp error no args (MatchVarsRE)",
+ expression: &MatchExpression{
+ Expr: `vars_regexp()`,
+ },
+ wantErr: true,
+ },
}
)
@@ -380,7 +451,9 @@ func TestMatchExpressionMatch(t *testing.T) {
for _, tst := range matcherTests {
tc := tst
t.Run(tc.name, func(t *testing.T) {
- err := tc.expression.Provision(caddy.Context{})
+ caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
+ defer cancel()
+ err := tc.expression.Provision(caddyCtx)
if err != nil {
if !tc.wantErr {
t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tc.wantErr)
@@ -394,6 +467,9 @@ func TestMatchExpressionMatch(t *testing.T) {
}
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
+ ctx = context.WithValue(ctx, VarsCtxKey, map[string]any{
+ "foo": "bar",
+ })
req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
@@ -413,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)
}
})
@@ -434,6 +514,9 @@ func BenchmarkMatchExpressionMatch(b *testing.B) {
}
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
+ ctx = context.WithValue(ctx, VarsCtxKey, map[string]any{
+ "foo": "bar",
+ })
req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
if tc.clientCertificate != nil {
@@ -453,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)
}
})
}
@@ -482,7 +565,9 @@ func TestMatchExpressionProvision(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- if err := tt.expression.Provision(caddy.Context{}); (err != nil) != tt.wantErr {
+ ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
+ defer cancel()
+ if err := tt.expression.Provision(ctx); (err != nil) != tt.wantErr {
t.Errorf("MatchExpression.Provision() error = %v, wantErr %v", err, tt.wantErr)
}
})
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 7d5f556f1..bea86083a 100644
--- a/modules/caddyhttp/encode/encode.go
+++ b/modules/caddyhttp/encode/encode.go
@@ -24,6 +24,7 @@ import (
"io"
"math"
"net/http"
+ "slices"
"sort"
"strconv"
"strings"
@@ -112,7 +113,8 @@ func (enc *Encode) Provision(ctx caddy.Context) error {
"application/x-ttf*",
"application/xhtml+xml*",
"application/xml*",
- "font/*",
+ "font/ttf*",
+ "font/otf*",
"image/svg+xml*",
"image/vnd.microsoft.icon*",
"image/x-icon*",
@@ -154,8 +156,23 @@ 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
+ // by appending a hyphen and the encoder name; the problem is, the client will
+ // send back that Etag in a If-None-Match header, but upstream handlers that set
+ // the Etag in the first place don't know that we appended to their Etag! so here
+ // we have to strip our addition so the upstream handlers can still honor client
+ // caches without knowing about our changes...
+ if etag := r.Header.Get("If-None-Match"); etag != "" && !strings.HasPrefix(etag, "W/") {
+ ourSuffix := "-" + encName + `"`
+ if strings.HasSuffix(etag, ourSuffix) {
+ etag = strings.TrimSuffix(etag, ourSuffix) + `"`
+ r.Header.Set("If-None-Match", etag)
+ }
+ }
+
break
}
}
@@ -184,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 {
@@ -199,6 +216,7 @@ func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, w
}
rw.encodingName = encodingName
rw.config = enc
+ rw.isConnect = isConnect
return rw
}
@@ -213,6 +231,7 @@ type responseWriter struct {
config *Encode
statusCode int
wroteHeader bool
+ isConnect bool
}
// WriteHeader stores the status to write when the time comes
@@ -220,6 +239,22 @@ type responseWriter struct {
func (rw *responseWriter) WriteHeader(status int) {
rw.statusCode = status
+ // See #5849 and RFC 9110 section 15.4.5 (https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4.5) - 304
+ // Not Modified must have certain headers set as if it was a 200 response, and according to the issue
+ // we would miss the Vary header in this case when compression was also enabled; note that we set this
+ // header in the responseWriter.init() method but that is only called if we are writing a response body
+ if status == http.StatusNotModified && !hasVaryValue(rw.Header(), "Accept-Encoding") {
+ 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 {
@@ -235,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
@@ -242,6 +283,14 @@ func (rw *responseWriter) FlushError() error {
// to rw.Write (see bug in #4314)
return nil
}
+ // also flushes the encoder, if any
+ // see: https://github.com/jjiang-stripe/caddy-slow-gzip
+ if rw.w != nil {
+ err := rw.w.Flush()
+ if err != nil {
+ return err
+ }
+ }
//nolint:bodyclose
return http.NewResponseController(rw.ResponseWriter).Flush()
}
@@ -255,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
@@ -292,6 +347,49 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
}
}
+// used to mask ReadFrom method
+type writerOnly struct {
+ io.Writer
+}
+
+// copied from stdlib
+const sniffLen = 512
+
+// ReadFrom will try to use sendfile to copy from the reader to the response writer.
+// It's only used if the response writer implements io.ReaderFrom and the data can't be compressed.
+// It's based on stdlin http1.1 response writer implementation.
+// https://github.com/golang/go/blob/f4e3ec3dbe3b8e04a058d266adf8e048bab563f2/src/net/http/server.go#L586
+func (rw *responseWriter) ReadFrom(r io.Reader) (int64, error) {
+ rf, ok := rw.ResponseWriter.(io.ReaderFrom)
+ // sendfile can't be used anyway
+ if !ok {
+ // mask ReadFrom to avoid infinite recursion
+ return io.Copy(writerOnly{rw}, r)
+ }
+
+ var ns int64
+ // try to sniff the content type and determine if the response should be compressed
+ if !rw.wroteHeader && rw.config.MinLength > 0 {
+ var (
+ err error
+ buf [sniffLen]byte
+ )
+ // mask ReadFrom to let Write determine if the response should be compressed
+ ns, err = io.CopyBuffer(writerOnly{rw}, io.LimitReader(r, sniffLen), buf[:])
+ if err != nil || ns < sniffLen {
+ return ns, err
+ }
+ }
+
+ // the response will be compressed, no sendfile support
+ if rw.w != nil {
+ nr, err := io.Copy(rw.w, r)
+ return nr + ns, err
+ }
+ nr, err := rf.ReadFrom(r)
+ return nr + ns, err
+}
+
// Close writes any remaining buffered response and
// deallocates any active resources.
func (rw *responseWriter) Close() error {
@@ -326,17 +424,44 @@ func (rw *responseWriter) Unwrap() http.ResponseWriter {
// init should be called before we write a response, if rw.buf has contents.
func (rw *responseWriter) init() {
- if rw.Header().Get("Content-Encoding") == "" && isEncodeAllowed(rw.Header()) &&
+ hdr := rw.Header()
+ if hdr.Get("Content-Encoding") == "" && isEncodeAllowed(hdr) &&
rw.config.Match(rw) {
rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder)
rw.w.Reset(rw.ResponseWriter)
- rw.Header().Del("Content-Length") // https://github.com/golang/go/issues/14975
- rw.Header().Set("Content-Encoding", rw.encodingName)
- rw.Header().Add("Vary", "Accept-Encoding")
- rw.Header().Del("Accept-Ranges") // we don't know ranges for dynamically-encoded content
+ hdr.Del("Content-Length") // https://github.com/golang/go/issues/14975
+ hdr.Set("Content-Encoding", rw.encodingName)
+ if !hasVaryValue(hdr, "Accept-Encoding") {
+ hdr.Add("Vary", "Accept-Encoding")
+ }
+ hdr.Del("Accept-Ranges") // we don't know ranges for dynamically-encoded content
+
+ // strong ETags need to be distinct depending on the encoding ("selected representation")
+ // see RFC 9110 section 8.8.3.3:
+ // https://www.rfc-editor.org/rfc/rfc9110.html#name-example-entity-tags-varying
+ // I don't know a great way to do this... how about appending? That's a neat trick!
+ // (We have to strip the value we append from If-None-Match headers before
+ // sending subsequent requests back upstream, however, since upstream handlers
+ // don't know about our appending to their Etag since they've already done their work)
+ if etag := hdr.Get("Etag"); etag != "" && !strings.HasPrefix(etag, "W/") {
+ etag = fmt.Sprintf(`%s-%s"`, strings.TrimSuffix(etag, `"`), rw.encodingName)
+ hdr.Set("Etag", etag)
+ }
}
}
+func hasVaryValue(hdr http.Header, target string) bool {
+ for _, vary := range hdr.Values("Vary") {
+ vals := strings.Split(vary, ",")
+ for _, val := range vals {
+ if strings.EqualFold(strings.TrimSpace(val), target) {
+ return true
+ }
+ }
+ }
+ return false
+}
+
// AcceptedEncodings returns the list of encodings that the
// client supports, in descending order of preference.
// The client preference via q-factor and the server
@@ -382,12 +507,9 @@ func AcceptedEncodings(r *http.Request, preferredOrder []string) []string {
}
// set server preference
- prefOrder := -1
- for i, p := range preferredOrder {
- if encName == p {
- prefOrder = len(preferredOrder) - i
- break
- }
+ prefOrder := slices.Index(preferredOrder, encName)
+ if prefOrder > -1 {
+ prefOrder = len(preferredOrder) - prefOrder
}
prefs = append(prefs, encodingPreference{
@@ -424,6 +546,7 @@ type encodingPreference struct {
type Encoder interface {
io.WriteCloser
Reset(io.Writer)
+ Flush() error // encoder by default buffers data to maximize compressing rate
}
// Encoding is a type which can create encoders of its kind
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/encode/zstd/zstd.go b/modules/caddyhttp/encode/zstd/zstd.go
index b5a0299d1..1706de89d 100644
--- a/modules/caddyhttp/encode/zstd/zstd.go
+++ b/modules/caddyhttp/encode/zstd/zstd.go
@@ -15,6 +15,8 @@
package caddyzstd
import (
+ "fmt"
+
"github.com/klauspost/compress/zstd"
"github.com/caddyserver/caddy/v2"
@@ -27,7 +29,13 @@ func init() {
}
// Zstd can create Zstandard encoders.
-type Zstd struct{}
+type Zstd struct {
+ // The compression level. Accepted values: fastest, better, best, default.
+ Level string `json:"level,omitempty"`
+
+ // Compression level refer to type constants value from zstd.SpeedFastest to zstd.SpeedBestCompression
+ level zstd.EncoderLevel
+}
// CaddyModule returns the Caddy module information.
func (Zstd) CaddyModule() caddy.ModuleInfo {
@@ -39,6 +47,37 @@ func (Zstd) CaddyModule() caddy.ModuleInfo {
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens.
func (z *Zstd) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
+ d.Next() // consume option name
+ if !d.NextArg() {
+ return nil
+ }
+ levelStr := d.Val()
+ if ok, _ := zstd.EncoderLevelFromString(levelStr); !ok {
+ return d.Errf("unexpected compression level, use one of '%s', '%s', '%s', '%s'",
+ zstd.SpeedFastest,
+ zstd.SpeedBetterCompression,
+ zstd.SpeedBestCompression,
+ zstd.SpeedDefault,
+ )
+ }
+ z.Level = levelStr
+ return nil
+}
+
+// Provision provisions z's configuration.
+func (z *Zstd) Provision(ctx caddy.Context) error {
+ if z.Level == "" {
+ z.Level = zstd.SpeedDefault.String()
+ }
+ var ok bool
+ if ok, z.level = zstd.EncoderLevelFromString(z.Level); !ok {
+ return fmt.Errorf("unexpected compression level, use one of '%s', '%s', '%s', '%s'",
+ zstd.SpeedFastest,
+ zstd.SpeedDefault,
+ zstd.SpeedBetterCompression,
+ zstd.SpeedBestCompression,
+ )
+ }
return nil
}
@@ -51,7 +90,13 @@ func (z Zstd) NewEncoder() encode.Encoder {
// The default of 8MB for the window is
// too large for many clients, so we limit
// it to 128K to lighten their load.
- writer, _ := zstd.NewWriter(nil, zstd.WithWindowSize(128<<10), zstd.WithEncoderConcurrency(1), zstd.WithZeroFrames(true))
+ writer, _ := zstd.NewWriter(
+ nil,
+ zstd.WithWindowSize(128<<10),
+ zstd.WithEncoderConcurrency(1),
+ zstd.WithZeroFrames(true),
+ zstd.WithEncoderLevel(z.level),
+ )
return writer
}
@@ -59,4 +104,5 @@ func (z Zstd) NewEncoder() encode.Encoder {
var (
_ encode.Encoding = (*Zstd)(nil)
_ caddyfile.Unmarshaler = (*Zstd)(nil)
+ _ caddy.Provisioner = (*Zstd)(nil)
)
diff --git a/modules/caddyhttp/fileserver/browse.go b/modules/caddyhttp/fileserver/browse.go
index 86adc7e39..52aa7a9f8 100644
--- a/modules/caddyhttp/fileserver/browse.go
+++ b/modules/caddyhttp/fileserver/browse.go
@@ -28,9 +28,12 @@ import (
"path"
"strings"
"sync"
+ "text/tabwriter"
"text/template"
+ "time"
"go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
@@ -50,14 +53,32 @@ var BrowseTemplate string
type Browse struct {
// Filename of the template to use instead of the embedded browse template.
TemplateFile string `json:"template_file,omitempty"`
+
// Determines whether or not targets of symlinks should be revealed.
RevealSymlinks bool `json:"reveal_symlinks,omitempty"`
+
+ // Override the default sort.
+ // It includes the following options:
+ // - sort_by: name(default), namedirfirst, size, time
+ // - order: asc(default), desc
+ // eg.:
+ // - `sort time desc` will sort by time in descending order
+ // - `sort size` will sort by size in ascending order
+ // The first option must be `sort_by` and the second option must be `order` (if exists).
+ SortOptions []string `json:"sort,omitempty"`
+
+ // 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 {
- fsrv.logger.Debug("browse enabled; listing directory contents",
- zap.String("path", dirPath),
- zap.String("root", root))
+ if c := fsrv.logger.Check(zapcore.DebugLevel, "browse enabled; listing directory contents"); c != nil {
+ c.Write(zap.String("path", dirPath), zap.String("root", root))
+ }
// Navigation on the client-side gets messed up if the
// URL doesn't end in a trailing slash because hrefs to
@@ -79,7 +100,9 @@ func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w ht
origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
if r.URL.Path == "" || path.Base(origReq.URL.Path) == path.Base(r.URL.Path) {
if !strings.HasSuffix(origReq.URL.Path, "/") {
- fsrv.logger.Debug("redirecting to trailing slash to preserve hrefs", zap.String("request_path", r.URL.Path))
+ if c := fsrv.logger.Check(zapcore.DebugLevel, "redirecting to trailing slash to preserve hrefs"); c != nil {
+ c.Write(zap.String("request_path", r.URL.Path))
+ }
return redirect(w, r, origReq.URL.Path+"/")
}
}
@@ -103,6 +126,18 @@ func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w ht
return caddyhttp.Error(http.StatusInternalServerError, err)
}
+ w.Header().Add("Vary", "Accept, Accept-Encoding")
+
+ // speed up browser/client experience and caching by supporting If-Modified-Since
+ if ifModSinceStr := r.Header.Get("If-Modified-Since"); ifModSinceStr != "" {
+ // basically a copy of stdlib file server's handling of If-Modified-Since
+ ifModSince, err := http.ParseTime(ifModSinceStr)
+ if err == nil && listing.lastModified.Truncate(time.Second).Compare(ifModSince) <= 0 {
+ w.WriteHeader(http.StatusNotModified)
+ return nil
+ }
+ }
+
fsrv.browseApplyQueryParams(w, r, listing)
buf := bufPool.Get().(*bytes.Buffer)
@@ -110,14 +145,44 @@ func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w ht
defer bufPool.Put(buf)
acceptHeader := strings.ToLower(strings.Join(r.Header["Accept"], ","))
+ w.Header().Set("Last-Modified", listing.lastModified.Format(http.TimeFormat))
- // write response as either JSON or HTML
- if strings.Contains(acceptHeader, "application/json") {
+ switch {
+ case strings.Contains(acceptHeader, "application/json"):
if err := json.NewEncoder(buf).Encode(listing.Items); err != nil {
return caddyhttp.Error(http.StatusInternalServerError, err)
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
- } else {
+
+ case strings.Contains(acceptHeader, "text/plain"):
+ writer := tabwriter.NewWriter(buf, 0, 8, 1, '\t', tabwriter.AlignRight)
+
+ // Header on top
+ if _, err := fmt.Fprintln(writer, "Name\tSize\tModified"); err != nil {
+ return caddyhttp.Error(http.StatusInternalServerError, err)
+ }
+
+ // Lines to separate the header
+ if _, err := fmt.Fprintln(writer, "----\t----\t--------"); err != nil {
+ return caddyhttp.Error(http.StatusInternalServerError, err)
+ }
+
+ // Actual files
+ for _, item := range listing.Items {
+ if _, err := fmt.Fprintf(writer, "%s\t%s\t%s\n",
+ item.Name, item.HumanSize(), item.HumanModTime("January 2, 2006 at 15:04:05"),
+ ); err != nil {
+ return caddyhttp.Error(http.StatusInternalServerError, err)
+ }
+ }
+
+ if err := writer.Flush(); err != nil {
+ return caddyhttp.Error(http.StatusInternalServerError, err)
+ }
+
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+
+ default:
var fs http.FileSystem
if fsrv.Root != "" {
fs = http.Dir(repl.ReplaceAll(fsrv.Root, "."))
@@ -148,7 +213,16 @@ 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
+ // modTime for the directory itself
+ stat, err := dir.Stat()
+ if err != nil {
+ return nil, err
+ }
+ 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
}
@@ -156,17 +230,40 @@ func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, fileSystem fs
// user can presumably browse "up" to parent folder if path is longer than "/"
canGoUp := len(urlPath) > 1
- return fsrv.directoryListing(ctx, fileSystem, files, canGoUp, root, urlPath, repl), nil
+ return fsrv.directoryListing(ctx, fileSystem, stat.ModTime(), files, canGoUp, root, urlPath, repl), nil
}
// browseApplyQueryParams applies query parameters to the listing.
// It mutates the listing and may set cookies.
func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Request, listing *browseTemplateContext) {
+ var orderParam, sortParam string
+
+ // The configs in Caddyfile have lower priority than Query params,
+ // so put it at first.
+ for idx, item := range fsrv.Browse.SortOptions {
+ // Only `sort` & `order`, 2 params are allowed
+ if idx >= 2 {
+ break
+ }
+ switch item {
+ case sortByName, sortByNameDirFirst, sortBySize, sortByTime:
+ sortParam = item
+ case sortOrderAsc, sortOrderDesc:
+ orderParam = item
+ }
+ }
+
layoutParam := r.URL.Query().Get("layout")
- sortParam := r.URL.Query().Get("sort")
- orderParam := r.URL.Query().Get("order")
limitParam := r.URL.Query().Get("limit")
offsetParam := r.URL.Query().Get("offset")
+ sortParamTmp := r.URL.Query().Get("sort")
+ if sortParamTmp != "" {
+ sortParam = sortParamTmp
+ }
+ orderParamTmp := r.URL.Query().Get("order")
+ if orderParamTmp != "" {
+ orderParam = orderParamTmp
+ }
switch layoutParam {
case "list", "grid", "":
@@ -189,11 +286,11 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re
// then figure out the order
switch orderParam {
case "":
- orderParam = "asc"
+ orderParam = sortOrderAsc
if orderCookie, orderErr := r.Cookie("order"); orderErr == nil {
orderParam = orderCookie.Value
}
- case "asc", "desc":
+ case sortOrderAsc, sortOrderDesc:
http.SetCookie(w, &http.Cookie{Name: "order", Value: orderParam, Secure: r.TLS != nil})
}
diff --git a/modules/caddyhttp/fileserver/browse.html b/modules/caddyhttp/fileserver/browse.html
index 484d90020..704661f21 100644
--- a/modules/caddyhttp/fileserver/browse.html
+++ b/modules/caddyhttp/fileserver/browse.html
@@ -1,10 +1,17 @@
+{{ $nonce := uuidv4 -}}
+{{ $nonceAttribute := print "nonce=" (quote $nonce) -}}
+{{ $csp := printf "default-src 'none'; img-src 'self'; object-src 'none'; base-uri 'none'; script-src 'nonce-%s'; style-src 'nonce-%s'; frame-ancestors 'self'; form-action 'self';" $nonce $nonce -}}
+{{/* To disable the Content-Security-Policy, set this to false */}}{{ $enableCsp := true -}}
+{{ if $enableCsp -}}
+ {{- .RespHeader.Set "Content-Security-Policy" $csp -}}
+{{ end -}}
{{- define "icon"}}
{{- if .IsDir}}
{{- if .IsSymlink}}
{{- else}}
- {{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp" ".tiff" ".bmp" ".heif" ".heic" ".svg"}}
+ {{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp" ".tiff" ".bmp" ".heif" ".heic" ".svg" ".avif"}}
{{- if eq .Tpl.Layout "grid"}}
-
+
{{- else}}
-
+
@@ -980,7 +991,7 @@ footer {
- {{.HumanSize}}
+ {{if .IsSymlink}}↱ {{end}}{{.HumanSize}}
@@ -1000,70 +1011,70 @@ footer {
- |