diff --git a/.editorconfig b/.editorconfig index 7134bf7ec..96948b9db 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,5 +1,5 @@ [*] end_of_line = lf -[caddytest/integration/caddyfile_adapt/*.txt] +[caddytest/integration/caddyfile_adapt/*.caddyfiletest] indent_style = tab \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20f2fc5a9..309ef7935 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,45 +19,49 @@ jobs: fail-fast: false matrix: os: - - ubuntu-latest - - macos-latest - - windows-latest + - linux + - mac + - windows go: - - '1.20' - '1.21' + - '1.22' include: # Set the minimum Go patch version for the given Go minor # Usable via ${{ matrix.GO_SEMVER }} - - go: '1.20' - GO_SEMVER: '~1.20.6' - - go: '1.21' GO_SEMVER: '~1.21.0' + - go: '1.22' + GO_SEMVER: '~1.22.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) # CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing # SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True') - - os: ubuntu-latest + - os: linux + OS_LABEL: ubuntu-latest CADDY_BIN_PATH: ./cmd/caddy/caddy SUCCESS: 0 - - os: macos-latest + - os: mac + OS_LABEL: macos-14 CADDY_BIN_PATH: ./cmd/caddy/caddy SUCCESS: 0 - - os: windows-latest + - os: windows + OS_LABEL: windows-latest CADDY_BIN_PATH: ./cmd/caddy/caddy.exe SUCCESS: 'True' - runs-on: ${{ matrix.os }} + runs-on: ${{ matrix.OS_LABEL }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.GO_SEMVER }} check-latest: true @@ -95,13 +99,14 @@ jobs: env: CGO_ENABLED: 0 run: | - go build -trimpath -ldflags="-w -s" -v + go build -tags nobdger -trimpath -ldflags="-w -s" -v - name: Publish Build Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: caddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }} path: ${{ matrix.CADDY_BIN_PATH }} + compression-level: 0 # Commented bits below were useful to allow the job to continue # even if the tests fail, so we can publish the report separately @@ -111,7 +116,7 @@ jobs: # continue-on-error: true run: | # (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out - go test -v -coverprofile="cover-profile.out" -short -race ./... + go test -tags nobadger -v -coverprofile="cover-profile.out" -short -race ./... # echo "status=$?" >> $GITHUB_OUTPUT # Relevant step if we reinvestigate publishing test/coverage reports @@ -124,7 +129,7 @@ jobs: # To return the correct result even though we set 'continue-on-error: true' # - name: Coerce correct build result - # if: matrix.os != 'windows-latest' && steps.step_test.outputs.status != ${{ matrix.SUCCESS }} + # if: matrix.os != 'windows' && steps.step_test.outputs.status != ${{ matrix.SUCCESS }} # run: | # echo "step_test ${{ steps.step_test.outputs.status }}\n" # exit 1 @@ -136,7 +141,7 @@ jobs: continue-on-error: true # August 2020: s390x VM is down due to weather and power issues steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run Tests run: | mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa @@ -146,7 +151,7 @@ jobs: # The environment is fresh, so there's no point in keeping accepting and adding the key. rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . "$CI_USER"@ci-s390x.caddyserver.com:/var/tmp/"$short_sha" - ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t "$CI_USER"@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -v ./..." + ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t "$CI_USER"@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -tags nobadger -v ./..." test_result=$? # There's no need leaving the files around @@ -162,11 +167,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - uses: goreleaser/goreleaser-action@v4 + - uses: goreleaser/goreleaser-action@v5 with: version: latest args: check - env: - TAG: ${{ steps.vars.outputs.version_tag }} diff --git a/.github/workflows/cross-build.yml b/.github/workflows/cross-build.yml index 678ac178b..676607d0e 100644 --- a/.github/workflows/cross-build.yml +++ b/.github/workflows/cross-build.yml @@ -11,11 +11,12 @@ on: - 2.* jobs: - cross-build-test: + build: strategy: fail-fast: false matrix: goos: + - 'aix' - 'android' - 'linux' - 'solaris' @@ -28,22 +29,22 @@ jobs: - 'darwin' - 'netbsd' go: - - '1.21' + - '1.22' 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' runs-on: ubuntu-latest continue-on-error: true steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.GO_SEMVER }} check-latest: true @@ -62,11 +63,12 @@ jobs: env: CGO_ENABLED: 0 GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goos == 'aix' && 'ppc64' || 'amd64' }} shell: bash continue-on-error: true working-directory: ./cmd/caddy run: | - GOOS=$GOOS go build -trimpath -o caddy-"$GOOS"-amd64 2> /dev/null + 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 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ea5e791ad..bfb91dc66 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,24 +23,33 @@ jobs: strategy: matrix: os: - - ubuntu-latest - - macos-latest - - windows-latest - runs-on: ${{ matrix.os }} + - linux + - mac + - windows + + include: + - os: linux + OS_LABEL: ubuntu-latest + + - os: mac + OS_LABEL: macos-14 + + - os: windows + OS_LABEL: windows-latest + + runs-on: ${{ matrix.OS_LABEL }} + steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: '~1.21.0' + go-version: '~1.22.1' check-latest: true - # Workaround for https://github.com/golangci/golangci-lint-action/issues/135 - skip-pkg-cache: true - - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v4 with: - version: v1.54 + version: v1.55 # Workaround for https://github.com/golangci/golangci-lint-action/issues/135 skip-pkg-cache: true @@ -50,3 +59,12 @@ jobs: # Optional: show only new issues if it's a pull request. The default value is `false`. # only-new-issues: true + + govulncheck: + runs-on: ubuntu-latest + steps: + - name: govulncheck + uses: golang/govulncheck-action@v1 + with: + go-version-input: '~1.22.1' + check-latest: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ec9c97260..b11686626 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,18 +32,18 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.GO_SEMVER }} check-latest: true # Force fetch upstream tags -- because 65 minutes - # tl;dr: actions/checkout@v3 runs this line: + # tl;dr: actions/checkout@v4 runs this line: # git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/ # which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran: # git fetch --prune --unshallow @@ -106,7 +106,7 @@ jobs: run: syft version # GoReleaser will take care of publishing those artifacts into the release - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v4 + uses: goreleaser/goreleaser-action@v5 with: version: latest args: release --clean --timeout 60m diff --git a/.github/workflows/release_published.yml b/.github/workflows/release_published.yml index f304888e8..491dae75d 100644 --- a/.github/workflows/release_published.yml +++ b/.github/workflows/release_published.yml @@ -18,7 +18,7 @@ jobs: # See https://github.com/peter-evans/repository-dispatch - name: Trigger event on caddyserver/dist - uses: peter-evans/repository-dispatch@v2 + uses: peter-evans/repository-dispatch@v3 with: token: ${{ secrets.REPO_DISPATCH_TOKEN }} repository: caddyserver/dist @@ -26,7 +26,7 @@ jobs: client-payload: '{"tag": "${{ github.event.release.tag_name }}"}' - name: Trigger event on caddyserver/caddy-docker - uses: peter-evans/repository-dispatch@v2 + uses: peter-evans/repository-dispatch@v3 with: token: ${{ secrets.REPO_DISPATCH_TOKEN }} repository: caddyserver/caddy-docker diff --git a/.gitignore b/.gitignore index d6ee0e812..6cde9db00 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ Caddyfile.* cmd/caddy/caddy cmd/caddy/caddy.exe cmd/caddy/tmp/*.exe +cmd/caddy/.env # mac specific .DS_Store diff --git a/.golangci.yml b/.golangci.yml index 5f018970e..d144395db 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -15,35 +15,68 @@ linters-settings: # If `true`, make the section order the same as the order of `sections`. # Default: false custom-order: true + exhaustive: + ignore-enum-types: reflect.Kind|svc.Cmd linters: disable-all: true enable: + - asasalint + - asciicheck + - bidichk - bodyclose + - decorder + - dogsled + - dupl + - dupword + - durationcheck - errcheck + - errname + - exhaustive + - exportloopref - gci + - gofmt + - goimports - gofumpt - gosec - gosimple - govet - ineffassign + - importas - misspell - prealloc + - promlinter + - sloglint + - sqlclosecheck - staticcheck + - tenv + - testableexamples + - testifylint + - tparallel - typecheck - unconvert - unused + - wastedassign + - whitespace + - zerologlint # these are implicitly disabled: - # - asciicheck + # - containedctx + # - contextcheck + # - cyclop # - depguard - # - dogsled - # - dupl - # - exhaustive - # - exportloopref + # - errchkjson + # - errorlint + # - exhaustruct + # - execinquery + # - exhaustruct + # - forbidigo + # - forcetypeassert # - funlen - # - gci + # - ginkgolinter + # - gocheckcompilerdirectives # - gochecknoglobals # - gochecknoinits + # - gochecksumtype # - gocognit # - goconst # - gocritic @@ -51,27 +84,47 @@ linters: # - godot # - godox # - goerr113 - # - gofumpt # - goheader - # - golint # - gomnd + # - gomoddirectives # - gomodguard # - goprintffuncname - # - interfacer + # - gosmopolitan + # - grouper + # - inamedparam + # - interfacebloat + # - ireturn # - lll - # - maligned + # - loggercheck + # - maintidx + # - makezero + # - mirror + # - musttag # - nakedret # - nestif + # - nilerr + # - nilnil # - nlreturn # - noctx # - nolintlint + # - nonamedreturns + # - nosprintfhostport + # - paralleltest + # - perfsprint + # - predeclared + # - protogetter + # - reassign + # - revive # - rowserrcheck - # - scopelint - # - sqlclosecheck # - stylecheck + # - tagalign + # - tagliatelle # - testpackage + # - thelper # - unparam - # - whitespace + # - usestdlibvars + # - varnamelen + # - wrapcheck # - wsl run: @@ -110,3 +163,6 @@ issues: text: 'G404' # G404: Insecure random number source (rand) linters: - gosec + - path: modules/logging/filters.go + linters: + - dupl diff --git a/.goreleaser.yml b/.goreleaser.yml index dfd6589df..0fba8e2c4 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -77,6 +77,8 @@ builds: - -mod=readonly ldflags: - -s -w + tags: + - nobadger signs: - cmd: cosign diff --git a/README.md b/README.md index 6dd046887..e5f99efdf 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ See [our online documentation](https://caddyserver.com/docs/install) for other i Requirements: -- [Go 1.20 or newer](https://golang.org/dl/) +- [Go 1.21 or newer](https://golang.org/dl/) ### For development diff --git a/admin.go b/admin.go index 335b8e895..0a9bfc49b 100644 --- a/admin.go +++ b/admin.go @@ -1196,15 +1196,27 @@ traverseLoop: } case http.MethodPut: if _, ok := v[part]; ok { - return fmt.Errorf("[%s] key already exists: %s", path, part) + return APIError{ + HTTPStatus: http.StatusConflict, + Err: fmt.Errorf("[%s] key already exists: %s", path, part), + } } v[part] = val case http.MethodPatch: if _, ok := v[part]; !ok { - return fmt.Errorf("[%s] key does not exist: %s", path, part) + return APIError{ + HTTPStatus: http.StatusNotFound, + Err: fmt.Errorf("[%s] key does not exist: %s", path, part), + } } v[part] = val case http.MethodDelete: + if _, ok := v[part]; !ok { + return APIError{ + HTTPStatus: http.StatusNotFound, + Err: fmt.Errorf("[%s] key does not exist: %s", path, part), + } + } delete(v, part) default: return fmt.Errorf("unrecognized method %s", method) diff --git a/admin_test.go b/admin_test.go index 04aa8867f..9137a8881 100644 --- a/admin_test.go +++ b/admin_test.go @@ -75,6 +75,12 @@ func TestUnsyncedConfigAccess(t *testing.T) { path: "/bar/qq", expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`, }, + { + method: "DELETE", + path: "/bar/qq", + expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`, + shouldErr: true, + }, { method: "POST", path: "/list", diff --git a/caddy.go b/caddy.go index 8e7dce507..1e0eacd2b 100644 --- a/caddy.go +++ b/caddy.go @@ -22,6 +22,7 @@ import ( "errors" "fmt" "io" + "io/fs" "log" "net/http" "os" @@ -38,6 +39,7 @@ import ( "github.com/google/uuid" "go.uber.org/zap" + "github.com/caddyserver/caddy/v2/internal/filesystems" "github.com/caddyserver/caddy/v2/notify" ) @@ -83,6 +85,9 @@ type Config struct { storage certmagic.Storage cancelFunc context.CancelFunc + + // filesystems is a dict of filesystems that will later be loaded from and added to. + filesystems FileSystems } // App is a thing that Caddy runs. @@ -446,6 +451,9 @@ func run(newCfg *Config, start bool) (Context, error) { } } + // create the new filesystem map + newCfg.filesystems = &filesystems.FilesystemMap{} + // prepare the new config for use newCfg.apps = make(map[string]App) @@ -707,6 +715,7 @@ func exitProcess(ctx context.Context, logger *zap.Logger) { logger.Warn("exiting; byeee!! 👋") exitCode := ExitCodeSuccess + lastContext := ActiveContext() // stop all apps if err := Stop(); err != nil { @@ -728,6 +737,16 @@ func exitProcess(ctx context.Context, logger *zap.Logger) { } } + // execute any process-exit callbacks + for _, exitFunc := range lastContext.exitFuncs { + exitFunc(ctx) + } + exitFuncsMu.Lock() + for _, exitFunc := range exitFuncs { + exitFunc(ctx) + } + exitFuncsMu.Unlock() + // shut down admin endpoint(s) in goroutines so that // if this function was called from an admin handler, // it has a chance to return gracefully @@ -766,6 +785,23 @@ var exiting = new(int32) // accessed atomically // EXPERIMENTAL API: subject to change or removal. func Exiting() bool { return atomic.LoadInt32(exiting) == 1 } +// OnExit registers a callback to invoke during process exit. +// This registration is PROCESS-GLOBAL, meaning that each +// function should only be registered once forever, NOT once +// per config load (etc). +// +// EXPERIMENTAL API: subject to change or removal. +func OnExit(f func(context.Context)) { + exitFuncsMu.Lock() + exitFuncs = append(exitFuncs, f) + exitFuncsMu.Unlock() +} + +var ( + exitFuncs []func(context.Context) + exitFuncsMu sync.Mutex +) + // Duration can be an integer or a string. An integer is // interpreted as nanoseconds. If a string, it is a Go // time.Duration value such as `300ms`, `1.5h`, or `2h45m`; @@ -825,13 +861,18 @@ func ParseDuration(s string) (time.Duration, error) { // regardless of storage configuration, since each instance is intended to // have its own unique ID. func InstanceID() (uuid.UUID, error) { - uuidFilePath := filepath.Join(AppDataDir(), "instance.uuid") + appDataDir := AppDataDir() + uuidFilePath := filepath.Join(appDataDir, "instance.uuid") uuidFileBytes, err := os.ReadFile(uuidFilePath) - if os.IsNotExist(err) { + if errors.Is(err, fs.ErrNotExist) { uuid, err := uuid.NewRandom() if err != nil { return uuid, err } + err = os.MkdirAll(appDataDir, 0o600) + if err != nil { + return uuid, err + } err = os.WriteFile(uuidFilePath, []byte(uuid.String()), 0o600) return uuid, err } else if err != nil { diff --git a/caddyconfig/caddyfile/adapter.go b/caddyconfig/caddyfile/adapter.go index d6ef602dc..da4f98337 100644 --- a/caddyconfig/caddyfile/adapter.go +++ b/caddyconfig/caddyfile/adapter.go @@ -52,7 +52,7 @@ func (a Adapter) Adapt(body []byte, options map[string]any) ([]byte, []caddyconf return nil, warnings, err } - // lint check: see if input was properly formatted; sometimes messy files files parse + // lint check: see if input was properly formatted; sometimes messy files parse // successfully but result in logical errors (the Caddyfile is a bad format, I'm sorry) if warning, different := FormattingDifference(filename, body); different { warnings = append(warnings, warning) @@ -92,30 +92,26 @@ func FormattingDifference(filename string, body []byte) (caddyconfig.Warning, bo }, true } -// Unmarshaler is a type that can unmarshal -// Caddyfile tokens to set itself up for a -// JSON encoding. The goal of an unmarshaler -// is not to set itself up for actual use, -// but to set itself up for being marshaled -// into JSON. Caddyfile-unmarshaled values -// will not be used directly; they will be -// encoded as JSON and then used from that. -// Implementations must be able to support -// multiple segments (instances of their -// directive or batch of tokens); typically -// this means wrapping all token logic in -// a loop: `for d.Next() { ... }`. +// Unmarshaler is a type that can unmarshal Caddyfile tokens to +// set itself up for a JSON encoding. The goal of an unmarshaler +// is not to set itself up for actual use, but to set itself up for +// being marshaled into JSON. Caddyfile-unmarshaled values will not +// be used directly; they will be encoded as JSON and then used from +// that. Implementations _may_ be able to support multiple segments +// (instances of their directive or batch of tokens); typically this +// means wrapping parsing logic in a loop: `for d.Next() { ... }`. +// More commonly, only a single segment is supported, so a simple +// `d.Next()` at the start should be used to consume the module +// identifier token (directive name, etc). type Unmarshaler interface { UnmarshalCaddyfile(d *Dispenser) error } // ServerType is a type that can evaluate a Caddyfile and set up a caddy config. type ServerType interface { - // Setup takes the server blocks which - // contain tokens, as well as options - // (e.g. CLI flags) and creates a Caddy - // config, along with any warnings or - // an error. + // Setup takes the server blocks which contain tokens, + // as well as options (e.g. CLI flags) and creates a + // Caddy config, along with any warnings or an error. Setup([]ServerBlock, map[string]any) (*caddy.Config, []caddyconfig.Warning, error) } diff --git a/caddyconfig/caddyfile/dispenser_test.go b/caddyconfig/caddyfile/dispenser_test.go index b64a97354..0f6ee5043 100644 --- a/caddyconfig/caddyfile/dispenser_test.go +++ b/caddyconfig/caddyfile/dispenser_test.go @@ -305,7 +305,7 @@ func TestDispenser_ArgErr_Err(t *testing.T) { t.Errorf("Expected error message with custom message in it ('foobar'); got '%v'", err) } - var ErrBarIsFull = errors.New("bar is full") + ErrBarIsFull := errors.New("bar is full") bookingError := d.Errf("unable to reserve: %w", ErrBarIsFull) if !errors.Is(bookingError, ErrBarIsFull) { t.Errorf("Errf(): should be able to unwrap the error chain") diff --git a/caddyconfig/caddyfile/formatter.go b/caddyconfig/caddyfile/formatter.go index 82581a3d3..d506219c4 100644 --- a/caddyconfig/caddyfile/formatter.go +++ b/caddyconfig/caddyfile/formatter.go @@ -18,6 +18,8 @@ import ( "bytes" "io" "unicode" + + "golang.org/x/exp/slices" ) // Format formats the input Caddyfile to a standard, nice-looking @@ -31,6 +33,14 @@ func Format(input []byte) []byte { out := new(bytes.Buffer) rdr := bytes.NewReader(input) + type heredocState int + + const ( + heredocClosed heredocState = 0 + heredocOpening heredocState = 1 + heredocOpened heredocState = 2 + ) + var ( last rune // the last character that was written to the result @@ -47,6 +57,11 @@ func Format(input []byte) []byte { quoted bool // whether we're in a quoted segment escaped bool // whether current char is escaped + heredoc heredocState // whether we're in a heredoc + heredocEscaped bool // whether heredoc is escaped + heredocMarker []rune + heredocClosingMarker []rune + nesting int // indentation level ) @@ -75,6 +90,62 @@ func Format(input []byte) []byte { panic(err) } + // detect whether we have the start of a heredoc + if !quoted && !(heredoc != heredocClosed || heredocEscaped) && + space && last == '<' && ch == '<' { + write(ch) + heredoc = heredocOpening + space = false + continue + } + + if heredoc == heredocOpening { + if ch == '\n' { + if len(heredocMarker) > 0 && heredocMarkerRegexp.MatchString(string(heredocMarker)) { + heredoc = heredocOpened + } else { + heredocMarker = nil + heredoc = heredocClosed + nextLine() + continue + } + write(ch) + continue + } + if unicode.IsSpace(ch) { + // a space means it's just a regular token and not a heredoc + heredocMarker = nil + heredoc = heredocClosed + } else { + heredocMarker = append(heredocMarker, ch) + write(ch) + continue + } + } + // if we're in a heredoc, all characters are read&write as-is + if heredoc == heredocOpened { + heredocClosingMarker = append(heredocClosingMarker, ch) + if len(heredocClosingMarker) > len(heredocMarker)+1 { // We assert that the heredocClosingMarker is followed by a unicode.Space + heredocClosingMarker = heredocClosingMarker[1:] + } + // check if we're done + if unicode.IsSpace(ch) && slices.Equal(heredocClosingMarker[:len(heredocClosingMarker)-1], heredocMarker) { + heredocMarker = nil + heredocClosingMarker = nil + heredoc = heredocClosed + } else { + write(ch) + if ch == '\n' { + heredocClosingMarker = heredocClosingMarker[:0] + } + continue + } + } + + if last == '<' && space { + space = false + } + if comment { if ch == '\n' { comment = false @@ -98,6 +169,9 @@ func Format(input []byte) []byte { } if escaped { + if ch == '<' { + heredocEscaped = true + } write(ch) escaped = false continue @@ -117,6 +191,7 @@ func Format(input []byte) []byte { if unicode.IsSpace(ch) { space = true + heredocEscaped = false if ch == '\n' { newLines++ } @@ -205,6 +280,11 @@ func Format(input []byte) []byte { write('{') openBraceWritten = true } + + if spacePrior && ch == '<' { + space = true + } + write(ch) beginningOfLine = false diff --git a/caddyconfig/caddyfile/formatter_test.go b/caddyconfig/caddyfile/formatter_test.go index 8e5b36860..6eec822fe 100644 --- a/caddyconfig/caddyfile/formatter_test.go +++ b/caddyconfig/caddyfile/formatter_test.go @@ -362,6 +362,76 @@ block { block { } +`, + }, + { + description: "keep heredoc as-is", + input: `block { + heredoc < len(heredocMarker) && heredocMarker == string(val[len(val)-len(heredocMarker):]) { + if len(val) >= len(heredocMarker) && heredocMarker == string(val[len(val)-len(heredocMarker):]) { // set the final value val, err = l.finalizeHeredoc(val, heredocMarker) if err != nil { @@ -313,6 +313,11 @@ func (l *lexer) finalizeHeredoc(val []rune, marker string) ([]rune, error) { // iterate over each line and strip the whitespace from the front var out string for lineNum, lineText := range lines[:len(lines)-1] { + if lineText == "" || lineText == "\r" { + out += "\n" + continue + } + // find an exact match for the padding index := strings.Index(lineText, paddingToStrip) diff --git a/caddyconfig/caddyfile/lexer_test.go b/caddyconfig/caddyfile/lexer_test.go index 92acc4da9..7389af79b 100644 --- a/caddyconfig/caddyfile/lexer_test.go +++ b/caddyconfig/caddyfile/lexer_test.go @@ -285,6 +285,18 @@ EOF same-line-arg }, { input: []byte(`heredoc < func parseBind(h Helper) ([]ConfigValue, error) { - var lnHosts []string - for h.Next() { - lnHosts = append(lnHosts, h.RemainingArgs()...) - } - return h.NewBindAddresses(lnHosts), nil + h.Next() // consume directive name + return []ConfigValue{{Class: "bind", Value: h.RemainingArgs()}}, nil } // parseTLS parses the tls directive. Syntax: @@ -90,12 +86,15 @@ func parseBind(h Helper) ([]ConfigValue, error) { // dns_ttl // dns_challenge_override_domain // on_demand +// reuse_private_keys // eab // issuer [...] // get_certificate [...] // insecure_secrets_log // } func parseTLS(h Helper) ([]ConfigValue, error) { + h.Next() // consume directive name + cp := new(caddytls.ConnectionPolicy) var fileLoader caddytls.FileLoader var folderLoader caddytls.FolderLoader @@ -106,388 +105,347 @@ func parseTLS(h Helper) ([]ConfigValue, error) { var issuers []certmagic.Issuer var certManagers []certmagic.Manager var onDemand bool + var reusePrivateKeys bool - for h.Next() { - // file certificate loader - firstLine := h.RemainingArgs() - switch len(firstLine) { - case 0: - case 1: - if firstLine[0] == "internal" { - internalIssuer = new(caddytls.InternalIssuer) - } else if !strings.Contains(firstLine[0], "@") { - return nil, h.Err("single argument must either be 'internal' or an email address") - } else { - if acmeIssuer == nil { - acmeIssuer = new(caddytls.ACMEIssuer) + // file certificate loader + firstLine := h.RemainingArgs() + switch len(firstLine) { + case 0: + case 1: + if firstLine[0] == "internal" { + internalIssuer = new(caddytls.InternalIssuer) + } else if !strings.Contains(firstLine[0], "@") { + return nil, h.Err("single argument must either be 'internal' or an email address") + } else { + if acmeIssuer == nil { + acmeIssuer = new(caddytls.ACMEIssuer) + } + acmeIssuer.Email = firstLine[0] + } + + case 2: + certFilename := firstLine[0] + keyFilename := firstLine[1] + + // tag this certificate so if multiple certs match, specifically + // this one that the user has provided will be used, see #2588: + // https://github.com/caddyserver/caddy/issues/2588 ... but we + // must be careful about how we do this; being careless will + // lead to failed handshakes + // + // we need to remember which cert files we've seen, since we + // must load each cert only once; otherwise, they each get a + // different tag... since a cert loaded twice has the same + // bytes, it will overwrite the first one in the cache, and + // only the last cert (and its tag) will survive, so any conn + // policy that is looking for any tag other than the last one + // to be loaded won't find it, and TLS handshakes will fail + // (see end of issue #3004) + // + // tlsCertTags maps certificate filenames to their tag. + // This is used to remember which tag is used for each + // certificate files, since we need to avoid loading + // the same certificate files more than once, overwriting + // previous tags + tlsCertTags, ok := h.State["tlsCertTags"].(map[string]string) + if !ok { + tlsCertTags = make(map[string]string) + h.State["tlsCertTags"] = tlsCertTags + } + + tag, ok := tlsCertTags[certFilename] + if !ok { + // haven't seen this cert file yet, let's give it a tag + // and add a loader for it + tag = fmt.Sprintf("cert%d", len(tlsCertTags)) + fileLoader = append(fileLoader, caddytls.CertKeyFilePair{ + Certificate: certFilename, + Key: keyFilename, + Tags: []string{tag}, + }) + // remember this for next time we see this cert file + tlsCertTags[certFilename] = tag + } + certSelector.AnyTag = append(certSelector.AnyTag, tag) + + default: + return nil, h.ArgErr() + } + + var hasBlock bool + for h.NextBlock(0) { + hasBlock = true + + switch h.Val() { + case "protocols": + args := h.RemainingArgs() + if len(args) == 0 { + return nil, h.Errf("protocols requires one or two arguments") + } + if len(args) > 0 { + if _, ok := caddytls.SupportedProtocols[args[0]]; !ok { + return nil, h.Errf("wrong protocol name or protocol not supported: '%s'", args[0]) } - acmeIssuer.Email = firstLine[0] + cp.ProtocolMin = args[0] + } + if len(args) > 1 { + if _, ok := caddytls.SupportedProtocols[args[1]]; !ok { + return nil, h.Errf("wrong protocol name or protocol not supported: '%s'", args[1]) + } + cp.ProtocolMax = args[1] } - case 2: - certFilename := firstLine[0] - keyFilename := firstLine[1] + case "ciphers": + for h.NextArg() { + if !caddytls.CipherSuiteNameSupported(h.Val()) { + return nil, h.Errf("wrong cipher suite name or cipher suite not supported: '%s'", h.Val()) + } + cp.CipherSuites = append(cp.CipherSuites, h.Val()) + } - // tag this certificate so if multiple certs match, specifically - // this one that the user has provided will be used, see #2588: - // https://github.com/caddyserver/caddy/issues/2588 ... but we - // must be careful about how we do this; being careless will - // lead to failed handshakes - // - // we need to remember which cert files we've seen, since we - // must load each cert only once; otherwise, they each get a - // different tag... since a cert loaded twice has the same - // bytes, it will overwrite the first one in the cache, and - // only the last cert (and its tag) will survive, so any conn - // policy that is looking for any tag other than the last one - // to be loaded won't find it, and TLS handshakes will fail - // (see end of issue #3004) - // - // tlsCertTags maps certificate filenames to their tag. - // This is used to remember which tag is used for each - // certificate files, since we need to avoid loading - // the same certificate files more than once, overwriting - // previous tags - tlsCertTags, ok := h.State["tlsCertTags"].(map[string]string) + case "curves": + for h.NextArg() { + if _, ok := caddytls.SupportedCurves[h.Val()]; !ok { + return nil, h.Errf("Wrong curve name or curve not supported: '%s'", h.Val()) + } + cp.Curves = append(cp.Curves, h.Val()) + } + + case "client_auth": + cp.ClientAuthentication = &caddytls.ClientAuthentication{} + if err := cp.ClientAuthentication.UnmarshalCaddyfile(h.NewFromNextSegment()); err != nil { + return nil, err + } + case "alpn": + args := h.RemainingArgs() + if len(args) == 0 { + return nil, h.ArgErr() + } + cp.ALPN = args + + case "load": + folderLoader = append(folderLoader, h.RemainingArgs()...) + + case "ca": + arg := h.RemainingArgs() + if len(arg) != 1 { + return nil, h.ArgErr() + } + if acmeIssuer == nil { + acmeIssuer = new(caddytls.ACMEIssuer) + } + acmeIssuer.CA = arg[0] + + case "key_type": + arg := h.RemainingArgs() + if len(arg) != 1 { + return nil, h.ArgErr() + } + keyType = arg[0] + + case "eab": + arg := h.RemainingArgs() + if len(arg) != 2 { + return nil, h.ArgErr() + } + if acmeIssuer == nil { + acmeIssuer = new(caddytls.ACMEIssuer) + } + acmeIssuer.ExternalAccount = &acme.EAB{ + KeyID: arg[0], + MACKey: arg[1], + } + + case "issuer": + if !h.NextArg() { + return nil, h.ArgErr() + } + modName := h.Val() + modID := "tls.issuance." + modName + unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID) + if err != nil { + return nil, err + } + issuer, ok := unm.(certmagic.Issuer) if !ok { - tlsCertTags = make(map[string]string) - h.State["tlsCertTags"] = tlsCertTags + return nil, h.Errf("module %s (%T) is not a certmagic.Issuer", modID, unm) } + issuers = append(issuers, issuer) - tag, ok := tlsCertTags[certFilename] - if !ok { - // haven't seen this cert file yet, let's give it a tag - // and add a loader for it - tag = fmt.Sprintf("cert%d", len(tlsCertTags)) - fileLoader = append(fileLoader, caddytls.CertKeyFilePair{ - Certificate: certFilename, - Key: keyFilename, - Tags: []string{tag}, - }) - // remember this for next time we see this cert file - tlsCertTags[certFilename] = tag + case "get_certificate": + if !h.NextArg() { + return nil, h.ArgErr() } - certSelector.AnyTag = append(certSelector.AnyTag, tag) + modName := h.Val() + modID := "tls.get_certificate." + modName + unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID) + if err != nil { + return nil, err + } + certManager, ok := unm.(certmagic.Manager) + if !ok { + return nil, h.Errf("module %s (%T) is not a certmagic.CertificateManager", modID, unm) + } + certManagers = append(certManagers, certManager) + + case "dns": + if !h.NextArg() { + return nil, h.ArgErr() + } + provName := h.Val() + if acmeIssuer == nil { + acmeIssuer = new(caddytls.ACMEIssuer) + } + if acmeIssuer.Challenges == nil { + acmeIssuer.Challenges = new(caddytls.ChallengesConfig) + } + 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 + } + acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, h.warnings) + + case "resolvers": + args := h.RemainingArgs() + if len(args) == 0 { + return nil, h.ArgErr() + } + if acmeIssuer == nil { + acmeIssuer = new(caddytls.ACMEIssuer) + } + if acmeIssuer.Challenges == nil { + acmeIssuer.Challenges = new(caddytls.ChallengesConfig) + } + if acmeIssuer.Challenges.DNS == nil { + acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig) + } + acmeIssuer.Challenges.DNS.Resolvers = args + + case "propagation_delay": + arg := h.RemainingArgs() + if len(arg) != 1 { + return nil, h.ArgErr() + } + delayStr := arg[0] + delay, err := caddy.ParseDuration(delayStr) + if err != nil { + return nil, h.Errf("invalid propagation_delay duration %s: %v", delayStr, err) + } + if acmeIssuer == nil { + acmeIssuer = new(caddytls.ACMEIssuer) + } + if acmeIssuer.Challenges == nil { + acmeIssuer.Challenges = new(caddytls.ChallengesConfig) + } + if acmeIssuer.Challenges.DNS == nil { + acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig) + } + acmeIssuer.Challenges.DNS.PropagationDelay = caddy.Duration(delay) + + case "propagation_timeout": + arg := h.RemainingArgs() + if len(arg) != 1 { + return nil, h.ArgErr() + } + timeoutStr := arg[0] + var timeout time.Duration + if timeoutStr == "-1" { + timeout = time.Duration(-1) + } else { + var err error + timeout, err = caddy.ParseDuration(timeoutStr) + if err != nil { + return nil, h.Errf("invalid propagation_timeout duration %s: %v", timeoutStr, err) + } + } + if acmeIssuer == nil { + acmeIssuer = new(caddytls.ACMEIssuer) + } + if acmeIssuer.Challenges == nil { + acmeIssuer.Challenges = new(caddytls.ChallengesConfig) + } + if acmeIssuer.Challenges.DNS == nil { + acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig) + } + acmeIssuer.Challenges.DNS.PropagationTimeout = caddy.Duration(timeout) + + case "dns_ttl": + arg := h.RemainingArgs() + if len(arg) != 1 { + return nil, h.ArgErr() + } + ttlStr := arg[0] + ttl, err := caddy.ParseDuration(ttlStr) + if err != nil { + return nil, h.Errf("invalid dns_ttl duration %s: %v", ttlStr, err) + } + if acmeIssuer == nil { + acmeIssuer = new(caddytls.ACMEIssuer) + } + if acmeIssuer.Challenges == nil { + acmeIssuer.Challenges = new(caddytls.ChallengesConfig) + } + if acmeIssuer.Challenges.DNS == nil { + acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig) + } + acmeIssuer.Challenges.DNS.TTL = caddy.Duration(ttl) + + case "dns_challenge_override_domain": + arg := h.RemainingArgs() + if len(arg) != 1 { + return nil, h.ArgErr() + } + if acmeIssuer == nil { + acmeIssuer = new(caddytls.ACMEIssuer) + } + if acmeIssuer.Challenges == nil { + acmeIssuer.Challenges = new(caddytls.ChallengesConfig) + } + if acmeIssuer.Challenges.DNS == nil { + acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig) + } + acmeIssuer.Challenges.DNS.OverrideDomain = arg[0] + + case "ca_root": + arg := h.RemainingArgs() + if len(arg) != 1 { + return nil, h.ArgErr() + } + if acmeIssuer == nil { + acmeIssuer = new(caddytls.ACMEIssuer) + } + acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, arg[0]) + + case "on_demand": + if h.NextArg() { + return nil, h.ArgErr() + } + onDemand = true + + case "reuse_private_keys": + if h.NextArg() { + return nil, h.ArgErr() + } + reusePrivateKeys = true + + case "insecure_secrets_log": + if !h.NextArg() { + return nil, h.ArgErr() + } + cp.InsecureSecretsLog = h.Val() default: - return nil, h.ArgErr() + return nil, h.Errf("unknown subdirective: %s", h.Val()) } + } - var hasBlock bool - for nesting := h.Nesting(); h.NextBlock(nesting); { - hasBlock = true - - switch h.Val() { - case "protocols": - args := h.RemainingArgs() - if len(args) == 0 { - return nil, h.Errf("protocols requires one or two arguments") - } - if len(args) > 0 { - if _, ok := caddytls.SupportedProtocols[args[0]]; !ok { - return nil, h.Errf("wrong protocol name or protocol not supported: '%s'", args[0]) - } - cp.ProtocolMin = args[0] - } - if len(args) > 1 { - if _, ok := caddytls.SupportedProtocols[args[1]]; !ok { - return nil, h.Errf("wrong protocol name or protocol not supported: '%s'", args[1]) - } - cp.ProtocolMax = args[1] - } - - case "ciphers": - for h.NextArg() { - if !caddytls.CipherSuiteNameSupported(h.Val()) { - return nil, h.Errf("wrong cipher suite name or cipher suite not supported: '%s'", h.Val()) - } - cp.CipherSuites = append(cp.CipherSuites, h.Val()) - } - - case "curves": - for h.NextArg() { - if _, ok := caddytls.SupportedCurves[h.Val()]; !ok { - return nil, h.Errf("Wrong curve name or curve not supported: '%s'", h.Val()) - } - cp.Curves = append(cp.Curves, h.Val()) - } - - case "client_auth": - cp.ClientAuthentication = &caddytls.ClientAuthentication{} - for nesting := h.Nesting(); h.NextBlock(nesting); { - subdir := h.Val() - switch subdir { - case "mode": - if !h.Args(&cp.ClientAuthentication.Mode) { - return nil, h.ArgErr() - } - if h.NextArg() { - return nil, h.ArgErr() - } - - case "trusted_ca_cert", - "trusted_leaf_cert": - if !h.NextArg() { - return nil, h.ArgErr() - } - if subdir == "trusted_ca_cert" { - cp.ClientAuthentication.TrustedCACerts = append(cp.ClientAuthentication.TrustedCACerts, h.Val()) - } else { - cp.ClientAuthentication.TrustedLeafCerts = append(cp.ClientAuthentication.TrustedLeafCerts, h.Val()) - } - - case "trusted_ca_cert_file", - "trusted_leaf_cert_file": - if !h.NextArg() { - return nil, h.ArgErr() - } - filename := h.Val() - certDataPEM, err := os.ReadFile(filename) - if err != nil { - return nil, err - } - block, _ := pem.Decode(certDataPEM) - if block == nil || block.Type != "CERTIFICATE" { - return nil, h.Errf("no CERTIFICATE pem block found in %s", h.Val()) - } - if subdir == "trusted_ca_cert_file" { - cp.ClientAuthentication.TrustedCACerts = append(cp.ClientAuthentication.TrustedCACerts, - base64.StdEncoding.EncodeToString(block.Bytes)) - } else { - cp.ClientAuthentication.TrustedLeafCerts = append(cp.ClientAuthentication.TrustedLeafCerts, - base64.StdEncoding.EncodeToString(block.Bytes)) - } - - default: - return nil, h.Errf("unknown subdirective for client_auth: %s", subdir) - } - } - - case "alpn": - args := h.RemainingArgs() - if len(args) == 0 { - return nil, h.ArgErr() - } - cp.ALPN = args - - case "load": - folderLoader = append(folderLoader, h.RemainingArgs()...) - - case "ca": - arg := h.RemainingArgs() - if len(arg) != 1 { - return nil, h.ArgErr() - } - if acmeIssuer == nil { - acmeIssuer = new(caddytls.ACMEIssuer) - } - acmeIssuer.CA = arg[0] - - case "key_type": - arg := h.RemainingArgs() - if len(arg) != 1 { - return nil, h.ArgErr() - } - keyType = arg[0] - - case "eab": - arg := h.RemainingArgs() - if len(arg) != 2 { - return nil, h.ArgErr() - } - if acmeIssuer == nil { - acmeIssuer = new(caddytls.ACMEIssuer) - } - acmeIssuer.ExternalAccount = &acme.EAB{ - KeyID: arg[0], - MACKey: arg[1], - } - - case "issuer": - if !h.NextArg() { - return nil, h.ArgErr() - } - modName := h.Val() - modID := "tls.issuance." + modName - unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID) - if err != nil { - return nil, err - } - issuer, ok := unm.(certmagic.Issuer) - if !ok { - return nil, h.Errf("module %s (%T) is not a certmagic.Issuer", modID, unm) - } - issuers = append(issuers, issuer) - - case "get_certificate": - if !h.NextArg() { - return nil, h.ArgErr() - } - modName := h.Val() - modID := "tls.get_certificate." + modName - unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID) - if err != nil { - return nil, err - } - certManager, ok := unm.(certmagic.Manager) - if !ok { - return nil, h.Errf("module %s (%T) is not a certmagic.CertificateManager", modID, unm) - } - certManagers = append(certManagers, certManager) - - case "dns": - if !h.NextArg() { - return nil, h.ArgErr() - } - provName := h.Val() - if acmeIssuer == nil { - acmeIssuer = new(caddytls.ACMEIssuer) - } - if acmeIssuer.Challenges == nil { - acmeIssuer.Challenges = new(caddytls.ChallengesConfig) - } - 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 - } - acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, h.warnings) - - case "resolvers": - args := h.RemainingArgs() - if len(args) == 0 { - return nil, h.ArgErr() - } - if acmeIssuer == nil { - acmeIssuer = new(caddytls.ACMEIssuer) - } - if acmeIssuer.Challenges == nil { - acmeIssuer.Challenges = new(caddytls.ChallengesConfig) - } - if acmeIssuer.Challenges.DNS == nil { - acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig) - } - acmeIssuer.Challenges.DNS.Resolvers = args - - case "propagation_delay": - arg := h.RemainingArgs() - if len(arg) != 1 { - return nil, h.ArgErr() - } - delayStr := arg[0] - delay, err := caddy.ParseDuration(delayStr) - if err != nil { - return nil, h.Errf("invalid propagation_delay duration %s: %v", delayStr, err) - } - if acmeIssuer == nil { - acmeIssuer = new(caddytls.ACMEIssuer) - } - if acmeIssuer.Challenges == nil { - acmeIssuer.Challenges = new(caddytls.ChallengesConfig) - } - if acmeIssuer.Challenges.DNS == nil { - acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig) - } - acmeIssuer.Challenges.DNS.PropagationDelay = caddy.Duration(delay) - - case "propagation_timeout": - arg := h.RemainingArgs() - if len(arg) != 1 { - return nil, h.ArgErr() - } - timeoutStr := arg[0] - var timeout time.Duration - if timeoutStr == "-1" { - timeout = time.Duration(-1) - } else { - var err error - timeout, err = caddy.ParseDuration(timeoutStr) - if err != nil { - return nil, h.Errf("invalid propagation_timeout duration %s: %v", timeoutStr, err) - } - } - if acmeIssuer == nil { - acmeIssuer = new(caddytls.ACMEIssuer) - } - if acmeIssuer.Challenges == nil { - acmeIssuer.Challenges = new(caddytls.ChallengesConfig) - } - if acmeIssuer.Challenges.DNS == nil { - acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig) - } - acmeIssuer.Challenges.DNS.PropagationTimeout = caddy.Duration(timeout) - - case "dns_ttl": - arg := h.RemainingArgs() - if len(arg) != 1 { - return nil, h.ArgErr() - } - ttlStr := arg[0] - ttl, err := caddy.ParseDuration(ttlStr) - if err != nil { - return nil, h.Errf("invalid dns_ttl duration %s: %v", ttlStr, err) - } - if acmeIssuer == nil { - acmeIssuer = new(caddytls.ACMEIssuer) - } - if acmeIssuer.Challenges == nil { - acmeIssuer.Challenges = new(caddytls.ChallengesConfig) - } - if acmeIssuer.Challenges.DNS == nil { - acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig) - } - acmeIssuer.Challenges.DNS.TTL = caddy.Duration(ttl) - - case "dns_challenge_override_domain": - arg := h.RemainingArgs() - if len(arg) != 1 { - return nil, h.ArgErr() - } - if acmeIssuer == nil { - acmeIssuer = new(caddytls.ACMEIssuer) - } - if acmeIssuer.Challenges == nil { - acmeIssuer.Challenges = new(caddytls.ChallengesConfig) - } - if acmeIssuer.Challenges.DNS == nil { - acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig) - } - acmeIssuer.Challenges.DNS.OverrideDomain = arg[0] - - case "ca_root": - arg := h.RemainingArgs() - if len(arg) != 1 { - return nil, h.ArgErr() - } - if acmeIssuer == nil { - acmeIssuer = new(caddytls.ACMEIssuer) - } - acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, arg[0]) - - case "on_demand": - if h.NextArg() { - return nil, h.ArgErr() - } - onDemand = true - - case "insecure_secrets_log": - if !h.NextArg() { - return nil, h.ArgErr() - } - cp.InsecureSecretsLog = h.Val() - - default: - return nil, h.Errf("unknown subdirective: %s", h.Val()) - } - } - - // a naked tls directive is not allowed - if len(firstLine) == 0 && !hasBlock { - return nil, h.ArgErr() - } + // a naked tls directive is not allowed + if len(firstLine) == 0 && !hasBlock { + return nil, h.ArgErr() } // begin building the final config values @@ -579,6 +537,14 @@ func parseTLS(h Helper) ([]ConfigValue, error) { }) } + // reuse private keys TLS + if reusePrivateKeys { + configVals = append(configVals, ConfigValue{ + Class: "tls.reuse_private_keys", + Value: true, + }) + } + // custom certificate selection if len(certSelector.AnyTag) > 0 { cp.CertSelection = &certSelector @@ -600,18 +566,53 @@ func parseTLS(h Helper) ([]ConfigValue, error) { // parseRoot parses the root directive. Syntax: // // root [] -func parseRoot(h Helper) (caddyhttp.MiddlewareHandler, error) { - var root string - for h.Next() { +func parseRoot(h Helper) ([]ConfigValue, error) { + h.Next() // consume directive name + + // count the tokens to determine what to do + argsCount := h.CountRemainingArgs() + if argsCount == 0 { + return nil, h.Errf("too few arguments; must have at least a root path") + } + if argsCount > 2 { + return nil, h.Errf("too many arguments; should only be a matcher and a path") + } + + // with only one arg, assume it's a root path with no matcher token + if argsCount == 1 { if !h.NextArg() { return nil, h.ArgErr() } - root = h.Val() - if h.NextArg() { - return nil, h.ArgErr() - } + return h.NewRoute(nil, caddyhttp.VarsMiddleware{"root": h.Val()}), nil } - return caddyhttp.VarsMiddleware{"root": root}, nil + + // parse the matcher token into a matcher set + userMatcherSet, err := h.ExtractMatcherSet() + if err != nil { + return nil, err + } + h.Next() // consume directive name again, matcher parsing does a reset + + // advance to the root path + if !h.NextArg() { + return nil, h.ArgErr() + } + // make the route with the matcher + return h.NewRoute(userMatcherSet, caddyhttp.VarsMiddleware{"root": h.Val()}), nil +} + +// parseFilesystem parses the fs directive. Syntax: +// +// fs +func parseFilesystem(h Helper) (caddyhttp.MiddlewareHandler, error) { + h.Next() // consume directive name + if !h.NextArg() { + return nil, h.ArgErr() + } + if h.NextArg() { + return nil, h.ArgErr() + } + return caddyhttp.VarsMiddleware{"fs": h.Val()}, nil } // parseVars parses the vars directive. See its UnmarshalCaddyfile method for syntax. @@ -631,10 +632,7 @@ func parseVars(h Helper) (caddyhttp.MiddlewareHandler, error) { // respond with HTTP 200 and no Location header; redirect is performed // with JS and a meta tag). func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) { - if !h.Next() { - return nil, h.ArgErr() - } - + h.Next() // consume directive name if !h.NextArg() { return nil, h.ArgErr() } @@ -650,8 +648,10 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) { switch code { case "permanent": code = "301" + case "temporary", "": code = "302" + case "html": // Script tag comes first since that will better imitate a redirect in the browser's // history, but the meta tag is a fallback for most non-JS clients. @@ -667,7 +667,9 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) { ` safeTo := html.EscapeString(to) body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo) + hdr = http.Header{"Content-Type": []string{"text/html; charset=utf-8"}} code = "200" // don't redirect non-browser clients + default: // Allow placeholders for the code if strings.HasPrefix(code, "{") { @@ -706,10 +708,7 @@ func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) { func parseRespond(h Helper) (caddyhttp.MiddlewareHandler, error) { sr := new(caddyhttp.StaticResponse) err := sr.UnmarshalCaddyfile(h.Dispenser) - if err != nil { - return nil, err - } - return sr, nil + return sr, err } // parseAbort parses the abort directive. @@ -725,10 +724,7 @@ func parseAbort(h Helper) (caddyhttp.MiddlewareHandler, error) { func parseError(h Helper) (caddyhttp.MiddlewareHandler, error) { se := new(caddyhttp.StaticError) err := se.UnmarshalCaddyfile(h.Dispenser) - if err != nil { - return nil, err - } - return se, nil + return se, err } // parseRoute parses the route directive. @@ -754,10 +750,67 @@ func parseHandle(h Helper) (caddyhttp.MiddlewareHandler, error) { } func parseHandleErrors(h Helper) ([]ConfigValue, error) { - subroute, err := ParseSegmentAsSubroute(h) + h.Next() // consume directive name + + expression := "" + args := h.RemainingArgs() + if len(args) > 0 { + codes := []string{} + for _, val := range args { + if len(val) != 3 { + return nil, h.Errf("bad status value '%s'", val) + } + if strings.HasSuffix(val, "xx") { + val = val[:1] + _, err := strconv.Atoi(val) + if err != nil { + return nil, h.Errf("bad status value '%s': %v", val, err) + } + if expression != "" { + expression += " || " + } + expression += fmt.Sprintf("{http.error.status_code} >= %s00 && {http.error.status_code} <= %s99", val, val) + continue + } + _, err := strconv.Atoi(val) + if err != nil { + return nil, h.Errf("bad status value '%s': %v", val, err) + } + codes = append(codes, val) + } + if len(codes) > 0 { + if expression != "" { + expression += " || " + } + expression += "{http.error.status_code} in [" + strings.Join(codes, ", ") + "]" + } + // Reset cursor position to get ready for ParseSegmentAsSubroute + h.Reset() + h.Next() + h.RemainingArgs() + h.Prev() + } else { + // If no arguments present reset the cursor position to get ready for ParseSegmentAsSubroute + h.Prev() + } + + handler, err := ParseSegmentAsSubroute(h) if err != nil { return nil, err } + subroute, ok := handler.(*caddyhttp.Subroute) + if !ok { + return nil, h.Errf("segment was not parsed as a subroute") + } + + if expression != "" { + statusMatcher := caddy.ModuleMap{ + "expression": h.JSON(caddyhttp.MatchExpression{Expr: expression}), + } + for i := range subroute.Routes { + subroute.Routes[i].MatcherSetsRaw = []caddy.ModuleMap{statusMatcher} + } + } return []ConfigValue{ { Class: "error_route", @@ -804,195 +857,201 @@ func parseLog(h Helper) ([]ConfigValue, error) { // level. The parseAsGlobalOption parameter is used to distinguish any differing logic // between the two. func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue, error) { + h.Next() // consume option name + // When the globalLogNames parameter is passed in, we make // modifications to the parsing behavior. parseAsGlobalOption := globalLogNames != nil var configValues []ConfigValue - for h.Next() { - // Logic below expects that a name is always present when a - // global option is being parsed; or an optional override - // is supported for access logs. - var logName string - if parseAsGlobalOption { + // Logic below expects that a name is always present when a + // global option is being parsed; or an optional override + // is supported for access logs. + var logName string + + if parseAsGlobalOption { + if h.NextArg() { + logName = h.Val() + + // Only a single argument is supported. if h.NextArg() { - logName = h.Val() - - // Only a single argument is supported. - if h.NextArg() { - return nil, h.ArgErr() - } - } else { - // If there is no log name specified, we - // reference the default logger. See the - // setupNewDefault function in the logging - // package for where this is configured. - logName = caddy.DefaultLoggerName + return nil, h.ArgErr() } - - // Verify this name is unused. - _, used := globalLogNames[logName] - if used { - return nil, h.Err("duplicate global log option for: " + logName) - } - globalLogNames[logName] = struct{}{} } else { - // An optional override of the logger name can be provided; - // otherwise a default will be used, like "log0", "log1", etc. - if h.NextArg() { - logName = h.Val() - - // Only a single argument is supported. - if h.NextArg() { - return nil, h.ArgErr() - } - } + // If there is no log name specified, we + // reference the default logger. See the + // setupNewDefault function in the logging + // package for where this is configured. + logName = caddy.DefaultLoggerName } - cl := new(caddy.CustomLog) + // Verify this name is unused. + _, used := globalLogNames[logName] + if used { + return nil, h.Err("duplicate global log option for: " + logName) + } + globalLogNames[logName] = struct{}{} + } else { + // An optional override of the logger name can be provided; + // otherwise a default will be used, like "log0", "log1", etc. + if h.NextArg() { + logName = h.Val() - // allow overriding the current site block's hostnames for this logger; - // this is useful for setting up loggers per subdomain in a site block - // with a wildcard domain - customHostnames := []string{} + // Only a single argument is supported. + if h.NextArg() { + return nil, h.ArgErr() + } + } + } - for h.NextBlock(0) { - switch h.Val() { - case "hostnames": - if parseAsGlobalOption { - return nil, h.Err("hostnames is not allowed in the log global options") - } - args := h.RemainingArgs() - if len(args) == 0 { - return nil, h.ArgErr() - } - customHostnames = append(customHostnames, args...) + cl := new(caddy.CustomLog) - case "output": - if !h.NextArg() { - return nil, h.ArgErr() - } - moduleName := h.Val() + // allow overriding the current site block's hostnames for this logger; + // this is useful for setting up loggers per subdomain in a site block + // with a wildcard domain + customHostnames := []string{} - // can't use the usual caddyfile.Unmarshaler flow with the - // standard writers because they are in the caddy package - // (because they are the default) and implementing that - // interface there would unfortunately create circular import - var wo caddy.WriterOpener - switch moduleName { - case "stdout": - wo = caddy.StdoutWriter{} - case "stderr": - wo = caddy.StderrWriter{} - case "discard": - wo = caddy.DiscardWriter{} - default: - modID := "caddy.logging.writers." + moduleName - unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID) - if err != nil { - return nil, err - } - var ok bool - wo, ok = unm.(caddy.WriterOpener) - if !ok { - return nil, h.Errf("module %s (%T) is not a WriterOpener", modID, unm) - } - } - cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings) + for h.NextBlock(0) { + switch h.Val() { + case "hostnames": + if parseAsGlobalOption { + return nil, h.Err("hostnames is not allowed in the log global options") + } + args := h.RemainingArgs() + if len(args) == 0 { + return nil, h.ArgErr() + } + customHostnames = append(customHostnames, args...) - case "format": - if !h.NextArg() { - return nil, h.ArgErr() - } - moduleName := h.Val() - moduleID := "caddy.logging.encoders." + moduleName - unm, err := caddyfile.UnmarshalModule(h.Dispenser, moduleID) + case "output": + if !h.NextArg() { + return nil, h.ArgErr() + } + moduleName := h.Val() + + // can't use the usual caddyfile.Unmarshaler flow with the + // standard writers because they are in the caddy package + // (because they are the default) and implementing that + // interface there would unfortunately create circular import + var wo caddy.WriterOpener + switch moduleName { + case "stdout": + wo = caddy.StdoutWriter{} + case "stderr": + wo = caddy.StderrWriter{} + case "discard": + wo = caddy.DiscardWriter{} + default: + modID := "caddy.logging.writers." + moduleName + unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID) if err != nil { return nil, err } - enc, ok := unm.(zapcore.Encoder) + var ok bool + wo, ok = unm.(caddy.WriterOpener) if !ok { - return nil, h.Errf("module %s (%T) is not a zapcore.Encoder", moduleID, unm) + return nil, h.Errf("module %s (%T) is not a WriterOpener", modID, unm) } - cl.EncoderRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, h.warnings) - - case "level": - if !h.NextArg() { - return nil, h.ArgErr() - } - cl.Level = h.Val() - if h.NextArg() { - return nil, h.ArgErr() - } - - case "include": - if !parseAsGlobalOption { - return nil, h.Err("include is not allowed in the log directive") - } - for h.NextArg() { - cl.Include = append(cl.Include, h.Val()) - } - - case "exclude": - if !parseAsGlobalOption { - return nil, h.Err("exclude is not allowed in the log directive") - } - for h.NextArg() { - cl.Exclude = append(cl.Exclude, h.Val()) - } - - default: - return nil, h.Errf("unrecognized subdirective: %s", h.Val()) } - } + cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings) - var val namedCustomLog - val.hostnames = customHostnames - - isEmptyConfig := reflect.DeepEqual(cl, new(caddy.CustomLog)) - - // Skip handling of empty logging configs - - if parseAsGlobalOption { - // Use indicated name for global log options - val.name = logName - } else { - if logName != "" { - val.name = logName - } else if !isEmptyConfig { - // Construct a log name for server log streams - logCounter, ok := h.State["logCounter"].(int) - if !ok { - logCounter = 0 - } - val.name = fmt.Sprintf("log%d", logCounter) - logCounter++ - h.State["logCounter"] = logCounter + case "format": + if !h.NextArg() { + return nil, h.ArgErr() } - if val.name != "" { - cl.Include = []string{"http.log.access." + val.name} + moduleName := h.Val() + moduleID := "caddy.logging.encoders." + moduleName + unm, err := caddyfile.UnmarshalModule(h.Dispenser, moduleID) + if err != nil { + return nil, err } + enc, ok := unm.(zapcore.Encoder) + if !ok { + return nil, h.Errf("module %s (%T) is not a zapcore.Encoder", moduleID, unm) + } + cl.EncoderRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, h.warnings) + + case "level": + if !h.NextArg() { + return nil, h.ArgErr() + } + cl.Level = h.Val() + if h.NextArg() { + return nil, h.ArgErr() + } + + case "include": + if !parseAsGlobalOption { + return nil, h.Err("include is not allowed in the log directive") + } + for h.NextArg() { + cl.Include = append(cl.Include, h.Val()) + } + + case "exclude": + if !parseAsGlobalOption { + return nil, h.Err("exclude is not allowed in the log directive") + } + for h.NextArg() { + cl.Exclude = append(cl.Exclude, h.Val()) + } + + default: + return nil, h.Errf("unrecognized subdirective: %s", h.Val()) } - if !isEmptyConfig { - val.log = cl - } - configValues = append(configValues, ConfigValue{ - Class: "custom_log", - Value: val, - }) } + + var val namedCustomLog + val.hostnames = customHostnames + + isEmptyConfig := reflect.DeepEqual(cl, new(caddy.CustomLog)) + + // Skip handling of empty logging configs + + if parseAsGlobalOption { + // Use indicated name for global log options + val.name = logName + } else { + if logName != "" { + val.name = logName + } else if !isEmptyConfig { + // Construct a log name for server log streams + logCounter, ok := h.State["logCounter"].(int) + if !ok { + logCounter = 0 + } + val.name = fmt.Sprintf("log%d", logCounter) + logCounter++ + h.State["logCounter"] = logCounter + } + if val.name != "" { + cl.Include = []string{"http.log.access." + val.name} + } + } + if !isEmptyConfig { + val.log = cl + } + configValues = append(configValues, ConfigValue{ + Class: "custom_log", + Value: val, + }) return configValues, nil } -// parseSkipLog parses the skip_log directive. Syntax: +// parseLogSkip parses the log_skip directive. Syntax: // -// skip_log [] -func parseSkipLog(h Helper) (caddyhttp.MiddlewareHandler, error) { - for h.Next() { - if h.NextArg() { - return nil, h.ArgErr() - } +// log_skip [] +func parseLogSkip(h Helper) (caddyhttp.MiddlewareHandler, error) { + h.Next() // consume directive name + + // "skip_log" is deprecated, replaced by "log_skip" + if h.Val() == "skip_log" { + caddy.Log().Named("config.adapter.caddyfile").Warn("the 'skip_log' directive is deprecated, please use 'log_skip' instead!") } - return caddyhttp.VarsMiddleware{"skip_log": true}, nil + + if h.NextArg() { + return nil, h.ArgErr() + } + return caddyhttp.VarsMiddleware{"log_skip": true}, nil } diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go index 13229ed5c..bde25031a 100644 --- a/caddyconfig/httpcaddyfile/directives.go +++ b/caddyconfig/httpcaddyfile/directives.go @@ -27,22 +27,31 @@ import ( "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) -// directiveOrder specifies the order -// to apply directives in HTTP routes. +// defaultDirectiveOrder specifies the default order +// to apply directives in HTTP routes. This must only +// consist of directives that are included in Caddy's +// standard distribution. // -// The root directive goes first in case rewrites or -// redirects depend on existence of files, i.e. the -// file matcher, which must know the root first. +// e.g. The 'root' directive goes near the start in +// case rewrites or redirects depend on existence of +// files, i.e. the file matcher, which must know the +// root first. // -// The header directive goes second so that headers -// can be manipulated before doing redirects. -var directiveOrder = []string{ +// e.g. The 'header' directive goes before 'redir' so +// that headers can be manipulated before doing redirects. +// +// e.g. The 'respond' directive is near the end because it +// writes a response and terminates the middleware chain. +var defaultDirectiveOrder = []string{ "tracing", + // set variables that may be used by other directives "map", "vars", + "fs", "root", - "skip_log", + "log_append", + "log_skip", "header", "copy_response_headers", // only in reverse_proxy's handle_response @@ -57,7 +66,8 @@ var directiveOrder = []string{ "try_files", // middleware handlers; some wrap responses - "basicauth", + "basicauth", // TODO: deprecated, renamed to basic_auth + "basic_auth", "forward_auth", "request_header", "encode", @@ -82,6 +92,11 @@ var directiveOrder = []string{ "acme_server", } +// directiveOrder specifies the order to apply directives +// in HTTP routes, after being modified by either the +// 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 { @@ -128,6 +143,58 @@ func RegisterHandlerDirective(dir string, setupFunc UnmarshalHandlerFunc) { }) } +// RegisterDirectiveOrder registers the default order for a +// directive from a plugin. +// +// This is useful when a plugin has a well-understood place +// it should run in the middleware pipeline, and it allows +// users to avoid having to define the order themselves. +// +// The directive dir may be placed in the position relative +// to ('before' or 'after') a directive included in Caddy's +// standard distribution. It cannot be relative to another +// plugin's directive. +// +// 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) { + panic("directive '" + dir + "' already ordered") + } + + if position != Before && position != After { + panic("the 2nd argument must be either 'before' or 'after', got '" + position + "'") + } + + // 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 + } + } + if !foundStandardDir { + panic("the 3rd argument '" + standardDir + "' must be a directive that exists in the standard distribution of Caddy") + } + + // insert directive into proper position + newOrder := directiveOrder + for i, d := range newOrder { + if d != standardDir { + continue + } + if position == Before { + newOrder = append(newOrder[:i], append([]string{dir}, newOrder[i:]...)...) + } else if position == After { + newOrder = append(newOrder[:i+1], append([]string{dir}, newOrder[i+1:]...)...) + } + break + } + directiveOrder = newOrder +} + // RegisterGlobalOption registers a unique global option opt with // an associated unmarshaling (setup) function. When the global // option opt is encountered in a Caddyfile, setupFunc will be @@ -270,12 +337,6 @@ func (h Helper) GroupRoutes(vals []ConfigValue) { } } -// NewBindAddresses returns config values relevant to adding -// listener bind addresses to the config. -func (h Helper) NewBindAddresses(addrs []string) []ConfigValue { - return []ConfigValue{{Class: "bind", Value: addrs}} -} - // WithDispenser returns a new instance based on d. All others Helper // fields are copied, so typically maps are shared with this new instance. func (h Helper) WithDispenser(d *caddyfile.Dispenser) Helper { @@ -558,6 +619,16 @@ func (sb serverBlock) isAllHTTP() bool { return true } +// Positional are the supported modes for ordering directives. +type Positional string + +const ( + Before Positional = "before" + After Positional = "after" + First Positional = "first" + Last Positional = "last" +) + type ( // UnmarshalFunc is a function which can unmarshal Caddyfile // tokens into zero or more config values using a Helper type. diff --git a/caddyconfig/httpcaddyfile/directives_test.go b/caddyconfig/httpcaddyfile/directives_test.go index e46a6d2af..db0282299 100644 --- a/caddyconfig/httpcaddyfile/directives_test.go +++ b/caddyconfig/httpcaddyfile/directives_test.go @@ -31,20 +31,23 @@ func TestHostsFromKeys(t *testing.T) { []Address{ {Original: ":2015", Port: "2015"}, }, - []string{}, []string{}, + []string{}, + []string{}, }, { []Address{ {Original: ":443", Port: "443"}, }, - []string{}, []string{}, + []string{}, + []string{}, }, { []Address{ {Original: "foo", Host: "foo"}, {Original: ":2015", Port: "2015"}, }, - []string{}, []string{"foo"}, + []string{}, + []string{"foo"}, }, { []Address{ diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index 6fa5a9f7c..da5557aa8 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -17,8 +17,8 @@ package httpcaddyfile import ( "encoding/json" "fmt" + "net" "reflect" - "regexp" "sort" "strconv" "strings" @@ -65,8 +65,11 @@ func (st ServerType) Setup( originalServerBlocks := make([]serverBlock, 0, len(inputServerBlocks)) for _, sblock := range inputServerBlocks { for j, k := range sblock.Keys { - if j == 0 && strings.HasPrefix(k, "@") { - return nil, warnings, fmt.Errorf("cannot define a matcher outside of a site block: '%s'", k) + if j == 0 && strings.HasPrefix(k.Text, "@") { + return nil, warnings, fmt.Errorf("%s:%d: cannot define a matcher outside of a site block: '%s'", k.File, k.Line, k.Text) + } + if _, ok := registeredDirectives[k.Text]; ok { + return nil, warnings, fmt.Errorf("%s:%d: parsed '%s' as a site address, but it is a known directive; directives must appear in a site block", k.File, k.Line, k.Text) } } originalServerBlocks = append(originalServerBlocks, serverBlock{ @@ -82,46 +85,18 @@ func (st ServerType) Setup( return nil, warnings, err } - originalServerBlocks, err = st.extractNamedRoutes(originalServerBlocks, options, &warnings) + // this will replace both static and user-defined placeholder shorthands + // with actual identifiers used by Caddy + replacer := NewShorthandReplacer() + + originalServerBlocks, err = st.extractNamedRoutes(originalServerBlocks, options, &warnings, replacer) if err != nil { return nil, warnings, err } - // replace shorthand placeholders (which are convenient - // when writing a Caddyfile) with their actual placeholder - // identifiers or variable names - replacer := strings.NewReplacer(placeholderShorthands()...) - - // these are placeholders that allow a user-defined final - // parameters, but we still want to provide a shorthand - // for those, so we use a regexp to replace - regexpReplacements := []struct { - search *regexp.Regexp - replace string - }{ - {regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"}, - {regexp.MustCompile(`{cookie\.([\w-]*)}`), "{http.request.cookie.$1}"}, - {regexp.MustCompile(`{labels\.([\w-]*)}`), "{http.request.host.labels.$1}"}, - {regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"}, - {regexp.MustCompile(`{file\.([\w-]*)}`), "{http.request.uri.path.file.$1}"}, - {regexp.MustCompile(`{query\.([\w-]*)}`), "{http.request.uri.query.$1}"}, - {regexp.MustCompile(`{re\.([\w-]*)\.([\w-]*)}`), "{http.regexp.$1.$2}"}, - {regexp.MustCompile(`{vars\.([\w-]*)}`), "{http.vars.$1}"}, - {regexp.MustCompile(`{rp\.([\w-\.]*)}`), "{http.reverse_proxy.$1}"}, - {regexp.MustCompile(`{err\.([\w-\.]*)}`), "{http.error.$1}"}, - {regexp.MustCompile(`{file_match\.([\w-]*)}`), "{http.matchers.file.$1}"}, - } - for _, sb := range originalServerBlocks { - for _, segment := range sb.block.Segments { - for i := 0; i < len(segment); i++ { - // simple string replacements - segment[i].Text = replacer.Replace(segment[i].Text) - // complex regexp replacements - for _, r := range regexpReplacements { - segment[i].Text = r.search.ReplaceAllString(segment[i].Text, r.replace) - } - } + for i := range sb.block.Segments { + replacer.ApplyToSegment(&sb.block.Segments[i]) } if len(sb.block.Keys) == 0 { @@ -299,6 +274,12 @@ func (st ServerType) Setup( if !reflect.DeepEqual(pkiApp, &caddypki.PKI{CAs: make(map[string]*caddypki.CA)}) { cfg.AppsRaw["pki"] = caddyconfig.JSON(pkiApp, &warnings) } + if filesystems, ok := options["filesystem"].(caddy.Module); ok { + cfg.AppsRaw["caddy.filesystems"] = caddyconfig.JSON( + filesystems, + &warnings) + } + if storageCvtr, ok := options["storage"].(caddy.StorageConverter); ok { cfg.StorageRaw = caddyconfig.JSONModuleObject(storageCvtr, "module", @@ -308,7 +289,6 @@ func (st ServerType) Setup( if adminConfig, ok := options["admin"].(*caddy.AdminConfig); ok && adminConfig != nil { cfg.Admin = adminConfig } - if pc, ok := options["persist_config"].(string); ok && pc == "off" { if cfg.Admin == nil { cfg.Admin = new(caddy.AdminConfig) @@ -452,6 +432,7 @@ func (ServerType) extractNamedRoutes( serverBlocks []serverBlock, options map[string]any, warnings *[]caddyconfig.Warning, + replacer ShorthandReplacer, ) ([]serverBlock, error) { namedRoutes := map[string]*caddyhttp.Route{} @@ -477,11 +458,14 @@ func (ServerType) extractNamedRoutes( continue } - // zip up all the segments since ParseSegmentAsSubroute - // was designed to take a directive+ wholeSegment := caddyfile.Segment{} - for _, segment := range sb.block.Segments { - wholeSegment = append(wholeSegment, segment...) + for i := range sb.block.Segments { + // replace user-defined placeholder shorthands in extracted named routes + replacer.ApplyToSegment(&sb.block.Segments[i]) + + // zip up all the segments since ParseSegmentAsSubroute + // was designed to take a directive+ + wholeSegment = append(wholeSegment, sb.block.Segments[i]...) } h := Helper{ @@ -509,7 +493,7 @@ func (ServerType) extractNamedRoutes( route.HandlersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", subroute.CaddyModule().ID.Name(), h.warnings)} } - namedRoutes[sb.block.Keys[0]] = &route + namedRoutes[sb.block.GetKeysText()[0]] = &route } options["named_routes"] = namedRoutes @@ -547,12 +531,12 @@ func (st *ServerType) serversFromPairings( // address), otherwise their routes will improperly be added // to the same server (see issue #4635) for j, sblock1 := range p.serverBlocks { - for _, key := range sblock1.block.Keys { + for _, key := range sblock1.block.GetKeysText() { for k, sblock2 := range p.serverBlocks { if k == j { continue } - if sliceContains(sblock2.block.Keys, key) { + if sliceContains(sblock2.block.GetKeysText(), key) { return nil, fmt.Errorf("ambiguous site definition: %s", key) } } @@ -710,6 +694,7 @@ func (st *ServerType) serversFromPairings( } if len(hosts) > 0 { + slices.Sort(hosts) // for deterministic JSON output cp.MatchersRaw = caddy.ModuleMap{ "sni": caddyconfig.JSON(hosts, warnings), // make sure to match all hosts, not just auto-HTTPS-qualified ones } @@ -741,10 +726,20 @@ func (st *ServerType) serversFromPairings( } } + // If TLS is specified as directive, it will also result in 1 or more connection policy being created + // Thus, catch-all address with non-standard port, e.g. :8443, can have TLS enabled without + // specifying prefix "https://" + // Second part of the condition is to allow creating TLS conn policy even though `auto_https` has been disabled + // ensuring compatibility with behavior described in below link + // 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)) + // we'll need to remember if the address qualifies for auto-HTTPS, so we // can add a TLS conn policy if necessary if addr.Scheme == "https" || - (addr.Scheme != "http" && addr.Host != "" && addr.Port != httpPort) { + (addr.Scheme != "http" && addr.Port != httpPort && hasTLSEnabled) { addressQualifiesForTLS = true } // predict whether auto-HTTPS will add the conn policy for us; if so, we @@ -782,10 +777,19 @@ func (st *ServerType) serversFromPairings( if srv.Errors == nil { srv.Errors = new(caddyhttp.HTTPErrorConfig) } + sort.SliceStable(errorSubrouteVals, func(i, j int) bool { + sri, srj := errorSubrouteVals[i].Value.(*caddyhttp.Subroute), errorSubrouteVals[j].Value.(*caddyhttp.Subroute) + if len(sri.Routes[0].MatcherSetsRaw) == 0 && len(srj.Routes[0].MatcherSetsRaw) != 0 { + return false + } + return true + }) + errorsSubroute := &caddyhttp.Subroute{} for _, val := range errorSubrouteVals { sr := val.Value.(*caddyhttp.Subroute) - srv.Errors.Routes = appendSubrouteToRouteList(srv.Errors.Routes, sr, matcherSetsEnc, p, warnings) + errorsSubroute.Routes = append(errorsSubroute.Routes, sr.Routes...) } + srv.Errors.Routes = appendSubrouteToRouteList(srv.Errors.Routes, errorsSubroute, matcherSetsEnc, p, warnings) } // add log associations @@ -811,7 +815,12 @@ func (st *ServerType) serversFromPairings( if srv.Logs.LoggerNames == nil { srv.Logs.LoggerNames = make(map[string]string) } - srv.Logs.LoggerNames[h] = ncl.name + // strip the port from the host, if any + host, _, err := net.SplitHostPort(h) + if err != nil { + host = h + } + srv.Logs.LoggerNames[host] = ncl.name } } } @@ -828,6 +837,11 @@ func (st *ServerType) serversFromPairings( } } + // sort for deterministic JSON output + if srv.Logs != nil { + slices.Sort(srv.Logs.SkipHosts) + } + // a server cannot (natively) serve both HTTP and HTTPS at the // same time, so make sure the configuration isn't in conflict err := detectConflictingSchemes(srv, p.serverBlocks, options) @@ -1370,68 +1384,73 @@ func (st *ServerType) compileEncodedMatcherSets(sblock serverBlock) ([]caddy.Mod } func parseMatcherDefinitions(d *caddyfile.Dispenser, matchers map[string]caddy.ModuleMap) error { - for d.Next() { - // this is the "name" for "named matchers" - definitionName := d.Val() + d.Next() // advance to the first token - if _, ok := matchers[definitionName]; ok { - return fmt.Errorf("matcher is defined more than once: %s", definitionName) + // this is the "name" for "named matchers" + definitionName := d.Val() + + if _, ok := matchers[definitionName]; ok { + return fmt.Errorf("matcher is defined more than once: %s", definitionName) + } + matchers[definitionName] = make(caddy.ModuleMap) + + // 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 { + mod, err := caddy.GetModule("http.matchers." + matcherName) + if err != nil { + return fmt.Errorf("getting matcher module '%s': %v", matcherName, err) } - matchers[definitionName] = make(caddy.ModuleMap) + unm, ok := mod.New().(caddyfile.Unmarshaler) + if !ok { + return fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName) + } + err = unm.UnmarshalCaddyfile(caddyfile.NewDispenser(tokens)) + if err != nil { + return err + } + rm, ok := unm.(caddyhttp.RequestMatcher) + if !ok { + return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName) + } + matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil) + return nil + } - // 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 { - mod, err := caddy.GetModule("http.matchers." + matcherName) - if err != nil { - return fmt.Errorf("getting matcher module '%s': %v", matcherName, err) - } - unm, ok := mod.New().(caddyfile.Unmarshaler) - if !ok { - return fmt.Errorf("matcher module '%s' is not a Caddyfile unmarshaler", matcherName) - } - err = unm.UnmarshalCaddyfile(caddyfile.NewDispenser(tokens)) + // if the next token is quoted, we can assume it's not a matcher name + // and that it's probably an 'expression' matcher + 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(), + }) if err != nil { return err } - rm, ok := unm.(caddyhttp.RequestMatcher) - if !ok { - return fmt.Errorf("matcher module '%s' is not a request matcher", matcherName) - } - matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil) return nil } - // if the next token is quoted, we can assume it's not a matcher name - // and that it's probably an 'expression' matcher - if d.NextArg() { - if d.Token().Quoted() { - err := makeMatcher("expression", []caddyfile.Token{d.Token()}) - if err != nil { - return err - } - continue - } + // if it wasn't quoted, then we need to rewind after calling + // d.NextArg() so the below properly grabs the matcher name + d.Prev() + } - // if it wasn't quoted, then we need to rewind after calling - // d.NextArg() so the below properly grabs the matcher name - d.Prev() - } - - // in case there are multiple instances of the same matcher, concatenate - // their tokens (we expect that UnmarshalCaddyfile should be able to - // handle more than one segment); otherwise, we'd overwrite other - // instances of the matcher in this set - tokensByMatcherName := make(map[string][]caddyfile.Token) - for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); { - matcherName := d.Val() - tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...) - } - for matcherName, tokens := range tokensByMatcherName { - err := makeMatcher(matcherName, tokens) - if err != nil { - return err - } + // in case there are multiple instances of the same matcher, concatenate + // their tokens (we expect that UnmarshalCaddyfile should be able to + // handle more than one segment); otherwise, we'd overwrite other + // instances of the matcher in this set + tokensByMatcherName := make(map[string][]caddyfile.Token) + for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); { + matcherName := d.Val() + tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...) + } + for matcherName, tokens := range tokensByMatcherName { + err := makeMatcher(matcherName, tokens) + if err != nil { + return err } } return nil @@ -1449,37 +1468,6 @@ func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (caddy.Modul return msEncoded, nil } -// placeholderShorthands returns a slice of old-new string pairs, -// where the left of the pair is a placeholder shorthand that may -// 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}", - "{method}", "{http.request.method}", - "{path}", "{http.request.uri.path}", - "{query}", "{http.request.uri.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}", - "{tls_cipher}", "{http.request.tls.cipher_suite}", - "{tls_version}", "{http.request.tls.version}", - "{tls_client_fingerprint}", "{http.request.tls.client.fingerprint}", - "{tls_client_issuer}", "{http.request.tls.client.issuer}", - "{tls_client_serial}", "{http.request.tls.client.serial}", - "{tls_client_subject}", "{http.request.tls.client.subject}", - "{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}", - "{tls_client_certificate_der_base64}", "{http.request.tls.client.certificate_der_base64}", - "{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}", - "{client_ip}", "{http.vars.client_ip}", - } -} - // WasReplacedPlaceholderShorthand checks if a token string was // likely a replaced shorthand of the known Caddyfile placeholder // replacement outputs. Useful to prevent some user-defined map diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go index ba1896b6b..70d475d6b 100644 --- a/caddyconfig/httpcaddyfile/options.go +++ b/caddyconfig/httpcaddyfile/options.go @@ -62,105 +62,103 @@ func init() { func parseOptTrue(d *caddyfile.Dispenser, _ any) (any, error) { return true, nil } func parseOptHTTPPort(d *caddyfile.Dispenser, _ any) (any, error) { + d.Next() // consume option name var httpPort int - for d.Next() { - var httpPortStr string - if !d.AllArgs(&httpPortStr) { - return 0, d.ArgErr() - } - var err error - httpPort, err = strconv.Atoi(httpPortStr) - if err != nil { - return 0, d.Errf("converting port '%s' to integer value: %v", httpPortStr, err) - } + var httpPortStr string + if !d.AllArgs(&httpPortStr) { + return 0, d.ArgErr() + } + var err error + httpPort, err = strconv.Atoi(httpPortStr) + if err != nil { + return 0, d.Errf("converting port '%s' to integer value: %v", httpPortStr, err) } return httpPort, nil } func parseOptHTTPSPort(d *caddyfile.Dispenser, _ any) (any, error) { + d.Next() // consume option name var httpsPort int - for d.Next() { - var httpsPortStr string - if !d.AllArgs(&httpsPortStr) { - return 0, d.ArgErr() - } - var err error - httpsPort, err = strconv.Atoi(httpsPortStr) - if err != nil { - return 0, d.Errf("converting port '%s' to integer value: %v", httpsPortStr, err) - } + var httpsPortStr string + if !d.AllArgs(&httpsPortStr) { + return 0, d.ArgErr() + } + var err error + httpsPort, err = strconv.Atoi(httpsPortStr) + if err != nil { + return 0, d.Errf("converting port '%s' to integer value: %v", httpsPortStr, err) } return httpsPort, nil } func parseOptOrder(d *caddyfile.Dispenser, _ any) (any, error) { + d.Next() // consume option name + + // get directive name + if !d.Next() { + return nil, d.ArgErr() + } + dirName := d.Val() + if _, ok := registeredDirectives[dirName]; !ok { + return nil, d.Errf("%s is not a registered directive", dirName) + } + + // get positional token + if !d.Next() { + return nil, d.ArgErr() + } + pos := Positional(d.Val()) + newOrder := directiveOrder - for d.Next() { - // get directive name - if !d.Next() { - return nil, d.ArgErr() - } - dirName := d.Val() - if _, ok := registeredDirectives[dirName]; !ok { - return nil, d.Errf("%s is not a registered directive", dirName) + // if directive exists, first remove it + for i, d := range newOrder { + if d == dirName { + newOrder = append(newOrder[:i], newOrder[i+1:]...) + break } + } - // get positional token - if !d.Next() { - return nil, d.ArgErr() - } - pos := d.Val() - - // 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 - switch pos { - case "first": - newOrder = append([]string{dirName}, newOrder...) - if d.NextArg() { - return nil, d.ArgErr() - } - directiveOrder = newOrder - return newOrder, nil - case "last": - newOrder = append(newOrder, dirName) - if d.NextArg() { - return nil, d.ArgErr() - } - directiveOrder = newOrder - return newOrder, nil - case "before": - case "after": - default: - return nil, d.Errf("unknown positional '%s'", pos) - } - - // get name of other directive - if !d.NextArg() { - return nil, d.ArgErr() - } - otherDir := d.Val() + // act on the positional + switch pos { + case First: + newOrder = append([]string{dirName}, newOrder...) if d.NextArg() { return nil, d.ArgErr() } + directiveOrder = newOrder + return newOrder, nil + case Last: + newOrder = append(newOrder, dirName) + if d.NextArg() { + return nil, d.ArgErr() + } + directiveOrder = newOrder + return newOrder, nil + case Before: + case After: + default: + return nil, d.Errf("unknown positional '%s'", pos) + } - // 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 name of other directive + if !d.NextArg() { + return nil, d.ArgErr() + } + otherDir := d.Val() + if d.NextArg() { + 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 } } @@ -223,57 +221,58 @@ func parseOptACMEDNS(d *caddyfile.Dispenser, _ any) (any, error) { func parseOptACMEEAB(d *caddyfile.Dispenser, _ any) (any, error) { eab := new(acme.EAB) - for d.Next() { - if d.NextArg() { - return nil, d.ArgErr() - } - for nesting := d.Nesting(); d.NextBlock(nesting); { - switch d.Val() { - case "key_id": - if !d.NextArg() { - return nil, d.ArgErr() - } - eab.KeyID = d.Val() - - case "mac_key": - if !d.NextArg() { - return nil, d.ArgErr() - } - eab.MACKey = d.Val() - - default: - return nil, d.Errf("unrecognized parameter '%s'", d.Val()) + d.Next() // consume option name + if d.NextArg() { + return nil, d.ArgErr() + } + for d.NextBlock(0) { + switch d.Val() { + case "key_id": + if !d.NextArg() { + return nil, d.ArgErr() } + eab.KeyID = d.Val() + + case "mac_key": + if !d.NextArg() { + return nil, d.ArgErr() + } + eab.MACKey = d.Val() + + default: + return nil, d.Errf("unrecognized parameter '%s'", d.Val()) } } return eab, nil } func parseOptCertIssuer(d *caddyfile.Dispenser, existing any) (any, error) { + d.Next() // consume option name + var issuers []certmagic.Issuer if existing != nil { issuers = existing.([]certmagic.Issuer) } - for d.Next() { // consume option name - if !d.Next() { // get issuer module name - return nil, d.ArgErr() - } - modID := "tls.issuance." + d.Val() - unm, err := caddyfile.UnmarshalModule(d, modID) - if err != nil { - return nil, err - } - iss, ok := unm.(certmagic.Issuer) - if !ok { - return nil, d.Errf("module %s (%T) is not a certmagic.Issuer", modID, unm) - } - issuers = append(issuers, iss) + + // get issuer module name + if !d.Next() { + return nil, d.ArgErr() } + modID := "tls.issuance." + d.Val() + unm, err := caddyfile.UnmarshalModule(d, modID) + if err != nil { + return nil, err + } + iss, ok := unm.(certmagic.Issuer) + if !ok { + return nil, d.Errf("module %s (%T) is not a certmagic.Issuer", modID, unm) + } + issuers = append(issuers, iss) return issuers, nil } func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) { - d.Next() // consume parameter name + d.Next() // consume option name if !d.Next() { return "", d.ArgErr() } @@ -285,7 +284,7 @@ func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) { } func parseOptStringList(d *caddyfile.Dispenser, _ any) (any, error) { - d.Next() // consume parameter name + d.Next() // consume option name val := d.RemainingArgs() if len(val) == 0 { return "", d.ArgErr() @@ -294,33 +293,33 @@ func parseOptStringList(d *caddyfile.Dispenser, _ any) (any, error) { } func parseOptAdmin(d *caddyfile.Dispenser, _ any) (any, error) { + d.Next() // consume option name + adminCfg := new(caddy.AdminConfig) - for d.Next() { - if d.NextArg() { - listenAddress := d.Val() - if listenAddress == "off" { - adminCfg.Disabled = true - if d.Next() { // Do not accept any remaining options including block - return nil, d.Err("No more option is allowed after turning off admin config") - } - } else { - adminCfg.Listen = listenAddress - if d.NextArg() { // At most 1 arg is allowed - return nil, d.ArgErr() - } + if d.NextArg() { + listenAddress := d.Val() + if listenAddress == "off" { + adminCfg.Disabled = true + if d.Next() { // Do not accept any remaining options including block + return nil, d.Err("No more option is allowed after turning off admin config") + } + } else { + adminCfg.Listen = listenAddress + if d.NextArg() { // At most 1 arg is allowed + return nil, d.ArgErr() } } - for nesting := d.Nesting(); d.NextBlock(nesting); { - switch d.Val() { - case "enforce_origin": - adminCfg.EnforceOrigin = true + } + for d.NextBlock(0) { + switch d.Val() { + case "enforce_origin": + adminCfg.EnforceOrigin = true - case "origins": - adminCfg.Origins = d.RemainingArgs() + case "origins": + adminCfg.Origins = d.RemainingArgs() - default: - return nil, d.Errf("unrecognized parameter '%s'", d.Val()) - } + default: + return nil, d.Errf("unrecognized parameter '%s'", d.Val()) } } if adminCfg.Listen == "" && !adminCfg.Disabled { @@ -330,57 +329,59 @@ func parseOptAdmin(d *caddyfile.Dispenser, _ any) (any, error) { } func parseOptOnDemand(d *caddyfile.Dispenser, _ any) (any, error) { + d.Next() // consume option name + if d.NextArg() { + return nil, d.ArgErr() + } + var ond *caddytls.OnDemandConfig - for d.Next() { - if d.NextArg() { - return nil, d.ArgErr() - } - for nesting := d.Nesting(); d.NextBlock(nesting); { - switch d.Val() { - case "ask": - if !d.NextArg() { - return nil, d.ArgErr() - } - if ond == nil { - ond = new(caddytls.OnDemandConfig) - } - ond.Ask = d.Val() - case "interval": - 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) - } - ond.RateLimit.Interval = caddy.Duration(dur) - - 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 - - default: - return nil, d.Errf("unrecognized parameter '%s'", d.Val()) + for nesting := d.Nesting(); d.NextBlock(nesting); { + switch d.Val() { + case "ask": + if !d.NextArg() { + return nil, d.ArgErr() } + if ond == nil { + ond = new(caddytls.OnDemandConfig) + } + perm := caddytls.PermissionByHTTP{Endpoint: d.Val()} + ond.PermissionRaw = caddyconfig.JSONModuleObject(perm, "module", "http", nil) + + case "interval": + 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) + } + ond.RateLimit.Interval = caddy.Duration(dur) + + 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 + + default: + return nil, d.Errf("unrecognized parameter '%s'", d.Val()) } } if ond == nil { @@ -390,7 +391,7 @@ func parseOptOnDemand(d *caddyfile.Dispenser, _ any) (any, error) { } func parseOptPersistConfig(d *caddyfile.Dispenser, _ any) (any, error) { - d.Next() // consume parameter name + d.Next() // consume option name if !d.Next() { return "", d.ArgErr() } @@ -405,7 +406,7 @@ func parseOptPersistConfig(d *caddyfile.Dispenser, _ any) (any, error) { } func parseOptAutoHTTPS(d *caddyfile.Dispenser, _ any) (any, error) { - d.Next() // consume parameter name + d.Next() // consume option name if !d.Next() { return "", d.ArgErr() } diff --git a/caddyconfig/httpcaddyfile/pkiapp.go b/caddyconfig/httpcaddyfile/pkiapp.go index b5c682187..c57263baf 100644 --- a/caddyconfig/httpcaddyfile/pkiapp.go +++ b/caddyconfig/httpcaddyfile/pkiapp.go @@ -48,124 +48,124 @@ func init() { // // When the CA ID is unspecified, 'local' is assumed. func parsePKIApp(d *caddyfile.Dispenser, existingVal any) (any, error) { - pki := &caddypki.PKI{CAs: make(map[string]*caddypki.CA)} + d.Next() // consume app name - for d.Next() { - for nesting := d.Nesting(); d.NextBlock(nesting); { - switch d.Val() { - case "ca": - pkiCa := new(caddypki.CA) + pki := &caddypki.PKI{ + CAs: make(map[string]*caddypki.CA), + } + for d.NextBlock(0) { + switch d.Val() { + case "ca": + pkiCa := new(caddypki.CA) + if d.NextArg() { + pkiCa.ID = d.Val() if d.NextArg() { - pkiCa.ID = d.Val() - if d.NextArg() { + return nil, d.ArgErr() + } + } + if pkiCa.ID == "" { + pkiCa.ID = caddypki.DefaultCAID + } + + for nesting := d.Nesting(); d.NextBlock(nesting); { + switch d.Val() { + case "name": + if !d.NextArg() { return nil, d.ArgErr() } - } - if pkiCa.ID == "" { - pkiCa.ID = caddypki.DefaultCAID - } + pkiCa.Name = d.Val() - for nesting := d.Nesting(); d.NextBlock(nesting); { - switch d.Val() { - case "name": - if !d.NextArg() { - return nil, d.ArgErr() - } - pkiCa.Name = d.Val() - - case "root_cn": - if !d.NextArg() { - return nil, d.ArgErr() - } - pkiCa.RootCommonName = d.Val() - - case "intermediate_cn": - if !d.NextArg() { - return nil, d.ArgErr() - } - pkiCa.IntermediateCommonName = d.Val() - - case "intermediate_lifetime": - if !d.NextArg() { - return nil, d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return nil, err - } - pkiCa.IntermediateLifetime = caddy.Duration(dur) - - case "root": - if pkiCa.Root == nil { - pkiCa.Root = new(caddypki.KeyPair) - } - for nesting := d.Nesting(); d.NextBlock(nesting); { - switch d.Val() { - case "cert": - if !d.NextArg() { - return nil, d.ArgErr() - } - pkiCa.Root.Certificate = d.Val() - - case "key": - if !d.NextArg() { - return nil, d.ArgErr() - } - pkiCa.Root.PrivateKey = d.Val() - - case "format": - if !d.NextArg() { - return nil, d.ArgErr() - } - pkiCa.Root.Format = d.Val() - - default: - return nil, d.Errf("unrecognized pki ca root option '%s'", d.Val()) - } - } - - case "intermediate": - if pkiCa.Intermediate == nil { - pkiCa.Intermediate = new(caddypki.KeyPair) - } - for nesting := d.Nesting(); d.NextBlock(nesting); { - switch d.Val() { - case "cert": - if !d.NextArg() { - return nil, d.ArgErr() - } - pkiCa.Intermediate.Certificate = d.Val() - - case "key": - if !d.NextArg() { - return nil, d.ArgErr() - } - pkiCa.Intermediate.PrivateKey = d.Val() - - case "format": - if !d.NextArg() { - return nil, d.ArgErr() - } - pkiCa.Intermediate.Format = d.Val() - - default: - return nil, d.Errf("unrecognized pki ca intermediate option '%s'", d.Val()) - } - } - - default: - return nil, d.Errf("unrecognized pki ca option '%s'", d.Val()) + case "root_cn": + if !d.NextArg() { + return nil, d.ArgErr() } + pkiCa.RootCommonName = d.Val() + + case "intermediate_cn": + if !d.NextArg() { + return nil, d.ArgErr() + } + pkiCa.IntermediateCommonName = d.Val() + + case "intermediate_lifetime": + if !d.NextArg() { + return nil, d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return nil, err + } + pkiCa.IntermediateLifetime = caddy.Duration(dur) + + case "root": + if pkiCa.Root == nil { + pkiCa.Root = new(caddypki.KeyPair) + } + for nesting := d.Nesting(); d.NextBlock(nesting); { + switch d.Val() { + case "cert": + if !d.NextArg() { + return nil, d.ArgErr() + } + pkiCa.Root.Certificate = d.Val() + + case "key": + if !d.NextArg() { + return nil, d.ArgErr() + } + pkiCa.Root.PrivateKey = d.Val() + + case "format": + if !d.NextArg() { + return nil, d.ArgErr() + } + pkiCa.Root.Format = d.Val() + + default: + return nil, d.Errf("unrecognized pki ca root option '%s'", d.Val()) + } + } + + case "intermediate": + if pkiCa.Intermediate == nil { + pkiCa.Intermediate = new(caddypki.KeyPair) + } + for nesting := d.Nesting(); d.NextBlock(nesting); { + switch d.Val() { + case "cert": + if !d.NextArg() { + return nil, d.ArgErr() + } + pkiCa.Intermediate.Certificate = d.Val() + + case "key": + if !d.NextArg() { + return nil, d.ArgErr() + } + pkiCa.Intermediate.PrivateKey = d.Val() + + case "format": + if !d.NextArg() { + return nil, d.ArgErr() + } + pkiCa.Intermediate.Format = d.Val() + + default: + return nil, d.Errf("unrecognized pki ca intermediate option '%s'", d.Val()) + } + } + + default: + return nil, d.Errf("unrecognized pki ca option '%s'", d.Val()) } - - pki.CAs[pkiCa.ID] = pkiCa - - default: - return nil, d.Errf("unrecognized pki option '%s'", d.Val()) } + + pki.CAs[pkiCa.ID] = pkiCa + + default: + return nil, d.Errf("unrecognized pki option '%s'", d.Val()) } } - return pki, nil } diff --git a/caddyconfig/httpcaddyfile/serveroptions.go b/caddyconfig/httpcaddyfile/serveroptions.go index 6d7c6787f..62902b964 100644 --- a/caddyconfig/httpcaddyfile/serveroptions.go +++ b/caddyconfig/httpcaddyfile/serveroptions.go @@ -46,235 +46,242 @@ type serverOptions struct { Protocols []string StrictSNIHost *bool TrustedProxiesRaw json.RawMessage + TrustedProxiesStrict int ClientIPHeaders []string ShouldLogCredentials bool Metrics *caddyhttp.Metrics } func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) { + d.Next() // consume option name + serverOpts := serverOptions{} - for d.Next() { + if d.NextArg() { + serverOpts.ListenerAddress = d.Val() if d.NextArg() { - serverOpts.ListenerAddress = d.Val() - if d.NextArg() { + return nil, d.ArgErr() + } + } + for d.NextBlock(0) { + switch d.Val() { + case "name": + if serverOpts.ListenerAddress == "" { + return nil, d.Errf("cannot set a name for a server without a listener address") + } + if !d.NextArg() { return nil, d.ArgErr() } - } - for nesting := d.Nesting(); d.NextBlock(nesting); { - switch d.Val() { - case "name": - if serverOpts.ListenerAddress == "" { - return nil, d.Errf("cannot set a name for a server without a listener address") - } - if !d.NextArg() { - return nil, d.ArgErr() - } - serverOpts.Name = d.Val() + serverOpts.Name = d.Val() - case "listener_wrappers": - for nesting := d.Nesting(); d.NextBlock(nesting); { - modID := "caddy.listeners." + d.Val() - unm, err := caddyfile.UnmarshalModule(d, modID) - if err != nil { - return nil, err - } - listenerWrapper, ok := unm.(caddy.ListenerWrapper) - if !ok { - return nil, fmt.Errorf("module %s (%T) is not a listener wrapper", modID, unm) - } - jsonListenerWrapper := caddyconfig.JSONModuleObject( - listenerWrapper, - "wrapper", - listenerWrapper.(caddy.Module).CaddyModule().ID.Name(), - nil, - ) - serverOpts.ListenerWrappersRaw = append(serverOpts.ListenerWrappersRaw, jsonListenerWrapper) - } - - case "timeouts": - for nesting := d.Nesting(); d.NextBlock(nesting); { - switch d.Val() { - case "read_body": - if !d.NextArg() { - return nil, d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return nil, d.Errf("parsing read_body timeout duration: %v", err) - } - serverOpts.ReadTimeout = caddy.Duration(dur) - - case "read_header": - if !d.NextArg() { - return nil, d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return nil, d.Errf("parsing read_header timeout duration: %v", err) - } - serverOpts.ReadHeaderTimeout = caddy.Duration(dur) - - case "write": - if !d.NextArg() { - return nil, d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return nil, d.Errf("parsing write timeout duration: %v", err) - } - serverOpts.WriteTimeout = caddy.Duration(dur) - - case "idle": - if !d.NextArg() { - return nil, d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return nil, d.Errf("parsing idle timeout duration: %v", err) - } - serverOpts.IdleTimeout = caddy.Duration(dur) - - default: - return nil, d.Errf("unrecognized timeouts option '%s'", d.Val()) - } - } - case "keepalive_interval": - if !d.NextArg() { - return nil, d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return nil, d.Errf("parsing keepalive interval duration: %v", err) - } - serverOpts.KeepAliveInterval = caddy.Duration(dur) - - case "max_header_size": - var sizeStr string - if !d.AllArgs(&sizeStr) { - return nil, d.ArgErr() - } - size, err := humanize.ParseBytes(sizeStr) - if err != nil { - return nil, d.Errf("parsing max_header_size: %v", err) - } - serverOpts.MaxHeaderBytes = int(size) - - case "enable_full_duplex": - if d.NextArg() { - return nil, d.ArgErr() - } - serverOpts.EnableFullDuplex = true - - case "log_credentials": - if d.NextArg() { - return nil, d.ArgErr() - } - serverOpts.ShouldLogCredentials = true - - case "protocols": - protos := d.RemainingArgs() - for _, proto := range protos { - 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) { - return nil, d.Errf("protocol %s specified more than once", proto) - } - serverOpts.Protocols = append(serverOpts.Protocols, proto) - } - if nesting := d.Nesting(); d.NextBlock(nesting) { - return nil, d.ArgErr() - } - - case "strict_sni_host": - 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 - - case "trusted_proxies": - if !d.NextArg() { - return nil, d.Err("trusted_proxies expects an IP range source module name as its first argument") - } - modID := "http.ip_sources." + d.Val() + case "listener_wrappers": + for nesting := d.Nesting(); d.NextBlock(nesting); { + modID := "caddy.listeners." + d.Val() unm, err := caddyfile.UnmarshalModule(d, modID) if err != nil { return nil, err } - source, ok := unm.(caddyhttp.IPRangeSource) + listenerWrapper, ok := unm.(caddy.ListenerWrapper) if !ok { - return nil, fmt.Errorf("module %s (%T) is not an IP range source", modID, unm) + return nil, fmt.Errorf("module %s (%T) is not a listener wrapper", modID, unm) } - jsonSource := caddyconfig.JSONModuleObject( - source, - "source", - source.(caddy.Module).CaddyModule().ID.Name(), + jsonListenerWrapper := caddyconfig.JSONModuleObject( + listenerWrapper, + "wrapper", + listenerWrapper.(caddy.Module).CaddyModule().ID.Name(), nil, ) - serverOpts.TrustedProxiesRaw = jsonSource - - case "client_ip_headers": - headers := d.RemainingArgs() - for _, header := range headers { - if sliceContains(serverOpts.ClientIPHeaders, header) { - return nil, d.Errf("client IP header %s specified more than once", header) - } - serverOpts.ClientIPHeaders = append(serverOpts.ClientIPHeaders, header) - } - if nesting := d.Nesting(); d.NextBlock(nesting) { - return nil, d.ArgErr() - } - - case "metrics": - 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()) - } - } - - default: - return nil, d.Errf("unrecognized servers option '%s'", d.Val()) + serverOpts.ListenerWrappersRaw = append(serverOpts.ListenerWrappersRaw, jsonListenerWrapper) } + + case "timeouts": + for nesting := d.Nesting(); d.NextBlock(nesting); { + switch d.Val() { + case "read_body": + if !d.NextArg() { + return nil, d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return nil, d.Errf("parsing read_body timeout duration: %v", err) + } + serverOpts.ReadTimeout = caddy.Duration(dur) + + case "read_header": + if !d.NextArg() { + return nil, d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return nil, d.Errf("parsing read_header timeout duration: %v", err) + } + serverOpts.ReadHeaderTimeout = caddy.Duration(dur) + + case "write": + if !d.NextArg() { + return nil, d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return nil, d.Errf("parsing write timeout duration: %v", err) + } + serverOpts.WriteTimeout = caddy.Duration(dur) + + case "idle": + if !d.NextArg() { + return nil, d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return nil, d.Errf("parsing idle timeout duration: %v", err) + } + serverOpts.IdleTimeout = caddy.Duration(dur) + + default: + return nil, d.Errf("unrecognized timeouts option '%s'", d.Val()) + } + } + case "keepalive_interval": + if !d.NextArg() { + return nil, d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return nil, d.Errf("parsing keepalive interval duration: %v", err) + } + serverOpts.KeepAliveInterval = caddy.Duration(dur) + + case "max_header_size": + var sizeStr string + if !d.AllArgs(&sizeStr) { + return nil, d.ArgErr() + } + size, err := humanize.ParseBytes(sizeStr) + if err != nil { + return nil, d.Errf("parsing max_header_size: %v", err) + } + serverOpts.MaxHeaderBytes = int(size) + + case "enable_full_duplex": + if d.NextArg() { + return nil, d.ArgErr() + } + serverOpts.EnableFullDuplex = true + + case "log_credentials": + if d.NextArg() { + return nil, d.ArgErr() + } + serverOpts.ShouldLogCredentials = true + + case "protocols": + protos := d.RemainingArgs() + for _, proto := range protos { + 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) { + return nil, d.Errf("protocol %s specified more than once", proto) + } + serverOpts.Protocols = append(serverOpts.Protocols, proto) + } + if nesting := d.Nesting(); d.NextBlock(nesting) { + return nil, d.ArgErr() + } + + case "strict_sni_host": + 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 + + case "trusted_proxies": + if !d.NextArg() { + return nil, d.Err("trusted_proxies expects an IP range source module name as its first argument") + } + modID := "http.ip_sources." + d.Val() + unm, err := caddyfile.UnmarshalModule(d, modID) + if err != nil { + return nil, err + } + source, ok := unm.(caddyhttp.IPRangeSource) + if !ok { + return nil, fmt.Errorf("module %s (%T) is not an IP range source", modID, unm) + } + jsonSource := caddyconfig.JSONModuleObject( + source, + "source", + source.(caddy.Module).CaddyModule().ID.Name(), + nil, + ) + serverOpts.TrustedProxiesRaw = jsonSource + + case "trusted_proxies_strict": + if d.NextArg() { + return nil, d.ArgErr() + } + serverOpts.TrustedProxiesStrict = 1 + + case "client_ip_headers": + headers := d.RemainingArgs() + for _, header := range headers { + if sliceContains(serverOpts.ClientIPHeaders, header) { + return nil, d.Errf("client IP header %s specified more than once", header) + } + serverOpts.ClientIPHeaders = append(serverOpts.ClientIPHeaders, header) + } + if nesting := d.Nesting(); d.NextBlock(nesting) { + return nil, d.ArgErr() + } + + case "metrics": + 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()) + } + } + + default: + return nil, d.Errf("unrecognized servers option '%s'", d.Val()) } } return serverOpts, nil @@ -340,6 +347,7 @@ func applyServerOptions( server.StrictSNIHost = opts.StrictSNIHost server.TrustedProxiesRaw = opts.TrustedProxiesRaw server.ClientIPHeaders = opts.ClientIPHeaders + server.TrustedProxiesStrict = opts.TrustedProxiesStrict server.Metrics = opts.Metrics if opts.ShouldLogCredentials { if server.Logs == nil { diff --git a/caddyconfig/httpcaddyfile/shorthands.go b/caddyconfig/httpcaddyfile/shorthands.go new file mode 100644 index 000000000..5855d127c --- /dev/null +++ b/caddyconfig/httpcaddyfile/shorthands.go @@ -0,0 +1,93 @@ +package httpcaddyfile + +import ( + "regexp" + "strings" + + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" +) + +type ComplexShorthandReplacer struct { + search *regexp.Regexp + replace string +} + +type ShorthandReplacer struct { + complex []ComplexShorthandReplacer + simple *strings.Replacer +} + +func NewShorthandReplacer() ShorthandReplacer { + // replace shorthand placeholders (which are convenient + // when writing a Caddyfile) with their actual placeholder + // identifiers or variable names + replacer := strings.NewReplacer(placeholderShorthands()...) + + // these are placeholders that allow a user-defined final + // parameters, but we still want to provide a shorthand + // for those, so we use a regexp to replace + regexpReplacements := []ComplexShorthandReplacer{ + {regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"}, + {regexp.MustCompile(`{cookie\.([\w-]*)}`), "{http.request.cookie.$1}"}, + {regexp.MustCompile(`{labels\.([\w-]*)}`), "{http.request.host.labels.$1}"}, + {regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"}, + {regexp.MustCompile(`{file\.([\w-]*)}`), "{http.request.uri.path.file.$1}"}, + {regexp.MustCompile(`{query\.([\w-]*)}`), "{http.request.uri.query.$1}"}, + {regexp.MustCompile(`{re\.([\w-\.]*)}`), "{http.regexp.$1}"}, + {regexp.MustCompile(`{vars\.([\w-]*)}`), "{http.vars.$1}"}, + {regexp.MustCompile(`{rp\.([\w-\.]*)}`), "{http.reverse_proxy.$1}"}, + {regexp.MustCompile(`{err\.([\w-\.]*)}`), "{http.error.$1}"}, + {regexp.MustCompile(`{file_match\.([\w-]*)}`), "{http.matchers.file.$1}"}, + } + + return ShorthandReplacer{ + complex: regexpReplacements, + simple: replacer, + } +} + +// placeholderShorthands returns a slice of old-new string pairs, +// where the left of the pair is a placeholder shorthand that may +// 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}", + "{method}", "{http.request.method}", + "{path}", "{http.request.uri.path}", + "{query}", "{http.request.uri.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}", + "{tls_client_fingerprint}", "{http.request.tls.client.fingerprint}", + "{tls_client_issuer}", "{http.request.tls.client.issuer}", + "{tls_client_serial}", "{http.request.tls.client.serial}", + "{tls_client_subject}", "{http.request.tls.client.subject}", + "{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}", + "{tls_client_certificate_der_base64}", "{http.request.tls.client.certificate_der_base64}", + "{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}", + "{client_ip}", "{http.vars.client_ip}", + } +} + +// ApplyToSegment replaces shorthand placeholder to its full placeholder, understandable by Caddy. +func (s ShorthandReplacer) ApplyToSegment(segment *caddyfile.Segment) { + if segment != nil { + for i := 0; i < len(*segment); i++ { + // simple string replacements + (*segment)[i].Text = s.simple.Replace((*segment)[i].Text) + // complex regexp replacements + for _, r := range s.complex { + (*segment)[i].Text = r.search.ReplaceAllString((*segment)[i].Text, r.replace) + } + } + } +} diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index 927f225df..1adb2b6e0 100644 --- a/caddyconfig/httpcaddyfile/tlsapp.go +++ b/caddyconfig/httpcaddyfile/tlsapp.go @@ -118,6 +118,11 @@ func (st ServerType) buildTLSApp( ap.OnDemand = true } + // reuse private keys tls + if _, ok := sblock.pile["tls.reuse_private_keys"]; ok { + ap.ReusePrivateKeys = true + } + if keyTypeVals, ok := sblock.pile["tls.key_type"]; ok { ap.KeyType = keyTypeVals[0].Value.(string) } @@ -582,10 +587,12 @@ outer: // eaten up by the one with subjects; and if both have subjects, we // need to combine their lists if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) && + reflect.DeepEqual(aps[i].ManagersRaw, aps[j].ManagersRaw) && bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) && aps[i].MustStaple == aps[j].MustStaple && aps[i].KeyType == aps[j].KeyType && aps[i].OnDemand == aps[j].OnDemand && + aps[i].ReusePrivateKeys == aps[j].ReusePrivateKeys && aps[i].RenewalWindowRatio == aps[j].RenewalWindowRatio { if len(aps[i].SubjectsRaw) > 0 && len(aps[j].SubjectsRaw) == 0 { // later policy (at j) has no subjects ("catch-all"), so we can diff --git a/caddytest/caddytest.go b/caddytest/caddytest.go index 9c170703c..deb567b3b 100644 --- a/caddytest/caddytest.go +++ b/caddytest/caddytest.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "io/fs" "log" "net" "net/http" @@ -59,11 +60,11 @@ var ( type Tester struct { Client *http.Client configLoaded bool - t *testing.T + t testing.TB } // NewTester will create a new testing client with an attached cookie jar -func NewTester(t *testing.T) *Tester { +func NewTester(t testing.TB) *Tester { jar, err := cookiejar.New(nil) if err != nil { t.Fatalf("failed to create cookiejar: %s", err) @@ -120,7 +121,6 @@ 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)) if err != nil { tc.t.Log("unable to read the current config") @@ -229,10 +229,10 @@ const initConfig = `{ // validateTestPrerequisites ensures the certificates are available in the // designated path and Caddy sub-process is running. -func validateTestPrerequisites(t *testing.T) error { +func validateTestPrerequisites(t testing.TB) error { // check certificates are found for _, certName := range Default.Certifcates { - if _, err := os.Stat(getIntegrationDir() + certName); os.IsNotExist(err) { + if _, err := os.Stat(getIntegrationDir() + certName); errors.Is(err, fs.ErrNotExist) { return fmt.Errorf("caddy integration test certificates (%s) not found", certName) } } @@ -373,7 +373,7 @@ func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, e } // CompareAdapt adapts a config and then compares it against an expected result -func CompareAdapt(t *testing.T, filename, rawConfig string, adapterName string, expectedResponse string) bool { +func CompareAdapt(t testing.TB, filename, rawConfig string, adapterName string, expectedResponse string) bool { cfgAdapter := caddyconfig.GetAdapter(adapterName) if cfgAdapter == nil { t.Logf("unrecognized config adapter '%s'", adapterName) @@ -432,7 +432,7 @@ func CompareAdapt(t *testing.T, filename, rawConfig string, adapterName string, } // AssertAdapt adapts a config and then tests it against an expected result -func AssertAdapt(t *testing.T, rawConfig string, adapterName string, expectedResponse string) { +func AssertAdapt(t testing.TB, rawConfig string, adapterName string, expectedResponse string) { ok := CompareAdapt(t, "Caddyfile", rawConfig, adapterName, expectedResponse) if !ok { t.Fail() @@ -441,7 +441,7 @@ func AssertAdapt(t *testing.T, rawConfig string, adapterName string, expectedRes // Generic request functions -func applyHeaders(t *testing.T, req *http.Request, requestHeaders []string) { +func applyHeaders(t testing.TB, req *http.Request, requestHeaders []string) { requestContentType := "" for _, requestHeader := range requestHeaders { arr := strings.SplitAfterN(requestHeader, ":", 2) @@ -467,7 +467,7 @@ func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) } if expectedStatusCode != resp.StatusCode { - tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.RequestURI, expectedStatusCode, resp.StatusCode) + tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.URL.RequestURI(), expectedStatusCode, resp.StatusCode) } return resp diff --git a/caddytest/integration/acme_test.go b/caddytest/integration/acme_test.go new file mode 100644 index 000000000..840af023f --- /dev/null +++ b/caddytest/integration/acme_test.go @@ -0,0 +1,206 @@ +package integration + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "fmt" + "net" + "net/http" + "strings" + "testing" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddytest" + "github.com/mholt/acmez" + "github.com/mholt/acmez/acme" + smallstepacme "github.com/smallstep/certificates/acme" + "go.uber.org/zap" +) + +const acmeChallengePort = 9081 + +// Test the basic functionality of Caddy's ACME server +func TestACMEServerWithDefaults(t *testing.T) { + ctx := context.Background() + logger, err := zap.NewDevelopment() + if err != nil { + t.Error(err) + return + } + + tester := caddytest.NewTester(t) + tester.InitServer(` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + local_certs + } + acme.localhost { + acme_server + } + `, "caddyfile") + + client := acmez.Client{ + Client: &acme.Client{ + Directory: "https://acme.localhost:9443/acme/local/directory", + HTTPClient: tester.Client, + Logger: logger, + }, + ChallengeSolvers: map[string]acmez.Solver{ + acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger}, + }, + } + + accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Errorf("generating account key: %v", err) + } + account := acme.Account{ + Contact: []string{"mailto:you@example.com"}, + TermsOfServiceAgreed: true, + PrivateKey: accountPrivateKey, + } + account, err = client.NewAccount(ctx, account) + if err != nil { + t.Errorf("new account: %v", err) + return + } + + // Every certificate needs a key. + certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Errorf("generating certificate key: %v", err) + return + } + + certs, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"localhost"}) + if err != nil { + t.Errorf("obtaining certificate: %v", err) + return + } + + // ACME servers should usually give you the entire certificate chain + // in PEM format, and sometimes even alternate chains! It's up to you + // which one(s) to store and use, but whatever you do, be sure to + // store the certificate and key somewhere safe and secure, i.e. don't + // lose them! + for _, cert := range certs { + t.Logf("Certificate %q:\n%s\n\n", cert.URL, cert.ChainPEM) + } +} + +func TestACMEServerWithMismatchedChallenges(t *testing.T) { + ctx := context.Background() + logger := caddy.Log().Named("acmez") + + tester := caddytest.NewTester(t) + tester.InitServer(` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + local_certs + } + acme.localhost { + acme_server { + challenges tls-alpn-01 + } + } + `, "caddyfile") + + client := acmez.Client{ + Client: &acme.Client{ + Directory: "https://acme.localhost:9443/acme/local/directory", + HTTPClient: tester.Client, + Logger: logger, + }, + ChallengeSolvers: map[string]acmez.Solver{ + acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger}, + }, + } + + accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Errorf("generating account key: %v", err) + } + account := acme.Account{ + Contact: []string{"mailto:you@example.com"}, + TermsOfServiceAgreed: true, + PrivateKey: accountPrivateKey, + } + account, err = client.NewAccount(ctx, account) + if err != nil { + t.Errorf("new account: %v", err) + return + } + + // Every certificate needs a key. + certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Errorf("generating certificate key: %v", err) + return + } + + certs, err := client.ObtainCertificate(ctx, account, certPrivateKey, []string{"localhost"}) + if len(certs) > 0 { + t.Errorf("expected '0' certificates, but received '%d'", len(certs)) + } + if err == nil { + t.Error("expected errors, but received none") + } + const expectedErrMsg = "no solvers available for remaining challenges (configured=[http-01] offered=[tls-alpn-01] remaining=[tls-alpn-01])" + if !strings.Contains(err.Error(), expectedErrMsg) { + t.Errorf(`received error message does not match expectation: expected="%s" received="%s"`, expectedErrMsg, err.Error()) + } +} + +// naiveHTTPSolver is a no-op acmez.Solver for example purposes only. +type naiveHTTPSolver struct { + srv *http.Server + logger *zap.Logger +} + +func (s *naiveHTTPSolver) Present(ctx context.Context, challenge acme.Challenge) error { + smallstepacme.InsecurePortHTTP01 = acmeChallengePort + s.srv = &http.Server{ + Addr: fmt.Sprintf(":%d", acmeChallengePort), + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + host, _, err := net.SplitHostPort(r.Host) + if err != nil { + host = r.Host + } + s.logger.Info("received request on challenge server", zap.String("path", r.URL.Path)) + if r.Method == "GET" && r.URL.Path == challenge.HTTP01ResourcePath() && strings.EqualFold(host, challenge.Identifier.Value) { + w.Header().Add("Content-Type", "text/plain") + w.Write([]byte(challenge.KeyAuthorization)) + r.Close = true + s.logger.Info("served key authentication", + zap.String("identifier", challenge.Identifier.Value), + zap.String("challenge", "http-01"), + zap.String("remote", r.RemoteAddr), + ) + } + }), + } + l, err := net.Listen("tcp", fmt.Sprintf(":%d", acmeChallengePort)) + if err != nil { + return err + } + s.logger.Info("present challenge", zap.Any("challenge", challenge)) + go s.srv.Serve(l) + return nil +} + +func (s naiveHTTPSolver) CleanUp(ctx context.Context, challenge acme.Challenge) error { + smallstepacme.InsecurePortHTTP01 = 0 + s.logger.Info("cleanup", zap.Any("challenge", challenge)) + if s.srv != nil { + s.srv.Close() + } + return nil +} diff --git a/caddytest/integration/acmeserver_test.go b/caddytest/integration/acmeserver_test.go new file mode 100644 index 000000000..435bfc7b4 --- /dev/null +++ b/caddytest/integration/acmeserver_test.go @@ -0,0 +1,209 @@ +package integration + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "strings" + "testing" + + "github.com/caddyserver/caddy/v2/caddytest" + "github.com/mholt/acmez" + "github.com/mholt/acmez/acme" + "go.uber.org/zap" +) + +func TestACMEServerDirectory(t *testing.T) { + tester := caddytest.NewTester(t) + tester.InitServer(` + { + skip_install_trust + local_certs + admin localhost:2999 + http_port 9080 + https_port 9443 + pki { + ca local { + name "Caddy Local Authority" + } + } + } + acme.localhost:9443 { + acme_server + } + `, "caddyfile") + tester.AssertGetResponse( + "https://acme.localhost:9443/acme/local/directory", + 200, + `{"newNonce":"https://acme.localhost:9443/acme/local/new-nonce","newAccount":"https://acme.localhost:9443/acme/local/new-account","newOrder":"https://acme.localhost:9443/acme/local/new-order","revokeCert":"https://acme.localhost:9443/acme/local/revoke-cert","keyChange":"https://acme.localhost:9443/acme/local/key-change"} +`) +} + +func TestACMEServerAllowPolicy(t *testing.T) { + tester := caddytest.NewTester(t) + tester.InitServer(` + { + skip_install_trust + local_certs + admin localhost:2999 + http_port 9080 + https_port 9443 + pki { + ca local { + name "Caddy Local Authority" + } + } + } + acme.localhost { + acme_server { + challenges http-01 + allow { + domains localhost + } + } + } + `, "caddyfile") + + ctx := context.Background() + logger, err := zap.NewDevelopment() + if err != nil { + t.Error(err) + return + } + + client := acmez.Client{ + Client: &acme.Client{ + Directory: "https://acme.localhost:9443/acme/local/directory", + HTTPClient: tester.Client, + Logger: logger, + }, + ChallengeSolvers: map[string]acmez.Solver{ + acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger}, + }, + } + + accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Errorf("generating account key: %v", err) + } + account := acme.Account{ + Contact: []string{"mailto:you@example.com"}, + TermsOfServiceAgreed: true, + PrivateKey: accountPrivateKey, + } + account, err = client.NewAccount(ctx, account) + if err != nil { + t.Errorf("new account: %v", err) + return + } + + // Every certificate needs a key. + certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Errorf("generating certificate key: %v", err) + return + } + { + certs, err := client.ObtainCertificate( + ctx, + account, + certPrivateKey, + []string{"localhost"}, + ) + if err != nil { + t.Errorf("obtaining certificate for allowed domain: %v", err) + return + } + + // ACME servers should usually give you the entire certificate chain + // in PEM format, and sometimes even alternate chains! It's up to you + // which one(s) to store and use, but whatever you do, be sure to + // store the certificate and key somewhere safe and secure, i.e. don't + // lose them! + for _, cert := range certs { + t.Logf("Certificate %q:\n%s\n\n", cert.URL, cert.ChainPEM) + } + } + { + _, err := client.ObtainCertificate(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") { + t.Logf("unexpected error: %v", err) + } + } +} + +func TestACMEServerDenyPolicy(t *testing.T) { + tester := caddytest.NewTester(t) + tester.InitServer(` + { + skip_install_trust + local_certs + admin localhost:2999 + http_port 9080 + https_port 9443 + pki { + ca local { + name "Caddy Local Authority" + } + } + } + acme.localhost { + acme_server { + deny { + domains deny.localhost + } + } + } + `, "caddyfile") + + ctx := context.Background() + logger, err := zap.NewDevelopment() + if err != nil { + t.Error(err) + return + } + + client := acmez.Client{ + Client: &acme.Client{ + Directory: "https://acme.localhost:9443/acme/local/directory", + HTTPClient: tester.Client, + Logger: logger, + }, + ChallengeSolvers: map[string]acmez.Solver{ + acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger}, + }, + } + + accountPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Errorf("generating account key: %v", err) + } + account := acme.Account{ + Contact: []string{"mailto:you@example.com"}, + TermsOfServiceAgreed: true, + PrivateKey: accountPrivateKey, + } + account, err = client.NewAccount(ctx, account) + if err != nil { + t.Errorf("new account: %v", err) + return + } + + // Every certificate needs a key. + certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Errorf("generating certificate key: %v", err) + return + } + { + _, err := client.ObtainCertificate(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") { + t.Logf("unexpected error: %v", err) + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/acme_server_custom_challenges.caddyfiletest b/caddytest/integration/caddyfile_adapt/acme_server_custom_challenges.caddyfiletest new file mode 100644 index 000000000..2a7a51492 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/acme_server_custom_challenges.caddyfiletest @@ -0,0 +1,65 @@ +{ + pki { + ca custom-ca { + name "Custom CA" + } + } +} + +acme.example.com { + acme_server { + ca custom-ca + challenges dns-01 + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "acme.example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "ca": "custom-ca", + "challenges": [ + "dns-01" + ], + "handler": "acme_server" + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + }, + "pki": { + "certificate_authorities": { + "custom-ca": { + "name": "Custom CA" + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/acme_server_default_challenges.caddyfiletest b/caddytest/integration/caddyfile_adapt/acme_server_default_challenges.caddyfiletest new file mode 100644 index 000000000..26d345047 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/acme_server_default_challenges.caddyfiletest @@ -0,0 +1,62 @@ +{ + pki { + ca custom-ca { + name "Custom CA" + } + } +} + +acme.example.com { + acme_server { + ca custom-ca + challenges + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "acme.example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "ca": "custom-ca", + "handler": "acme_server" + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + }, + "pki": { + "certificate_authorities": { + "custom-ca": { + "name": "Custom CA" + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/acme_server_lifetime.txt b/caddytest/integration/caddyfile_adapt/acme_server_lifetime.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/acme_server_lifetime.txt rename to caddytest/integration/caddyfile_adapt/acme_server_lifetime.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/acme_server_multi_custom_challenges.caddyfiletest b/caddytest/integration/caddyfile_adapt/acme_server_multi_custom_challenges.caddyfiletest new file mode 100644 index 000000000..7fe3ca663 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/acme_server_multi_custom_challenges.caddyfiletest @@ -0,0 +1,66 @@ +{ + pki { + ca custom-ca { + name "Custom CA" + } + } +} + +acme.example.com { + acme_server { + ca custom-ca + challenges dns-01 http-01 + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "acme.example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "ca": "custom-ca", + "challenges": [ + "dns-01", + "http-01" + ], + "handler": "acme_server" + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + }, + "pki": { + "certificate_authorities": { + "custom-ca": { + "name": "Custom CA" + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/auto_https_disable_redirects.txt b/caddytest/integration/caddyfile_adapt/auto_https_disable_redirects.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/auto_https_disable_redirects.txt rename to caddytest/integration/caddyfile_adapt/auto_https_disable_redirects.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/auto_https_ignore_loaded_certs.txt b/caddytest/integration/caddyfile_adapt/auto_https_ignore_loaded_certs.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/auto_https_ignore_loaded_certs.txt rename to caddytest/integration/caddyfile_adapt/auto_https_ignore_loaded_certs.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/auto_https_off.txt b/caddytest/integration/caddyfile_adapt/auto_https_off.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/auto_https_off.txt rename to caddytest/integration/caddyfile_adapt/auto_https_off.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/bind_ipv6.txt b/caddytest/integration/caddyfile_adapt/bind_ipv6.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/bind_ipv6.txt rename to caddytest/integration/caddyfile_adapt/bind_ipv6.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/enable_tls_for_catch_all_site.caddyfiletest b/caddytest/integration/caddyfile_adapt/enable_tls_for_catch_all_site.caddyfiletest new file mode 100644 index 000000000..b37b40c00 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/enable_tls_for_catch_all_site.caddyfiletest @@ -0,0 +1,37 @@ +:8443 { + tls internal { + on_demand + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":8443" + ], + "tls_connection_policies": [ + {} + ] + } + } + }, + "tls": { + "automation": { + "policies": [ + { + "issuers": [ + { + "module": "internal" + } + ], + "on_demand": true + } + ] + } + } + } +} + diff --git a/caddytest/integration/caddyfile_adapt/encode_options.txt b/caddytest/integration/caddyfile_adapt/encode_options.caddyfiletest similarity index 95% rename from caddytest/integration/caddyfile_adapt/encode_options.txt rename to caddytest/integration/caddyfile_adapt/encode_options.caddyfiletest index 6f811abff..181bc2264 100644 --- a/caddytest/integration/caddyfile_adapt/encode_options.txt +++ b/caddytest/integration/caddyfile_adapt/encode_options.caddyfiletest @@ -11,6 +11,7 @@ encode gzip zstd { header Content-Type application/xhtml+xml* header Content-Type application/atom+xml* header Content-Type application/rss+xml* + header Content-Type application/wasm* header Content-Type image/svg+xml* } } @@ -47,6 +48,7 @@ encode { "application/xhtml+xml*", "application/atom+xml*", "application/rss+xml*", + "application/wasm*", "image/svg+xml*" ] }, diff --git a/caddytest/integration/caddyfile_adapt/error_example.txt b/caddytest/integration/caddyfile_adapt/error_example.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/error_example.txt rename to caddytest/integration/caddyfile_adapt/error_example.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/error_multi_site_blocks.caddyfiletest b/caddytest/integration/caddyfile_adapt/error_multi_site_blocks.caddyfiletest new file mode 100644 index 000000000..0e84a13c2 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/error_multi_site_blocks.caddyfiletest @@ -0,0 +1,245 @@ +foo.localhost { + root * /srv + error /private* "Unauthorized" 410 + error /fivehundred* "Internal Server Error" 500 + + handle_errors 5xx { + respond "Error In range [500 .. 599]" + } + handle_errors 410 { + respond "404 or 410 error" + } +} + +bar.localhost { + root * /srv + error /private* "Unauthorized" 410 + error /fivehundred* "Internal Server Error" 500 + + handle_errors 5xx { + respond "Error In range [500 .. 599] from second site" + } + handle_errors 410 { + respond "404 or 410 error from second site" + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "foo.localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "vars", + "root": "/srv" + } + ] + }, + { + "handle": [ + { + "error": "Internal Server Error", + "handler": "error", + "status_code": 500 + } + ], + "match": [ + { + "path": [ + "/fivehundred*" + ] + } + ] + }, + { + "handle": [ + { + "error": "Unauthorized", + "handler": "error", + "status_code": 410 + } + ], + "match": [ + { + "path": [ + "/private*" + ] + } + ] + } + ] + } + ], + "terminal": true + }, + { + "match": [ + { + "host": [ + "bar.localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "vars", + "root": "/srv" + } + ] + }, + { + "handle": [ + { + "error": "Internal Server Error", + "handler": "error", + "status_code": 500 + } + ], + "match": [ + { + "path": [ + "/fivehundred*" + ] + } + ] + }, + { + "handle": [ + { + "error": "Unauthorized", + "handler": "error", + "status_code": 410 + } + ], + "match": [ + { + "path": [ + "/private*" + ] + } + ] + } + ] + } + ], + "terminal": true + } + ], + "errors": { + "routes": [ + { + "match": [ + { + "host": [ + "foo.localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "404 or 410 error", + "handler": "static_response" + } + ], + "match": [ + { + "expression": "{http.error.status_code} in [410]" + } + ] + }, + { + "handle": [ + { + "body": "Error In range [500 .. 599]", + "handler": "static_response" + } + ], + "match": [ + { + "expression": "{http.error.status_code} \u003e= 500 \u0026\u0026 {http.error.status_code} \u003c= 599" + } + ] + } + ] + } + ], + "terminal": true + }, + { + "match": [ + { + "host": [ + "bar.localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "404 or 410 error from second site", + "handler": "static_response" + } + ], + "match": [ + { + "expression": "{http.error.status_code} in [410]" + } + ] + }, + { + "handle": [ + { + "body": "Error In range [500 .. 599] from second site", + "handler": "static_response" + } + ], + "match": [ + { + "expression": "{http.error.status_code} \u003e= 500 \u0026\u0026 {http.error.status_code} \u003c= 599" + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/error_range_codes.caddyfiletest b/caddytest/integration/caddyfile_adapt/error_range_codes.caddyfiletest new file mode 100644 index 000000000..46b70c8e3 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/error_range_codes.caddyfiletest @@ -0,0 +1,120 @@ +{ + http_port 3010 +} +localhost:3010 { + root * /srv + error /private* "Unauthorized" 410 + error /hidden* "Not found" 404 + + handle_errors 4xx { + respond "Error in the [400 .. 499] range" + } +} +---------- +{ + "apps": { + "http": { + "http_port": 3010, + "servers": { + "srv0": { + "listen": [ + ":3010" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "vars", + "root": "/srv" + } + ] + }, + { + "handle": [ + { + "error": "Unauthorized", + "handler": "error", + "status_code": 410 + } + ], + "match": [ + { + "path": [ + "/private*" + ] + } + ] + }, + { + "handle": [ + { + "error": "Not found", + "handler": "error", + "status_code": 404 + } + ], + "match": [ + { + "path": [ + "/hidden*" + ] + } + ] + } + ] + } + ], + "terminal": true + } + ], + "errors": { + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "Error in the [400 .. 499] range", + "handler": "static_response" + } + ], + "match": [ + { + "expression": "{http.error.status_code} \u003e= 400 \u0026\u0026 {http.error.status_code} \u003c= 499" + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/error_range_simple_codes.caddyfiletest b/caddytest/integration/caddyfile_adapt/error_range_simple_codes.caddyfiletest new file mode 100644 index 000000000..70158830c --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/error_range_simple_codes.caddyfiletest @@ -0,0 +1,153 @@ +{ + http_port 2099 +} +localhost:2099 { + root * /srv + error /private* "Unauthorized" 410 + error /threehundred* "Moved Permanently" 301 + error /internalerr* "Internal Server Error" 500 + + handle_errors 500 3xx { + respond "Error code is equal to 500 or in the [300..399] range" + } + handle_errors 4xx { + respond "Error in the [400 .. 499] range" + } +} +---------- +{ + "apps": { + "http": { + "http_port": 2099, + "servers": { + "srv0": { + "listen": [ + ":2099" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "vars", + "root": "/srv" + } + ] + }, + { + "handle": [ + { + "error": "Moved Permanently", + "handler": "error", + "status_code": 301 + } + ], + "match": [ + { + "path": [ + "/threehundred*" + ] + } + ] + }, + { + "handle": [ + { + "error": "Internal Server Error", + "handler": "error", + "status_code": 500 + } + ], + "match": [ + { + "path": [ + "/internalerr*" + ] + } + ] + }, + { + "handle": [ + { + "error": "Unauthorized", + "handler": "error", + "status_code": 410 + } + ], + "match": [ + { + "path": [ + "/private*" + ] + } + ] + } + ] + } + ], + "terminal": true + } + ], + "errors": { + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "Error in the [400 .. 499] range", + "handler": "static_response" + } + ], + "match": [ + { + "expression": "{http.error.status_code} \u003e= 400 \u0026\u0026 {http.error.status_code} \u003c= 499" + } + ] + }, + { + "handle": [ + { + "body": "Error code is equal to 500 or in the [300..399] range", + "handler": "static_response" + } + ], + "match": [ + { + "expression": "{http.error.status_code} \u003e= 300 \u0026\u0026 {http.error.status_code} \u003c= 399 || {http.error.status_code} in [500]" + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/error_simple_codes.caddyfiletest b/caddytest/integration/caddyfile_adapt/error_simple_codes.caddyfiletest new file mode 100644 index 000000000..5ac5863e3 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/error_simple_codes.caddyfiletest @@ -0,0 +1,120 @@ +{ + http_port 3010 +} +localhost:3010 { + root * /srv + error /private* "Unauthorized" 410 + error /hidden* "Not found" 404 + + handle_errors 404 410 { + respond "404 or 410 error" + } +} +---------- +{ + "apps": { + "http": { + "http_port": 3010, + "servers": { + "srv0": { + "listen": [ + ":3010" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "vars", + "root": "/srv" + } + ] + }, + { + "handle": [ + { + "error": "Unauthorized", + "handler": "error", + "status_code": 410 + } + ], + "match": [ + { + "path": [ + "/private*" + ] + } + ] + }, + { + "handle": [ + { + "error": "Not found", + "handler": "error", + "status_code": 404 + } + ], + "match": [ + { + "path": [ + "/hidden*" + ] + } + ] + } + ] + } + ], + "terminal": true + } + ], + "errors": { + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "404 or 410 error", + "handler": "static_response" + } + ], + "match": [ + { + "expression": "{http.error.status_code} in [404, 410]" + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/error_sort.caddyfiletest b/caddytest/integration/caddyfile_adapt/error_sort.caddyfiletest new file mode 100644 index 000000000..63701cccb --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/error_sort.caddyfiletest @@ -0,0 +1,148 @@ +{ + http_port 2099 +} +localhost:2099 { + root * /srv + error /private* "Unauthorized" 410 + error /hidden* "Not found" 404 + error /internalerr* "Internal Server Error" 500 + + handle_errors { + respond "Fallback route: code outside the [400..499] range" + } + handle_errors 4xx { + respond "Error in the [400 .. 499] range" + } +} +---------- +{ + "apps": { + "http": { + "http_port": 2099, + "servers": { + "srv0": { + "listen": [ + ":2099" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "vars", + "root": "/srv" + } + ] + }, + { + "handle": [ + { + "error": "Internal Server Error", + "handler": "error", + "status_code": 500 + } + ], + "match": [ + { + "path": [ + "/internalerr*" + ] + } + ] + }, + { + "handle": [ + { + "error": "Unauthorized", + "handler": "error", + "status_code": 410 + } + ], + "match": [ + { + "path": [ + "/private*" + ] + } + ] + }, + { + "handle": [ + { + "error": "Not found", + "handler": "error", + "status_code": 404 + } + ], + "match": [ + { + "path": [ + "/hidden*" + ] + } + ] + } + ] + } + ], + "terminal": true + } + ], + "errors": { + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "Error in the [400 .. 499] range", + "handler": "static_response" + } + ], + "match": [ + { + "expression": "{http.error.status_code} \u003e= 400 \u0026\u0026 {http.error.status_code} \u003c= 499" + } + ] + }, + { + "handle": [ + { + "body": "Fallback route: code outside the [400..499] range", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/expression_quotes.txt b/caddytest/integration/caddyfile_adapt/expression_quotes.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/expression_quotes.txt rename to caddytest/integration/caddyfile_adapt/expression_quotes.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/file_server_disable_canonical_uris.txt b/caddytest/integration/caddyfile_adapt/file_server_disable_canonical_uris.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/file_server_disable_canonical_uris.txt rename to caddytest/integration/caddyfile_adapt/file_server_disable_canonical_uris.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/file_server_pass_thru.txt b/caddytest/integration/caddyfile_adapt/file_server_pass_thru.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/file_server_pass_thru.txt rename to caddytest/integration/caddyfile_adapt/file_server_pass_thru.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/file_server_precompressed.txt b/caddytest/integration/caddyfile_adapt/file_server_precompressed.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/file_server_precompressed.txt rename to caddytest/integration/caddyfile_adapt/file_server_precompressed.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/file_server_status.txt b/caddytest/integration/caddyfile_adapt/file_server_status.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/file_server_status.txt rename to caddytest/integration/caddyfile_adapt/file_server_status.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/forward_auth_authelia.txt b/caddytest/integration/caddyfile_adapt/forward_auth_authelia.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/forward_auth_authelia.txt rename to caddytest/integration/caddyfile_adapt/forward_auth_authelia.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/forward_auth_rename_headers.txt b/caddytest/integration/caddyfile_adapt/forward_auth_rename_headers.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/forward_auth_rename_headers.txt rename to caddytest/integration/caddyfile_adapt/forward_auth_rename_headers.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/global_options.txt b/caddytest/integration/caddyfile_adapt/global_options.caddyfiletest similarity index 92% rename from caddytest/integration/caddyfile_adapt/global_options.txt rename to caddytest/integration/caddyfile_adapt/global_options.caddyfiletest index 603209802..88729c512 100644 --- a/caddytest/integration/caddyfile_adapt/global_options.txt +++ b/caddytest/integration/caddyfile_adapt/global_options.caddyfiletest @@ -69,7 +69,10 @@ } ], "on_demand": { - "ask": "https://example.com", + "permission": { + "endpoint": "https://example.com", + "module": "http" + }, "rate_limit": { "interval": 30000000000, "burst": 20 diff --git a/caddytest/integration/caddyfile_adapt/global_options_acme.txt b/caddytest/integration/caddyfile_adapt/global_options_acme.caddyfiletest similarity index 94% rename from caddytest/integration/caddyfile_adapt/global_options_acme.txt rename to caddytest/integration/caddyfile_adapt/global_options_acme.caddyfiletest index 03aee2cec..f51779253 100644 --- a/caddytest/integration/caddyfile_adapt/global_options_acme.txt +++ b/caddytest/integration/caddyfile_adapt/global_options_acme.caddyfiletest @@ -78,7 +78,10 @@ } ], "on_demand": { - "ask": "https://example.com", + "permission": { + "endpoint": "https://example.com", + "module": "http" + }, "rate_limit": { "interval": 30000000000, "burst": 20 diff --git a/caddytest/integration/caddyfile_adapt/global_options_admin.txt b/caddytest/integration/caddyfile_adapt/global_options_admin.caddyfiletest similarity index 92% rename from caddytest/integration/caddyfile_adapt/global_options_admin.txt rename to caddytest/integration/caddyfile_adapt/global_options_admin.caddyfiletest index 2b90d6de7..cfc578826 100644 --- a/caddytest/integration/caddyfile_adapt/global_options_admin.txt +++ b/caddytest/integration/caddyfile_adapt/global_options_admin.caddyfiletest @@ -71,7 +71,10 @@ } ], "on_demand": { - "ask": "https://example.com", + "permission": { + "endpoint": "https://example.com", + "module": "http" + }, "rate_limit": { "interval": 30000000000, "burst": 20 diff --git a/caddytest/integration/caddyfile_adapt/global_options_admin_with_persist_config_off.txt b/caddytest/integration/caddyfile_adapt/global_options_admin_with_persist_config_off.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/global_options_admin_with_persist_config_off.txt rename to caddytest/integration/caddyfile_adapt/global_options_admin_with_persist_config_off.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/global_options_debug_with_access_log.txt b/caddytest/integration/caddyfile_adapt/global_options_debug_with_access_log.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/global_options_debug_with_access_log.txt rename to caddytest/integration/caddyfile_adapt/global_options_debug_with_access_log.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/global_options_default_bind.txt b/caddytest/integration/caddyfile_adapt/global_options_default_bind.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/global_options_default_bind.txt rename to caddytest/integration/caddyfile_adapt/global_options_default_bind.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/global_options_log_and_site.txt b/caddytest/integration/caddyfile_adapt/global_options_log_and_site.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/global_options_log_and_site.txt rename to caddytest/integration/caddyfile_adapt/global_options_log_and_site.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/global_options_log_basic.txt b/caddytest/integration/caddyfile_adapt/global_options_log_basic.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/global_options_log_basic.txt rename to caddytest/integration/caddyfile_adapt/global_options_log_basic.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/global_options_log_custom.txt b/caddytest/integration/caddyfile_adapt/global_options_log_custom.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/global_options_log_custom.txt rename to caddytest/integration/caddyfile_adapt/global_options_log_custom.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/global_options_log_multi.txt b/caddytest/integration/caddyfile_adapt/global_options_log_multi.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/global_options_log_multi.txt rename to caddytest/integration/caddyfile_adapt/global_options_log_multi.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/global_options_persist_config.txt b/caddytest/integration/caddyfile_adapt/global_options_persist_config.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/global_options_persist_config.txt rename to caddytest/integration/caddyfile_adapt/global_options_persist_config.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/global_options_preferred_chains.txt b/caddytest/integration/caddyfile_adapt/global_options_preferred_chains.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/global_options_preferred_chains.txt rename to caddytest/integration/caddyfile_adapt/global_options_preferred_chains.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/global_options_skip_install_trust.txt b/caddytest/integration/caddyfile_adapt/global_options_skip_install_trust.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/global_options_skip_install_trust.txt rename to caddytest/integration/caddyfile_adapt/global_options_skip_install_trust.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/global_server_options_multi.txt b/caddytest/integration/caddyfile_adapt/global_server_options_multi.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/global_server_options_multi.txt rename to caddytest/integration/caddyfile_adapt/global_server_options_multi.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/global_server_options_single.txt b/caddytest/integration/caddyfile_adapt/global_server_options_single.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/global_server_options_single.txt rename to caddytest/integration/caddyfile_adapt/global_server_options_single.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/handle_nested_in_route.txt b/caddytest/integration/caddyfile_adapt/handle_nested_in_route.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/handle_nested_in_route.txt rename to caddytest/integration/caddyfile_adapt/handle_nested_in_route.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/handle_path.txt b/caddytest/integration/caddyfile_adapt/handle_path.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/handle_path.txt rename to caddytest/integration/caddyfile_adapt/handle_path.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/handle_path_sorting.txt b/caddytest/integration/caddyfile_adapt/handle_path_sorting.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/handle_path_sorting.txt rename to caddytest/integration/caddyfile_adapt/handle_path_sorting.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/header.txt b/caddytest/integration/caddyfile_adapt/header.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/header.txt rename to caddytest/integration/caddyfile_adapt/header.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/heredoc.txt b/caddytest/integration/caddyfile_adapt/heredoc.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/heredoc.txt rename to caddytest/integration/caddyfile_adapt/heredoc.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/http_only_hostnames.txt b/caddytest/integration/caddyfile_adapt/http_only_hostnames.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/http_only_hostnames.txt rename to caddytest/integration/caddyfile_adapt/http_only_hostnames.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/http_only_on_any_address.txt b/caddytest/integration/caddyfile_adapt/http_only_on_any_address.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/http_only_on_any_address.txt rename to caddytest/integration/caddyfile_adapt/http_only_on_any_address.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/http_only_on_domain.txt b/caddytest/integration/caddyfile_adapt/http_only_on_domain.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/http_only_on_domain.txt rename to caddytest/integration/caddyfile_adapt/http_only_on_domain.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/http_only_on_hostless_block.txt b/caddytest/integration/caddyfile_adapt/http_only_on_hostless_block.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/http_only_on_hostless_block.txt rename to caddytest/integration/caddyfile_adapt/http_only_on_hostless_block.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/http_only_on_localhost.txt b/caddytest/integration/caddyfile_adapt/http_only_on_localhost.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/http_only_on_localhost.txt rename to caddytest/integration/caddyfile_adapt/http_only_on_localhost.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/http_only_on_non_standard_port.txt b/caddytest/integration/caddyfile_adapt/http_only_on_non_standard_port.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/http_only_on_non_standard_port.txt rename to caddytest/integration/caddyfile_adapt/http_only_on_non_standard_port.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/http_valid_directive_like_site_address.caddyfiletest b/caddytest/integration/caddyfile_adapt/http_valid_directive_like_site_address.caddyfiletest new file mode 100644 index 000000000..675523a57 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/http_valid_directive_like_site_address.caddyfiletest @@ -0,0 +1,46 @@ +http://handle { + file_server +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":80" + ], + "routes": [ + { + "match": [ + { + "host": [ + "handle" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "file_server", + "hide": [ + "./Caddyfile" + ] + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/https_on_domain.txt b/caddytest/integration/caddyfile_adapt/https_on_domain.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/https_on_domain.txt rename to caddytest/integration/caddyfile_adapt/https_on_domain.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/import_args_file.txt b/caddytest/integration/caddyfile_adapt/import_args_file.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/import_args_file.txt rename to caddytest/integration/caddyfile_adapt/import_args_file.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/import_args_snippet.txt b/caddytest/integration/caddyfile_adapt/import_args_snippet.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/import_args_snippet.txt rename to caddytest/integration/caddyfile_adapt/import_args_snippet.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/import_args_snippet_env_placeholder.txt b/caddytest/integration/caddyfile_adapt/import_args_snippet_env_placeholder.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/import_args_snippet_env_placeholder.txt rename to caddytest/integration/caddyfile_adapt/import_args_snippet_env_placeholder.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/invoke_named_routes.txt b/caddytest/integration/caddyfile_adapt/invoke_named_routes.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/invoke_named_routes.txt rename to caddytest/integration/caddyfile_adapt/invoke_named_routes.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/log_add.caddyfiletest b/caddytest/integration/caddyfile_adapt/log_add.caddyfiletest new file mode 100644 index 000000000..4f91e4644 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/log_add.caddyfiletest @@ -0,0 +1,71 @@ +:80 { + log + + vars foo foo + + log_append const bar + log_append vars foo + log_append placeholder {path} + + log_append /only-for-this-path secret value +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":80" + ], + "routes": [ + { + "handle": [ + { + "foo": "foo", + "handler": "vars" + } + ] + }, + { + "match": [ + { + "path": [ + "/only-for-this-path" + ] + } + ], + "handle": [ + { + "handler": "log_append", + "key": "secret", + "value": "value" + } + ] + }, + { + "handle": [ + { + "handler": "log_append", + "key": "const", + "value": "bar" + }, + { + "handler": "log_append", + "key": "vars", + "value": "foo" + }, + { + "handler": "log_append", + "key": "placeholder", + "value": "{http.request.uri.path}" + } + ] + } + ], + "logs": {} + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/log_append_encoder.caddyfiletest b/caddytest/integration/caddyfile_adapt/log_append_encoder.caddyfiletest new file mode 100644 index 000000000..88a6cd6be --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/log_append_encoder.caddyfiletest @@ -0,0 +1,63 @@ +{ + log { + format append { + wrap json + fields { + wrap "foo" + } + env {env.EXAMPLE} + int 1 + float 1.1 + bool true + string "string" + } + } +} + +:80 { + respond "Hello, World!" +} +---------- +{ + "logging": { + "logs": { + "default": { + "encoder": { + "fields": { + "bool": true, + "env": "{env.EXAMPLE}", + "float": 1.1, + "int": 1, + "string": "string", + "wrap": "foo" + }, + "format": "append", + "wrap": { + "format": "json" + } + } + } + } + }, + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":80" + ], + "routes": [ + { + "handle": [ + { + "body": "Hello, World!", + "handler": "static_response" + } + ] + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/log_except_catchall_blocks.txt b/caddytest/integration/caddyfile_adapt/log_except_catchall_blocks.caddyfiletest similarity index 92% rename from caddytest/integration/caddyfile_adapt/log_except_catchall_blocks.txt rename to caddytest/integration/caddyfile_adapt/log_except_catchall_blocks.caddyfiletest index ed3b20de4..e1362f8fb 100644 --- a/caddytest/integration/caddyfile_adapt/log_except_catchall_blocks.txt +++ b/caddytest/integration/caddyfile_adapt/log_except_catchall_blocks.caddyfiletest @@ -1,7 +1,7 @@ http://localhost:2020 { log - skip_log /first-hidden* - skip_log /second-hidden* + log_skip /first-hidden* + log_skip /second-hidden* respond 200 } @@ -34,7 +34,7 @@ http://localhost:2020 { "handle": [ { "handler": "vars", - "skip_log": true + "log_skip": true } ], "match": [ @@ -49,7 +49,7 @@ http://localhost:2020 { "handle": [ { "handler": "vars", - "skip_log": true + "log_skip": true } ], "match": [ @@ -99,7 +99,7 @@ http://localhost:2020 { }, "logs": { "logger_names": { - "localhost:2020": "" + "localhost": "" }, "skip_unmapped_hosts": true } diff --git a/caddytest/integration/caddyfile_adapt/log_filter_no_wrap.caddyfiletest b/caddytest/integration/caddyfile_adapt/log_filter_no_wrap.caddyfiletest new file mode 100644 index 000000000..f63a1d925 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/log_filter_no_wrap.caddyfiletest @@ -0,0 +1,52 @@ +:80 + +log { + output stdout + format filter { + fields { + request>headers>Server delete + } + } +} +---------- +{ + "logging": { + "logs": { + "default": { + "exclude": [ + "http.log.access.log0" + ] + }, + "log0": { + "writer": { + "output": "stdout" + }, + "encoder": { + "fields": { + "request\u003eheaders\u003eServer": { + "filter": "delete" + } + }, + "format": "filter" + }, + "include": [ + "http.log.access.log0" + ] + } + } + }, + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":80" + ], + "logs": { + "default_logger_name": "log0" + } + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/log_filters.txt b/caddytest/integration/caddyfile_adapt/log_filters.caddyfiletest similarity index 78% rename from caddytest/integration/caddyfile_adapt/log_filters.txt rename to caddytest/integration/caddyfile_adapt/log_filters.caddyfiletest index 776fa68d3..1b2fc2e50 100644 --- a/caddytest/integration/caddyfile_adapt/log_filters.txt +++ b/caddytest/integration/caddyfile_adapt/log_filters.caddyfiletest @@ -4,26 +4,31 @@ log { output stdout format filter { wrap console + + # long form, with "fields" wrapper fields { uri query { replace foo REDACTED delete bar hash baz } - request>headers>Authorization replace REDACTED - request>headers>Server delete - request>headers>Cookie cookie { - replace foo REDACTED - delete bar - hash baz - } - request>remote_ip ip_mask { - ipv4 24 - ipv6 32 - } - request>headers>Regexp regexp secret REDACTED - request>headers>Hash hash } + + # short form, flatter structure + request>headers>Authorization replace REDACTED + request>headers>Server delete + request>headers>Cookie cookie { + replace foo REDACTED + delete bar + hash baz + } + request>remote_ip ip_mask { + ipv4 24 + ipv6 32 + } + request>client_ip ip_mask 16 32 + request>headers>Regexp regexp secret REDACTED + request>headers>Hash hash } } ---------- @@ -41,6 +46,11 @@ log { }, "encoder": { "fields": { + "request\u003eclient_ip": { + "filter": "ip_mask", + "ipv4_cidr": 16, + "ipv6_cidr": 32 + }, "request\u003eheaders\u003eAuthorization": { "filter": "replace", "value": "REDACTED" diff --git a/caddytest/integration/caddyfile_adapt/log_override_hostname.txt b/caddytest/integration/caddyfile_adapt/log_override_hostname.caddyfiletest similarity index 63% rename from caddytest/integration/caddyfile_adapt/log_override_hostname.txt rename to caddytest/integration/caddyfile_adapt/log_override_hostname.caddyfiletest index 4511fd494..c36ba23f8 100644 --- a/caddytest/integration/caddyfile_adapt/log_override_hostname.txt +++ b/caddytest/integration/caddyfile_adapt/log_override_hostname.caddyfiletest @@ -8,6 +8,12 @@ output file /baz.txt } } + +example.com:8443 { + log { + output file /port.txt + } +} ---------- { "logging": { @@ -15,7 +21,8 @@ "default": { "exclude": [ "http.log.access.log0", - "http.log.access.log1" + "http.log.access.log1", + "http.log.access.log2" ] }, "log0": { @@ -35,6 +42,15 @@ "include": [ "http.log.access.log1" ] + }, + "log2": { + "writer": { + "filename": "/port.txt", + "output": "file" + }, + "include": [ + "http.log.access.log2" + ] } } }, @@ -64,6 +80,28 @@ "foo.example.com": "log0" } } + }, + "srv1": { + "listen": [ + ":8443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "terminal": true + } + ], + "logs": { + "logger_names": { + "example.com": "log2" + } + } } } } diff --git a/caddytest/integration/caddyfile_adapt/log_override_name_multiaccess.txt b/caddytest/integration/caddyfile_adapt/log_override_name_multiaccess.caddyfiletest similarity index 97% rename from caddytest/integration/caddyfile_adapt/log_override_name_multiaccess.txt rename to caddytest/integration/caddyfile_adapt/log_override_name_multiaccess.caddyfiletest index a3b0cec63..d48857a2d 100644 --- a/caddytest/integration/caddyfile_adapt/log_override_name_multiaccess.txt +++ b/caddytest/integration/caddyfile_adapt/log_override_name_multiaccess.caddyfiletest @@ -76,7 +76,7 @@ http://localhost:8881 { }, "logs": { "logger_names": { - "localhost:8881": "foo" + "localhost": "foo" } } } diff --git a/caddytest/integration/caddyfile_adapt/log_override_name_multiaccess_debug.txt b/caddytest/integration/caddyfile_adapt/log_override_name_multiaccess_debug.caddyfiletest similarity index 97% rename from caddytest/integration/caddyfile_adapt/log_override_name_multiaccess_debug.txt rename to caddytest/integration/caddyfile_adapt/log_override_name_multiaccess_debug.caddyfiletest index e6698e4fb..d024dc386 100644 --- a/caddytest/integration/caddyfile_adapt/log_override_name_multiaccess_debug.txt +++ b/caddytest/integration/caddyfile_adapt/log_override_name_multiaccess_debug.caddyfiletest @@ -81,7 +81,7 @@ http://localhost:8881 { }, "logs": { "logger_names": { - "localhost:8881": "foo" + "localhost": "foo" } } } diff --git a/caddytest/integration/caddyfile_adapt/log_roll_days.txt b/caddytest/integration/caddyfile_adapt/log_roll_days.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/log_roll_days.txt rename to caddytest/integration/caddyfile_adapt/log_roll_days.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/log_skip_hosts.txt b/caddytest/integration/caddyfile_adapt/log_skip_hosts.caddyfiletest similarity index 95% rename from caddytest/integration/caddyfile_adapt/log_skip_hosts.txt rename to caddytest/integration/caddyfile_adapt/log_skip_hosts.caddyfiletest index 25e4bd1c2..8fdd57156 100644 --- a/caddytest/integration/caddyfile_adapt/log_skip_hosts.txt +++ b/caddytest/integration/caddyfile_adapt/log_skip_hosts.caddyfiletest @@ -66,9 +66,9 @@ example.com { "one.example.com": "" }, "skip_hosts": [ + "example.com", "three.example.com", - "two.example.com", - "example.com" + "two.example.com" ] } } diff --git a/caddytest/integration/caddyfile_adapt/map_and_vars_with_raw_types.txt b/caddytest/integration/caddyfile_adapt/map_and_vars_with_raw_types.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/map_and_vars_with_raw_types.txt rename to caddytest/integration/caddyfile_adapt/map_and_vars_with_raw_types.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/matcher_syntax.txt b/caddytest/integration/caddyfile_adapt/matcher_syntax.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/matcher_syntax.txt rename to caddytest/integration/caddyfile_adapt/matcher_syntax.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/matchers_in_route.txt b/caddytest/integration/caddyfile_adapt/matchers_in_route.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/matchers_in_route.txt rename to caddytest/integration/caddyfile_adapt/matchers_in_route.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/method_directive.txt b/caddytest/integration/caddyfile_adapt/method_directive.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/method_directive.txt rename to caddytest/integration/caddyfile_adapt/method_directive.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/metrics_disable_om.txt b/caddytest/integration/caddyfile_adapt/metrics_disable_om.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/metrics_disable_om.txt rename to caddytest/integration/caddyfile_adapt/metrics_disable_om.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/metrics_syntax.txt b/caddytest/integration/caddyfile_adapt/metrics_syntax.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/metrics_syntax.txt rename to caddytest/integration/caddyfile_adapt/metrics_syntax.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/not_block_merging.txt b/caddytest/integration/caddyfile_adapt/not_block_merging.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/not_block_merging.txt rename to caddytest/integration/caddyfile_adapt/not_block_merging.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/php_fastcgi_expanded_form.txt b/caddytest/integration/caddyfile_adapt/php_fastcgi_expanded_form.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/php_fastcgi_expanded_form.txt rename to caddytest/integration/caddyfile_adapt/php_fastcgi_expanded_form.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/php_fastcgi_handle_response.txt b/caddytest/integration/caddyfile_adapt/php_fastcgi_handle_response.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/php_fastcgi_handle_response.txt rename to caddytest/integration/caddyfile_adapt/php_fastcgi_handle_response.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/php_fastcgi_index_off.txt b/caddytest/integration/caddyfile_adapt/php_fastcgi_index_off.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/php_fastcgi_index_off.txt rename to caddytest/integration/caddyfile_adapt/php_fastcgi_index_off.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/php_fastcgi_matcher.txt b/caddytest/integration/caddyfile_adapt/php_fastcgi_matcher.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/php_fastcgi_matcher.txt rename to caddytest/integration/caddyfile_adapt/php_fastcgi_matcher.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/php_fastcgi_subdirectives.txt b/caddytest/integration/caddyfile_adapt/php_fastcgi_subdirectives.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/php_fastcgi_subdirectives.txt rename to caddytest/integration/caddyfile_adapt/php_fastcgi_subdirectives.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/php_fastcgi_try_files_override.txt b/caddytest/integration/caddyfile_adapt/php_fastcgi_try_files_override.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/php_fastcgi_try_files_override.txt rename to caddytest/integration/caddyfile_adapt/php_fastcgi_try_files_override.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/portless_upstream.txt b/caddytest/integration/caddyfile_adapt/portless_upstream.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/portless_upstream.txt rename to caddytest/integration/caddyfile_adapt/portless_upstream.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/push.caddyfiletest b/caddytest/integration/caddyfile_adapt/push.caddyfiletest new file mode 100644 index 000000000..1fe344e09 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/push.caddyfiletest @@ -0,0 +1,78 @@ +:80 + +push * /foo.txt + +push { + GET /foo.txt +} + +push { + GET /foo.txt + HEAD /foo.txt +} + +push { + headers { + Foo bar + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":80" + ], + "routes": [ + { + "handle": [ + { + "handler": "push", + "resources": [ + { + "target": "/foo.txt" + } + ] + }, + { + "handler": "push", + "resources": [ + { + "method": "GET", + "target": "/foo.txt" + } + ] + }, + { + "handler": "push", + "resources": [ + { + "method": "GET", + "target": "/foo.txt" + }, + { + "method": "HEAD", + "target": "/foo.txt" + } + ] + }, + { + "handler": "push", + "headers": { + "set": { + "Foo": [ + "bar" + ] + } + } + } + ] + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/replaceable_upstream.txt b/caddytest/integration/caddyfile_adapt/replaceable_upstream.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/replaceable_upstream.txt rename to caddytest/integration/caddyfile_adapt/replaceable_upstream.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/replaceable_upstream_partial_port.txt b/caddytest/integration/caddyfile_adapt/replaceable_upstream_partial_port.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/replaceable_upstream_partial_port.txt rename to caddytest/integration/caddyfile_adapt/replaceable_upstream_partial_port.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/replaceable_upstream_port.txt b/caddytest/integration/caddyfile_adapt/replaceable_upstream_port.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/replaceable_upstream_port.txt rename to caddytest/integration/caddyfile_adapt/replaceable_upstream_port.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/request_body.txt b/caddytest/integration/caddyfile_adapt/request_body.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/request_body.txt rename to caddytest/integration/caddyfile_adapt/request_body.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/request_header.txt b/caddytest/integration/caddyfile_adapt/request_header.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/request_header.txt rename to caddytest/integration/caddyfile_adapt/request_header.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_buffers.caddyfiletest b/caddytest/integration/caddyfile_adapt/reverse_proxy_buffers.caddyfiletest new file mode 100644 index 000000000..317899470 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_buffers.caddyfiletest @@ -0,0 +1,58 @@ +https://example.com { + reverse_proxy https://localhost:54321 { + request_buffers unlimited + response_buffers unlimited + } +} + +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "reverse_proxy", + "request_buffers": -1, + "response_buffers": -1, + "transport": { + "protocol": "http", + "tls": {} + }, + "upstreams": [ + { + "dial": "localhost:54321" + } + ] + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_dynamic_upstreams.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_dynamic_upstreams.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/reverse_proxy_dynamic_upstreams.txt rename to caddytest/integration/caddyfile_adapt/reverse_proxy_dynamic_upstreams.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_empty_non_http_transport.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_empty_non_http_transport.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/reverse_proxy_empty_non_http_transport.txt rename to caddytest/integration/caddyfile_adapt/reverse_proxy_empty_non_http_transport.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_h2c_shorthand.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_h2c_shorthand.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/reverse_proxy_h2c_shorthand.txt rename to caddytest/integration/caddyfile_adapt/reverse_proxy_h2c_shorthand.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_handle_response.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_handle_response.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/reverse_proxy_handle_response.txt rename to caddytest/integration/caddyfile_adapt/reverse_proxy_handle_response.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_health_headers.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_health_headers.caddyfiletest similarity index 81% rename from caddytest/integration/caddyfile_adapt/reverse_proxy_health_headers.txt rename to caddytest/integration/caddyfile_adapt/reverse_proxy_health_headers.caddyfiletest index 17adcaa68..800c11f18 100644 --- a/caddytest/integration/caddyfile_adapt/reverse_proxy_health_headers.txt +++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_health_headers.caddyfiletest @@ -6,6 +6,9 @@ reverse_proxy 127.0.0.1:65535 { X-Header-Key 95ca39e3cbe7 X-Header-Keys VbG4NZwWnipo 335Q9/MhqcNU3s2TO X-Empty-Value + Same-Key 1 + Same-Key 2 + X-System-Hostname {system.hostname} } health_uri /health } @@ -29,6 +32,10 @@ reverse_proxy 127.0.0.1:65535 { "Host": [ "example.com" ], + "Same-Key": [ + "1", + "2" + ], "X-Empty-Value": [ "" ], @@ -38,6 +45,9 @@ reverse_proxy 127.0.0.1:65535 { "X-Header-Keys": [ "VbG4NZwWnipo", "335Q9/MhqcNU3s2TO" + ], + "X-System-Hostname": [ + "{system.hostname}" ] }, "uri": "/health" diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_health_path_query.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_health_path_query.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/reverse_proxy_health_path_query.txt rename to caddytest/integration/caddyfile_adapt/reverse_proxy_health_path_query.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_load_balance.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_load_balance.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/reverse_proxy_load_balance.txt rename to caddytest/integration/caddyfile_adapt/reverse_proxy_load_balance.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_load_balance_wrr.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_load_balance_wrr.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/reverse_proxy_load_balance_wrr.txt rename to caddytest/integration/caddyfile_adapt/reverse_proxy_load_balance_wrr.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_options.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_options.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/reverse_proxy_options.txt rename to caddytest/integration/caddyfile_adapt/reverse_proxy_options.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_port_range.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_port_range.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/reverse_proxy_port_range.txt rename to caddytest/integration/caddyfile_adapt/reverse_proxy_port_range.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_trusted_proxies.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_trusted_proxies.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/reverse_proxy_trusted_proxies.txt rename to caddytest/integration/caddyfile_adapt/reverse_proxy_trusted_proxies.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_upstream_placeholder.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_upstream_placeholder.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/reverse_proxy_upstream_placeholder.txt rename to caddytest/integration/caddyfile_adapt/reverse_proxy_upstream_placeholder.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/rewrite_directive_permutations.caddyfiletest b/caddytest/integration/caddyfile_adapt/rewrite_directive_permutations.caddyfiletest new file mode 100644 index 000000000..870e82afd --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/rewrite_directive_permutations.caddyfiletest @@ -0,0 +1,112 @@ +:8080 + +# With explicit wildcard matcher +route { + rewrite * /a +} + +# With path matcher +route { + rewrite /path /b +} + +# With named matcher +route { + @named method GET + rewrite @named /c +} + +# With no matcher, assumed to be wildcard +route { + rewrite /d +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":8080" + ], + "routes": [ + { + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "group": "group0", + "handle": [ + { + "handler": "rewrite", + "uri": "/a" + } + ] + } + ] + }, + { + "handler": "subroute", + "routes": [ + { + "group": "group1", + "handle": [ + { + "handler": "rewrite", + "uri": "/b" + } + ], + "match": [ + { + "path": [ + "/path" + ] + } + ] + } + ] + }, + { + "handler": "subroute", + "routes": [ + { + "group": "group2", + "handle": [ + { + "handler": "rewrite", + "uri": "/c" + } + ], + "match": [ + { + "method": [ + "GET" + ] + } + ] + } + ] + }, + { + "handler": "subroute", + "routes": [ + { + "group": "group3", + "handle": [ + { + "handler": "rewrite", + "uri": "/d" + } + ] + } + ] + } + ] + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/root_directive_permutations.caddyfiletest b/caddytest/integration/caddyfile_adapt/root_directive_permutations.caddyfiletest new file mode 100644 index 000000000..b2ef86c45 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/root_directive_permutations.caddyfiletest @@ -0,0 +1,108 @@ +:8080 + +# With explicit wildcard matcher +route { + root * /a +} + +# With path matcher +route { + root /path /b +} + +# With named matcher +route { + @named method GET + root @named /c +} + +# With no matcher, assumed to be wildcard +route { + root /d +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":8080" + ], + "routes": [ + { + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "vars", + "root": "/a" + } + ] + } + ] + }, + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "vars", + "root": "/b" + } + ], + "match": [ + { + "path": [ + "/path" + ] + } + ] + } + ] + }, + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "vars", + "root": "/c" + } + ], + "match": [ + { + "method": [ + "GET" + ] + } + ] + } + ] + }, + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "vars", + "root": "/d" + } + ] + } + ] + } + ] + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/server_names.txt b/caddytest/integration/caddyfile_adapt/server_names.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/server_names.txt rename to caddytest/integration/caddyfile_adapt/server_names.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/shorthand_parameterized_placeholders.txt b/caddytest/integration/caddyfile_adapt/shorthand_parameterized_placeholders.caddyfiletest similarity index 67% rename from caddytest/integration/caddyfile_adapt/shorthand_parameterized_placeholders.txt rename to caddytest/integration/caddyfile_adapt/shorthand_parameterized_placeholders.caddyfiletest index d5c35b3c3..30bc2c128 100644 --- a/caddytest/integration/caddyfile_adapt/shorthand_parameterized_placeholders.txt +++ b/caddytest/integration/caddyfile_adapt/shorthand_parameterized_placeholders.caddyfiletest @@ -1,5 +1,9 @@ localhost:80 + respond * "{header.content-type} {labels.0} {query.p} {path.0} {re.name.0}" + +@match path_regexp ^/foo(.*)$ +respond @match "{re.1}" ---------- { "apps": { @@ -22,6 +26,21 @@ respond * "{header.content-type} {labels.0} {query.p} {path.0} {re.name.0}" { "handler": "subroute", "routes": [ + { + "handle": [ + { + "body": "{http.regexp.1}", + "handler": "static_response" + } + ], + "match": [ + { + "path_regexp": { + "pattern": "^/foo(.*)$" + } + } + ] + }, { "handle": [ { diff --git a/caddytest/integration/caddyfile_adapt/site_block_sorting.txt b/caddytest/integration/caddyfile_adapt/site_block_sorting.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/site_block_sorting.txt rename to caddytest/integration/caddyfile_adapt/site_block_sorting.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/sort_directives_with_any_matcher_first.txt b/caddytest/integration/caddyfile_adapt/sort_directives_with_any_matcher_first.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/sort_directives_with_any_matcher_first.txt rename to caddytest/integration/caddyfile_adapt/sort_directives_with_any_matcher_first.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/sort_directives_within_handle.txt b/caddytest/integration/caddyfile_adapt/sort_directives_within_handle.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/sort_directives_within_handle.txt rename to caddytest/integration/caddyfile_adapt/sort_directives_within_handle.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/sort_vars_in_reverse.txt b/caddytest/integration/caddyfile_adapt/sort_vars_in_reverse.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/sort_vars_in_reverse.txt rename to caddytest/integration/caddyfile_adapt/sort_vars_in_reverse.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/tls_acme_preferred_chains.txt b/caddytest/integration/caddyfile_adapt/tls_acme_preferred_chains.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/tls_acme_preferred_chains.txt rename to caddytest/integration/caddyfile_adapt/tls_acme_preferred_chains.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_1.txt b/caddytest/integration/caddyfile_adapt/tls_automation_policies_1.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/tls_automation_policies_1.txt rename to caddytest/integration/caddyfile_adapt/tls_automation_policies_1.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_10.txt b/caddytest/integration/caddyfile_adapt/tls_automation_policies_10.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/tls_automation_policies_10.txt rename to caddytest/integration/caddyfile_adapt/tls_automation_policies_10.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_11.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_automation_policies_11.caddyfiletest new file mode 100644 index 000000000..9cdfd1200 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_automation_policies_11.caddyfiletest @@ -0,0 +1,67 @@ +# example from https://caddy.community/t/21415 +a.com { + tls { + get_certificate http http://foo.com/get + } +} + +b.com { +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "a.com" + ] + } + ], + "terminal": true + }, + { + "match": [ + { + "host": [ + "b.com" + ] + } + ], + "terminal": true + } + ] + } + } + }, + "tls": { + "automation": { + "policies": [ + { + "subjects": [ + "a.com" + ], + "get_certificate": [ + { + "url": "http://foo.com/get", + "via": "http" + } + ] + }, + { + "subjects": [ + "b.com" + ] + } + ] + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_2.txt b/caddytest/integration/caddyfile_adapt/tls_automation_policies_2.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/tls_automation_policies_2.txt rename to caddytest/integration/caddyfile_adapt/tls_automation_policies_2.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_3.txt b/caddytest/integration/caddyfile_adapt/tls_automation_policies_3.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/tls_automation_policies_3.txt rename to caddytest/integration/caddyfile_adapt/tls_automation_policies_3.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_4.txt b/caddytest/integration/caddyfile_adapt/tls_automation_policies_4.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/tls_automation_policies_4.txt rename to caddytest/integration/caddyfile_adapt/tls_automation_policies_4.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_5.txt b/caddytest/integration/caddyfile_adapt/tls_automation_policies_5.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/tls_automation_policies_5.txt rename to caddytest/integration/caddyfile_adapt/tls_automation_policies_5.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_6.txt b/caddytest/integration/caddyfile_adapt/tls_automation_policies_6.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/tls_automation_policies_6.txt rename to caddytest/integration/caddyfile_adapt/tls_automation_policies_6.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_7.txt b/caddytest/integration/caddyfile_adapt/tls_automation_policies_7.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/tls_automation_policies_7.txt rename to caddytest/integration/caddyfile_adapt/tls_automation_policies_7.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_8.txt b/caddytest/integration/caddyfile_adapt/tls_automation_policies_8.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/tls_automation_policies_8.txt rename to caddytest/integration/caddyfile_adapt/tls_automation_policies_8.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_9.txt b/caddytest/integration/caddyfile_adapt/tls_automation_policies_9.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/tls_automation_policies_9.txt rename to caddytest/integration/caddyfile_adapt/tls_automation_policies_9.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/tls_automation_policies_global_email_localhost.txt b/caddytest/integration/caddyfile_adapt/tls_automation_policies_global_email_localhost.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/tls_automation_policies_global_email_localhost.txt rename to caddytest/integration/caddyfile_adapt/tls_automation_policies_global_email_localhost.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/tls_client_auth_cert_file-legacy-with-verifier.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_client_auth_cert_file-legacy-with-verifier.caddyfiletest new file mode 100644 index 000000000..302d8fd1e --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_client_auth_cert_file-legacy-with-verifier.caddyfiletest @@ -0,0 +1,75 @@ +localhost + +respond "hello from localhost" +tls { + client_auth { + mode request + trusted_ca_cert_file ../caddy.ca.cer + verifier dummy + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "hello from localhost", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + } + ], + "tls_connection_policies": [ + { + "match": { + "sni": [ + "localhost" + ] + }, + "client_authentication": { + "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==" + ] + }, + "verifiers": [ + { + "verifier": "dummy" + } + ], + "mode": "request" + } + }, + {} + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/tls_client_auth_cert_file-legacy.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_client_auth_cert_file-legacy.caddyfiletest new file mode 100644 index 000000000..36fd978ee --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_client_auth_cert_file-legacy.caddyfiletest @@ -0,0 +1,69 @@ +localhost + +respond "hello from localhost" +tls { + client_auth { + mode request + trusted_ca_cert_file ../caddy.ca.cer + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "hello from localhost", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + } + ], + "tls_connection_policies": [ + { + "match": { + "sni": [ + "localhost" + ] + }, + "client_authentication": { + "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==" + ] + }, + "mode": "request" + } + }, + {} + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/tls_client_auth_cert_file.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_client_auth_cert_file.caddyfiletest new file mode 100644 index 000000000..dbf408fa1 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_client_auth_cert_file.caddyfiletest @@ -0,0 +1,71 @@ +localhost + +respond "hello from localhost" +tls { + client_auth { + mode request + trust_pool file { + pem_file ../caddy.ca.cer + } + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "hello from localhost", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + } + ], + "tls_connection_policies": [ + { + "match": { + "sni": [ + "localhost" + ] + }, + "client_authentication": { + "ca": { + "pem_files": [ + "../caddy.ca.cer" + ], + "provider": "file" + }, + "mode": "request" + } + }, + {} + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/tls_client_auth_cert_file.txt b/caddytest/integration/caddyfile_adapt/tls_client_auth_cert_file.txt deleted file mode 100644 index aaa5abffc..000000000 --- a/caddytest/integration/caddyfile_adapt/tls_client_auth_cert_file.txt +++ /dev/null @@ -1,66 +0,0 @@ -localhost - -respond "hello from localhost" -tls { - client_auth { - mode request - trusted_ca_cert_file ../caddy.ca.cer - } -} ----------- -{ - "apps": { - "http": { - "servers": { - "srv0": { - "listen": [ - ":443" - ], - "routes": [ - { - "match": [ - { - "host": [ - "localhost" - ] - } - ], - "handle": [ - { - "handler": "subroute", - "routes": [ - { - "handle": [ - { - "body": "hello from localhost", - "handler": "static_response" - } - ] - } - ] - } - ], - "terminal": true - } - ], - "tls_connection_policies": [ - { - "match": { - "sni": [ - "localhost" - ] - }, - "client_authentication": { - "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==" - ], - "mode": "request" - } - }, - {} - ] - } - } - } - } -} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/tls_client_auth_inline_cert.txt b/caddytest/integration/caddyfile_adapt/tls_client_auth_inline_cert-legacy.caddyfiletest similarity index 61% rename from caddytest/integration/caddyfile_adapt/tls_client_auth_inline_cert.txt rename to caddytest/integration/caddyfile_adapt/tls_client_auth_inline_cert-legacy.caddyfiletest index 4cd45813b..3a91e832b 100644 --- a/caddytest/integration/caddyfile_adapt/tls_client_auth_inline_cert.txt +++ b/caddytest/integration/caddyfile_adapt/tls_client_auth_inline_cert-legacy.caddyfiletest @@ -51,9 +51,12 @@ tls { ] }, "client_authentication": { - "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==" - ], + "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==" + ] + }, "mode": "request" } }, diff --git a/caddytest/integration/caddyfile_adapt/tls_client_auth_inline_cert.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_client_auth_inline_cert.caddyfiletest new file mode 100644 index 000000000..7b8e5a206 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_client_auth_inline_cert.caddyfiletest @@ -0,0 +1,71 @@ +localhost + +respond "hello from localhost" +tls { + client_auth { + mode request + 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": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "hello from localhost", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + } + ], + "tls_connection_policies": [ + { + "match": { + "sni": [ + "localhost" + ] + }, + "client_authentication": { + "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==" + ] + }, + "mode": "request" + } + }, + {} + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/tls_client_auth_inline_cert_with_leaf_trust.caddyfiletest b/caddytest/integration/caddyfile_adapt/tls_client_auth_inline_cert_with_leaf_trust.caddyfiletest new file mode 100644 index 000000000..66c3a3c36 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/tls_client_auth_inline_cert_with_leaf_trust.caddyfiletest @@ -0,0 +1,75 @@ +localhost + +respond "hello from localhost" +tls { + client_auth { + mode request + 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== + } + trusted_leaf_cert_file ../caddy.ca.cer + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "body": "hello from localhost", + "handler": "static_response" + } + ] + } + ] + } + ], + "terminal": true + } + ], + "tls_connection_policies": [ + { + "match": { + "sni": [ + "localhost" + ] + }, + "client_authentication": { + "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==" + ] + }, + "trusted_leaf_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==" + ], + "mode": "request" + } + }, + {} + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/tls_conn_policy_consolidate.txt b/caddytest/integration/caddyfile_adapt/tls_conn_policy_consolidate.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/tls_conn_policy_consolidate.txt rename to caddytest/integration/caddyfile_adapt/tls_conn_policy_consolidate.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/tls_dns_ttl.txt b/caddytest/integration/caddyfile_adapt/tls_dns_ttl.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/tls_dns_ttl.txt rename to caddytest/integration/caddyfile_adapt/tls_dns_ttl.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/tls_explicit_issuer_dns_ttl.txt b/caddytest/integration/caddyfile_adapt/tls_explicit_issuer_dns_ttl.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/tls_explicit_issuer_dns_ttl.txt rename to caddytest/integration/caddyfile_adapt/tls_explicit_issuer_dns_ttl.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/tls_explicit_issuer_propagation_options.txt b/caddytest/integration/caddyfile_adapt/tls_explicit_issuer_propagation_options.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/tls_explicit_issuer_propagation_options.txt rename to caddytest/integration/caddyfile_adapt/tls_explicit_issuer_propagation_options.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/tls_internal_options.txt b/caddytest/integration/caddyfile_adapt/tls_internal_options.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/tls_internal_options.txt rename to caddytest/integration/caddyfile_adapt/tls_internal_options.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/tls_propagation_options.txt b/caddytest/integration/caddyfile_adapt/tls_propagation_options.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/tls_propagation_options.txt rename to caddytest/integration/caddyfile_adapt/tls_propagation_options.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/tracing.txt b/caddytest/integration/caddyfile_adapt/tracing.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/tracing.txt rename to caddytest/integration/caddyfile_adapt/tracing.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/uri_replace_brace_escape.caddyfiletest b/caddytest/integration/caddyfile_adapt/uri_replace_brace_escape.caddyfiletest new file mode 100644 index 000000000..860b8a8df --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/uri_replace_brace_escape.caddyfiletest @@ -0,0 +1,47 @@ +:9080 +uri replace "\}" %7D +uri replace "\{" %7B + +respond "{query}" +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":9080" + ], + "routes": [ + { + "handle": [ + { + "handler": "rewrite", + "uri_substring": [ + { + "find": "\\}", + "replace": "%7D" + } + ] + }, + { + "handler": "rewrite", + "uri_substring": [ + { + "find": "\\{", + "replace": "%7B" + } + ] + }, + { + "body": "{http.request.uri.query}", + "handler": "static_response" + } + ] + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt_test.go b/caddytest/integration/caddyfile_adapt_test.go index 5b052df49..0d9f0fa47 100644 --- a/caddytest/integration/caddyfile_adapt_test.go +++ b/caddytest/integration/caddyfile_adapt_test.go @@ -10,6 +10,8 @@ import ( "testing" "github.com/caddyserver/caddy/v2/caddytest" + + _ "github.com/caddyserver/caddy/v2/internal/testmocks" ) func TestCaddyfileAdaptToJSON(t *testing.T) { diff --git a/caddytest/integration/caddyfile_test.go b/caddytest/integration/caddyfile_test.go index 205bc5b91..5d1fa3f08 100644 --- a/caddytest/integration/caddyfile_test.go +++ b/caddytest/integration/caddyfile_test.go @@ -9,7 +9,6 @@ import ( ) func TestRespond(t *testing.T) { - // arrange tester := caddytest.NewTester(t) tester.InitServer(` @@ -32,7 +31,6 @@ func TestRespond(t *testing.T) { } func TestRedirect(t *testing.T) { - // arrange tester := caddytest.NewTester(t) tester.InitServer(` @@ -61,7 +59,6 @@ func TestRedirect(t *testing.T) { } func TestDuplicateHosts(t *testing.T) { - // act and assert caddytest.AssertLoadError(t, ` @@ -76,7 +73,6 @@ func TestDuplicateHosts(t *testing.T) { } func TestReadCookie(t *testing.T) { - localhost, _ := url.Parse("http://localhost") cookie := http.Cookie{ Name: "clientname", @@ -110,7 +106,6 @@ func TestReadCookie(t *testing.T) { } func TestReplIndex(t *testing.T) { - tester := caddytest.NewTester(t) tester.InitServer(` { @@ -484,3 +479,227 @@ func TestValidPrefix(t *testing.T) { caddytest.AssertAdapt(t, successCase.rawConfig, "caddyfile", successCase.expectedResponse) } } + +func TestUriReplace(t *testing.T) { + tester := caddytest.NewTester(t) + + tester.InitServer(` + { + admin localhost:2999 + http_port 9080 + } + :9080 + uri replace "\}" %7D + uri replace "\{" %7B + + respond "{query}"`, "caddyfile") + + tester.AssertGetResponse("http://localhost:9080/endpoint?test={%20content%20}", 200, "test=%7B%20content%20%7D") +} + +func TestUriOps(t *testing.T) { + tester := caddytest.NewTester(t) + + tester.InitServer(` + { + admin localhost:2999 + http_port 9080 + } + :9080 + uri query +foo bar + uri query -baz + uri query taz test + uri query key=value example + uri query changethis>changed + + respond "{query}"`, "caddyfile") + + 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") +} + +func TestSetThenAddQueryParams(t *testing.T) { + tester := caddytest.NewTester(t) + + tester.InitServer(` + { + admin localhost:2999 + http_port 9080 + } + :9080 + uri query foo bar + uri query +foo baz + + respond "{query}"`, "caddyfile") + + tester.AssertGetResponse("http://localhost:9080/endpoint", 200, "foo=bar&foo=baz") +} + +func TestSetThenDeleteParams(t *testing.T) { + tester := caddytest.NewTester(t) + + tester.InitServer(` + { + admin localhost:2999 + http_port 9080 + } + :9080 + uri query bar foo{query.foo} + uri query -foo + + respond "{query}"`, "caddyfile") + + tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "bar=foobar") +} + +func TestRenameAndOtherOps(t *testing.T) { + tester := caddytest.NewTester(t) + + tester.InitServer(` + { + admin localhost:2999 + http_port 9080 + } + :9080 + uri query foo>bar + uri query bar taz + uri query +bar baz + + respond "{query}"`, "caddyfile") + + tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "bar=taz&bar=baz") +} + +func TestUriOpsBlock(t *testing.T) { + tester := caddytest.NewTester(t) + + tester.InitServer(` + { + admin localhost:2999 + http_port 9080 + } + :9080 + uri query { + +foo bar + -baz + taz test + } + respond "{query}"`, "caddyfile") + + tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar0&baz=buz&taz=nottest", 200, "foo=bar0&foo=bar&taz=test") +} + +func TestHandleErrorSimpleCodes(t *testing.T) { + tester := caddytest.NewTester(t) + tester.InitServer(`{ + admin localhost:2999 + http_port 9080 + } + localhost:9080 { + root * /srv + error /private* "Unauthorized" 410 + error /hidden* "Not found" 404 + + handle_errors 404 410 { + respond "404 or 410 error" + } + }`, "caddyfile") + // act and assert + tester.AssertGetResponse("http://localhost:9080/private", 410, "404 or 410 error") + tester.AssertGetResponse("http://localhost:9080/hidden", 404, "404 or 410 error") +} + +func TestHandleErrorRange(t *testing.T) { + tester := caddytest.NewTester(t) + tester.InitServer(`{ + admin localhost:2999 + http_port 9080 + } + localhost:9080 { + root * /srv + error /private* "Unauthorized" 410 + error /hidden* "Not found" 404 + + handle_errors 4xx { + respond "Error in the [400 .. 499] range" + } + }`, "caddyfile") + // act and assert + tester.AssertGetResponse("http://localhost:9080/private", 410, "Error in the [400 .. 499] range") + tester.AssertGetResponse("http://localhost:9080/hidden", 404, "Error in the [400 .. 499] range") +} + +func TestHandleErrorSort(t *testing.T) { + tester := caddytest.NewTester(t) + tester.InitServer(`{ + admin localhost:2999 + http_port 9080 + } + localhost:9080 { + root * /srv + error /private* "Unauthorized" 410 + error /hidden* "Not found" 404 + error /internalerr* "Internal Server Error" 500 + + handle_errors { + respond "Fallback route: code outside the [400..499] range" + } + handle_errors 4xx { + respond "Error in the [400 .. 499] range" + } + }`, "caddyfile") + // act and assert + tester.AssertGetResponse("http://localhost:9080/internalerr", 500, "Fallback route: code outside the [400..499] range") + tester.AssertGetResponse("http://localhost:9080/hidden", 404, "Error in the [400 .. 499] range") +} + +func TestHandleErrorRangeAndCodes(t *testing.T) { + tester := caddytest.NewTester(t) + tester.InitServer(`{ + admin localhost:2999 + http_port 9080 + } + localhost:9080 { + root * /srv + error /private* "Unauthorized" 410 + error /threehundred* "Moved Permanently" 301 + error /internalerr* "Internal Server Error" 500 + + handle_errors 500 3xx { + respond "Error code is equal to 500 or in the [300..399] range" + } + handle_errors 4xx { + respond "Error in the [400 .. 499] range" + } + }`, "caddyfile") + // act and assert + tester.AssertGetResponse("http://localhost:9080/internalerr", 500, "Error code is equal to 500 or in the [300..399] range") + tester.AssertGetResponse("http://localhost:9080/threehundred", 301, "Error code is equal to 500 or in the [300..399] range") + tester.AssertGetResponse("http://localhost:9080/private", 410, "Error in the [400 .. 499] range") +} + +func TestInvalidSiteAddressesAsDirectives(t *testing.T) { + type testCase struct { + config, expectedError string + } + + failureCases := []testCase{ + { + config: ` + handle { + file_server + }`, + expectedError: `Caddyfile:2: parsed 'handle' as a site address, but it is a known directive; directives must appear in a site block`, + }, + { + config: ` + reverse_proxy localhost:9000 localhost:9001 { + file_server + }`, + expectedError: `Caddyfile:2: parsed 'reverse_proxy' as a site address, but it is a known directive; directives must appear in a site block`, + }, + } + + for _, failureCase := range failureCases { + caddytest.AssertLoadError(t, failureCase.config, "caddyfile", failureCase.expectedError) + } +} diff --git a/caddytest/integration/leafcertloaders_test.go b/caddytest/integration/leafcertloaders_test.go new file mode 100644 index 000000000..4399902ea --- /dev/null +++ b/caddytest/integration/leafcertloaders_test.go @@ -0,0 +1,70 @@ +package integration + +import ( + "testing" + + "github.com/caddyserver/caddy/v2/caddytest" +) + +func TestLeafCertLoaders(t *testing.T) { + tester := caddytest.NewTester(t) + tester.InitServer(` + { + "admin": { + "listen": "localhost:2999" + }, + "apps": { + "http": { + "http_port": 9080, + "https_port": 9443, + "grace_period": 1, + "servers": { + "srv0": { + "listen": [ + ":9443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "localhost" + ] + } + ], + "terminal": true + } + ], + "tls_connection_policies": [ + { + "client_authentication": { + "verifiers": [ + { + "verifier": "leaf", + "leaf_certs_loaders": [ + { + "loader": "file", + "files": ["../leafcert.pem"] + }, + { + "loader": "folder", + "folders": ["../"] + }, + { + "loader": "storage" + }, + { + "loader": "pem" + } + ] + } + ] + } + } + ] + } + } + } + } + }`, "json") +} diff --git a/caddytest/integration/listener_test.go b/caddytest/integration/listener_test.go new file mode 100644 index 000000000..30642b1ae --- /dev/null +++ b/caddytest/integration/listener_test.go @@ -0,0 +1,94 @@ +package integration + +import ( + "bytes" + "fmt" + "math/rand" + "net" + "net/http" + "strings" + "testing" + + "github.com/caddyserver/caddy/v2/caddytest" +) + +func setupListenerWrapperTest(t *testing.T, handlerFunc http.HandlerFunc) *caddytest.Tester { + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to listen: %s", err) + } + + mux := http.NewServeMux() + mux.Handle("/", handlerFunc) + srv := &http.Server{ + Handler: mux, + } + go srv.Serve(l) + t.Cleanup(func() { + _ = srv.Close() + _ = l.Close() + }) + tester := caddytest.NewTester(t) + tester.InitServer(fmt.Sprintf(` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + local_certs + servers :9443 { + listener_wrappers { + http_redirect + tls + } + } + } + localhost { + reverse_proxy %s + } + `, l.Addr().String()), "caddyfile") + return tester +} + +func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) { + const uploadSize = (1024 * 1024) + 1 // 1 MB + 1 byte + // 1 more than an MB + body := make([]byte, uploadSize) + rand.New(rand.NewSource(0)).Read(body) + + tester := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) { + buf := new(bytes.Buffer) + _, err := buf.ReadFrom(request.Body) + if err != nil { + t.Fatalf("failed to read body: %s", err) + } + + if !bytes.Equal(buf.Bytes(), body) { + t.Fatalf("body not the same") + } + + writer.WriteHeader(http.StatusNoContent) + }) + resp, err := tester.Client.Post("https://localhost:9443", "application/octet-stream", bytes.NewReader(body)) + if err != nil { + t.Fatalf("failed to post: %s", err) + } + + if resp.StatusCode != http.StatusNoContent { + t.Fatalf("unexpected status: %d != %d", resp.StatusCode, http.StatusNoContent) + } +} + +func TestLargeHttpRequest(t *testing.T) { + tester := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) { + t.Fatal("not supposed to handle a request") + }) + + // We never read the body in any way, set an extra long header instead. + req, _ := http.NewRequest("POST", "http://localhost:9443", nil) + req.Header.Set("Long-Header", strings.Repeat("X", 1024*1024)) + _, err := tester.Client.Do(req) + if err == nil { + t.Fatal("not supposed to succeed") + } +} diff --git a/caddytest/integration/pki_test.go b/caddytest/integration/pki_test.go index 5e9928c0c..846798209 100644 --- a/caddytest/integration/pki_test.go +++ b/caddytest/integration/pki_test.go @@ -9,6 +9,9 @@ import ( func TestLeafCertLifetimeLessThanIntermediate(t *testing.T) { caddytest.AssertLoadError(t, ` { + "admin": { + "disabled": true + }, "apps": { "http": { "servers": { @@ -56,6 +59,9 @@ func TestLeafCertLifetimeLessThanIntermediate(t *testing.T) { func TestIntermediateLifetimeLessThanRoot(t *testing.T) { caddytest.AssertLoadError(t, ` { + "admin": { + "disabled": true + }, "apps": { "http": { "servers": { diff --git a/caddytest/integration/reverseproxy_test.go b/caddytest/integration/reverseproxy_test.go index 4f4261b87..0beb71afc 100644 --- a/caddytest/integration/reverseproxy_test.go +++ b/caddytest/integration/reverseproxy_test.go @@ -57,7 +57,6 @@ func TestSRVReverseProxy(t *testing.T) { } func TestDialWithPlaceholderUnix(t *testing.T) { - if runtime.GOOS == "windows" { t.SkipNow() } diff --git a/caddytest/integration/sni_test.go b/caddytest/integration/sni_test.go index 24dddc59a..188f93541 100644 --- a/caddytest/integration/sni_test.go +++ b/caddytest/integration/sni_test.go @@ -7,7 +7,6 @@ import ( ) func TestDefaultSNI(t *testing.T) { - // arrange tester := caddytest.NewTester(t) tester.InitServer(`{ @@ -107,7 +106,6 @@ func TestDefaultSNI(t *testing.T) { } func TestDefaultSNIWithNamedHostAndExplicitIP(t *testing.T) { - // arrange tester := caddytest.NewTester(t) tester.InitServer(` diff --git a/caddytest/integration/stream_test.go b/caddytest/integration/stream_test.go index 6bc612d36..ef3ea498d 100644 --- a/caddytest/integration/stream_test.go +++ b/caddytest/integration/stream_test.go @@ -360,7 +360,6 @@ func TestH2ToH1ChunkedResponse(t *testing.T) { func testH2ToH1ChunkedResponseServeH1(t *testing.T) *http.Server { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Host != "127.0.0.1:9443" { t.Errorf("r.Host doesn't match, %v!", r.Host) w.WriteHeader(http.StatusNotFound) diff --git a/caddytest/leafcert.pem b/caddytest/leafcert.pem new file mode 100644 index 000000000..03febfd3a --- /dev/null +++ b/caddytest/leafcert.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICUTCCAfugAwIBAgIBADANBgkqhkiG9w0BAQQFADBXMQswCQYDVQQGEwJDTjEL +MAkGA1UECBMCUE4xCzAJBgNVBAcTAkNOMQswCQYDVQQKEwJPTjELMAkGA1UECxMC +VU4xFDASBgNVBAMTC0hlcm9uZyBZYW5nMB4XDTA1MDcxNTIxMTk0N1oXDTA1MDgx +NDIxMTk0N1owVzELMAkGA1UEBhMCQ04xCzAJBgNVBAgTAlBOMQswCQYDVQQHEwJD +TjELMAkGA1UEChMCT04xCzAJBgNVBAsTAlVOMRQwEgYDVQQDEwtIZXJvbmcgWWFu +ZzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQCp5hnG7ogBhtlynpOS21cBewKE/B7j +V14qeyslnr26xZUsSVko36ZnhiaO/zbMOoRcKK9vEcgMtcLFuQTWDl3RAgMBAAGj +gbEwga4wHQYDVR0OBBYEFFXI70krXeQDxZgbaCQoR4jUDncEMH8GA1UdIwR4MHaA +FFXI70krXeQDxZgbaCQoR4jUDncEoVukWTBXMQswCQYDVQQGEwJDTjELMAkGA1UE +CBMCUE4xCzAJBgNVBAcTAkNOMQswCQYDVQQKEwJPTjELMAkGA1UECxMCVU4xFDAS +BgNVBAMTC0hlcm9uZyBZYW5nggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEE +BQADQQA/ugzBrjjK9jcWnDVfGHlk3icNRq0oV7Ri32z/+HQX67aRfgZu7KWdI+Ju +Wm7DCfrPNGVwFWUQOmsPue9rZBgO +-----END CERTIFICATE----- diff --git a/cmd/caddy/setcap.sh b/cmd/caddy/setcap.sh index d777e5a62..ac5335c56 100755 --- a/cmd/caddy/setcap.sh +++ b/cmd/caddy/setcap.sh @@ -1,6 +1,14 @@ #!/bin/sh -# USAGE: go run -exec ./setcap.sh +# 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. +# +# sudo setcap cap_net_bind_service=+ep "$1" "$@" diff --git a/cmd/cobra.go b/cmd/cobra.go index 4339cdbfb..1a2509206 100644 --- a/cmd/cobra.go +++ b/cmd/cobra.go @@ -1,7 +1,11 @@ package caddycmd import ( + "fmt" + "github.com/spf13/cobra" + + "github.com/caddyserver/caddy/v2" ) var rootCmd = &cobra.Command{ @@ -95,18 +99,25 @@ https://caddyserver.com/docs/running // 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") } +func onlyVersionText() string { + _, f := caddy.Version() + return f +} + func caddyCmdToCobra(caddyCmd Command) *cobra.Command { cmd := &cobra.Command{ - Use: caddyCmd.Name, + Use: caddyCmd.Name + " " + caddyCmd.Usage, Short: caddyCmd.Short, Long: caddyCmd.Long, } @@ -123,7 +134,24 @@ func caddyCmdToCobra(caddyCmd Command) *cobra.Command { // in a cobra command's RunE field. func WrapCommandFuncForCobra(f CommandFunc) func(cmd *cobra.Command, _ []string) error { return func(cmd *cobra.Command, _ []string) error { - _, err := f(Flags{cmd.Flags()}) + status, err := f(Flags{cmd.Flags()}) + if status > 1 { + cmd.SilenceErrors = true + return &exitError{ExitCode: status, Err: err} + } return err } } + +// exitError carries the exit code from CommandFunc to Main() +type exitError struct { + ExitCode int + Err error +} + +func (e *exitError) Error() string { + if e.Err == nil { + return fmt.Sprintf("exiting with status %d", e.ExitCode) + } + return e.Err.Error() +} diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index f284b89b9..746cf3da6 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -42,11 +42,18 @@ import ( ) func cmdStart(fl Flags) (int, error) { - startCmdConfigFlag := fl.String("config") - startCmdConfigAdapterFlag := fl.String("adapter") - startCmdPidfileFlag := fl.String("pidfile") - startCmdWatchFlag := fl.Bool("watch") - startCmdEnvfileFlag := fl.String("envfile") + configFlag := fl.String("config") + configAdapterFlag := fl.String("adapter") + pidfileFlag := fl.String("pidfile") + watchFlag := fl.Bool("watch") + + var err error + var envfileFlag []string + envfileFlag, err = fl.GetStringSlice("envfile") + if err != nil { + return caddy.ExitCodeFailedStartup, + fmt.Errorf("reading envfile flag: %v", err) + } // open a listener to which the child process will connect when // it is ready to confirm that it has successfully started @@ -67,22 +74,23 @@ 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()) - if startCmdConfigFlag != "" { - cmd.Args = append(cmd.Args, "--config", startCmdConfigFlag) + if configFlag != "" { + cmd.Args = append(cmd.Args, "--config", configFlag) } - if startCmdEnvfileFlag != "" { - cmd.Args = append(cmd.Args, "--envfile", startCmdEnvfileFlag) + + for _, envfile := range envfileFlag { + cmd.Args = append(cmd.Args, "--envfile", envfile) } - if startCmdConfigAdapterFlag != "" { - cmd.Args = append(cmd.Args, "--adapter", startCmdConfigAdapterFlag) + if configAdapterFlag != "" { + cmd.Args = append(cmd.Args, "--adapter", configAdapterFlag) } - if startCmdWatchFlag { + if watchFlag { cmd.Args = append(cmd.Args, "--watch") } - if startCmdPidfileFlag != "" { - cmd.Args = append(cmd.Args, "--pidfile", startCmdPidfileFlag) + if pidfileFlag != "" { + cmd.Args = append(cmd.Args, "--pidfile", pidfileFlag) } - stdinpipe, err := cmd.StdinPipe() + stdinPipe, err := cmd.StdinPipe() if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("creating stdin pipe: %v", err) @@ -94,7 +102,8 @@ func cmdStart(fl Flags) (int, error) { expect := make([]byte, 32) _, err = rand.Read(expect) if err != nil { - return caddy.ExitCodeFailedStartup, fmt.Errorf("generating random confirmation bytes: %v", err) + return caddy.ExitCodeFailedStartup, + fmt.Errorf("generating random confirmation bytes: %v", err) } // begin writing the confirmation bytes to the child's @@ -102,14 +111,15 @@ func cmdStart(fl Flags) (int, error) { // started yet, and writing synchronously would result // in a deadlock go func() { - _, _ = stdinpipe.Write(expect) - stdinpipe.Close() + _, _ = stdinPipe.Write(expect) + stdinPipe.Close() }() // start the process err = cmd.Start() if err != nil { - return caddy.ExitCodeFailedStartup, fmt.Errorf("starting caddy process: %v", err) + return caddy.ExitCodeFailedStartup, + fmt.Errorf("starting caddy process: %v", err) } // there are two ways we know we're done: either @@ -157,41 +167,37 @@ func cmdStart(fl Flags) (int, error) { func cmdRun(fl Flags) (int, error) { caddy.TrapSignals() - runCmdConfigFlag := fl.String("config") - runCmdConfigAdapterFlag := fl.String("adapter") - runCmdResumeFlag := fl.Bool("resume") - runCmdLoadEnvfileFlag := fl.String("envfile") - runCmdPrintEnvFlag := fl.Bool("environ") - runCmdWatchFlag := fl.Bool("watch") - runCmdPidfileFlag := fl.String("pidfile") - runCmdPingbackFlag := fl.String("pingback") + configFlag := fl.String("config") + configAdapterFlag := fl.String("adapter") + resumeFlag := fl.Bool("resume") + printEnvFlag := fl.Bool("environ") + watchFlag := fl.Bool("watch") + pidfileFlag := fl.String("pidfile") + pingbackFlag := fl.String("pingback") // load all additional envs as soon as possible - if runCmdLoadEnvfileFlag != "" { - if err := loadEnvFromFile(runCmdLoadEnvfileFlag); err != nil { - return caddy.ExitCodeFailedStartup, - fmt.Errorf("loading additional environment variables: %v", err) - } + err := handleEnvFileFlag(fl) + if err != nil { + return caddy.ExitCodeFailedStartup, err } // if we are supposed to print the environment, do that first - if runCmdPrintEnvFlag { + if printEnvFlag { printEnvironment() } // load the config, depending on flags var config []byte - var err error - if runCmdResumeFlag { + if resumeFlag { config, err = os.ReadFile(caddy.ConfigAutosavePath) - if os.IsNotExist(err) { + 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)) - runCmdResumeFlag = false + resumeFlag = false } else if err != nil { return caddy.ExitCodeFailedStartup, err } else { - if runCmdConfigFlag == "" { + if configFlag == "" { caddy.Log().Info("resuming from last configuration", zap.String("autosave_file", caddy.ConfigAutosavePath)) } else { @@ -204,19 +210,19 @@ func cmdRun(fl Flags) (int, error) { } // we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive var configFile string - if !runCmdResumeFlag { - config, configFile, err = LoadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag) + if !resumeFlag { + config, configFile, err = LoadConfig(configFlag, configAdapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, err } } // create pidfile now, in case loading config takes a while (issue #5477) - if runCmdPidfileFlag != "" { - err := caddy.PIDFile(runCmdPidfileFlag) + if pidfileFlag != "" { + err := caddy.PIDFile(pidfileFlag) if err != nil { caddy.Log().Error("unable to write PID file", - zap.String("pidfile", runCmdPidfileFlag), + zap.String("pidfile", pidfileFlag), zap.Error(err)) } } @@ -230,13 +236,13 @@ func cmdRun(fl Flags) (int, error) { // if we are to report to another process the successful start // of the server, do so now by echoing back contents of stdin - if runCmdPingbackFlag != "" { + if pingbackFlag != "" { confirmationBytes, err := io.ReadAll(os.Stdin) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("reading confirmation bytes from stdin: %v", err) } - conn, err := net.Dial("tcp", runCmdPingbackFlag) + conn, err := net.Dial("tcp", pingbackFlag) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("dialing confirmation address: %v", err) @@ -245,14 +251,14 @@ func cmdRun(fl Flags) (int, error) { _, err = conn.Write(confirmationBytes) if err != nil { return caddy.ExitCodeFailedStartup, - fmt.Errorf("writing confirmation bytes to %s: %v", runCmdPingbackFlag, err) + fmt.Errorf("writing confirmation bytes to %s: %v", pingbackFlag, err) } } // if enabled, reload config file automatically on changes // (this better only be used in dev!) - if runCmdWatchFlag { - go watchConfigFile(configFile, runCmdConfigAdapterFlag) + if watchFlag { + go watchConfigFile(configFile, configAdapterFlag) } // warn if the environment does not provide enough information about the disk @@ -278,11 +284,11 @@ func cmdRun(fl Flags) (int, error) { } func cmdStop(fl Flags) (int, error) { - addrFlag := fl.String("address") + addressFlag := fl.String("address") configFlag := fl.String("config") configAdapterFlag := fl.String("adapter") - adminAddr, err := DetermineAdminAPIAddress(addrFlag, nil, configFlag, configAdapterFlag) + adminAddr, err := DetermineAdminAPIAddress(addressFlag, nil, configFlag, configAdapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err) } @@ -300,7 +306,7 @@ func cmdStop(fl Flags) (int, error) { func cmdReload(fl Flags) (int, error) { configFlag := fl.String("config") configAdapterFlag := fl.String("adapter") - addrFlag := fl.String("address") + addressFlag := fl.String("address") forceFlag := fl.Bool("force") // get the config in caddy's native format @@ -312,7 +318,7 @@ func cmdReload(fl Flags) (int, error) { return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load") } - adminAddr, err := DetermineAdminAPIAddress(addrFlag, config, configFlag, configAdapterFlag) + adminAddr, err := DetermineAdminAPIAddress(addressFlag, config, configFlag, configAdapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err) } @@ -414,48 +420,60 @@ func cmdListModules(fl Flags) (int, error) { return caddy.ExitCodeSuccess, nil } -func cmdEnviron(_ Flags) (int, error) { +func cmdEnviron(fl Flags) (int, error) { + // load all additional envs as soon as possible + err := handleEnvFileFlag(fl) + if err != nil { + return caddy.ExitCodeFailedStartup, err + } + printEnvironment() return caddy.ExitCodeSuccess, nil } func cmdAdaptConfig(fl Flags) (int, error) { - adaptCmdInputFlag := fl.String("config") - adaptCmdAdapterFlag := fl.String("adapter") - adaptCmdPrettyFlag := fl.Bool("pretty") - adaptCmdValidateFlag := fl.Bool("validate") + inputFlag := fl.String("config") + adapterFlag := fl.String("adapter") + prettyFlag := fl.Bool("pretty") + validateFlag := fl.Bool("validate") var err error - adaptCmdInputFlag, err = configFileWithRespectToDefault(caddy.Log(), adaptCmdInputFlag) + inputFlag, err = configFileWithRespectToDefault(caddy.Log(), inputFlag) if err != nil { return caddy.ExitCodeFailedStartup, err } - if adaptCmdAdapterFlag == "" { + // load all additional envs as soon as possible + err = handleEnvFileFlag(fl) + if err != nil { + return caddy.ExitCodeFailedStartup, err + } + + if adapterFlag == "" { return caddy.ExitCodeFailedStartup, fmt.Errorf("adapter name is required (use --adapt flag or leave unspecified for default)") } - cfgAdapter := caddyconfig.GetAdapter(adaptCmdAdapterFlag) + cfgAdapter := caddyconfig.GetAdapter(adapterFlag) if cfgAdapter == nil { return caddy.ExitCodeFailedStartup, - fmt.Errorf("unrecognized config adapter: %s", adaptCmdAdapterFlag) + fmt.Errorf("unrecognized config adapter: %s", adapterFlag) } - input, err := os.ReadFile(adaptCmdInputFlag) + input, err := os.ReadFile(inputFlag) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("reading input file: %v", err) } - opts := map[string]any{"filename": adaptCmdInputFlag} + opts := map[string]any{"filename": inputFlag} adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts) if err != nil { return caddy.ExitCodeFailedStartup, err } - if adaptCmdPrettyFlag { + if prettyFlag { var prettyBuf bytes.Buffer err = json.Indent(&prettyBuf, adaptedConfig, "", "\t") if err != nil { @@ -473,13 +491,13 @@ func cmdAdaptConfig(fl Flags) (int, error) { if warn.Directive != "" { msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message) } - caddy.Log().Named(adaptCmdAdapterFlag).Warn(msg, + caddy.Log().Named(adapterFlag).Warn(msg, zap.String("file", warn.File), zap.Int("line", warn.Line)) } // validate output if requested - if adaptCmdValidateFlag { + if validateFlag { var cfg *caddy.Config err = caddy.StrictUnmarshalJSON(adaptedConfig, &cfg) if err != nil { @@ -495,30 +513,26 @@ func cmdAdaptConfig(fl Flags) (int, error) { } func cmdValidateConfig(fl Flags) (int, error) { - validateCmdConfigFlag := fl.String("config") - validateCmdAdapterFlag := fl.String("adapter") - runCmdLoadEnvfileFlag := fl.String("envfile") + configFlag := fl.String("config") + adapterFlag := fl.String("adapter") // load all additional envs as soon as possible - if runCmdLoadEnvfileFlag != "" { - if err := loadEnvFromFile(runCmdLoadEnvfileFlag); err != nil { - return caddy.ExitCodeFailedStartup, - fmt.Errorf("loading additional environment variables: %v", err) - } - } - - // use default config and ensure a config file is specified - var err error - validateCmdConfigFlag, err = configFileWithRespectToDefault(caddy.Log(), validateCmdConfigFlag) + err := handleEnvFileFlag(fl) if err != nil { return caddy.ExitCodeFailedStartup, err } - if validateCmdConfigFlag == "" { + + // use default config and ensure a config file is specified + configFlag, err = configFileWithRespectToDefault(caddy.Log(), configFlag) + if err != nil { + return caddy.ExitCodeFailedStartup, err + } + if configFlag == "" { return caddy.ExitCodeFailedStartup, fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)") } - input, _, err := LoadConfig(validateCmdConfigFlag, validateCmdAdapterFlag) + input, _, err := LoadConfig(configFlag, adapterFlag) if err != nil { return caddy.ExitCodeFailedStartup, err } @@ -541,13 +555,13 @@ func cmdValidateConfig(fl Flags) (int, error) { } func cmdFmt(fl Flags) (int, error) { - formatCmdConfigFile := fl.Arg(0) - if formatCmdConfigFile == "" { - formatCmdConfigFile = "Caddyfile" + configFile := fl.Arg(0) + if configFile == "" { + configFile = "Caddyfile" } // as a special case, read from stdin if the file name is "-" - if formatCmdConfigFile == "-" { + if configFile == "-" { input, err := io.ReadAll(os.Stdin) if err != nil { return caddy.ExitCodeFailedStartup, @@ -557,7 +571,7 @@ func cmdFmt(fl Flags) (int, error) { return caddy.ExitCodeSuccess, nil } - input, err := os.ReadFile(formatCmdConfigFile) + input, err := os.ReadFile(configFile) if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("reading input file: %v", err) @@ -566,7 +580,7 @@ func cmdFmt(fl Flags) (int, error) { output := caddyfile.Format(input) if fl.Bool("overwrite") { - if err := os.WriteFile(formatCmdConfigFile, output, 0o600); err != nil { + if err := os.WriteFile(configFile, output, 0o600); err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err) } return caddy.ExitCodeSuccess, nil @@ -590,7 +604,7 @@ func cmdFmt(fl Flags) (int, error) { fmt.Print(string(output)) } - if warning, diff := caddyfile.FormattingDifference(formatCmdConfigFile, input); diff { + if warning, diff := caddyfile.FormattingDifference(configFile, input); diff { return caddy.ExitCodeFailedStartup, fmt.Errorf(`%s:%d: Caddyfile input is not formatted; Tip: use '--overwrite' to update your Caddyfile in-place instead of previewing it. Consult '--help' for more options`, warning.File, warning.Line, @@ -600,6 +614,25 @@ func cmdFmt(fl Flags) (int, error) { return caddy.ExitCodeSuccess, nil } +// handleEnvFileFlag loads the environment variables from the given --envfile +// flag if specified. This should be called as early in the command function. +func handleEnvFileFlag(fl Flags) error { + var err error + var envfileFlag []string + envfileFlag, err = fl.GetStringSlice("envfile") + if err != nil { + return fmt.Errorf("reading envfile flag: %v", err) + } + + for _, envfile := range envfileFlag { + if err := loadEnvFromFile(envfile); err != nil { + return fmt.Errorf("loading additional environment variables: %v", err) + } + } + + return nil +} + // AdminAPIRequest makes an API request according to the CLI flags given, // with the given HTTP method and request URI. If body is non-nil, it will // be assumed to be Content-Type application/json. The caller should close diff --git a/cmd/commands.go b/cmd/commands.go index 0885577ea..e5e1265e4 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -94,8 +94,8 @@ func init() { Starts the Caddy process, optionally bootstrapped with an initial config file. This command unblocks after the server starts running or fails to run. -If --envfile is specified, an environment file with environment variables in -the KEY=VALUE format will be loaded into the Caddy process. +If --envfile is specified, an environment file with environment variables +in the KEY=VALUE format will be loaded into the Caddy process. On Windows, the spawned child process will remain attached to the terminal, so closing the window will forcefully stop Caddy; to avoid forgetting this, try @@ -104,7 +104,7 @@ using 'caddy run' instead to keep it in the foreground. CobraFunc: func(cmd *cobra.Command) { cmd.Flags().StringP("config", "c", "", "Configuration file") cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply") - cmd.Flags().StringP("envfile", "", "", "Environment file to load") + cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load") cmd.Flags().BoolP("watch", "w", false, "Reload changed config file automatically") cmd.Flags().StringP("pidfile", "", "", "Path of file to which to write process ID") cmd.RunE = WrapCommandFuncForCobra(cmdStart) @@ -133,8 +133,8 @@ As a special case, if the current working directory has a file called that file will be loaded and used to configure Caddy, even without any command line flags. -If --envfile is specified, an environment file with environment variables in -the KEY=VALUE format will be loaded into the Caddy process. +If --envfile is specified, an environment file with environment variables +in the KEY=VALUE format will be loaded into the Caddy process. If --environ is specified, the environment as seen by the Caddy process will be printed before starting. This is the same as the environ command but does @@ -150,7 +150,7 @@ option in a local development environment. CobraFunc: func(cmd *cobra.Command) { cmd.Flags().StringP("config", "c", "", "Configuration file") cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply") - cmd.Flags().StringP("envfile", "", "", "Environment file to load") + cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load") cmd.Flags().BoolP("environ", "e", false, "Print environment") cmd.Flags().BoolP("resume", "r", false, "Use saved config, if any (and prefer over --config file)") cmd.Flags().BoolP("watch", "w", false, "Watch config file for changes and reload it automatically") @@ -240,6 +240,7 @@ documentation: https://go.dev/doc/modules/version-numbers RegisterCommand(Command{ Name: "environ", + Usage: "[--envfile ]", Short: "Prints the environment", Long: ` Prints the environment as seen by this Caddy process. @@ -249,6 +250,9 @@ configuration uses environment variables (e.g. "{env.VARIABLE}") then this command can be useful for verifying that the variables will have the values you expect in your config. +If --envfile is specified, an environment file with environment variables +in the KEY=VALUE format will be loaded into the Caddy process. + Note that environments may be different depending on how you run Caddy. Environments for Caddy instances started by service managers such as systemd are often different than the environment inherited from your @@ -259,12 +263,15 @@ by adding the "--environ" flag. Environments may contain sensitive data. `, - Func: cmdEnviron, + CobraFunc: func(cmd *cobra.Command) { + cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load") + cmd.RunE = WrapCommandFuncForCobra(cmdEnviron) + }, }) RegisterCommand(Command{ Name: "adapt", - Usage: "--config [--adapter ] [--pretty] [--validate]", + Usage: "--config [--adapter ] [--pretty] [--validate] [--envfile ]", Short: "Adapts a configuration to Caddy's native JSON", Long: ` Adapts a configuration to Caddy's native JSON format and writes the @@ -276,12 +283,16 @@ for human readability. If --validate is used, the adapted config will be checked for validity. If the config is invalid, an error will be printed to stderr and a non- zero exit status will be returned. + +If --envfile is specified, an environment file with environment variables +in the KEY=VALUE format will be loaded into the Caddy process. `, CobraFunc: func(cmd *cobra.Command) { cmd.Flags().StringP("config", "c", "", "Configuration file to adapt (required)") cmd.Flags().StringP("adapter", "a", "caddyfile", "Name of config adapter") cmd.Flags().BoolP("pretty", "p", false, "Format the output for human readability") cmd.Flags().BoolP("validate", "", false, "Validate the output") + cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load") cmd.RunE = WrapCommandFuncForCobra(cmdAdaptConfig) }, }) @@ -295,13 +306,13 @@ Loads and provisions the provided config, but does not start running it. This reveals any errors with the configuration through the loading and provisioning stages. -If --envfile is specified, an environment file with environment variables in -the KEY=VALUE format will be loaded into the Caddy process. +If --envfile is specified, an environment file with environment variables +in the KEY=VALUE format will be loaded into the Caddy process. `, CobraFunc: func(cmd *cobra.Command) { cmd.Flags().StringP("config", "c", "", "Input configuration file") cmd.Flags().StringP("adapter", "a", "", "Name of config adapter") - cmd.Flags().StringP("envfile", "", "", "Environment file to load") + cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load") cmd.RunE = WrapCommandFuncForCobra(cmdValidateConfig) }, }) @@ -402,7 +413,7 @@ latest versions. EXPERIMENTAL: May be changed or removed. 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 +added. Retains existing packages. Returns an error if the any of packages are already included. EXPERIMENTAL: May be changed or removed. `, CobraFunc: func(cmd *cobra.Command) { @@ -417,8 +428,8 @@ already included. EXPERIMENTAL: May be changed or removed. Usage: "", Short: "Removes Caddy packages (EXPERIMENTAL)", Long: ` -Downloads an updated Caddy binaries without the specified packages (module/plugin). -Returns an error if any of the packages are not included. +Downloads an updated Caddy binaries without the specified packages (module/plugin). +Returns an error if any of the packages are not included. EXPERIMENTAL: May be changed or removed. `, CobraFunc: func(cmd *cobra.Command) { @@ -464,40 +475,40 @@ argument of --directory. If the directory does not exist, it will be created. Use: "completion [bash|zsh|fish|powershell]", Short: "Generate completion script", Long: fmt.Sprintf(`To load completions: - + Bash: - + $ source <(%[1]s completion bash) - + # To load completions for each session, execute once: # Linux: $ %[1]s completion bash > /etc/bash_completion.d/%[1]s # macOS: $ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s - + Zsh: - + # If shell completion is not already enabled in your environment, # you will need to enable it. You can execute the following once: - + $ echo "autoload -U compinit; compinit" >> ~/.zshrc - + # To load completions for each session, execute once: $ %[1]s completion zsh > "${fpath[1]}/_%[1]s" - + # You will need to start a new shell for this setup to take effect. - + fish: - + $ %[1]s completion fish | source - + # To load completions for each session, execute once: $ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish - + PowerShell: - + PS> %[1]s completion powershell | Out-String | Invoke-Expression - + # To load completions for every new session, run: PS> %[1]s completion powershell > %[1]s.ps1 # and source this file from your PowerShell profile. diff --git a/cmd/main.go b/cmd/main.go index 1d6478a47..d832cbc5e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -17,9 +17,12 @@ package caddycmd import ( "bufio" "bytes" + "encoding/json" + "errors" "flag" "fmt" "io" + "io/fs" "log" "net" "os" @@ -32,6 +35,7 @@ import ( "github.com/caddyserver/certmagic" "github.com/spf13/pflag" + "go.uber.org/automaxprocs/maxprocs" "go.uber.org/zap" "github.com/caddyserver/caddy/v2" @@ -62,7 +66,17 @@ 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 { + var exitError *exitError + if errors.As(err, &exitError) { + os.Exit(exitError.ExitCode) + } os.Exit(1) } } @@ -94,6 +108,12 @@ func LoadConfig(configFile, adapterName string) ([]byte, string, error) { } 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 + if logger == nil { + logger = zap.NewNop() + } + // specifying an adapter without a config file is ambiguous if adapterName != "" && configFile == "" { return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)") @@ -106,16 +126,16 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([ if configFile != "" { if configFile == "-" { config, err = io.ReadAll(os.Stdin) + if err != nil { + return nil, "", fmt.Errorf("reading config from stdin: %v", err) + } + logger.Info("using config from stdin") } else { config, err = os.ReadFile(configFile) - } - if err != nil { - return nil, "", fmt.Errorf("reading config file: %v", err) - } - if logger != nil { - logger.Info("using provided configuration", - zap.String("config_file", configFile), - zap.String("config_adapter", adapterName)) + if err != nil { + return nil, "", fmt.Errorf("reading config from file: %v", err) + } + logger.Info("using config from file", zap.String("file", configFile)) } } else if adapterName == "" { // if the Caddyfile adapter is plugged in, we can try using an @@ -123,7 +143,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([ cfgAdapter = caddyconfig.GetAdapter("caddyfile") if cfgAdapter != nil { config, err = os.ReadFile("Caddyfile") - if os.IsNotExist(err) { + if errors.Is(err, fs.ErrNotExist) { // okay, no default Caddyfile; pretend like this never happened cfgAdapter = nil } else if err != nil { @@ -132,9 +152,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([ } else { // success reading default Caddyfile configFile = "Caddyfile" - if logger != nil { - logger.Info("using adjacent Caddyfile") - } + logger.Info("using adjacent Caddyfile") } } } @@ -164,16 +182,24 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([ if err != nil { return nil, "", fmt.Errorf("adapting config using %s: %v", adapterName, err) } + logger.Info("adapted config to JSON", zap.String("adapter", adapterName)) for _, warn := range warnings { msg := warn.Message if warn.Directive != "" { msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message) } - if logger != nil { - logger.Warn(msg, zap.String("adapter", adapterName), zap.String("file", warn.File), zap.Int("line", warn.Line)) - } + logger.Warn(msg, + zap.String("adapter", adapterName), + zap.String("file", warn.File), + zap.Int("line", warn.Line)) } config = adaptedConfig + } else { + // validate that the config is at least valid JSON + err = json.Unmarshal(config, new(any)) + if err != nil { + return nil, "", fmt.Errorf("config is not valid JSON: %v; did you mean to use a config adapter (the --adapter flag)?", err) + } } return config, configFile, nil @@ -300,8 +326,12 @@ func loadEnvFromFile(envFile string) error { } for k, v := range envMap { - if err := os.Setenv(k, v); err != nil { - return fmt.Errorf("setting environment variables: %v", err) + // do not overwrite existing environment variables + _, exists := os.LookupEnv(k) + if !exists { + if err := os.Setenv(k, v); err != nil { + return fmt.Errorf("setting environment variables: %v", err) + } } } diff --git a/cmd/packagesfuncs.go b/cmd/packagesfuncs.go index 5d77e4d54..0784f7ee6 100644 --- a/cmd/packagesfuncs.go +++ b/cmd/packagesfuncs.go @@ -22,6 +22,7 @@ import ( "net/url" "os" "os/exec" + "path/filepath" "reflect" "runtime" "runtime/debug" @@ -103,6 +104,15 @@ func upgradeBuild(pluginPkgs map[string]struct{}, fl Flags) (int, error) { if err != nil { return caddy.ExitCodeFailedStartup, fmt.Errorf("retrieving current executable permission bits: %v", err) } + if thisExecStat.Mode()&os.ModeSymlink == os.ModeSymlink { + symSource := thisExecPath + // we are a symlink; resolve it + thisExecPath, err = filepath.EvalSymlinks(thisExecPath) + if err != nil { + return caddy.ExitCodeFailedStartup, fmt.Errorf("resolving current executable symlink: %v", err) + } + l.Info("this executable is a symlink", zap.String("source", symSource), zap.String("target", thisExecPath)) + } l.Info("this executable will be replaced", zap.String("path", thisExecPath)) // build the request URL to download this custom build diff --git a/cmd/storagefuncs.go b/cmd/storagefuncs.go index 6a6daec76..60e25485d 100644 --- a/cmd/storagefuncs.go +++ b/cmd/storagefuncs.go @@ -200,9 +200,10 @@ func cmdExportStorage(fl Flags) (int, error) { } hdr := &tar.Header{ - Name: k, - Mode: 0o600, - Size: int64(len(v)), + Name: k, + Mode: 0o600, + Size: int64(len(v)), + ModTime: info.Modified, } if err = tw.WriteHeader(hdr); err != nil { diff --git a/context.go b/context.go index 85978d49c..d73af7702 100644 --- a/context.go +++ b/context.go @@ -19,10 +19,14 @@ import ( "encoding/json" "fmt" "log" + "log/slog" "reflect" "github.com/caddyserver/certmagic" "go.uber.org/zap" + "go.uber.org/zap/exp/zapslog" + + "github.com/caddyserver/caddy/v2/internal/filesystems" ) // Context is a type which defines the lifetime of modules that @@ -37,10 +41,12 @@ import ( // not actually need to do this). type Context struct { context.Context + moduleInstances map[string][]Module cfg *Config - cleanupFuncs []func() ancestry []Module + cleanupFuncs []func() // invoked at every config unload + exitFuncs []func(context.Context) // invoked at config unload ONLY IF the process is exiting (EXPERIMENTAL) } // NewContext provides a new context derived from the given @@ -81,6 +87,25 @@ func (ctx *Context) OnCancel(f func()) { ctx.cleanupFuncs = append(ctx.cleanupFuncs, f) } +// Filesystems returns a ref to the FilesystemMap. +// EXPERIMENTAL: This API is subject to change. +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 ctx.cfg.filesystems +} + +// OnExit executes f when the process exits gracefully. +// The function is only executed if the process is gracefully +// shut down while this context is active. +// +// EXPERIMENTAL API: subject to change or removal. +func (ctx *Context) OnExit(f func(context.Context)) { + ctx.exitFuncs = append(ctx.exitFuncs, f) +} + // LoadModule loads the Caddy module(s) from the specified field of the parent struct // pointer and returns the loaded module(s). The struct pointer and its field name as // a string are necessary so that reflection can be used to read the struct tag on the @@ -164,7 +189,6 @@ func (ctx Context) LoadModule(structPointer any, fieldName string) (any, error) return nil, err } result = val - } else if isJSONRawMessage(typ.Elem()) { // val is `[]json.RawMessage` @@ -180,7 +204,6 @@ func (ctx Context) LoadModule(structPointer any, fieldName string) (any, error) all = append(all, val) } result = all - } else if typ.Elem().Kind() == reflect.Slice && isJSONRawMessage(typ.Elem().Elem()) { // val is `[][]json.RawMessage` @@ -201,7 +224,6 @@ func (ctx Context) LoadModule(structPointer any, fieldName string) (any, error) all = append(all, allInner) } result = all - } else if isModuleMapType(typ.Elem()) { // val is `[]map[string]json.RawMessage` @@ -494,6 +516,30 @@ func (ctx Context) Logger(module ...Module) *zap.Logger { return ctx.cfg.Logging.Logger(mod) } +// Slogger returns a slog logger that is intended for use by +// the most recent module associated with the context. +func (ctx Context) Slogger() *slog.Logger { + if ctx.cfg == nil { + // often the case in tests; just use a dev logger + l, err := zap.NewDevelopment() + if err != nil { + panic("config missing, unable to create dev logger: " + err.Error()) + } + return slog.New(zapslog.NewHandler(l.Core(), nil)) + } + mod := ctx.Module() + if mod == nil { + return slog.New(zapslog.NewHandler(Log().Core(), nil)) + } + + return slog.New(zapslog.NewHandler( + ctx.cfg.Logging.Logger(mod).Core(), + &zapslog.HandlerOptions{ + LoggerName: string(mod.CaddyModule().ID), + }, + )) +} + // Modules returns the lineage of modules that this context provisioned, // with the most recent/current module being last in the list. func (ctx Context) Modules() []Module { diff --git a/filesystem.go b/filesystem.go new file mode 100644 index 000000000..9785f57d4 --- /dev/null +++ b/filesystem.go @@ -0,0 +1,10 @@ +package caddy + +import "io/fs" + +type FileSystems interface { + Register(k string, v fs.FS) + Unregister(k string) + Get(k string) (v fs.FS, ok bool) + Default() fs.FS +} diff --git a/go.mod b/go.mod index 5b9ae28f0..8760d8351 100644 --- a/go.mod +++ b/go.mod @@ -1,73 +1,76 @@ module github.com/caddyserver/caddy/v2 -go 1.20 +go 1.21 + +toolchain go1.21.4 require ( github.com/BurntSushi/toml v1.3.2 github.com/Masterminds/sprig/v3 v3.2.3 - github.com/alecthomas/chroma/v2 v2.7.0 + github.com/alecthomas/chroma/v2 v2.12.1-0.20240220090827-381050ba0001 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b - github.com/caddyserver/certmagic v0.19.2 + github.com/caddyserver/certmagic v0.20.0 github.com/dustin/go-humanize v1.0.1 - github.com/go-chi/chi v4.1.2+incompatible + github.com/go-chi/chi/v5 v5.0.10 github.com/google/cel-go v0.15.1 - github.com/google/uuid v1.3.0 - github.com/klauspost/compress v1.16.7 + github.com/google/uuid v1.3.1 + github.com/klauspost/compress v1.17.0 github.com/klauspost/cpuid/v2 v2.2.5 - github.com/mastercactapus/proxyprotocol v0.0.4 github.com/mholt/acmez v1.2.0 - github.com/prometheus/client_golang v1.14.0 - github.com/quic-go/quic-go v0.38.0 - github.com/smallstep/certificates v0.24.3-rc.5 + github.com/prometheus/client_golang v1.18.0 + github.com/quic-go/quic-go v0.41.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.7.0 + 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-20230509043813-4e9cb4f2b4ad - github.com/yuin/goldmark v1.5.5 + github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046 + github.com/yuin/goldmark v1.5.6 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 go.opentelemetry.io/contrib/propagators/autoprop v0.42.0 - go.opentelemetry.io/otel v1.16.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 - go.opentelemetry.io/otel/sdk v1.16.0 - go.uber.org/zap v1.25.0 - golang.org/x/crypto v0.12.0 - golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 - golang.org/x/net v0.14.0 - golang.org/x/sync v0.3.0 - golang.org/x/term v0.11.0 - google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 + 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.18.0 + golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 + golang.org/x/net v0.19.0 + golang.org/x/sync v0.5.0 + golang.org/x/term v0.16.0 + google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f 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 github.com/Microsoft/go-winio v0.6.0 // indirect github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fxamacker/cbor/v2 v2.4.0 // indirect - github.com/golang/glog v1.1.0 // indirect - github.com/golang/mock v1.6.0 // indirect - github.com/google/certificate-transparency-go v1.1.4 // indirect - github.com/google/go-tpm v0.3.3 // indirect + github.com/fxamacker/cbor/v2 v2.5.0 // indirect + github.com/golang/glog v1.1.2 // indirect + github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect + github.com/google/go-tpm v0.9.0 // indirect github.com/google/go-tspi v0.3.0 // indirect - github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect - github.com/onsi/ginkgo/v2 v2.9.5 // indirect + github.com/google/pprof v0.0.0-20231212022811-ec68065c825e // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/onsi/ginkgo/v2 v2.13.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect - github.com/quic-go/qtls-go1-20 v0.3.2 // indirect - github.com/smallstep/go-attestation v0.4.4-0.20230509120429-e17291421738 // indirect + github.com/smallstep/go-attestation v0.4.4-0.20230627102604-cf579e53cbd2 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/blake3 v0.2.3 // 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 - google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect + go.uber.org/mock v0.3.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect ) require ( @@ -80,18 +83,18 @@ require ( github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chzyer/readline v1.5.1 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.3 // 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/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect - github.com/dlclark/regexp2 v1.7.0 // indirect - github.com/felixge/httpsnoop v1.0.3 // indirect + github.com/dlclark/regexp2 v1.10.0 // 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.2.4 // indirect + github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-sql-driver/mysql v1.7.0 // 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/snappy v0.0.4 // indirect @@ -110,17 +113,17 @@ require ( 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/matttproud/golang_protobuf_extensions v1.0.1 // 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/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/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_model v0.3.0 // indirect - github.com/prometheus/common v0.37.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect 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 @@ -130,23 +133,22 @@ require ( github.com/spf13/cast v1.4.1 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect github.com/urfave/cli v1.22.14 // indirect - go.etcd.io/bbolt v1.3.7 // 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/internal/retry v1.16.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect - go.opentelemetry.io/otel/metric v1.16.0 // indirect - go.opentelemetry.io/otel/trace v1.16.0 - go.opentelemetry.io/proto/otlp v0.19.0 // 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.33.0 - go.step.sm/linkedca v0.20.0 // indirect + go.step.sm/crypto v0.35.1 + go.step.sm/linkedca v0.20.1 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.11.0 // indirect - golang.org/x/sys v0.11.0 - golang.org/x/text v0.12.0 // indirect - golang.org/x/tools v0.10.0 // indirect - google.golang.org/grpc v1.56.2 // indirect - google.golang.org/protobuf v1.31.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/sys v0.16.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 howett.net/plist v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index 907d7a753..1c7d91d07 100644 --- a/go.sum +++ b/go.sum @@ -1,105 +1,27 @@ -bazil.org/fuse v0.0.0-20180421153158-65cc252bf669/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= -bitbucket.org/creachadair/shell v0.0.6/go.mod h1:8Qqi/cYk7vPnsOePHroKXDJYmb5x7ENhtiFtfZq8K+M= cloud.google.com/go v0.26.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.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISts= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.60.0/go.mod h1:yw2G51M9IfRboUH61Us8GqCeF1PzPblB823Mn2q2eAU= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= -cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= -cloud.google.com/go v0.92.2/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.92.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.110.4 h1:1JYyxKMN9hd5dR2MYTPWkGUgcoxVVhg0LKNKEo0qvmk= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg= +cloud.google.com/go v0.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o= +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/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/iam v1.1.0 h1:67gSqaPukx7O8WLLHMa0PNs3EBGd2eE4d+psbO/CO94= -cloud.google.com/go/kms v1.15.0 h1:xYl5WEaSekKYN5gGRyhjvZKM22GVBBCzegGNVPy+aIs= -cloud.google.com/go/monitoring v0.1.0/go.mod h1:Hpm3XfzJv+UTiXzCG5Ffp0wijzHTC7Cv4eR7o3x/fEE= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/pubsub v1.5.0/go.mod h1:ZEwJccE3z93Z2HWvstpri00jOg7oO4UZDtKhwDwqF0w= -cloud.google.com/go/spanner v1.7.0/go.mod h1:sd3K2gZ9Fd0vMPLXzeCrF6fq4i63Q7aTLW/lBIfBkIk= -cloud.google.com/go/spanner v1.17.0/go.mod h1:+17t2ixFwRG4lWRwE+5kipDR9Ef07Jkmc8z0IbMDKUs= -cloud.google.com/go/spanner v1.18.0/go.mod h1:LvAjUXPeJRGNuGpikMULjhLj/t9cRvdc+fxRoLiugXA= -cloud.google.com/go/spanner v1.25.0/go.mod h1:kQUft3x355hzzaeFbObjsvkzZDgpDkesp3v75WBnI8w= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/trace v0.1.0/go.mod h1:wxEwsoeRVPbeSkt7ZC9nWCgmoKQRAoySN7XHW2AmI7g= -code.gitea.io/sdk/gitea v0.11.3/go.mod h1:z3uwDV/b9Ls47NGukYM9XhnHtqPh/J+t40lsUrR6JDY= -contrib.go.opencensus.io/exporter/aws v0.0.0-20181029163544-2befc13012d0/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= -contrib.go.opencensus.io/exporter/ocagent v0.5.0/go.mod h1:ImxhfLRpxoYiSq891pBrLVhN+qmP8BTVvdH2YLs7Gl0= -contrib.go.opencensus.io/exporter/stackdriver v0.12.1/go.mod h1:iwB6wGarfphGGe/e5CWqyUk/cLzKnWsOKPVW3no6OTw= -contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc= -contrib.go.opencensus.io/exporter/stackdriver v0.13.5/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc= -contrib.go.opencensus.io/exporter/stackdriver v0.13.8/go.mod h1:huNtlWx75MwO7qMs0KrMxPZXzNNWebav1Sq/pm02JdQ= -contrib.go.opencensus.io/integrations/ocsql v0.1.4/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= -contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +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= 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/Azure/azure-amqp-common-go/v2 v2.1.0/go.mod h1:R8rea+gJRuJR6QxTir/XuEd+YuKoUiazDC/N96FiDEU= -github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4= -github.com/Azure/azure-sdk-for-go v29.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go v30.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-service-bus-go v0.9.1/go.mod h1:yzBx6/BUGfjfeqbRZny9AQIbIe3AcV9WZbAdpkoXOa0= -github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0= -github.com/Azure/go-autorest v12.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 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/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20191009163259-e802c2cb94ae/go.mod h1:mjwGPas4yKduTyubHvD1Atl9r1rUq8DfVy+gkVvZ+oo= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= -github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 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 v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 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 v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= -github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= 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/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= @@ -110,82 +32,52 @@ github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWX 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/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= +github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= -github.com/alecthomas/chroma/v2 v2.7.0 h1:hm1rY6c/Ob4eGclpQ7X/A3yhqBOZNUTk9q+yhyLIViI= -github.com/alecthomas/chroma/v2 v2.7.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= -github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE= +github.com/alecthomas/chroma/v2 v2.9.1 h1:0O3lTQh9FxazJ4BYE/MOi/vDGuHn7B+6Bu902N2UZvU= +github.com/alecthomas/chroma/v2 v2.9.1/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw= +github.com/alecthomas/chroma/v2 v2.12.1-0.20240220090827-381050ba0001 h1:Nl5Om7AhgtN3tML9kLn2/lr8IDVKxHT2t2+xWc4Q6Fs= +github.com/alecthomas/chroma/v2 v2.12.1-0.20240220090827-381050ba0001/go.mod h1:b6DmXsg5hSmn0AcHaTsU+UH0vO73VzhR+JrpFihjsXM= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= +github.com/alecthomas/repr v0.2.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/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= -github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= -github.com/apache/beam v2.28.0+incompatible/go.mod h1:/8NX3Qi8vGstDLLaeaU7+lzVEu/ACaQhYjeefzQ0y1o= -github.com/apache/beam v2.32.0+incompatible/go.mod h1:/8NX3Qi8vGstDLLaeaU7+lzVEu/ACaQhYjeefzQ0y1o= 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/apex/log v1.1.4/go.mod h1:AlpoD9aScyQfJDVHmLMEcx4oU6LqzkWp4Mg9GdAcEvQ= -github.com/apex/logs v0.0.4/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= -github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= -github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= 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/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 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.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= -github.com/aws/aws-sdk-go v1.19.18/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.19.45/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.25.11/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= -github.com/aws/aws-sdk-go v1.44.307 h1:2R0/EPgpZcFSUwZhYImq/srjaOrOfLv5MNRzrFyAM38= +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/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= -github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= -github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= 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/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= -github.com/caarlos0/ctrlc v1.0.0/go.mod h1:CdXpj4rmq0q/1Eb44M9zi2nKB0QraNKuRGYGrrHhcQw= -github.com/caddyserver/certmagic v0.19.2 h1:HZd1AKLx4592MalEGQS39DKs2ZOAJCEM/xYPMQ2/ui0= -github.com/caddyserver/certmagic v0.19.2/go.mod h1:fsL01NomQ6N+kE2j37ZCnig2MFosG+MIO4ztnmG/zz8= -github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e/go.mod h1:9IOqJGCPMSc6E5ydlp5NIonxObaeu/Iub/X03EKPVYo= +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/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A= 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.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= -github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= 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.1.2/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/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -199,48 +91,26 @@ 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/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 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/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= -github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= -github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/etcd v3.3.13+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-semver v0.3.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-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/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.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 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/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY= github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8= github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE= github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o= @@ -253,11 +123,10 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm 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/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= +github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -266,131 +135,62 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m 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/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/protoc-gen-validate v0.0.14/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.3.0-java/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/etcd-io/gofail v0.0.0-20190801230047-ad7f989257ca/go.mod h1:49H/RkXP8pKaZy4h0d+NW16rSLhyVBt4o6VLJbmOqDE= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= -github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3nqZCxaQ2Ze/sM= -github.com/fullstorydev/grpcurl v1.8.0/go.mod h1:Mn2jWbdMrQGJQ8UD62uNyMumT2acsZUCkZIqFxsQf1o= -github.com/fullstorydev/grpcurl v1.8.1/go.mod h1:3BWhvHZwNO7iLXaQlojdg5NA6SxUDePli4ecpK1N7gw= -github.com/fullstorydev/grpcurl v1.8.2/go.mod h1:YvWNT3xRp2KIRuvCphFodG0fKkMXwaxA9CJgKCcyzUQ= -github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= -github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= -github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +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/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= -github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= -github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +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/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/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= 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-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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= -github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= -github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= -github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= -github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +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/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= -github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= -github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= +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-20190129154638-5b532d6fd5ef/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-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/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/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/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 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.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 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= @@ -399,137 +199,59 @@ 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.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= 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.15.1 h1:iTgVZor2x9okXtmTrqO8cg4uvqIeaBcWhXtruaWFMYQ= github.com/google/cel-go v0.15.1/go.mod h1:YzWEoI07MC/a/wj9in8GeVatqfypkldgBlwXh9bCwqY= github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= -github.com/google/certificate-transparency-go v1.1.1/go.mod h1:FDKqPvSXawb2ecErVRrD+nfy23RCzyl7eqVCEmlT1Zs= -github.com/google/certificate-transparency-go v1.1.2-0.20210422104406-9f33727a7a18/go.mod h1:6CKh9dscIRoqc2kC6YUFICHZMT9NrClyPrRVFrdw1QQ= -github.com/google/certificate-transparency-go v1.1.2-0.20210512142713-bed466244fa6/go.mod h1:aF2dp7Dh81mY8Y/zpzyXps4fQW5zQbDu2CxfpJB6NkI= -github.com/google/certificate-transparency-go v1.1.2/go.mod h1:3OL+HKDqHPUfdKrHVQxO6T8nDLO0HF7LRTlkIWXaWvQ= -github.com/google/certificate-transparency-go v1.1.4 h1:hCyXHDbtqlr/lMXU0D4WgbalXL0Zk4dSWWMbPV8VrqY= -github.com/google/certificate-transparency-go v1.1.4/go.mod h1:D6lvbfwckhNrbM9WVl1EVeMOyzC19mpIjMOI4nxBHtQ= -github.com/google/go-attestation v0.3.2/go.mod h1:N0ADdnY0cr7eLJyZ75o8kofGGTUF2XrZTJuTPo5acwk= -github.com/google/go-attestation v0.4.4-0.20220404204839-8820d49b18d9/go.mod h1:KDsPHk8a2MX9g20kYSdxB21t7je5NghSaFeVn0Zu3Ao= +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.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= -github.com/google/go-licenses v0.0.0-20210329231322-ce1d9163b77d/go.mod h1:+TYOmkVoJOpwnS0wfdsJCV9CoD5nJYsHoFk/0CrTK4M= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/go-replayers/grpcreplay v0.1.0/go.mod h1:8Ig2Idjpr6gifRd6pNVggX6TC1Zw6Jx74AKp7QNH2QE= -github.com/google/go-replayers/httpreplay v0.1.0/go.mod h1:YKZViNhiGgqdBlUbI2MwGpq4pXxNmhJLPHQ7cv2b5no= -github.com/google/go-tpm v0.1.2-0.20190725015402-ae6dd98980d4/go.mod h1:H9HbmUG2YgV/PHITkO7p6wxEEj/v5nlsVWIwumwH2NI= -github.com/google/go-tpm v0.3.0/go.mod h1:iVLWvrPp/bHeEkxTFi9WG6K9w0iy2yIszHwZGHPbzAw= -github.com/google/go-tpm v0.3.2/go.mod h1:j71sMBTfp3X5jPHz852ZOfQMUOf65Gb/Th8pRmp7fvg= -github.com/google/go-tpm v0.3.3 h1:P/ZFNBZYXRxc+z7i5uyd8VP7MaDteuLZInzrH2idRGo= -github.com/google/go-tpm v0.3.3/go.mod h1:9Hyn3rgnzWF9XBWVk6ml6A6hNkbWjNFlDQL51BeghL4= -github.com/google/go-tpm-tools v0.0.0-20190906225433-1614c142f845/go.mod h1:AVfHadzbdzHo54inR2x1v640jdi1YSi3NauM2DUsxk0= -github.com/google/go-tpm-tools v0.2.0/go.mod h1:npUd03rQ60lxN7tzeBJreG38RvWwme2N1reF/eeiBk4= -github.com/google/go-tpm-tools v0.2.1/go.mod h1:npUd03rQ60lxN7tzeBJreG38RvWwme2N1reF/eeiBk4= -github.com/google/go-tpm-tools v0.3.1/go.mod h1:PSg+r5hSZI5tP3X7LBQx2sW1VSZUqZHBSrKyDqrB21U= -github.com/google/go-tpm-tools v0.3.9/go.mod h1:22JvWmHcD5w55cs+nMeqDGDxgNS15/2pDq2cLqnc3rc= -github.com/google/go-tpm-tools v0.3.12 h1:hpWglH4RaZnGVbgOK3IThI5K++jnFvjQ94EIN34xrUU= -github.com/google/go-tspi v0.2.1-0.20190423175329-115dea689aad/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI= +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-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-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/licenseclassifier v0.0.0-20210325184830-bb04aff29e72/go.mod h1:qsqn2hxC+vURpyBRygGUuinTO42MFRLcsmQ/P8v94+M= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200507031123-427632fa3b1c/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +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/rpmpack v0.0.0-20191226140753-aa36bfddb3a0/go.mod h1:RaTPr0KUf2K7fnZYLNDrr8rxAamWs3iNywJLtQ2AzBg= -github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= -github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= -github.com/google/trillian v1.3.11/go.mod h1:0tPraVHrSDkA3BO6vKX67zgLXs6SsOAbHEivX+9mPgw= -github.com/google/trillian v1.3.14-0.20210409160123-c5ea3abd4a41/go.mod h1:1dPv0CUjNQVFEDuAUFhZql16pw/VlPgaX8qj+g5pVzQ= -github.com/google/trillian v1.3.14-0.20210511103300-67b5f349eefa/go.mod h1:s4jO3Ai4NSvxucdvqUHON0bCqJyoya32eNw6XJwsmNc= -github.com/google/trillian v1.4.0/go.mod h1:1Bja2nEgMDlEJWWRXBUemSPG9qYw84ZYX2gHRVHlR+g= -github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs16741s= -github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvkiqTYKBCKLNmlge2eVjoZfySzM= -github.com/googleapis/gax-go v2.0.2+incompatible h1:silFMLAnr330+NRuag/VjIGF7TLp/LBrV2CJKFLWEww= -github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +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/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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= -github.com/goreleaser/goreleaser v0.134.0/go.mod h1:ZT6Y2rSYa6NxQzIsdfWWNWAlYGXGbreo66NmE+3X3WQ= -github.com/goreleaser/nfpm v1.2.1/go.mod h1:TtWrABZozuLOttX2uDlYyECfQX7x5XYkVxhjYcR6G9w= 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/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda/go.mod h1:MyndkAZd5rUMdNogn35MWXBX1UiBigrU8eTj8DoAC2c= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.9.2/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.12.1/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c= -github.com/grpc-ecosystem/grpc-gateway v1.14.6/go.mod h1:zdiPV4Yse/1gnckTHtghG4GkDEdKCRJduHpTxT3/jcw= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QGdLI4y34qQGjGWO0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +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.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 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-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 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-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= 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= @@ -545,17 +267,11 @@ github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0m 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.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= -github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= 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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.4/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 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= @@ -611,109 +327,66 @@ github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0f 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/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jarcoal/httpmock v1.0.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jhump/protoreflect v1.6.1/go.mod h1:RZQ/lnuN+zqeRVpQigTwO6o0AJUkxbnSnpuG7toUTG4= -github.com/jhump/protoreflect v1.8.2/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg= -github.com/jhump/protoreflect v1.9.0/go.mod h1:7GcYQDdMU/O/BBrl/cX6PNHpXh6cenjd8pneu5yW7Tg= -github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 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/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jonboulle/clockwork v0.2.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= -github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= -github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 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/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 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.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= -github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +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/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/konsorten/go-windows-terminal-sequences v1.0.3/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.2.1/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.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= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= -github.com/letsencrypt/pkcs11key/v4 v4.0.0/go.mod h1:EFUvBDay26dErnNb70Nd0/VW3tJiIbETBPTl9ATXQag= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= 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/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= -github.com/mastercactapus/proxyprotocol v0.0.4 h1:qSY75IZF30ZqIU9iW1ip3I7gTnm8wRAnGWqPxCBVgq0= -github.com/mastercactapus/proxyprotocol v0.0.4/go.mod h1:X8FRVEDZz9FkrIoL4QYTBF4Ka4ELwTv0sah0/5NxCPw= 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.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 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-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= 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-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.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 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-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= -github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +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= @@ -723,8 +396,6 @@ github.com/micromdm/scep/v2 v2.1.0/go.mod h1:BkF7TkPPhmgJAMtHfP+sFTKXmgzNJgLQlvv 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/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= @@ -739,19 +410,13 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu 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.1/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/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007/go.mod h1:m2XC9Qq0AlmmVksL6FktJCdTYyLk7V3fKyp0sl1yWQo= -github.com/mwitkow/go-proto-validators v0.2.0/go.mod h1:ZfA1hW+UH/2ZHOWvQ3HnQaU0DtnpXu850MZiy+YUgcc= 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= @@ -759,26 +424,16 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE 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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/nishanths/predeclared v0.0.0-20190419143655-18a43bb90ffc/go.mod h1:62PewwiQTlm/7Rj+cxVYqZvDIUc+JjZq6GHAC1fsObQ= -github.com/nishanths/predeclared v0.0.0-20200524104333-86fad755b4d3/go.mod h1:nt3d53pc1VYcphSCIaYAJtnPYnr3Zyn8fMq2wvPGPso= 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/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ= -github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 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/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= -github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +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.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +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= @@ -788,90 +443,60 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS 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/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= -github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= -github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= -github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= -github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= 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/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= 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/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 v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 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.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= -github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +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_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.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +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.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.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +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.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 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/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= -github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/pseudomuto/protoc-gen-doc v1.3.2/go.mod h1:y5+P6n3iGrbKG+9O04V5ld71in3v/bX88wUwgt+U8EA= -github.com/pseudomuto/protoc-gen-doc v1.4.1/go.mod h1:exDTOVwqpp30eV/EDPFLZy3Pwr2sn6hBC1WIYH/UbIg= -github.com/pseudomuto/protoc-gen-doc v1.5.0/go.mod h1:exDTOVwqpp30eV/EDPFLZy3Pwr2sn6hBC1WIYH/UbIg= -github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q= +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/qtls-go1-20 v0.3.2 h1:rRgN3WfnKbyik4dBV8A6girlJVxGand/d+jVKbQq5GI= -github.com/quic-go/qtls-go1-20 v0.3.2/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= -github.com/quic-go/quic-go v0.38.0 h1:T45lASr5q/TrVwt+jrVccmqHhPL2XuSyoCLVCpfOSLc= -github.com/quic-go/quic-go v0.38.0/go.mod h1:MPCuRq7KBK2hNcfKj/1iD1BGuN3eAYMeNxp3T42LRUg= +github.com/quic-go/quic-go v0.41.0 h1:aD8MmHfgqTURWNJy48IYFg2OnxwHT3JL7ahGs73lb4k= +github.com/quic-go/quic-go v0.41.0/go.mod h1:qCkNjqczPEvgsOnxZ0eCD14lv+B2LHlFAB++CNOh9hA= 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/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= -github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM= +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/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= @@ -883,13 +508,10 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf 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/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I= 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/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 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= @@ -898,29 +520,24 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 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.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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/certificates v0.24.3-rc.5 h1:l9N7NmFqW5it5UcDtbyZ4CrrvYYiHRM7Dj7Mk+YC1Io= -github.com/smallstep/certificates v0.24.3-rc.5/go.mod h1:buhMLsuk9tp7JC1uHeN2sYQTXH1OzGn55erpzUIiOGo= -github.com/smallstep/go-attestation v0.4.4-0.20230509120429-e17291421738 h1:h+cZgVniTaE0uuRMdxTThLaJeuxsv4aas6oStz6f5VQ= -github.com/smallstep/go-attestation v0.4.4-0.20230509120429-e17291421738/go.mod h1:mk2hyNbyai1oon+ilW9t42BuBVw7ee8elDdgrPq4394= +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/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= -github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/soheilhy/cmux v0.1.5-0.20210205191134-5ec6847320e5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= -github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= @@ -932,20 +549,14 @@ 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/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= -github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 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/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= 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= @@ -956,7 +567,6 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -968,45 +578,22 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tailscale/tscert v0.0.0-20230509043813-4e9cb4f2b4ad h1:JEOo9j4RzDPBJFTU9YZ/QPkLtfV8+6PbZFFOSUx5VP4= -github.com/tailscale/tscert v0.0.0-20230509043813-4e9cb4f2b4ad/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU= -github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= -github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= -github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= -github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= +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/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= -github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 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.4/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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= -github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= -github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= 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.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 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.5 h1:IJznPe8wOzfIKETmMkd06F8nXkmlhaHqFRM9l1hAGsU= -github.com/yuin/goldmark v1.5.5/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-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= @@ -1016,54 +603,22 @@ github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvv 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.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= -go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= -go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +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.etcd.io/etcd v0.0.0-20200513171258-e048e166ab9c/go.mod h1:xCI7ZzBfRuGgBXyXO6yfWfDmlWd35khcWpUa4L0xI/k= -go.etcd.io/etcd/api/v3 v3.5.0-alpha.0/go.mod h1:mPcW6aZJukV6Aa81LSKpBjQXTWlXB5r74ymPoSWa3Sw= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0-alpha.0/go.mod h1:kdV+xzCJ3luEBSIeQyB/OEKkWKd8Zkux4sbDeANrosU= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.etcd.io/etcd/client/v3 v3.5.0-alpha.0/go.mod h1:wKt7jgDgf/OfKiYmCq5WFGxOFAkVMLxiiXgLDFhECr8= -go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= -go.etcd.io/etcd/etcdctl/v3 v3.5.0-alpha.0/go.mod h1:YPwSaBciV5G6Gpt435AasAG3ROetZsKNUzibRa/++oo= -go.etcd.io/etcd/etcdctl/v3 v3.5.0/go.mod h1:vGTfKdsh87RI7kA2JHFBEGxjQEYx+pi299wqEOdi34M= -go.etcd.io/etcd/etcdutl/v3 v3.5.0/go.mod h1:o98rKMCibbFAG8QS9KmvlYDGDShmmIbmRE8vSofzYNg= -go.etcd.io/etcd/pkg/v3 v3.5.0-alpha.0/go.mod h1:tV31atvwzcybuqejDoY3oaNRTtlD2l/Ot78Pc9w7DMY= -go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= -go.etcd.io/etcd/raft/v3 v3.5.0-alpha.0/go.mod h1:FAwse6Zlm5v4tEWZaTjmNhe17Int4Oxbu7+2r0DiD3w= -go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= -go.etcd.io/etcd/server/v3 v3.5.0-alpha.0/go.mod h1:tsKetYpt980ZTpzl/gb+UOJj9RkIyCb1u4wjzMg90BQ= -go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= -go.etcd.io/etcd/tests/v3 v3.5.0-alpha.0/go.mod h1:HnrHxjyCuZ8YDt8PYVyQQ5d1ZQfzJVEtQWllr5Vp/30= -go.etcd.io/etcd/tests/v3 v3.5.0/go.mod h1:f+mtZ1bE1YPvgKdOJV2BKy4JQW0nAFnQehgOE7+WyJE= -go.etcd.io/etcd/v3 v3.5.0-alpha.0/go.mod h1:JZ79d3LV6NUfPjUxXrpiFAYcjhT+06qqw+i28snx8To= -go.etcd.io/etcd/v3 v3.5.0/go.mod h1:FldM0/VzcxYWLvWx1sdA7ghKw7C3L2DvUTzGrcEtsC4= 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.15.0/go.mod h1:UffZAU+4sDEINUGP/B7UfBBkq4fqLu9zXAX7ke6CHW0= 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.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.22.6/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 h1:pginetY7+onl4qN1vl0xW/V/v6OBZ0vVdH+esuJgvmM= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0/go.mod h1:XiYsayHc36K3EByOO6nbAXnAWbrUxdjUROCEeeROOH8= +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/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= @@ -1074,134 +629,85 @@ 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 v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= -go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= -go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= -go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 h1:t4ZwRPU+emrcvM2e9DHd0Fsf0JTPVcbfa/BhTDF03d0= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0/go.mod h1:vLarbg68dH2Wa77g71zmKQqlQ8+8Rq3GRG31uc0WcWI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 h1:cbsD4cUcviQGXdw8+bo5x2wazq10SKz8hEbtCRPcU78= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0/go.mod h1:JgXSGah17croqhJfhByOLVY719k1emAXC8MVhCIJlRs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 h1:TVQp/bboR4mhZSav+MdgXB8FaRho1RC8UwVn3T0vjVc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0/go.mod h1:I33vtIe0sR96wfrUcilIzLoA3mLHhRmz9S9Te0S3gDo= -go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= -go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= -go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= -go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= -go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= -go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE= -go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4= -go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= -go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= -go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= -go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= -go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw= -go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +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.33.0 h1:fP8awo6YkZ0/rrLhzbHYA3U8g24VnWEebZRnGwUobRo= -go.step.sm/crypto v0.33.0/go.mod h1:rMETKeIA1ZsLBiKT6phQ2IIeBH3GL+XqimeobcqUw1g= -go.step.sm/linkedca v0.20.0 h1:bH41rvyDm3nSSJ5xgGsKUZOpzJcq5x2zacMIeqtq9oI= -go.step.sm/linkedca v0.20.0/go.mod h1:eybHw6ZTpuFmkUQnTBRWM2SPIGaP0VbYeo1bupfPT70= +go.step.sm/crypto v0.35.1 h1:QAZZ7Q8xaM4TdungGSAYw/zxpyH4fMYTkfaXVV9H7pY= +go.step.sm/crypto v0.35.1/go.mod h1:vn8Vkx/Mbqgoe7AG8btC0qZ995Udm3e+JySuDS1LCJA= +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/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +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/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= +go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= 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.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 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.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= -go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= -gocloud.dev v0.19.0/go.mod h1:SmKwiR8YwIMMJvQBKLsC3fHNyMwXLw3PMDO+VVteJMI= -golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +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= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/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-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/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-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191117063200-497ca9f6d64f/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 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-20210817164053-32db794688a5/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.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= -golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 h1:LGJsf5LRplCck6jUCH3dBL2dmycNruWNF5xugkSlfXw= -golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +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/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-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= -golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +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/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-20181108082009-03003ca0c849/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= @@ -1210,90 +716,29 @@ golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73r 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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 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-20190619014844-b5b0513f8c1b/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-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/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-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191119073136-fc4aabc6c914/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210126194326-f9ce19ea3013/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= +golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= +golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= 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-20190412183630-56d357773e84/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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +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/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1305,79 +750,22 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h 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-20190221075227-b4e8571b14e0/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-20190312061237-fead79001313/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-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190620070143-6f217b454f45/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/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-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191119060738-e882bf8e40c2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210316092937-0b90fd5c4c48/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210503080704-8803ae5d1324/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210629170331-7dc0b73dc9fb/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1385,114 +773,48 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc 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.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= 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.5/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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/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.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/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/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-20181030221726-6c7e314b6563/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-20190312151545-0bb0c0a6e846/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-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191010075000-0337d82405ff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191118222007-07fc4c7f2b98/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200630154851-b2d8b0336632/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200706234117-b22de6825cf7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201014170642-d1624618ad65/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= -golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= +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/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= @@ -1500,233 +822,74 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T 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.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.10.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.37.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.45.0/go.mod h1:ISLIJCedJolbZvDfAk+Ctuq5hf+aJ33WgtUsfyFoLXA= -google.golang.org/api v0.46.0/go.mod h1:ceL4oozhkAiTID8XMmJBsIxID/9wMXJVVFXPg4ylg3I= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.132.0 h1:8t2/+qZ26kAOGSmOiHwVycqVaDg7q3JDILrNi/Z6rvc= +google.golang.org/api v0.154.0 h1:X7QkVKZBskztmpPKWQXgjJRPA2dJYrL6r+sYPRLj050= +google.golang.org/api v0.154.0/go.mod h1:qhSMkM85hgqiokIYsrRyKxrjfBeIhgl4Z2JmeRkYylc= 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.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +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-20181107211654-5fc9ac540362/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-20190418145605-e7d98fc518a7/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-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8/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-20190620144150-6af8c5fc6601/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200626011028-ee7919e894b5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200707001353-8e8330bf89df/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210331142528-b7513248f0ba/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210413151531-c14fb6ef47c3/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210427215850-f767ed18ee4d/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210429181445-86c259c2b4ab/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= -google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 h1:Au6te5hbKUV8pIYWHqOUZ1pva5qK/rwbIhoXEUB9Lu8= -google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:O9kGHb51iE/nOGvQaDUuadVYqovW56s5emA88lQnj6Y= -google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 h1:XVeBY8d/FaK4848myy41HBqnDwvxeV3zMZhwN1TvAMU= -google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:mPBs5jNgx2GuQGvFwUvVKqtn6HsUw9nP64BedgvqEsQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= -google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +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/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.21.1/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.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.0/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI= -google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.25.1-0.20200805231151-a709e31e5d12/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +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.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +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= 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-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +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/cheggaaa/pb.v1 v1.0.28/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/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= -gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= 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/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= -gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= -gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= 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.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.6/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/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.0-20210107192922-496545a6307b/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= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/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.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= -pack.ag/amqp v0.11.2/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/internal/filesystems/map.go b/internal/filesystems/map.go new file mode 100644 index 000000000..e795ed1fe --- /dev/null +++ b/internal/filesystems/map.go @@ -0,0 +1,77 @@ +package filesystems + +import ( + "io/fs" + "strings" + "sync" +) + +const ( + DefaultFilesystemKey = "default" +) + +var DefaultFilesystem = &wrapperFs{key: DefaultFilesystemKey, FS: OsFS{}} + +// wrapperFs exists so can easily add to wrapperFs down the line +type wrapperFs struct { + key string + fs.FS +} + +// 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 { + m sync.Map +} + +// note that the first invocation of key cannot be called in a racy context. +func (f *FilesystemMap) key(k string) string { + if k == "" { + 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) { + k = f.key(k) + if v == nil { + f.Unregister(k) + return + } + f.m.Store(k, &wrapperFs{key: k, FS: v}) +} + +// 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) { + k = f.key(k) + 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) { + 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 + } + return nil, ok + } + return c.(fs.FS), true +} + +// Default will get the default filesystem in the filesystem map +func (f *FilesystemMap) Default() fs.FS { + val, _ := f.Get(DefaultFilesystemKey) + return val +} diff --git a/internal/filesystems/os.go b/internal/filesystems/os.go new file mode 100644 index 000000000..04b4d5b40 --- /dev/null +++ b/internal/filesystems/os.go @@ -0,0 +1,29 @@ +package filesystems + +import ( + "io/fs" + "os" + "path/filepath" +) + +// OsFS is a simple fs.FS implementation that uses the local +// file system. (We do not use os.DirFS because we do our own +// rooting or path prefixing without being constrained to a single +// root folder. The standard os.DirFS implementation is problematic +// since roots can be dynamic in our application.) +// +// OsFS also implements fs.StatFS, fs.GlobFS, fs.ReadDirFS, and fs.ReadFileFS. +type OsFS struct{} + +func (OsFS) Open(name string) (fs.File, error) { return os.Open(name) } +func (OsFS) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) } +func (OsFS) Glob(pattern string) ([]string, error) { return filepath.Glob(pattern) } +func (OsFS) ReadDir(name string) ([]fs.DirEntry, error) { return os.ReadDir(name) } +func (OsFS) ReadFile(name string) ([]byte, error) { return os.ReadFile(name) } + +var ( + _ fs.StatFS = (*OsFS)(nil) + _ fs.GlobFS = (*OsFS)(nil) + _ fs.ReadDirFS = (*OsFS)(nil) + _ fs.ReadFileFS = (*OsFS)(nil) +) diff --git a/internal/testmocks/dummyverifier.go b/internal/testmocks/dummyverifier.go new file mode 100644 index 000000000..1fbef32bf --- /dev/null +++ b/internal/testmocks/dummyverifier.go @@ -0,0 +1,41 @@ +package testmocks + +import ( + "crypto/x509" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/modules/caddytls" +) + +func init() { + caddy.RegisterModule(new(dummyVerifier)) +} + +type dummyVerifier struct{} + +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (dummyVerifier) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + return nil +} + +// CaddyModule implements caddy.Module. +func (dummyVerifier) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.client_auth.verifier.dummy", + New: func() caddy.Module { + return new(dummyVerifier) + }, + } +} + +// VerifyClientCertificate implements ClientCertificateVerifier. +func (dummyVerifier) VerifyClientCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + return nil +} + +var ( + _ caddy.Module = dummyVerifier{} + _ caddytls.ClientCertificateVerifier = dummyVerifier{} + _ caddyfile.Unmarshaler = dummyVerifier{} +) diff --git a/listen.go b/listen.go index e0d67c6ab..34812b54f 100644 --- a/listen.go +++ b/listen.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:build !unix +//go:build !unix || solaris package caddy @@ -30,18 +30,34 @@ func reuseUnixSocket(network, addr string) (any, error) { return nil, nil } -func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (net.Listener, error) { - sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) { - ln, err := config.Listen(ctx, network, address) +func listenReusable(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (any, error) { + switch network { + case "udp", "udp4", "udp6", "unixgram": + sharedPc, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) { + pc, err := config.ListenPacket(ctx, network, address) + if err != nil { + return nil, err + } + return &sharedPacketConn{PacketConn: pc, key: lnKey}, nil + }) if err != nil { return nil, err } - return &sharedListener{Listener: ln, key: lnKey}, nil - }) - if err != nil { - 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 + }) + if err != nil { + return nil, err + } + return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: config.KeepAlive}, nil } - return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: config.KeepAlive}, nil } // fakeCloseListener is a private wrapper over a listener that @@ -98,7 +114,7 @@ func (fcl *fakeCloseListener) Accept() (net.Conn, error) { // so that it's clear in the code that side-effects are shared with other // users of this listener, not just our own reference to it; we also don't // do anything with the error because all we could do is log it, but we - // expliclty assign it to nothing so we don't forget it's there if needed + // explicitly assign it to nothing so we don't forget it's there if needed _ = fcl.sharedListener.clearDeadline() if netErr, ok := err.(net.Error); ok && netErr.Timeout() { @@ -172,3 +188,75 @@ func (sl *sharedListener) setDeadline() error { func (sl *sharedListener) Destruct() error { return sl.Listener.Close() } + +// fakeClosePacketConn is like fakeCloseListener, but for PacketConns, +// or more specifically, *net.UDPConn +type fakeClosePacketConn struct { + closed int32 // accessed atomically; belongs to this struct only + *sharedPacketConn // embedded, so we also become a net.PacketConn; its key is used in Close +} + +func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + // if the listener is already "closed", return error + if atomic.LoadInt32(&fcpc.closed) == 1 { + return 0, nil, &net.OpError{ + Op: "readfrom", + Net: fcpc.LocalAddr().Network(), + Addr: fcpc.LocalAddr(), + Err: errFakeClosed, + } + } + + // call underlying readfrom + n, addr, err = fcpc.sharedPacketConn.ReadFrom(p) + if err != nil { + // this server was stopped, so clear the deadline and let + // any new server continue reading; but we will exit + if atomic.LoadInt32(&fcpc.closed) == 1 { + if netErr, ok := err.(net.Error); ok && netErr.Timeout() { + if err = fcpc.SetReadDeadline(time.Time{}); err != nil { + return + } + } + } + return + } + + return +} + +// Close won't close the underlying socket unless there is no more reference, then listenerPool will close it. +func (fcpc *fakeClosePacketConn) Close() error { + if atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) { + _ = fcpc.SetReadDeadline(time.Now()) // unblock ReadFrom() calls to kick old servers out of their loops + _, _ = listenerPool.Delete(fcpc.sharedPacketConn.key) + } + return nil +} + +func (fcpc *fakeClosePacketConn) Unwrap() net.PacketConn { + return fcpc.sharedPacketConn.PacketConn +} + +// sharedPacketConn is like sharedListener, but for net.PacketConns. +type sharedPacketConn struct { + net.PacketConn + key string +} + +// Destruct closes the underlying socket. +func (spc *sharedPacketConn) Destruct() error { + return spc.PacketConn.Close() +} + +// Unwrap returns the underlying socket +func (spc *sharedPacketConn) Unwrap() net.PacketConn { + return spc.PacketConn +} + +// Interface guards (see https://github.com/caddyserver/caddy/issues/3998) +var ( + _ (interface { + Unwrap() net.PacketConn + }) = (*fakeClosePacketConn)(nil) +) diff --git a/listen_unix.go b/listen_unix.go index 8870da5e9..9ec65c399 100644 --- a/listen_unix.go +++ b/listen_unix.go @@ -15,15 +15,17 @@ // Even though the filename ends in _unix.go, we still have to specify the // build constraint here, because the filename convention only works for // literal GOOS values, and "unix" is a shortcut unique to build tags. -//go:build unix +//go:build unix && !solaris package caddy import ( "context" "errors" + "io" "io/fs" "net" + "os" "sync/atomic" "syscall" @@ -87,7 +89,7 @@ func reuseUnixSocket(network, addr string) (any, error) { return nil, nil } -func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (net.Listener, error) { +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 { @@ -103,7 +105,14 @@ func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string, // 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). - ln, err := config.Listen(ctx, network, address) + 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) + } if err == nil { listenerPool.LoadOrStore(lnKey, nil) } @@ -117,9 +126,23 @@ func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string, 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 - return deleteListener{ln, lnKey}, err + switch specificLn := ln.(type) { + case net.Listener: + return deleteListener{specificLn, lnKey}, err + case net.PacketConn: + return deletePacketConn{specificLn, lnKey}, err + } + + // other types, I guess we just return them directly + return ln, err } // reusePort sets SO_REUSEPORT. Ineffective for unix sockets. @@ -158,6 +181,36 @@ func (uln *unixListener) Close() error { return uln.UnixListener.Close() } +type unixConn struct { + *net.UnixConn + filename string + mapKey string + count *int32 // accessed atomically +} + +func (uc *unixConn) Close() error { + newCount := atomic.AddInt32(uc.count, -1) + if newCount == 0 { + defer func() { + unixSocketsMu.Lock() + delete(unixSockets, uc.mapKey) + unixSocketsMu.Unlock() + _ = syscall.Unlink(uc.filename) + }() + } + return uc.UnixConn.Close() +} + +func (uc *unixConn) Unwrap() net.PacketConn { + return uc.UnixConn +} + +// unixSockets keeps track of the currently-active unix sockets +// so we can transfer their FDs gracefully during reloads. +var unixSockets = make(map[string]interface { + File() (*os.File, error) +}) + // 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. @@ -171,3 +224,19 @@ func (dl deleteListener) Close() error { _, _ = listenerPool.Delete(dl.lnKey) return dl.Listener.Close() } + +// deletePacketConn is like deleteListener, but +// for net.PacketConns. +type deletePacketConn struct { + net.PacketConn + lnKey string +} + +func (dl deletePacketConn) Close() error { + _, _ = listenerPool.Delete(dl.lnKey) + return dl.PacketConn.Close() +} + +func (dl deletePacketConn) Unwrap() net.PacketConn { + return dl.PacketConn +} diff --git a/listen_unix_setopt.go b/listen_unix_setopt.go index c9675f928..13ee7b830 100644 --- a/listen_unix_setopt.go +++ b/listen_unix_setopt.go @@ -1,4 +1,4 @@ -//go:build unix && !freebsd +//go:build unix && !freebsd && !solaris package caddy diff --git a/listeners.go b/listeners.go index 768d97711..84a32e45a 100644 --- a/listeners.go +++ b/listeners.go @@ -28,7 +28,6 @@ import ( "strings" "sync" "sync/atomic" - "syscall" "time" "github.com/quic-go/quic-go" @@ -149,11 +148,13 @@ 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 - var err error - var address string - var unixFileMode fs.FileMode - var isAbtractUnixSocket bool + var ( + ln any + err error + address string + unixFileMode fs.FileMode + isAbtractUnixSocket bool + ) // split unix socket addr early so lnKey // is independent of permissions bits @@ -181,27 +182,10 @@ func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net lnKey := listenerKey(na.Network, address) - switch na.Network { - case "tcp", "tcp4", "tcp6", "unix", "unixpacket": - ln, err = listenTCPOrUnix(ctx, lnKey, na.Network, address, config) - case "unixgram": - ln, err = config.ListenPacket(ctx, na.Network, address) - case "udp", "udp4", "udp6": - sharedPc, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) { - pc, err := config.ListenPacket(ctx, na.Network, address) - if err != nil { - return nil, err - } - return &sharedPacketConn{PacketConn: pc, key: lnKey}, nil - }) - if err != nil { - return nil, err - } - spc := sharedPc.(*sharedPacketConn) - ln = &fakeClosePacketConn{spc: spc, UDPConn: spc.PacketConn.(*net.UDPConn)} - } if strings.HasPrefix(na.Network, "ip") { ln, err = config.ListenPacket(ctx, na.Network, address) + } else { + ln, err = listenReusable(ctx, lnKey, na.Network, address, config) } if err != nil { return nil, err @@ -210,13 +194,6 @@ func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net return nil, fmt.Errorf("unsupported network type: %s", na.Network) } - // 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 { - one := int32(1) - ln = &unixConn{unix, address, lnKey, &one} - unixSockets[lnKey] = unix - } - if IsUnixNetwork(na.Network) { if !isAbtractUnixSocket { if err := os.Chmod(address, unixFileMode); err != nil { @@ -470,53 +447,93 @@ func ListenPacket(network, addr string) (net.PacketConn, error) { // unixgram will be used; otherwise, udp will be used). // // NOTE: This API is EXPERIMENTAL and may be changed or removed. -// -// TODO: See if we can find a more elegant solution closer to the new NetworkAddress.Listen API. -func ListenQUIC(ln net.PacketConn, tlsConf *tls.Config, activeRequests *int64) (http3.QUICEarlyListener, error) { - lnKey := listenerKey("quic+"+ln.LocalAddr().Network(), ln.LocalAddr().String()) +func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config net.ListenConfig, tlsConf *tls.Config, activeRequests *int64) (http3.QUICEarlyListener, error) { + lnKey := listenerKey("quic"+na.Network, na.JoinHostPort(portOffset)) sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) { - sqtc := newSharedQUICTLSConfig(tlsConf) + lnAny, err := na.Listen(ctx, portOffset, config) + if err != nil { + return nil, err + } + + ln := lnAny.(net.PacketConn) + + h3ln := ln + for { + // retrieve the underlying socket, so quic-go can optimize. + if unwrapper, ok := h3ln.(interface{ Unwrap() net.PacketConn }); ok { + h3ln = unwrapper.Unwrap() + } else { + break + } + } + + sqs := newSharedQUICState(tlsConf, activeRequests) // http3.ConfigureTLSConfig only uses this field and tls App sets this field as well //nolint:gosec - quicTlsConfig := &tls.Config{GetConfigForClient: sqtc.getConfigForClient} - earlyLn, err := quic.ListenEarly(ln, http3.ConfigureTLSConfig(quicTlsConfig), &quic.Config{ + quicTlsConfig := &tls.Config{GetConfigForClient: sqs.getConfigForClient} + earlyLn, err := quic.ListenEarly(h3ln, http3.ConfigureTLSConfig(quicTlsConfig), &quic.Config{ Allow0RTT: true, RequireAddressValidation: func(clientAddr net.Addr) bool { - var highLoad bool - if activeRequests != nil { - highLoad = atomic.LoadInt64(activeRequests) > 1000 // TODO: make tunable? - } - return highLoad + // TODO: make tunable? + return sqs.getActiveRequests() > 1000 }, }) if err != nil { return nil, err } - return &sharedQuicListener{EarlyListener: earlyLn, sqtc: sqtc, key: lnKey}, nil + // using the original net.PacketConn to close them properly + return &sharedQuicListener{EarlyListener: earlyLn, packetConn: ln, sqs: sqs, key: lnKey}, nil }) if err != nil { return nil, err } sql := sharedEarlyListener.(*sharedQuicListener) - // add current tls.Config to sqtc, so GetConfigForClient will always return the latest tls.Config in case of context cancellation - ctx, cancel := sql.sqtc.addTLSConfig(tlsConf) - - // TODO: to serve QUIC over a unix socket, currently we need to hold onto - // the underlying net.PacketConn (which we wrap as unixConn to keep count - // of closes) because closing the quic.EarlyListener doesn't actually close - // the underlying PacketConn, but we need to for unix sockets since we dup - // the file descriptor and thus need to close the original; track issue: - // https://github.com/quic-go/quic-go/issues/3560#issuecomment-1258959608 - var unix *unixConn - if uc, ok := ln.(*unixConn); ok { - unix = uc - } + // add current tls.Config to sqs, so GetConfigForClient will always return the latest tls.Config in case of context cancellation, + // and the request counter will reflect current http server + ctx, cancel := sql.sqs.addState(tlsConf, activeRequests) + + return &fakeCloseQuicListener{ + sharedQuicListener: sql, + context: ctx, + contextCancel: cancel, + }, nil +} + +// DEPRECATED: Use NetworkAddress.ListenQUIC instead. This function will likely be changed or removed in the future. +// TODO: See if we can find a more elegant solution closer to the new NetworkAddress.Listen API. +func ListenQUIC(ln net.PacketConn, tlsConf *tls.Config, activeRequests *int64) (http3.QUICEarlyListener, error) { + lnKey := listenerKey("quic+"+ln.LocalAddr().Network(), ln.LocalAddr().String()) + + sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) { + sqs := newSharedQUICState(tlsConf, activeRequests) + // http3.ConfigureTLSConfig only uses this field and tls App sets this field as well + //nolint:gosec + quicTlsConfig := &tls.Config{GetConfigForClient: sqs.getConfigForClient} + earlyLn, err := quic.ListenEarly(ln, http3.ConfigureTLSConfig(quicTlsConfig), &quic.Config{ + Allow0RTT: true, + RequireAddressValidation: func(clientAddr net.Addr) bool { + // TODO: make tunable? + return sqs.getActiveRequests() > 1000 + }, + }) + if err != nil { + return nil, err + } + return &sharedQuicListener{EarlyListener: earlyLn, sqs: sqs, key: lnKey}, nil + }) + if err != nil { + return nil, err + } + + sql := sharedEarlyListener.(*sharedQuicListener) + // add current tls.Config and request counter to sqs, so GetConfigForClient will always return the latest tls.Config in case of context cancellation, + // and the request counter will reflect current http server + ctx, cancel := sql.sqs.addState(tlsConf, activeRequests) return &fakeCloseQuicListener{ sharedQuicListener: sql, - uc: unix, context: ctx, contextCancel: cancel, }, nil @@ -534,38 +551,50 @@ type contextAndCancelFunc struct { context.CancelFunc } -// sharedQUICTLSConfig manages GetConfigForClient +// sharedQUICState manages GetConfigForClient and current number of active requests // see issue: https://github.com/caddyserver/caddy/pull/4849 -type sharedQUICTLSConfig struct { - rmu sync.RWMutex - tlsConfs map[*tls.Config]contextAndCancelFunc - activeTlsConf *tls.Config +type sharedQUICState struct { + rmu sync.RWMutex + tlsConfs map[*tls.Config]contextAndCancelFunc + requestCounters map[*tls.Config]*int64 + activeTlsConf *tls.Config + activeRequestsCounter *int64 } -// newSharedQUICTLSConfig creates a new sharedQUICTLSConfig -func newSharedQUICTLSConfig(tlsConfig *tls.Config) *sharedQUICTLSConfig { - sqtc := &sharedQUICTLSConfig{ - tlsConfs: make(map[*tls.Config]contextAndCancelFunc), - activeTlsConf: tlsConfig, +// newSharedQUICState creates a new sharedQUICState +func newSharedQUICState(tlsConfig *tls.Config, activeRequests *int64) *sharedQUICState { + sqtc := &sharedQUICState{ + tlsConfs: make(map[*tls.Config]contextAndCancelFunc), + requestCounters: make(map[*tls.Config]*int64), + activeTlsConf: tlsConfig, + activeRequestsCounter: activeRequests, } - sqtc.addTLSConfig(tlsConfig) + sqtc.addState(tlsConfig, activeRequests) return sqtc } // getConfigForClient is used as tls.Config's GetConfigForClient field -func (sqtc *sharedQUICTLSConfig) getConfigForClient(ch *tls.ClientHelloInfo) (*tls.Config, error) { - sqtc.rmu.RLock() - defer sqtc.rmu.RUnlock() - return sqtc.activeTlsConf.GetConfigForClient(ch) +func (sqs *sharedQUICState) getConfigForClient(ch *tls.ClientHelloInfo) (*tls.Config, error) { + sqs.rmu.RLock() + defer sqs.rmu.RUnlock() + return sqs.activeTlsConf.GetConfigForClient(ch) } -// addTLSConfig adds tls.Config to the map if not present and returns the corresponding context and its cancelFunc -// so that when cancelled, the active tls.Config will change -func (sqtc *sharedQUICTLSConfig) addTLSConfig(tlsConfig *tls.Config) (context.Context, context.CancelFunc) { - sqtc.rmu.Lock() - defer sqtc.rmu.Unlock() +// getActiveRequests returns the number of active requests +func (sqs *sharedQUICState) getActiveRequests() int64 { + // Prevent a race when a context is cancelled and active request counter is being changed + sqs.rmu.RLock() + defer sqs.rmu.RUnlock() + return atomic.LoadInt64(sqs.activeRequestsCounter) +} - if cacc, ok := sqtc.tlsConfs[tlsConfig]; ok { +// addState adds tls.Config and activeRequests to the map if not present and returns the corresponding context and its cancelFunc +// so that when cancelled, the active tls.Config and request counter will change +func (sqs *sharedQUICState) addState(tlsConfig *tls.Config, activeRequests *int64) (context.Context, context.CancelFunc) { + sqs.rmu.Lock() + defer sqs.rmu.Unlock() + + if cacc, ok := sqs.tlsConfs[tlsConfig]; ok { return cacc.Context, cacc.CancelFunc } @@ -573,23 +602,26 @@ func (sqtc *sharedQUICTLSConfig) addTLSConfig(tlsConfig *tls.Config) (context.Co wrappedCancel := func() { cancel() - sqtc.rmu.Lock() - defer sqtc.rmu.Unlock() + sqs.rmu.Lock() + defer sqs.rmu.Unlock() - delete(sqtc.tlsConfs, tlsConfig) - if sqtc.activeTlsConf == tlsConfig { - // select another tls.Config, if there is none, + delete(sqs.tlsConfs, tlsConfig) + delete(sqs.requestCounters, tlsConfig) + if sqs.activeTlsConf == tlsConfig { + // select another tls.Config and request counter, if there is none, // related sharedQuicListener will be destroyed anyway - for tc := range sqtc.tlsConfs { - sqtc.activeTlsConf = tc + for tc, counter := range sqs.requestCounters { + sqs.activeTlsConf = tc + sqs.activeRequestsCounter = counter break } } } - sqtc.tlsConfs[tlsConfig] = contextAndCancelFunc{ctx, wrappedCancel} + sqs.tlsConfs[tlsConfig] = contextAndCancelFunc{ctx, wrappedCancel} + sqs.requestCounters[tlsConfig] = activeRequests // there should be at most 2 tls.Configs - if len(sqtc.tlsConfs) > 2 { - Log().Warn("quic listener tls configs are more than 2", zap.Int("number of configs", len(sqtc.tlsConfs))) + if len(sqs.tlsConfs) > 2 { + Log().Warn("quic listener tls configs are more than 2", zap.Int("number of configs", len(sqs.tlsConfs))) } return ctx, wrappedCancel } @@ -597,24 +629,17 @@ func (sqtc *sharedQUICTLSConfig) addTLSConfig(tlsConfig *tls.Config) (context.Co // sharedQuicListener is like sharedListener, but for quic.EarlyListeners. type sharedQuicListener struct { *quic.EarlyListener - sqtc *sharedQUICTLSConfig - key string + packetConn net.PacketConn // we have to hold these because quic-go won't close listeners it didn't create + sqs *sharedQUICState + key string } -// Destruct closes the underlying QUIC listener. +// Destruct closes the underlying QUIC listener and its associated net.PacketConn. func (sql *sharedQuicListener) Destruct() error { - return sql.EarlyListener.Close() -} - -// sharedPacketConn is like sharedListener, but for net.PacketConns. -type sharedPacketConn struct { - net.PacketConn - key string -} - -// Destruct closes the underlying socket. -func (spc *sharedPacketConn) Destruct() error { - return spc.PacketConn.Close() + // close EarlyListener first to stop any operations being done to the net.PacketConn + _ = sql.EarlyListener.Close() + // then close the net.PacketConn + return sql.packetConn.Close() } // fakeClosedErr returns an error value that is not temporary @@ -636,34 +661,9 @@ func fakeClosedErr(l interface{ Addr() net.Addr }) error { // socket is actually left open. var errFakeClosed = fmt.Errorf("listener 'closed' 😉") -// fakeClosePacketConn is like fakeCloseListener, but for PacketConns, -// or more specifically, *net.UDPConn -type fakeClosePacketConn struct { - closed int32 // accessed atomically; belongs to this struct only - spc *sharedPacketConn // its key is used in Close - *net.UDPConn // embedded, so we also become a net.PacketConn and enable several other optimizations done by quic-go -} - -// interface guard for extra optimizations -// needed by QUIC implementation: https://github.com/caddyserver/caddy/issues/3998, https://github.com/caddyserver/caddy/issues/5605 -var _ quic.OOBCapablePacketConn = (*fakeClosePacketConn)(nil) - -// https://pkg.go.dev/golang.org/x/net/ipv4#NewPacketConn is used by quic-go and requires a net.PacketConn type assertable to a net.Conn, -// but doesn't actually use these methods, the only methods needed are `ReadMsgUDP` and `SyscallConn`. -var _ net.Conn = (*fakeClosePacketConn)(nil) - -// Close won't close the underlying socket unless there is no more reference, then listenerPool will close it. -func (fcpc *fakeClosePacketConn) Close() error { - if atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) { - _, _ = listenerPool.Delete(fcpc.spc.key) - } - return nil -} - type fakeCloseQuicListener struct { - closed int32 // accessed atomically; belongs to this struct only - *sharedQuicListener // embedded, so we also become a quic.EarlyListener - uc *unixConn // underlying unix socket, if UDS + closed int32 // accessed atomically; belongs to this struct only + *sharedQuicListener // embedded, so we also become a quic.EarlyListener context context.Context contextCancel context.CancelFunc } @@ -690,11 +690,6 @@ func (fcql *fakeCloseQuicListener) Close() error { if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) { fcql.contextCancel() _, _ = listenerPool.Delete(fcql.sharedQuicListener.key) - if fcql.uc != nil { - // unix sockets need to be closed ourselves because we dup() the file - // descriptor when we reuse them, so this avoids a resource leak - fcql.uc.Close() - } } return nil } @@ -720,34 +715,7 @@ func RegisterNetwork(network string, getListener ListenerFunc) { networkTypes[network] = getListener } -type unixConn struct { - *net.UnixConn - filename string - mapKey string - count *int32 // accessed atomically -} - -func (uc *unixConn) Close() error { - newCount := atomic.AddInt32(uc.count, -1) - if newCount == 0 { - defer func() { - unixSocketsMu.Lock() - delete(unixSockets, uc.mapKey) - unixSocketsMu.Unlock() - _ = syscall.Unlink(uc.filename) - }() - } - return uc.UnixConn.Close() -} - -// unixSockets keeps track of the currently-active unix sockets -// so we can transfer their FDs gracefully during reloads. -var ( - unixSockets = make(map[string]interface { - File() (*os.File, error) - }) - unixSocketsMu sync.Mutex -) +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 @@ -791,11 +759,3 @@ type ListenerWrapper interface { var listenerPool = NewUsagePool() const maxPortSpan = 65535 - -// Interface guards (see https://github.com/caddyserver/caddy/issues/3998) -var ( - _ (interface{ SetReadBuffer(int) error }) = (*fakeClosePacketConn)(nil) - _ (interface { - SyscallConn() (syscall.RawConn, error) - }) = (*fakeClosePacketConn)(nil) -) diff --git a/logging.go b/logging.go index 98b031c6a..a076f062c 100644 --- a/logging.go +++ b/logging.go @@ -150,12 +150,18 @@ func (logging *Logging) setupNewDefault(ctx Context) error { logging.Logs[DefaultLoggerName] = newDefault.CustomLog } - // set up this new log - err := newDefault.CustomLog.provision(ctx, logging) + // options for the default logger + options, err := newDefault.CustomLog.buildOptions() if err != nil { return fmt.Errorf("setting up default log: %v", err) } - newDefault.logger = zap.New(newDefault.CustomLog.core) + + // set up this new log + err = newDefault.CustomLog.provision(ctx, logging) + if err != nil { + return fmt.Errorf("setting up default log: %v", err) + } + newDefault.logger = zap.New(newDefault.CustomLog.core, options...) // redirect the default caddy logs defaultLoggerMu.Lock() @@ -201,6 +207,7 @@ func (logging *Logging) closeLogs() error { func (logging *Logging) Logger(mod Module) *zap.Logger { modID := string(mod.CaddyModule().ID) var cores []zapcore.Core + var options []zap.Option if logging != nil { for _, l := range logging.Logs { @@ -209,6 +216,13 @@ func (logging *Logging) Logger(mod Module) *zap.Logger { cores = append(cores, l.core) continue } + if len(options) == 0 { + newOptions, err := l.buildOptions() + if err != nil { + Log().Error("building options for logger", zap.String("module", modID), zap.Error(err)) + } + options = newOptions + } cores = append(cores, &filteringCore{Core: l.core, cl: l}) } } @@ -216,7 +230,7 @@ func (logging *Logging) Logger(mod Module) *zap.Logger { multiCore := zapcore.NewTee(cores...) - return zap.New(multiCore).Named(modID) + return zap.New(multiCore, options...).Named(modID) } // openWriter opens a writer using opener, and returns true if @@ -251,6 +265,17 @@ type WriterOpener interface { OpenWriter() (io.WriteCloser, error) } +// IsWriterStandardStream returns true if the input is a +// writer-opener to a standard stream (stdout, stderr). +func IsWriterStandardStream(wo WriterOpener) bool { + switch wo.(type) { + case StdoutWriter, StderrWriter, + *StdoutWriter, *StderrWriter: + return true + } + return false +} + type writerDestructor struct { io.WriteCloser } @@ -277,6 +302,20 @@ type BaseLog struct { // servers. Sampling *LogSampling `json:"sampling,omitempty"` + // If true, the log entry will include the caller's + // file name and line number. Default off. + WithCaller bool `json:"with_caller,omitempty"` + + // If non-zero, and `with_caller` is true, this many + // stack frames will be skipped when determining the + // caller. Default 0. + WithCallerSkip int `json:"with_caller_skip,omitempty"` + + // If not empty, the log entry will include a stack trace + // for all logs at the given level or higher. See `level` + // for possible values. Default off. + WithStacktrace string `json:"with_stacktrace,omitempty"` + writerOpener WriterOpener writer io.WriteCloser encoder zapcore.Encoder @@ -301,29 +340,10 @@ func (cl *BaseLog) provisionCommon(ctx Context, logging *Logging) error { return fmt.Errorf("opening log writer using %#v: %v", cl.writerOpener, err) } - repl := NewReplacer() - level, err := repl.ReplaceOrErr(cl.Level, true, true) - if err != nil { - return fmt.Errorf("invalid log level: %v", err) - } - level = strings.ToLower(level) - // set up the log level - switch level { - case "debug": - cl.levelEnabler = zapcore.DebugLevel - case "", "info": - cl.levelEnabler = zapcore.InfoLevel - case "warn": - cl.levelEnabler = zapcore.WarnLevel - case "error": - cl.levelEnabler = zapcore.ErrorLevel - case "panic": - cl.levelEnabler = zapcore.PanicLevel - case "fatal": - cl.levelEnabler = zapcore.FatalLevel - default: - return fmt.Errorf("unrecognized log level: %s", cl.Level) + cl.levelEnabler, err = parseLevel(cl.Level) + if err != nil { + return err } if cl.EncoderRaw != nil { @@ -332,16 +352,18 @@ func (cl *BaseLog) provisionCommon(ctx Context, logging *Logging) error { return fmt.Errorf("loading log encoder module: %v", err) } cl.encoder = mod.(zapcore.Encoder) + + // if the encoder module needs the writer to determine + // the correct default to use for a nested encoder, we + // pass it down as a secondary provisioning step + if cfd, ok := mod.(ConfiguresFormatterDefault); ok { + if err := cfd.ConfigureDefaultFormat(cl.writerOpener); err != nil { + return fmt.Errorf("configuring default format for encoder module: %v", err) + } + } } if cl.encoder == nil { - // only allow colorized output if this log is going to stdout or stderr - var colorize bool - switch cl.writerOpener.(type) { - case StdoutWriter, StderrWriter, - *StdoutWriter, *StderrWriter: - colorize = true - } - cl.encoder = newDefaultProductionLogEncoder(colorize) + cl.encoder = newDefaultProductionLogEncoder(cl.writerOpener) } cl.buildCore() return nil @@ -376,6 +398,24 @@ func (cl *BaseLog) buildCore() { cl.core = c } +func (cl *BaseLog) buildOptions() ([]zap.Option, error) { + var options []zap.Option + if cl.WithCaller { + options = append(options, zap.AddCaller()) + if cl.WithCallerSkip != 0 { + options = append(options, zap.AddCallerSkip(cl.WithCallerSkip)) + } + } + if cl.WithStacktrace != "" { + levelEnabler, err := parseLevel(cl.WithStacktrace) + if err != nil { + return options, fmt.Errorf("setting up default Caddy log: %v", err) + } + options = append(options, zap.AddStacktrace(levelEnabler)) + } + return options, nil +} + // SinkLog configures the default Go standard library // global logger in the log package. This is necessary because // module dependencies which are not built specifically for @@ -389,7 +429,14 @@ func (sll *SinkLog) provision(ctx Context, logging *Logging) error { if err := sll.provisionCommon(ctx, logging); err != nil { return err } - ctx.cleanupFuncs = append(ctx.cleanupFuncs, zap.RedirectStdLog(zap.New(sll.core))) + + options, err := sll.buildOptions() + if err != nil { + return err + } + + logger := zap.New(sll.core, options...) + ctx.cleanupFuncs = append(ctx.cleanupFuncs, zap.RedirectStdLog(logger)) return nil } @@ -470,7 +517,7 @@ func (cl *CustomLog) loggerAllowed(name string, isModule bool) bool { // append a dot so that partial names don't match // (i.e. we don't want "foo.b" to match "foo.bar"); we // will also have to append a dot when we do HasPrefix - // below to compensate for when when namespaces are equal + // below to compensate for when namespaces are equal if name != "" && name != "*" && name != "." { name += "." } @@ -646,7 +693,7 @@ func newDefaultProductionLog() (*defaultCustomLog, error) { if err != nil { return nil, err } - cl.encoder = newDefaultProductionLogEncoder(true) + cl.encoder = newDefaultProductionLogEncoder(cl.writerOpener) cl.levelEnabler = zapcore.InfoLevel cl.buildCore() @@ -663,21 +710,49 @@ func newDefaultProductionLog() (*defaultCustomLog, error) { }, nil } -func newDefaultProductionLogEncoder(colorize bool) zapcore.Encoder { +func newDefaultProductionLogEncoder(wo WriterOpener) zapcore.Encoder { encCfg := zap.NewProductionEncoderConfig() - if term.IsTerminal(int(os.Stdout.Fd())) { + if IsWriterStandardStream(wo) && term.IsTerminal(int(os.Stderr.Fd())) { // if interactive terminal, make output more human-readable by default encCfg.EncodeTime = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) { encoder.AppendString(ts.UTC().Format("2006/01/02 15:04:05.000")) } - if colorize { + if coloringEnabled { encCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder } + return zapcore.NewConsoleEncoder(encCfg) } return zapcore.NewJSONEncoder(encCfg) } +func parseLevel(levelInput string) (zapcore.LevelEnabler, error) { + repl := NewReplacer() + level, err := repl.ReplaceOrErr(levelInput, true, true) + if err != nil { + return nil, fmt.Errorf("invalid log level: %v", err) + } + level = strings.ToLower(level) + + // set up the log level + switch level { + case "debug": + return zapcore.DebugLevel, nil + case "", "info": + return zapcore.InfoLevel, nil + case "warn": + return zapcore.WarnLevel, nil + case "error": + return zapcore.ErrorLevel, nil + case "panic": + return zapcore.PanicLevel, nil + case "fatal": + return zapcore.FatalLevel, nil + default: + return nil, fmt.Errorf("unrecognized log level: %s", level) + } +} + // Log returns the current default logger. func Log() *zap.Logger { defaultLoggerMu.RLock() @@ -686,12 +761,22 @@ func Log() *zap.Logger { } var ( + coloringEnabled = os.Getenv("NO_COLOR") == "" && os.Getenv("TERM") != "xterm-mono" defaultLogger, _ = newDefaultProductionLog() defaultLoggerMu sync.RWMutex ) var writers = NewUsagePool() +// ConfiguresFormatterDefault is an optional interface that +// encoder modules can implement to configure the default +// format of their encoder. This is useful for encoders +// which nest an encoder, that needs to know the writer +// in order to determine the correct default. +type ConfiguresFormatterDefault interface { + ConfigureDefaultFormat(WriterOpener) error +} + const DefaultLoggerName = "default" // Interface guards diff --git a/modules/caddyevents/eventsconfig/caddyfile.go b/modules/caddyevents/eventsconfig/caddyfile.go index 9c3fae78c..93a4c3d38 100644 --- a/modules/caddyevents/eventsconfig/caddyfile.go +++ b/modules/caddyevents/eventsconfig/caddyfile.go @@ -40,14 +40,8 @@ func init() { // // If is *, then it will bind to all events. func parseApp(d *caddyfile.Dispenser, _ any) (any, error) { + d.Next() // consume option name app := new(caddyevents.App) - - // consume the option name - if !d.Next() { - return nil, d.ArgErr() - } - - // handle the block for d.NextBlock(0) { switch d.Val() { case "on": diff --git a/modules/caddyfs/filesystem.go b/modules/caddyfs/filesystem.go new file mode 100644 index 000000000..b3361df20 --- /dev/null +++ b/modules/caddyfs/filesystem.go @@ -0,0 +1,112 @@ +package caddyfs + +import ( + "encoding/json" + "fmt" + "io/fs" + + "go.uber.org/zap" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" +) + +func init() { + caddy.RegisterModule(Filesystems{}) + httpcaddyfile.RegisterGlobalOption("filesystem", parseFilesystems) +} + +type moduleEntry struct { + Key string `json:"name,omitempty"` + FileSystemRaw json.RawMessage `json:"file_system,omitempty" caddy:"namespace=caddy.fs inline_key=backend"` + fileSystem fs.FS +} + +// Filesystems loads caddy.fs modules into the global filesystem map +type Filesystems struct { + Filesystems []*moduleEntry `json:"filesystems"` + + defers []func() +} + +func parseFilesystems(d *caddyfile.Dispenser, existingVal any) (any, error) { + p := &Filesystems{} + current, ok := existingVal.(*Filesystems) + if ok { + p = current + } + x := &moduleEntry{} + err := x.UnmarshalCaddyfile(d) + if err != nil { + return nil, err + } + p.Filesystems = append(p.Filesystems, x) + return p, nil +} + +// CaddyModule returns the Caddy module information. +func (Filesystems) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "caddy.filesystems", + New: func() caddy.Module { return new(Filesystems) }, + } +} + +func (xs *Filesystems) Start() error { return nil } +func (xs *Filesystems) Stop() error { return nil } + +func (xs *Filesystems) Provision(ctx caddy.Context) error { + // load the filesystem module + for _, f := range xs.Filesystems { + if len(f.FileSystemRaw) > 0 { + mod, err := ctx.LoadModule(f, "FileSystemRaw") + if err != nil { + return fmt.Errorf("loading file system module: %v", err) + } + f.fileSystem = mod.(fs.FS) + } + // register that module + ctx.Logger().Debug("registering fs", zap.String("fs", f.Key)) + 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) + }) + } + return nil +} + +func (f *Filesystems) Cleanup() error { + for _, v := range f.defers { + v() + } + return nil +} + +func (f *moduleEntry) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + // key required for now + if !d.Args(&f.Key) { + return d.ArgErr() + } + // get the module json + if !d.NextArg() { + return d.ArgErr() + } + name := d.Val() + modID := "caddy.fs." + name + unm, err := caddyfile.UnmarshalModule(d, modID) + if err != nil { + return err + } + fsys, ok := unm.(fs.FS) + if !ok { + return d.Errf("module %s (%T) is not a supported file system implementation (requires fs.FS)", modID, unm) + } + f.FileSystemRaw = caddyconfig.JSONModuleObject(fsys, "backend", name, nil) + } + return nil +} diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index 457a5f4d3..cdb368c4f 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -20,9 +20,7 @@ import ( "fmt" "net" "net/http" - "runtime" "strconv" - "strings" "sync" "time" @@ -73,7 +71,7 @@ func init() { // `{http.request.remote.host}` | The host (IP) part of the remote client's address // `{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 +// `{http.request.scheme}` | The request scheme, typically `http` or `https` // `{http.request.tls.version}` | The TLS version name // `{http.request.tls.cipher_suite}` | The TLS cipher suite // `{http.request.tls.resumed}` | The TLS connection resumed a previous connection @@ -328,15 +326,9 @@ func (app *App) Provision(ctx caddy.Context) error { // Validate ensures the app's configuration is valid. func (app *App) Validate() error { - isGo120 := strings.Contains(runtime.Version(), "go1.20") - // each server must use distinct listener addresses lnAddrs := make(map[string]string) for srvName, srv := range app.Servers { - if isGo120 && srv.EnableFullDuplex { - app.logger.Warn("enable_full_duplex is not supported in Go 1.20, use a build made with Go 1.21 or later", zap.String("server", srvName)) - } - for _, addr := range srv.Listen { listenAddr, err := caddy.ParseNetworkAddress(addr) if err != nil { @@ -378,11 +370,7 @@ func (app *App) Start() error { return context.WithValue(ctx, ConnCtxKey, c) }, } - h2server := &http2.Server{ - NewWriteScheduler: func() http2.WriteScheduler { - return http2.NewPriorityWriteScheduler(nil) - }, - } + h2server := new(http2.Server) // disable HTTP/2, which we enabled by default during provisioning if !srv.protocol("h2") { @@ -617,17 +605,6 @@ func (app *App) Stop() error { zap.Error(err), zap.Strings("addresses", server.Listen)) } - - // TODO: we have to manually close our listeners because quic-go won't - // close listeners it didn't create along with the server itself... - // see https://github.com/quic-go/quic-go/issues/3560 - for _, el := range server.h3listeners { - if err := el.Close(); err != nil { - app.logger.Error("HTTP/3 listener close", - zap.Error(err), - zap.String("address", el.LocalAddr().String())) - } - } } stopH2Listener := func(server *Server) { defer finishedShutdown.Done() @@ -665,6 +642,15 @@ func (app *App) Stop() error { finishedShutdown.Wait() } + // run stop callbacks now that the server shutdowns are complete + for name, s := range app.Servers { + for _, stopHook := range s.onStopFuncs { + if err := stopHook(ctx); err != nil { + app.logger.Error("server stop hook", zap.String("server", name), zap.Error(err)) + } + } + } + return nil } diff --git a/modules/caddyhttp/caddyauth/basicauth.go b/modules/caddyhttp/caddyauth/basicauth.go index f30a8691a..52a5a08c1 100644 --- a/modules/caddyhttp/caddyauth/basicauth.go +++ b/modules/caddyhttp/caddyauth/basicauth.go @@ -108,7 +108,6 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error { acct.Username = repl.ReplaceAll(acct.Username, "") acct.Password = repl.ReplaceAll(acct.Password, "") - acct.Salt = repl.ReplaceAll(acct.Salt, "") if acct.Username == "" || acct.Password == "" { return fmt.Errorf("account %d: username and password are required", i) @@ -127,13 +126,6 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error { } } - if acct.Salt != "" { - acct.salt, err = base64.StdEncoding.DecodeString(acct.Salt) - if err != nil { - return fmt.Errorf("base64-decoding salt: %v", err) - } - } - hba.Accounts[acct.Username] = acct } hba.AccountList = nil // allow GC to deallocate @@ -172,7 +164,7 @@ func (hba HTTPBasicAuth) Authenticate(w http.ResponseWriter, req *http.Request) func (hba HTTPBasicAuth) correctPassword(account Account, plaintextPassword []byte) (bool, error) { compare := func() (bool, error) { - return hba.Hash.Compare(account.password, plaintextPassword, account.salt) + return hba.Hash.Compare(account.password, plaintextPassword) } // if no caching is enabled, simply return the result of hashing + comparing @@ -181,7 +173,7 @@ func (hba HTTPBasicAuth) correctPassword(account Account, plaintextPassword []by } // compute a cache key that is unique for these input parameters - cacheKey := hex.EncodeToString(append(append(account.password, account.salt...), plaintextPassword...)) + cacheKey := hex.EncodeToString(append(account.password, plaintextPassword...)) // fast track: if the result of the input is already cached, use it hba.HashCache.mu.RLock() @@ -231,7 +223,7 @@ type Cache struct { mu *sync.RWMutex g *singleflight.Group - // map of concatenated hashed password + plaintext password + salt, to result + // map of concatenated hashed password + plaintext password, to result cache map[string]bool } @@ -274,37 +266,33 @@ func (c *Cache) makeRoom() { // comparison. type Comparer interface { // Compare returns true if the result of hashing - // plaintextPassword with salt is hashedPassword, - // false otherwise. An error is returned only if + // plaintextPassword is hashedPassword, false + // otherwise. An error is returned only if // there is a technical/configuration error. - Compare(hashedPassword, plaintextPassword, salt []byte) (bool, error) + Compare(hashedPassword, plaintextPassword []byte) (bool, error) } // Hasher is a type that can generate a secure hash -// given a plaintext and optional salt (for algorithms -// that require a salt). Hashing modules which implement +// given a plaintext. Hashing modules which implement // this interface can be used with the hash-password // subcommand as well as benefitting from anti-timing // features. A hasher also returns a fake hash which // can be used for timing side-channel mitigation. type Hasher interface { - Hash(plaintext, salt []byte) ([]byte, error) + Hash(plaintext []byte) ([]byte, error) FakeHash() []byte } -// Account contains a username, password, and salt (if applicable). +// Account contains a username and password. type Account struct { // A user's username. Username string `json:"username"` - // The user's hashed password, base64-encoded. + // The user's hashed password, in Modular Crypt Format (with `$` prefix) + // or base64-encoded. Password string `json:"password"` - // The user's password salt, base64-encoded; for - // algorithms where external salt is needed. - Salt string `json:"salt,omitempty"` - - password, salt []byte + password []byte } // Interface guards diff --git a/modules/caddyhttp/caddyauth/caddyfile.go b/modules/caddyhttp/caddyauth/caddyfile.go index 05c023209..cc92477e5 100644 --- a/modules/caddyhttp/caddyauth/caddyfile.go +++ b/modules/caddyhttp/caddyauth/caddyfile.go @@ -22,68 +22,71 @@ import ( ) func init() { - httpcaddyfile.RegisterHandlerDirective("basicauth", parseCaddyfile) + httpcaddyfile.RegisterHandlerDirective("basicauth", parseCaddyfile) // deprecated + httpcaddyfile.RegisterHandlerDirective("basic_auth", parseCaddyfile) } // parseCaddyfile sets up the handler from Caddyfile tokens. Syntax: // -// basicauth [] [ []] { -// [] +// basic_auth [] [ []] { +// // ... // } // // If no hash algorithm is supplied, bcrypt will be assumed. func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + h.Next() // consume directive name + + // "basicauth" is deprecated, replaced by "basic_auth" + if h.Val() == "basicauth" { + caddy.Log().Named("config.adapter.caddyfile").Warn("the 'basicauth' directive is deprecated, please use 'basic_auth' instead!") + } + var ba HTTPBasicAuth ba.HashCache = new(Cache) - for h.Next() { - var cmp Comparer - args := h.RemainingArgs() + var cmp Comparer + args := h.RemainingArgs() - var hashName string - switch len(args) { - case 0: - hashName = "bcrypt" - case 1: - hashName = args[0] - case 2: - hashName = args[0] - ba.Realm = args[1] - default: + var hashName string + switch len(args) { + case 0: + hashName = "bcrypt" + case 1: + hashName = args[0] + case 2: + hashName = args[0] + ba.Realm = args[1] + default: + return nil, h.ArgErr() + } + + switch hashName { + case "bcrypt": + cmp = BcryptHash{} + default: + return nil, h.Errf("unrecognized hash algorithm: %s", hashName) + } + + ba.HashRaw = caddyconfig.JSONModuleObject(cmp, "algorithm", hashName, nil) + + for h.NextBlock(0) { + username := h.Val() + + var b64Pwd string + h.Args(&b64Pwd) + if h.NextArg() { return nil, h.ArgErr() } - switch hashName { - case "bcrypt": - cmp = BcryptHash{} - case "scrypt": - cmp = ScryptHash{} - default: - return nil, h.Errf("unrecognized hash algorithm: %s", hashName) + if username == "" || b64Pwd == "" { + return nil, h.Err("username and password cannot be empty or missing") } - ba.HashRaw = caddyconfig.JSONModuleObject(cmp, "algorithm", hashName, nil) - - for h.NextBlock(0) { - username := h.Val() - - var b64Pwd, b64Salt string - h.Args(&b64Pwd, &b64Salt) - if h.NextArg() { - return nil, h.ArgErr() - } - - if username == "" || b64Pwd == "" { - return nil, h.Err("username and password cannot be empty or missing") - } - - ba.AccountList = append(ba.AccountList, Account{ - Username: username, - Password: b64Pwd, - Salt: b64Salt, - }) - } + ba.AccountList = append(ba.AccountList, Account{ + Username: username, + Password: b64Pwd, + }) } return Authentication{ diff --git a/modules/caddyhttp/caddyauth/command.go b/modules/caddyhttp/caddyauth/command.go index b93b7a402..c9f440060 100644 --- a/modules/caddyhttp/caddyauth/command.go +++ b/modules/caddyhttp/caddyauth/command.go @@ -17,7 +17,6 @@ package caddyauth import ( "bufio" "bytes" - "encoding/base64" "fmt" "os" "os/signal" @@ -33,7 +32,7 @@ import ( func init() { caddycmd.RegisterCommand(caddycmd.Command{ Name: "hash-password", - Usage: "[--algorithm ] [--salt ] [--plaintext ]", + Usage: "[--plaintext ] [--algorithm ]", Short: "Hashes a password and writes base64", Long: ` Convenient way to hash a plaintext password. The resulting @@ -43,17 +42,10 @@ hash is written to stdout as a base64 string. Caddy is attached to a controlling tty, the plaintext will not be echoed. ---algorithm may be bcrypt or scrypt. If scrypt, the default -parameters are used. - -Use the --salt flag for algorithms which require a salt to -be provided (scrypt). - -Note that scrypt is deprecated. Please use 'bcrypt' instead. +--algorithm currently only supports 'bcrypt', and is the default. `, CobraFunc: func(cmd *cobra.Command) { cmd.Flags().StringP("plaintext", "p", "", "The plaintext password") - cmd.Flags().StringP("salt", "s", "", "The password salt") cmd.Flags().StringP("algorithm", "a", "bcrypt", "Name of the hash algorithm") cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdHashPassword) }, @@ -65,7 +57,6 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) { algorithm := fs.String("algorithm") plaintext := []byte(fs.String("plaintext")) - salt := []byte(fs.String("salt")) if len(plaintext) == 0 { fd := int(os.Stdin.Fd()) @@ -117,13 +108,8 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) { var hashString string switch algorithm { case "bcrypt": - hash, err = BcryptHash{}.Hash(plaintext, nil) + hash, err = BcryptHash{}.Hash(plaintext) hashString = string(hash) - case "scrypt": - def := ScryptHash{} - def.SetDefaults() - hash, err = def.Hash(plaintext, salt) - hashString = base64.StdEncoding.EncodeToString(hash) default: return caddy.ExitCodeFailedStartup, fmt.Errorf("unrecognized hash algorithm: %s", algorithm) } diff --git a/modules/caddyhttp/caddyauth/hashes.go b/modules/caddyhttp/caddyauth/hashes.go index 324cf1e1e..ce3df901e 100644 --- a/modules/caddyhttp/caddyauth/hashes.go +++ b/modules/caddyhttp/caddyauth/hashes.go @@ -15,18 +15,13 @@ package caddyauth import ( - "crypto/subtle" - "encoding/base64" - "golang.org/x/crypto/bcrypt" - "golang.org/x/crypto/scrypt" "github.com/caddyserver/caddy/v2" ) func init() { caddy.RegisterModule(BcryptHash{}) - caddy.RegisterModule(ScryptHash{}) } // BcryptHash implements the bcrypt hash. @@ -41,7 +36,7 @@ func (BcryptHash) CaddyModule() caddy.ModuleInfo { } // Compare compares passwords. -func (BcryptHash) Compare(hashed, plaintext, _ []byte) (bool, error) { +func (BcryptHash) Compare(hashed, plaintext []byte) (bool, error) { err := bcrypt.CompareHashAndPassword(hashed, plaintext) if err == bcrypt.ErrMismatchedHashAndPassword { return false, nil @@ -53,7 +48,7 @@ func (BcryptHash) Compare(hashed, plaintext, _ []byte) (bool, error) { } // Hash hashes plaintext using a random salt. -func (BcryptHash) Hash(plaintext, _ []byte) ([]byte, error) { +func (BcryptHash) Hash(plaintext []byte) ([]byte, error) { return bcrypt.GenerateFromPassword(plaintext, 14) } @@ -64,94 +59,8 @@ func (BcryptHash) FakeHash() []byte { return []byte("$2a$14$X3ulqf/iGxnf1k6oMZ.RZeJUoqI9PX2PM4rS5lkIKJXduLGXGPrt6") } -// ScryptHash implements the scrypt KDF as a hash. -// -// DEPRECATED, please use 'bcrypt' instead. -type ScryptHash struct { - // scrypt's N parameter. If unset or 0, a safe default is used. - N int `json:"N,omitempty"` - - // scrypt's r parameter. If unset or 0, a safe default is used. - R int `json:"r,omitempty"` - - // scrypt's p parameter. If unset or 0, a safe default is used. - P int `json:"p,omitempty"` - - // scrypt's key length parameter (in bytes). If unset or 0, a - // safe default is used. - KeyLength int `json:"key_length,omitempty"` -} - -// CaddyModule returns the Caddy module information. -func (ScryptHash) CaddyModule() caddy.ModuleInfo { - return caddy.ModuleInfo{ - ID: "http.authentication.hashes.scrypt", - New: func() caddy.Module { return new(ScryptHash) }, - } -} - -// Provision sets up s. -func (s *ScryptHash) Provision(ctx caddy.Context) error { - s.SetDefaults() - ctx.Logger().Warn("use of 'scrypt' is deprecated, please use 'bcrypt' instead") - return nil -} - -// SetDefaults sets safe default parameters, but does -// not overwrite existing values. Each default parameter -// is set independently; it does not check to ensure -// that r*p < 2^30. The defaults chosen are those as -// recommended in 2019 by -// https://godoc.org/golang.org/x/crypto/scrypt. -func (s *ScryptHash) SetDefaults() { - if s.N == 0 { - s.N = 32768 - } - if s.R == 0 { - s.R = 8 - } - if s.P == 0 { - s.P = 1 - } - if s.KeyLength == 0 { - s.KeyLength = 32 - } -} - -// Compare compares passwords. -func (s ScryptHash) Compare(hashed, plaintext, salt []byte) (bool, error) { - ourHash, err := scrypt.Key(plaintext, salt, s.N, s.R, s.P, s.KeyLength) - if err != nil { - return false, err - } - if hashesMatch(hashed, ourHash) { - return true, nil - } - return false, nil -} - -// Hash hashes plaintext using the given salt. -func (s ScryptHash) Hash(plaintext, salt []byte) ([]byte, error) { - return scrypt.Key(plaintext, salt, s.N, s.R, s.P, s.KeyLength) -} - -// FakeHash returns a fake hash. -func (ScryptHash) FakeHash() []byte { - // hashed with the following command: - // caddy hash-password --plaintext "antitiming" --salt "fakesalt" --algorithm "scrypt" - bytes, _ := base64.StdEncoding.DecodeString("kFbjiVemlwK/ZS0tS6/UQqEDeaNMigyCs48KEsGUse8=") - return bytes -} - -func hashesMatch(pwdHash1, pwdHash2 []byte) bool { - return subtle.ConstantTimeCompare(pwdHash1, pwdHash2) == 1 -} - // Interface guards var ( - _ Comparer = (*BcryptHash)(nil) - _ Comparer = (*ScryptHash)(nil) - _ Hasher = (*BcryptHash)(nil) - _ Hasher = (*ScryptHash)(nil) - _ caddy.Provisioner = (*ScryptHash)(nil) + _ Comparer = (*BcryptHash)(nil) + _ Hasher = (*BcryptHash)(nil) ) diff --git a/modules/caddyhttp/caddyhttp_test.go b/modules/caddyhttp/caddyhttp_test.go index a14de7814..84c0271f1 100644 --- a/modules/caddyhttp/caddyhttp_test.go +++ b/modules/caddyhttp/caddyhttp_test.go @@ -65,6 +65,16 @@ func TestSanitizedPathJoin(t *testing.T) { inputPath: "/%2e%2e%2f%2e%2e%2f", expect: filepath.Join("/", "a", "b") + separator, }, + { + inputRoot: "/a/b", + inputPath: "/foo%2fbar", + expect: filepath.Join("/", "a", "b", "foo", "bar"), + }, + { + inputRoot: "/a/b", + inputPath: "/foo%252fbar", + expect: filepath.Join("/", "a", "b", "foo%2fbar"), + }, { inputRoot: "C:\\www", inputPath: "/foo/bar", diff --git a/modules/caddyhttp/celmatcher.go b/modules/caddyhttp/celmatcher.go index e997336ff..376135364 100644 --- a/modules/caddyhttp/celmatcher.go +++ b/modules/caddyhttp/celmatcher.go @@ -176,13 +176,27 @@ func (m MatchExpression) Match(r *http.Request) bool { // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchExpression) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if d.CountRemainingArgs() > 1 { - m.Expr = strings.Join(d.RemainingArgsRaw(), " ") - } else { - m.Expr = d.Val() - } + d.Next() // consume matcher name + + // if there's multiple args, then we need to keep the raw + // tokens because the user may have used quotes within their + // CEL expression (e.g. strings) and we should retain that + if d.CountRemainingArgs() > 1 { + m.Expr = strings.Join(d.RemainingArgsRaw(), " ") + return nil } + + // there should at least be one arg + if !d.NextArg() { + return d.ArgErr() + } + + // if there's only one token, then we can safely grab the + // cleaned token (no quotes) and use that as the expression + // because there's no valid CEL expression that is only a + // quoted string; commonly quotes are used in Caddyfile to + // define the expression + m.Expr = d.Val() return nil } @@ -367,24 +381,24 @@ func CELMatcherImpl(macroName, funcName string, matcherDataTypes []*cel.Type, fa type CELMatcherFactory func(data ref.Val) (RequestMatcher, error) // matcherCELLibrary is a simplistic configurable cel.Library implementation. -type matcherCELLibary struct { +type matcherCELLibrary struct { envOptions []cel.EnvOption programOptions []cel.ProgramOption } // NewMatcherCELLibrary creates a matcherLibrary from option setes. func NewMatcherCELLibrary(envOptions []cel.EnvOption, programOptions []cel.ProgramOption) cel.Library { - return &matcherCELLibary{ + return &matcherCELLibrary{ envOptions: envOptions, programOptions: programOptions, } } -func (lib *matcherCELLibary) CompileOptions() []cel.EnvOption { +func (lib *matcherCELLibrary) CompileOptions() []cel.EnvOption { return lib.envOptions } -func (lib *matcherCELLibary) ProgramOptions() []cel.ProgramOption { +func (lib *matcherCELLibrary) ProgramOptions() []cel.ProgramOption { return lib.programOptions } diff --git a/modules/caddyhttp/celmatcher_test.go b/modules/caddyhttp/celmatcher_test.go index 3604562b3..e67b87c77 100644 --- a/modules/caddyhttp/celmatcher_test.go +++ b/modules/caddyhttp/celmatcher_test.go @@ -373,22 +373,6 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV urlTarget: "https://example.com/foo", wantResult: true, }, - { - name: "remote_ip forwarded (MatchRemoteIP)", - expression: &MatchExpression{ - Expr: `remote_ip('forwarded', '192.0.2.1')`, - }, - urlTarget: "https://example.com/foo", - wantResult: true, - }, - { - name: "remote_ip forwarded not first (MatchRemoteIP)", - expression: &MatchExpression{ - Expr: `remote_ip('192.0.2.1', 'forwarded')`, - }, - urlTarget: "https://example.com/foo", - wantErr: true, - }, } ) diff --git a/modules/caddyhttp/duplex_go120.go b/modules/caddyhttp/duplex_go120.go deleted file mode 100644 index 065ccf282..000000000 --- a/modules/caddyhttp/duplex_go120.go +++ /dev/null @@ -1,26 +0,0 @@ -// 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 !go1.21 - -package caddyhttp - -import ( - "net/http" -) - -func enableFullDuplex(w http.ResponseWriter) error { - // Do nothing, Go 1.20 and earlier do not support full duplex - return nil -} diff --git a/modules/caddyhttp/duplex_go121.go b/modules/caddyhttp/duplex_go121.go deleted file mode 100644 index a17d3afe7..000000000 --- a/modules/caddyhttp/duplex_go121.go +++ /dev/null @@ -1,26 +0,0 @@ -// 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 go1.21 - -package caddyhttp - -import ( - "net/http" -) - -func enableFullDuplex(w http.ResponseWriter) error { - //nolint:bodyclose - return http.NewResponseController(w).EnableFullDuplex() -} diff --git a/modules/caddyhttp/encode/caddyfile.go b/modules/caddyhttp/encode/caddyfile.go index 25d13f7c6..e8ea4b807 100644 --- a/modules/caddyhttp/encode/caddyfile.go +++ b/modules/caddyhttp/encode/caddyfile.go @@ -54,62 +54,60 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) // // Specifying the formats on the first line will use those formats' defaults. func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - var prefer []string + 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) + } responseMatchers := make(map[string]caddyhttp.ResponseMatcher) - - for d.Next() { - 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) + for d.NextBlock(0) { + switch d.Val() { + case "minimum_length": + if !d.NextArg() { + return d.ArgErr() } - encoding, ok := mod.New().(Encoding) + minLength, err := strconv.Atoi(d.Val()) + if err != nil { + return err + } + enc.MinLength = minLength + case "match": + err := caddyhttp.ParseNamedResponseMatcher(d.NewFromNextSegment(), responseMatchers) + if err != nil { + return err + } + matcher := responseMatchers["match"] + enc.Matcher = &matcher + default: + name := d.Val() + modID := "http.encoders." + name + unm, err := caddyfile.UnmarshalModule(d, modID) + if err != nil { + return err + } + encoding, ok := unm.(Encoding) if !ok { - return d.Errf("module %s is not an HTTP encoding", mod) + return d.Errf("module %s is not an HTTP encoding; is %T", modID, unm) } if enc.EncodingsRaw == nil { enc.EncodingsRaw = make(caddy.ModuleMap) } - enc.EncodingsRaw[arg] = caddyconfig.JSON(encoding, nil) - prefer = append(prefer, arg) - } - - for d.NextBlock(0) { - switch d.Val() { - case "minimum_length": - if !d.NextArg() { - return d.ArgErr() - } - minLength, err := strconv.Atoi(d.Val()) - if err != nil { - return err - } - enc.MinLength = minLength - case "match": - err := caddyhttp.ParseNamedResponseMatcher(d.NewFromNextSegment(), responseMatchers) - if err != nil { - return err - } - matcher := responseMatchers["match"] - enc.Matcher = &matcher - default: - name := d.Val() - modID := "http.encoders." + name - unm, err := caddyfile.UnmarshalModule(d, modID) - if err != nil { - return err - } - encoding, ok := unm.(Encoding) - if !ok { - return d.Errf("module %s is not an HTTP encoding; is %T", modID, unm) - } - if enc.EncodingsRaw == nil { - enc.EncodingsRaw = make(caddy.ModuleMap) - } - enc.EncodingsRaw[name] = caddyconfig.JSON(encoding, nil) - prefer = append(prefer, name) - } + enc.EncodingsRaw[name] = caddyconfig.JSON(encoding, nil) + prefer = append(prefer, name) } } diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go index ed3e59dae..bd3d84bc8 100644 --- a/modules/caddyhttp/encode/encode.go +++ b/modules/caddyhttp/encode/encode.go @@ -84,16 +84,43 @@ func (enc *Encode) Provision(ctx caddy.Context) error { if enc.Matcher == nil { // common text-based content types + // list based on https://developers.cloudflare.com/speed/optimization/content/brotli/content-compression/#compression-between-cloudflare-and-website-visitors enc.Matcher = &caddyhttp.ResponseMatcher{ Headers: http.Header{ "Content-Type": []string{ - "text/*", - "application/json*", - "application/javascript*", - "application/xhtml+xml*", "application/atom+xml*", + "application/eot*", + "application/font*", + "application/geo+json*", + "application/graphql+json*", + "application/javascript*", + "application/json*", + "application/ld+json*", + "application/manifest+json*", + "application/opentype*", + "application/otf*", "application/rss+xml*", + "application/truetype*", + "application/ttf*", + "application/vnd.api+json*", + "application/vnd.ms-fontobject*", + "application/wasm*", + "application/x-httpd-cgi*", + "application/x-javascript*", + "application/x-opentype*", + "application/x-otf*", + "application/x-perl*", + "application/x-protobuf*", + "application/x-ttf*", + "application/xhtml+xml*", + "application/xml*", + "font/*", "image/svg+xml*", + "image/vnd.microsoft.icon*", + "image/x-icon*", + "multipart/bag*", + "multipart/mixed*", + "text/*", }, }, } @@ -310,7 +337,6 @@ func (rw *responseWriter) Unwrap() http.ResponseWriter { func (rw *responseWriter) init() { if rw.Header().Get("Content-Encoding") == "" && isEncodeAllowed(rw.Header()) && 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 diff --git a/modules/caddyhttp/encode/encode_test.go b/modules/caddyhttp/encode/encode_test.go index 3374ee3b5..d76945498 100644 --- a/modules/caddyhttp/encode/encode_test.go +++ b/modules/caddyhttp/encode/encode_test.go @@ -105,7 +105,6 @@ func TestPreferOrder(t *testing.T) { for _, test := range testCases { t.Run(test.name, func(t *testing.T) { - if test.accept == "" { r.Header.Del("Accept-Encoding") } else { @@ -258,7 +257,6 @@ func TestValidate(t *testing.T) { t.Errorf("Validate() error = %v, wantErr = %v", err, test.wantErr) } }) - } } diff --git a/modules/caddyhttp/encode/gzip/gzip.go b/modules/caddyhttp/encode/gzip/gzip.go index 0af38b927..40f37ab8e 100644 --- a/modules/caddyhttp/encode/gzip/gzip.go +++ b/modules/caddyhttp/encode/gzip/gzip.go @@ -44,17 +44,16 @@ func (Gzip) CaddyModule() caddy.ModuleInfo { // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. func (g *Gzip) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if !d.NextArg() { - continue - } - levelStr := d.Val() - level, err := strconv.Atoi(levelStr) - if err != nil { - return err - } - g.Level = level + d.Next() // consume option name + if !d.NextArg() { + return nil } + levelStr := d.Val() + level, err := strconv.Atoi(levelStr) + if err != nil { + return err + } + g.Level = level return nil } diff --git a/modules/caddyhttp/fileserver/browse.go b/modules/caddyhttp/fileserver/browse.go index 917d14fde..86adc7e39 100644 --- a/modules/caddyhttp/fileserver/browse.go +++ b/modules/caddyhttp/fileserver/browse.go @@ -19,6 +19,7 @@ import ( "context" _ "embed" "encoding/json" + "errors" "fmt" "io" "io/fs" @@ -36,16 +37,24 @@ import ( "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates" ) +// BrowseTemplate is the default template document to use for +// file listings. By default, its default value is an embedded +// document. You can override this value at program start, or +// if you are running Caddy via config, you can specify a +// custom template_file in the browse configuration. +// //go:embed browse.html -var defaultBrowseTemplate string +var BrowseTemplate string // Browse configures directory browsing. type Browse struct { - // Use this template file instead of the default browse template. + // 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"` } -func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { +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)) @@ -75,7 +84,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter, } } - dir, err := fsrv.openFile(dirPath, w) + dir, err := fsrv.openFile(fileSystem, dirPath, w) if err != nil { return err } @@ -84,11 +93,11 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter, repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) // TODO: not entirely sure if path.Clean() is necessary here but seems like a safe plan (i.e. /%2e%2e%2f) - someone could verify this - listing, err := fsrv.loadDirectoryContents(r.Context(), dir.(fs.ReadDirFile), root, path.Clean(r.URL.EscapedPath()), repl) + listing, err := fsrv.loadDirectoryContents(r.Context(), fileSystem, dir.(fs.ReadDirFile), root, path.Clean(r.URL.EscapedPath()), repl) switch { - case os.IsPermission(err): + case errors.Is(err, fs.ErrPermission): return caddyhttp.Error(http.StatusForbidden, err) - case os.IsNotExist(err): + case errors.Is(err, fs.ErrNotExist): return fsrv.notFound(w, r, next) case err != nil: return caddyhttp.Error(http.StatusInternalServerError, err) @@ -138,7 +147,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter, return nil } -func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, dir fs.ReadDirFile, root, urlPath string, repl *caddy.Replacer) (*browseTemplateContext, error) { +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 if err != nil && err != io.EOF { return nil, err @@ -147,7 +156,7 @@ func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, dir fs.ReadDi // user can presumably browse "up" to parent folder if path is longer than "/" canGoUp := len(urlPath) > 1 - return fsrv.directoryListing(ctx, files, canGoUp, root, urlPath, repl), nil + return fsrv.directoryListing(ctx, fileSystem, files, canGoUp, root, urlPath, repl), nil } // browseApplyQueryParams applies query parameters to the listing. @@ -205,7 +214,7 @@ func (fsrv *FileServer) makeBrowseTemplate(tplCtx *templateContext) (*template.T } } else { tpl = tplCtx.NewTemplate("default_listing") - tpl, err = tpl.Parse(defaultBrowseTemplate) + tpl, err = tpl.Parse(BrowseTemplate) if err != nil { return nil, fmt.Errorf("parsing default browse template: %v", err) } @@ -216,12 +225,12 @@ func (fsrv *FileServer) makeBrowseTemplate(tplCtx *templateContext) (*template.T // isSymlinkTargetDir returns true if f's symbolic link target // is a directory. -func (fsrv *FileServer) isSymlinkTargetDir(f fs.FileInfo, root, urlPath string) bool { +func (fsrv *FileServer) isSymlinkTargetDir(fileSystem fs.FS, f fs.FileInfo, root, urlPath string) bool { if !isSymlink(f) { return false } target := caddyhttp.SanitizedPathJoin(root, path.Join(urlPath, f.Name())) - targetInfo, err := fs.Stat(fsrv.fileSystem, target) + targetInfo, err := fs.Stat(fileSystem, target) if err != nil { return false } diff --git a/modules/caddyhttp/fileserver/browse.html b/modules/caddyhttp/fileserver/browse.html index 0b558ce5f..484d90020 100644 --- a/modules/caddyhttp/fileserver/browse.html +++ b/modules/caddyhttp/fileserver/browse.html @@ -1,288 +1,296 @@ {{- define "icon"}} {{- if .IsDir}} - - - - + {{- if .IsSymlink}} + + + + + + {{- else}} + + + + + {{- end}} {{- else if or (eq .Name "LICENSE") (eq .Name "README")}} - - - - + + + + {{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp" ".tiff" ".bmp" ".heif" ".heic" ".svg"}} {{- if eq .Tpl.Layout "grid"}} {{- else}} - - - - - + + + + + {{- end}} - {{- else if .HasExt ".mp4" ".mov" ".mpeg" ".mpg" ".avi" ".ogg" ".webm" ".mkv" ".vob" ".gifv" ".3gp"}} + {{- else if .HasExt ".mp4" ".mov" ".m4v" ".mpeg" ".mpg" ".avi" ".ogg" ".webm" ".mkv" ".vob" ".gifv" ".3gp"}} - - - - - - - - - + + + + + + + + + {{- else if .HasExt ".mp3" ".m4a" ".aac" ".ogg" ".flac" ".wav" ".wma" ".midi" ".cda"}} - - - - - + + + + + {{- else if .HasExt ".pdf"}} - - - - - - - + + + + + + + {{- else if .HasExt ".csv" ".tsv"}} - - - - - - + + + + + + {{- else if .HasExt ".txt" ".doc" ".docx" ".odt" ".fodt" ".rtf"}} - - - - - - + + + + + + {{- else if .HasExt ".xls" ".xlsx" ".ods" ".fods"}} - - - - - - + + + + + + {{- else if .HasExt ".ppt" ".pptx" ".odp" ".fodp"}} - - - - - - - - + + + + + + + + {{- else if .HasExt ".zip" ".gz" ".xz" ".tar" ".7z" ".rar" ".xz" ".zst"}} - - - - - - - - - + + + + + + + + + {{- else if .HasExt ".deb" ".dpkg"}} - - - + + + {{- else if .HasExt ".rpm" ".exe" ".flatpak" ".appimage" ".jar" ".msi" ".apk"}} - - - - - - + + + + + + {{- else if .HasExt ".ps1"}} - - - - + + + + {{- else if .HasExt ".py" ".pyc" ".pyo"}} - - - - - - + + + + + + {{- else if .HasExt ".bash" ".sh" ".com" ".bat" ".dll" ".so"}} - - + + {{- else if .HasExt ".dmg"}} - - - - - - + + + + + + {{- else if .HasExt ".iso" ".img"}} - - - - - + + + + + {{- else if .HasExt ".md" ".mdown" ".markdown"}} - - - - + + + + {{- else if .HasExt ".ttf" ".otf" ".woff" ".woff2" ".eof"}} - - - - - - + + + + + + {{- else if .HasExt ".go"}} - - - - - - + + + + + + {{- else if .HasExt ".html" ".htm"}} - - - - - - - - - - + + + + + + + + + + {{- else if .HasExt ".js"}} - - - - - + + + + + {{- else if .HasExt ".css"}} - - - - - - + + + + + + {{- else if .HasExt ".json" ".json5" ".jsonc"}} - - - - - + + + + + {{- else if .HasExt ".ts"}} - - - - - - - + + + + + + + {{- else if .HasExt ".sql"}} - - - - - - - - + + + + + + + + {{- else if .HasExt ".db" ".sqlite" ".bak" ".mdb"}} - - - - + + + + {{- else if .HasExt ".eml" ".email" ".mailbox" ".mbox" ".msg"}} - - - + + + {{- else if .HasExt ".crt" ".pem" ".x509" ".cer" ".ca-bundle"}} - - - - - - - + + + + + + + {{- else if .HasExt ".key" ".keystore" ".jks" ".p12" ".pfx" ".pub"}} - - - + + + {{- else}} {{- if .IsSymlink}} - - - - - + + + + + {{- else}} - - - + + + {{- end}} {{- end}} @@ -291,6 +299,7 @@ {{html .Name}} + @@ -781,6 +790,9 @@ footer { {{.NumFiles}} file{{if ne 1 .NumFiles}}s{{end}} + + {{.HumanTotalFileSize}} total + {{- if ne 0 .Limit}} (of which only {{.Limit}} are displayed) @@ -789,19 +801,19 @@ footer { - - - + + + List - - - - - + + + + + Grid @@ -826,22 +838,22 @@ footer { {{- if and (eq .Sort "namedirfirst") (ne .Order "desc")}} - - + + {{- else if and (eq .Sort "namedirfirst") (ne .Order "asc")}} - - + + {{- else}} - - + + {{- end}} @@ -850,16 +862,16 @@ footer { Name - - + + {{- else if and (eq .Sort "name") (ne .Order "asc")}} Name - - + + {{- else}} @@ -870,11 +882,11 @@ footer {
- - - + + + - +
@@ -882,16 +894,16 @@ footer { Size - - + + {{- else if and (eq .Sort "size") (ne .Order "asc")}} Size - - + + {{- else}} @@ -905,16 +917,16 @@ footer { Modified - - + + {{- else if and (eq .Sort "time") (ne .Order "asc")}} Modified - - + + {{- else}} @@ -933,8 +945,8 @@ footer { - - + + Up @@ -950,7 +962,15 @@ footer { {{template "icon" .}} + {{- if not .SymlinkPath}} {{html .Name}} + {{- else}} + {{html .Name}} + + + + {{html .SymlinkPath}} + {{- end}} {{- if .IsDir}} @@ -1075,7 +1095,10 @@ footer { }); document.querySelectorAll('.size').forEach(el => { const size = Number(el.dataset.size); - el.querySelector('.sizebar-bar').style.width = `${size/largest * 100}%`; + const sizebar = el.querySelector('.sizebar-bar'); + if (sizebar) { + sizebar.style.width = `${size/largest * 100}%`; + } }); } diff --git a/modules/caddyhttp/fileserver/browsetplcontext.go b/modules/caddyhttp/fileserver/browsetplcontext.go index 682273c0a..5456f0597 100644 --- a/modules/caddyhttp/fileserver/browsetplcontext.go +++ b/modules/caddyhttp/fileserver/browsetplcontext.go @@ -20,6 +20,7 @@ import ( "net/url" "os" "path" + "path/filepath" "sort" "strconv" "strings" @@ -32,7 +33,7 @@ import ( "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) -func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEntry, canGoUp bool, root, urlPath string, repl *caddy.Replacer) *browseTemplateContext { +func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS, entries []fs.DirEntry, canGoUp bool, root, urlPath string, repl *caddy.Replacer) *browseTemplateContext { filesToHide := fsrv.transformHidePaths(repl) name, _ := url.PathUnescape(urlPath) @@ -62,7 +63,7 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEn continue } - isDir := entry.IsDir() || fsrv.isSymlinkTargetDir(info, root, urlPath) + isDir := entry.IsDir() || fsrv.isSymlinkTargetDir(fileSystem, info, root, urlPath) // add the slash after the escape of path to avoid escaping the slash as well if isDir { @@ -74,29 +75,43 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEn size := info.Size() fileIsSymlink := isSymlink(info) + symlinkPath := "" if fileIsSymlink { path := caddyhttp.SanitizedPathJoin(root, path.Join(urlPath, info.Name())) - fileInfo, err := fs.Stat(fsrv.fileSystem, path) + fileInfo, err := fs.Stat(fileSystem, path) if err == nil { size = fileInfo.Size() } + + if fsrv.Browse.RevealSymlinks { + symLinkTarget, err := filepath.EvalSymlinks(path) + if err == nil { + symlinkPath = symLinkTarget + } + } + // An error most likely means the symlink target doesn't exist, // which isn't entirely unusual and shouldn't fail the listing. // In this case, just use the size of the symlink itself, which // was already set above. } + if !isDir { + tplCtx.TotalFileSize += size + } + u := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name tplCtx.Items = append(tplCtx.Items, fileInfo{ - IsDir: isDir, - IsSymlink: fileIsSymlink, - Name: name, - Size: size, - URL: u.String(), - ModTime: info.ModTime().UTC(), - Mode: info.Mode(), - Tpl: tplCtx, // a reference up to the template context is useful + IsDir: isDir, + IsSymlink: fileIsSymlink, + Name: name, + Size: size, + URL: u.String(), + ModTime: info.ModTime().UTC(), + Mode: info.Mode(), + Tpl: tplCtx, // a reference up to the template context is useful + SymlinkPath: symlinkPath, }) } @@ -129,6 +144,9 @@ type browseTemplateContext struct { // The number of files (items that aren't directories) in the listing. NumFiles int `json:"num_files"` + // The total size of all files in the listing. + TotalFileSize int64 `json:"total_file_size"` + // Sort column used Sort string `json:"sort,omitempty"` @@ -223,13 +241,14 @@ type crumb struct { // fileInfo contains serializable information // about a file or directory. type fileInfo struct { - Name string `json:"name"` - Size int64 `json:"size"` - URL string `json:"url"` - ModTime time.Time `json:"mod_time"` - Mode os.FileMode `json:"mode"` - IsDir bool `json:"is_dir"` - IsSymlink bool `json:"is_symlink"` + Name string `json:"name"` + Size int64 `json:"size"` + URL string `json:"url"` + ModTime time.Time `json:"mod_time"` + Mode os.FileMode `json:"mode"` + IsDir bool `json:"is_dir"` + IsSymlink bool `json:"is_symlink"` + SymlinkPath string `json:"symlink_path,omitempty"` // a pointer to the template context is useful inside nested templates Tpl *browseTemplateContext `json:"-"` @@ -252,6 +271,13 @@ func (fi fileInfo) HumanSize() string { return humanize.IBytes(uint64(fi.Size)) } +// HumanTotalFileSize returns the total size of all files +// in the listing as a human-readable string in IEC format +// (i.e. power of 2 or base 1024). +func (btc browseTemplateContext) HumanTotalFileSize() string { + return humanize.IBytes(uint64(btc.TotalFileSize)) +} + // HumanModTime returns the modified time of the file // as a human-readable string given by format. func (fi fileInfo) HumanModTime(format string) string { diff --git a/modules/caddyhttp/fileserver/caddyfile.go b/modules/caddyhttp/fileserver/caddyfile.go index df56092b0..6ad9190f3 100644 --- a/modules/caddyhttp/fileserver/caddyfile.go +++ b/modules/caddyhttp/fileserver/caddyfile.go @@ -15,7 +15,6 @@ package fileserver import ( - "io/fs" "path/filepath" "strings" @@ -33,11 +32,26 @@ func init() { httpcaddyfile.RegisterDirective("try_files", parseTryFiles) } -// parseCaddyfile parses the file_server directive. It enables the static file -// server and configures it with this syntax: +// parseCaddyfile parses the file_server directive. +// See UnmarshalCaddyfile for the syntax. +func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + fsrv := new(FileServer) + err := fsrv.UnmarshalCaddyfile(h.Dispenser) + if err != nil { + return fsrv, err + } + err = fsrv.FinalizeUnmarshalCaddyfile(h) + if err != nil { + return nil, err + } + return fsrv, err +} + +// UnmarshalCaddyfile parses the file_server directive. It enables +// the static file server and configures it with this syntax: // // file_server [] [browse] { -// fs +// fs // root // hide // index @@ -46,114 +60,124 @@ func init() { // status // disable_canonical_uris // } -func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { - var fsrv FileServer +// +// The FinalizeUnmarshalCaddyfile method should be called after this +// to finalize setup of hidden Caddyfiles. +func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.Next() // consume directive name - for h.Next() { - args := h.RemainingArgs() - switch len(args) { - case 0: - case 1: - if args[0] != "browse" { - return nil, h.ArgErr() + args := d.RemainingArgs() + switch len(args) { + case 0: + case 1: + if args[0] != "browse" { + return d.ArgErr() + } + fsrv.Browse = new(Browse) + default: + return d.ArgErr() + } + + for d.NextBlock(0) { + switch d.Val() { + case "fs": + if !d.NextArg() { + return d.ArgErr() + } + if fsrv.FileSystem != "" { + return d.Err("file system already specified") + } + fsrv.FileSystem = d.Val() + + case "hide": + fsrv.Hide = d.RemainingArgs() + if len(fsrv.Hide) == 0 { + return d.ArgErr() + } + + case "index": + fsrv.IndexNames = d.RemainingArgs() + if len(fsrv.IndexNames) == 0 { + return d.ArgErr() + } + + case "root": + if !d.Args(&fsrv.Root) { + return d.ArgErr() + } + + case "browse": + if fsrv.Browse != nil { + return d.Err("browsing is already configured") } fsrv.Browse = new(Browse) - default: - return nil, h.ArgErr() - } - - for h.NextBlock(0) { - switch h.Val() { - case "fs": - if !h.NextArg() { - return nil, h.ArgErr() + d.Args(&fsrv.Browse.TemplateFile) + for nesting := d.Nesting(); d.NextBlock(nesting); { + if d.Val() != "reveal_symlinks" { + return d.Errf("unknown subdirective '%s'", d.Val()) } - if fsrv.FileSystemRaw != nil { - return nil, h.Err("file system module already specified") + if fsrv.Browse.RevealSymlinks { + return d.Err("Symlinks path reveal is already enabled") } - name := h.Val() - modID := "caddy.fs." + name - unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID) - if err != nil { - return nil, err - } - fsys, ok := unm.(fs.FS) - if !ok { - return nil, h.Errf("module %s (%T) is not a supported file system implementation (requires fs.FS)", modID, unm) - } - fsrv.FileSystemRaw = caddyconfig.JSONModuleObject(fsys, "backend", name, nil) - - case "hide": - fsrv.Hide = h.RemainingArgs() - if len(fsrv.Hide) == 0 { - return nil, h.ArgErr() - } - - case "index": - fsrv.IndexNames = h.RemainingArgs() - if len(fsrv.IndexNames) == 0 { - return nil, h.ArgErr() - } - - case "root": - if !h.Args(&fsrv.Root) { - return nil, h.ArgErr() - } - - case "browse": - if fsrv.Browse != nil { - return nil, h.Err("browsing is already configured") - } - fsrv.Browse = new(Browse) - h.Args(&fsrv.Browse.TemplateFile) - - case "precompressed": - var order []string - for h.NextArg() { - modID := "http.precompressed." + h.Val() - mod, err := caddy.GetModule(modID) - if err != nil { - return nil, h.Errf("getting module named '%s': %v", modID, err) - } - inst := mod.New() - precompress, ok := inst.(encode.Precompressed) - if !ok { - return nil, h.Errf("module %s is not a precompressor; is %T", modID, inst) - } - if fsrv.PrecompressedRaw == nil { - fsrv.PrecompressedRaw = make(caddy.ModuleMap) - } - fsrv.PrecompressedRaw[h.Val()] = caddyconfig.JSON(precompress, nil) - order = append(order, h.Val()) - } - fsrv.PrecompressedOrder = order - - case "status": - if !h.NextArg() { - return nil, h.ArgErr() - } - fsrv.StatusCode = caddyhttp.WeakString(h.Val()) - - case "disable_canonical_uris": - if h.NextArg() { - return nil, h.ArgErr() - } - falseBool := false - fsrv.CanonicalURIs = &falseBool - - case "pass_thru": - if h.NextArg() { - return nil, h.ArgErr() - } - fsrv.PassThru = true - - default: - return nil, h.Errf("unknown subdirective '%s'", h.Val()) + fsrv.Browse.RevealSymlinks = true } + + case "precompressed": + var order []string + for d.NextArg() { + modID := "http.precompressed." + d.Val() + mod, err := caddy.GetModule(modID) + if err != nil { + return d.Errf("getting module named '%s': %v", modID, err) + } + inst := mod.New() + precompress, ok := inst.(encode.Precompressed) + if !ok { + return d.Errf("module %s is not a precompressor; is %T", modID, inst) + } + if fsrv.PrecompressedRaw == nil { + fsrv.PrecompressedRaw = make(caddy.ModuleMap) + } + fsrv.PrecompressedRaw[d.Val()] = caddyconfig.JSON(precompress, nil) + order = append(order, d.Val()) + } + fsrv.PrecompressedOrder = order + + case "status": + if !d.NextArg() { + return d.ArgErr() + } + fsrv.StatusCode = caddyhttp.WeakString(d.Val()) + + case "disable_canonical_uris": + if d.NextArg() { + return d.ArgErr() + } + falseBool := false + fsrv.CanonicalURIs = &falseBool + + case "pass_thru": + if d.NextArg() { + return d.ArgErr() + } + fsrv.PassThru = true + + default: + return d.Errf("unknown subdirective '%s'", d.Val()) } } - // hide the Caddyfile (and any imported Caddyfiles) + return nil +} + +// FinalizeUnmarshalCaddyfile finalizes the Caddyfile parsing which +// requires having an httpcaddyfile.Helper to function, to setup hidden Caddyfiles. +func (fsrv *FileServer) FinalizeUnmarshalCaddyfile(h httpcaddyfile.Helper) error { + // Hide the Caddyfile (and any imported Caddyfiles). + // This needs to be done in here instead of UnmarshalCaddyfile + // because UnmarshalCaddyfile only has access to the dispenser + // and not the helper, and only the helper has access to the + // Caddyfiles function. if configFiles := h.Caddyfiles(); len(configFiles) > 0 { for _, file := range configFiles { file = filepath.Clean(file) @@ -168,8 +192,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) } } } - - return &fsrv, nil + return nil } // parseTryFiles parses the try_files directive. It combines a file matcher @@ -209,7 +232,7 @@ func parseTryFiles(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) // parse out the optional try policy var tryPolicy string - for nesting := h.Nesting(); h.NextBlock(nesting); { + for h.NextBlock(0) { switch h.Val() { case "policy": if tryPolicy != "" { @@ -270,3 +293,5 @@ func parseTryFiles(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) return result, nil } + +var _ caddyfile.Unmarshaler = (*FileServer)(nil) diff --git a/modules/caddyhttp/fileserver/command.go b/modules/caddyhttp/fileserver/command.go index fb145cb06..a76998405 100644 --- a/modules/caddyhttp/fileserver/command.go +++ b/modules/caddyhttp/fileserver/command.go @@ -16,6 +16,7 @@ package fileserver import ( "encoding/json" + "fmt" "io" "log" "os" @@ -31,13 +32,14 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" caddytpl "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates" ) func init() { caddycmd.RegisterCommand(caddycmd.Command{ Name: "file-server", - Usage: "[--domain ] [--root ] [--listen ] [--browse] [--access-log]", + Usage: "[--domain ] [--root ] [--listen ] [--browse] [--reveal-symlinks] [--access-log] [--precompressed]", Short: "Spins up a production-ready file server", Long: ` A simple but production-ready file server. Useful for quick deployments, @@ -50,23 +52,29 @@ will be changed to the HTTPS port and the server will use HTTPS. If using a public domain, ensure A/AAAA records are properly configured before using this option. +By default, Zstandard and Gzip compression are enabled. Use --no-compress +to disable compression. + If --browse is enabled, requests for folders without an index file will respond with a file listing.`, CobraFunc: func(cmd *cobra.Command) { cmd.Flags().StringP("domain", "d", "", "Domain name at which to serve the files") cmd.Flags().StringP("root", "r", "", "The path to the root of the site") - cmd.Flags().StringP("listen", "", "", "The address to which to bind the listener") + cmd.Flags().StringP("listen", "l", "", "The address to which to bind the listener") cmd.Flags().BoolP("browse", "b", false, "Enable directory browsing") + cmd.Flags().BoolP("reveal-symlinks", "", false, "Show symlink paths when browse is enabled.") cmd.Flags().BoolP("templates", "t", false, "Enable template rendering") - cmd.Flags().BoolP("access-log", "", false, "Enable the access log") + cmd.Flags().BoolP("access-log", "a", false, "Enable the access log") cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs") + cmd.Flags().BoolP("no-compress", "", false, "Disable Zstandard and Gzip compression") + cmd.Flags().StringSliceP("precompressed", "p", []string{}, "Specify precompression file extensions. Compression preference implied from flag order.") cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdFileServer) cmd.AddCommand(&cobra.Command{ Use: "export-template", Short: "Exports the default file browser template", Example: "caddy file-server export-template > browse.html", RunE: func(cmd *cobra.Command, args []string) error { - _, err := io.WriteString(os.Stdout, defaultBrowseTemplate) + _, err := io.WriteString(os.Stdout, BrowseTemplate) return err }, }) @@ -84,17 +92,66 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) { templates := fs.Bool("templates") accessLog := fs.Bool("access-log") debug := fs.Bool("debug") - + revealSymlinks := fs.Bool("reveal-symlinks") + compress := !fs.Bool("no-compress") + precompressed, err := fs.GetStringSlice("precompressed") + if err != nil { + return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid precompressed flag: %v", err) + } var handlers []json.RawMessage + if compress { + zstd, err := caddy.GetModule("http.encoders.zstd") + if err != nil { + return caddy.ExitCodeFailedStartup, err + } + + gzip, err := caddy.GetModule("http.encoders.gzip") + if err != nil { + return caddy.ExitCodeFailedStartup, err + } + + handlers = append(handlers, caddyconfig.JSONModuleObject(encode.Encode{ + EncodingsRaw: caddy.ModuleMap{ + "zstd": caddyconfig.JSON(zstd.New(), nil), + "gzip": caddyconfig.JSON(gzip.New(), nil), + }, + Prefer: []string{"zstd", "gzip"}, + }, "handler", "encode", nil)) + } + if templates { handler := caddytpl.Templates{FileRoot: root} handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "templates", nil)) } handler := FileServer{Root: root} + + if len(precompressed) != 0 { + // logic mirrors modules/caddyhttp/fileserver/caddyfile.go case "precompressed" + var order []string + for _, compression := range precompressed { + modID := "http.precompressed." + compression + mod, err := caddy.GetModule(modID) + if err != nil { + return caddy.ExitCodeFailedStartup, fmt.Errorf("getting module named '%s': %v", modID, err) + } + inst := mod.New() + precompress, ok := inst.(encode.Precompressed) + if !ok { + return caddy.ExitCodeFailedStartup, fmt.Errorf("module %s is not a precompressor; is %T", modID, inst) + } + if handler.PrecompressedRaw == nil { + handler.PrecompressedRaw = make(caddy.ModuleMap) + } + handler.PrecompressedRaw[compression] = caddyconfig.JSON(precompress, nil) + order = append(order, compression) + } + handler.PrecompressedOrder = order + } + if browse { - handler.Browse = new(Browse) + handler.Browse = &Browse{RevealSymlinks: revealSymlinks} } handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "file_server", nil)) @@ -154,7 +211,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) { } } - err := caddy.Run(cfg) + err = caddy.Run(cfg) if err != nil { return caddy.ExitCodeFailedStartup, err } diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go index c8f5b226e..d4a40b58d 100644 --- a/modules/caddyhttp/fileserver/matcher.go +++ b/modules/caddyhttp/fileserver/matcher.go @@ -15,7 +15,6 @@ package fileserver import ( - "encoding/json" "fmt" "io/fs" "net/http" @@ -64,8 +63,7 @@ func init() { type MatchFile struct { // The file system implementation to use. By default, the // local disk file system will be used. - FileSystemRaw json.RawMessage `json:"file_system,omitempty" caddy:"namespace=caddy.fs inline_key=backend"` - fileSystem fs.FS + FileSystem string `json:"fs,omitempty"` // The root directory, used for creating absolute // file paths, and required when working with @@ -108,6 +106,8 @@ type MatchFile struct { // component in order to be used as a split delimiter. SplitPath []string `json:"split_path,omitempty"` + fsmap caddy.FileSystems + logger *zap.Logger } @@ -127,6 +127,7 @@ func (MatchFile) CaddyModule() caddy.ModuleInfo { // try_policy first_exist|smallest_size|largest_size|most_recently_modified // } func (m *MatchFile) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + // iterate to merge multiple matchers into one for d.Next() { m.TryFiles = append(m.TryFiles, d.RemainingArgs()...) for d.NextBlock(0) { @@ -181,16 +182,22 @@ func (MatchFile) CELLibrary(ctx caddy.Context) (cel.Library, error) { root = values["root"][0] } + var fsName string + if len(values["fs"]) > 0 { + fsName = values["fs"][0] + } + var try_policy string if len(values["try_policy"]) > 0 { root = values["try_policy"][0] } m := MatchFile{ - Root: root, - TryFiles: values["try_files"], - TryPolicy: try_policy, - SplitPath: values["split_path"], + Root: root, + TryFiles: values["try_files"], + TryPolicy: try_policy, + SplitPath: values["split_path"], + FileSystem: fsName, } err = m.Provision(ctx) @@ -264,22 +271,16 @@ func celFileMatcherMacroExpander() parser.MacroExpander { func (m *MatchFile) Provision(ctx caddy.Context) error { m.logger = ctx.Logger() - // establish the file system to use - if len(m.FileSystemRaw) > 0 { - mod, err := ctx.LoadModule(m, "FileSystemRaw") - if err != nil { - return fmt.Errorf("loading file system module: %v", err) - } - m.fileSystem = mod.(fs.FS) - } - if m.fileSystem == nil { - m.fileSystem = osFS{} - } + m.fsmap = ctx.Filesystems() if m.Root == "" { m.Root = "{http.vars.root}" } + if m.FileSystem == "" { + m.FileSystem = "{http.vars.fs}" + } + // if list of files to try was omitted entirely, assume URL path // (use placeholder instead of r.URL.Path; see issue #4146) if m.TryFiles == nil { @@ -320,6 +321,13 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) { root := filepath.Clean(repl.ReplaceAll(m.Root, ".")) + fsName := repl.ReplaceAll(m.FileSystem, "") + + fileSystem, ok := m.fsmap.Get(fsName) + if !ok { + m.logger.Error("use of unregistered filesystem", zap.String("fs", fsName)) + return false + } type matchCandidate struct { fullpath, relative, splitRemainder string } @@ -368,7 +376,7 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) { if runtime.GOOS == "windows" { globResults = []string{fullPattern} // precious Windows } else { - globResults, err = fs.Glob(m.fileSystem, fullPattern) + globResults, err = fs.Glob(fileSystem, fullPattern) if err != nil { m.logger.Error("expanding glob", zap.Error(err)) } @@ -410,7 +418,7 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) { } candidates := makeCandidates(pattern) for _, c := range candidates { - if info, exists := m.strictFileExists(c.fullpath); exists { + if info, exists := m.strictFileExists(fileSystem, c.fullpath); exists { setPlaceholders(c, info) return true } @@ -424,7 +432,7 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) { for _, pattern := range m.TryFiles { candidates := makeCandidates(pattern) for _, c := range candidates { - info, err := fs.Stat(m.fileSystem, c.fullpath) + info, err := fs.Stat(fileSystem, c.fullpath) if err == nil && info.Size() > largestSize { largestSize = info.Size() largest = c @@ -445,7 +453,7 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) { for _, pattern := range m.TryFiles { candidates := makeCandidates(pattern) for _, c := range candidates { - info, err := fs.Stat(m.fileSystem, c.fullpath) + info, err := fs.Stat(fileSystem, c.fullpath) if err == nil && (smallestSize == 0 || info.Size() < smallestSize) { smallestSize = info.Size() smallest = c @@ -465,7 +473,7 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) { for _, pattern := range m.TryFiles { candidates := makeCandidates(pattern) for _, c := range candidates { - info, err := fs.Stat(m.fileSystem, c.fullpath) + info, err := fs.Stat(fileSystem, c.fullpath) if err == nil && (recentInfo == nil || info.ModTime().After(recentInfo.ModTime())) { recent = c @@ -503,8 +511,8 @@ func parseErrorCode(input string) error { // the file must also be a directory; if it does // NOT end in a forward slash, the file must NOT // be a directory. -func (m MatchFile) strictFileExists(file string) (os.FileInfo, bool) { - info, err := fs.Stat(m.fileSystem, file) +func (m MatchFile) strictFileExists(fileSystem fs.FS, file string) (os.FileInfo, bool) { + info, err := fs.Stat(fileSystem, file) if err != nil { // in reality, this can be any error // such as permission or even obscure diff --git a/modules/caddyhttp/fileserver/matcher_test.go b/modules/caddyhttp/fileserver/matcher_test.go index bab34cc63..5ffb63b6a 100644 --- a/modules/caddyhttp/fileserver/matcher_test.go +++ b/modules/caddyhttp/fileserver/matcher_test.go @@ -24,6 +24,7 @@ import ( "testing" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/internal/filesystems" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) @@ -116,9 +117,9 @@ func TestFileMatcher(t *testing.T) { }, } { m := &MatchFile{ - fileSystem: osFS{}, - Root: "./testdata", - TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"}, + fsmap: &filesystems.FilesystemMap{}, + Root: "./testdata", + TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"}, } u, err := url.Parse(tc.path) @@ -225,10 +226,10 @@ func TestPHPFileMatcher(t *testing.T) { }, } { m := &MatchFile{ - fileSystem: osFS{}, - Root: "./testdata", - TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php"}, - SplitPath: []string{".php"}, + fsmap: &filesystems.FilesystemMap{}, + Root: "./testdata", + TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php"}, + SplitPath: []string{".php"}, } u, err := url.Parse(tc.path) @@ -264,7 +265,10 @@ func TestPHPFileMatcher(t *testing.T) { } func TestFirstSplit(t *testing.T) { - m := MatchFile{SplitPath: []string{".php"}} + m := MatchFile{ + SplitPath: []string{".php"}, + fsmap: &filesystems.FilesystemMap{}, + } actual, remainder := m.firstSplit("index.PHP/somewhere") expected := "index.PHP" expectedRemainder := "/somewhere" @@ -276,83 +280,81 @@ func TestFirstSplit(t *testing.T) { } } -var ( - expressionTests = []struct { - name string - expression *caddyhttp.MatchExpression - urlTarget string - httpMethod string - httpHeader *http.Header - wantErr bool - wantResult bool - clientCertificate []byte - }{ - { - name: "file error no args (MatchFile)", - expression: &caddyhttp.MatchExpression{ - Expr: `file()`, - }, - urlTarget: "https://example.com/foo.txt", - wantResult: true, +var expressionTests = []struct { + name string + expression *caddyhttp.MatchExpression + urlTarget string + httpMethod string + httpHeader *http.Header + wantErr bool + wantResult bool + clientCertificate []byte +}{ + { + name: "file error no args (MatchFile)", + expression: &caddyhttp.MatchExpression{ + Expr: `file()`, }, - { - name: "file error bad try files (MatchFile)", - expression: &caddyhttp.MatchExpression{ - Expr: `file({"try_file": ["bad_arg"]})`, - }, - urlTarget: "https://example.com/foo", - wantErr: true, + urlTarget: "https://example.com/foo.txt", + wantResult: true, + }, + { + name: "file error bad try files (MatchFile)", + expression: &caddyhttp.MatchExpression{ + Expr: `file({"try_file": ["bad_arg"]})`, }, - { - name: "file match short pattern index.php (MatchFile)", - expression: &caddyhttp.MatchExpression{ - Expr: `file("index.php")`, - }, - urlTarget: "https://example.com/foo", - wantResult: true, + urlTarget: "https://example.com/foo", + wantErr: true, + }, + { + name: "file match short pattern index.php (MatchFile)", + expression: &caddyhttp.MatchExpression{ + Expr: `file("index.php")`, }, - { - name: "file match short pattern foo.txt (MatchFile)", - expression: &caddyhttp.MatchExpression{ - Expr: `file({http.request.uri.path})`, - }, - urlTarget: "https://example.com/foo.txt", - wantResult: true, + urlTarget: "https://example.com/foo", + wantResult: true, + }, + { + name: "file match short pattern foo.txt (MatchFile)", + expression: &caddyhttp.MatchExpression{ + Expr: `file({http.request.uri.path})`, }, - { - name: "file match index.php (MatchFile)", - expression: &caddyhttp.MatchExpression{ - Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}, "/index.php"]})`, - }, - urlTarget: "https://example.com/foo", - wantResult: true, + urlTarget: "https://example.com/foo.txt", + wantResult: true, + }, + { + name: "file match index.php (MatchFile)", + expression: &caddyhttp.MatchExpression{ + Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}, "/index.php"]})`, }, - { - name: "file match long pattern foo.txt (MatchFile)", - expression: &caddyhttp.MatchExpression{ - Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`, - }, - urlTarget: "https://example.com/foo.txt", - wantResult: true, + urlTarget: "https://example.com/foo", + wantResult: true, + }, + { + name: "file match long pattern foo.txt (MatchFile)", + expression: &caddyhttp.MatchExpression{ + Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`, }, - { - name: "file match long pattern foo.txt with concatenation (MatchFile)", - expression: &caddyhttp.MatchExpression{ - Expr: `file({"root": ".", "try_files": ["./testdata" + {http.request.uri.path}]})`, - }, - urlTarget: "https://example.com/foo.txt", - wantResult: true, + urlTarget: "https://example.com/foo.txt", + wantResult: true, + }, + { + name: "file match long pattern foo.txt with concatenation (MatchFile)", + expression: &caddyhttp.MatchExpression{ + Expr: `file({"root": ".", "try_files": ["./testdata" + {http.request.uri.path}]})`, }, - { - name: "file not match long pattern (MatchFile)", - expression: &caddyhttp.MatchExpression{ - Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`, - }, - urlTarget: "https://example.com/nopenope.txt", - wantResult: false, + urlTarget: "https://example.com/foo.txt", + wantResult: true, + }, + { + name: "file not match long pattern (MatchFile)", + expression: &caddyhttp.MatchExpression{ + Expr: `file({"root": "./testdata", "try_files": [{http.request.uri.path}]})`, }, - } -) + urlTarget: "https://example.com/nopenope.txt", + wantResult: false, + }, +} func TestMatchExpressionMatch(t *testing.T) { for _, tst := range expressionTests { diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index 0ed558e8b..57d1bc851 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -15,7 +15,6 @@ package fileserver import ( - "encoding/json" "errors" "fmt" "io" @@ -97,15 +96,8 @@ type FileServer struct { // The file system implementation to use. By default, Caddy uses the local // disk file system. // - // File system modules used here must adhere to the following requirements: - // - Implement fs.FS interface. - // - Support seeking on opened files; i.e.returned fs.File values must - // implement the io.Seeker interface. This is required for determining - // Content-Length and satisfying Range requests. - // - fs.File values that represent directories must implement the - // fs.ReadDirFile interface so that directory listings can be procured. - FileSystemRaw json.RawMessage `json:"file_system,omitempty" caddy:"namespace=caddy.fs inline_key=backend"` - fileSystem fs.FS + // if a non default filesystem is used, it must be first be registered in the globals section. + FileSystem string `json:"fs,omitempty"` // The path to the root of the site. Default is `{http.vars.root}` if set, // or current working directory otherwise. This should be a trusted value. @@ -169,6 +161,8 @@ type FileServer struct { PrecompressedOrder []string `json:"precompressed_order,omitempty"` precompressors map[string]encode.Precompressed + fsmap caddy.FileSystems + logger *zap.Logger } @@ -184,16 +178,10 @@ func (FileServer) CaddyModule() caddy.ModuleInfo { func (fsrv *FileServer) Provision(ctx caddy.Context) error { fsrv.logger = ctx.Logger() - // establish which file system (possibly a virtual one) we'll be using - if len(fsrv.FileSystemRaw) > 0 { - mod, err := ctx.LoadModule(fsrv, "FileSystemRaw") - if err != nil { - return fmt.Errorf("loading file system module: %v", err) - } - fsrv.fileSystem = mod.(fs.FS) - } - if fsrv.fileSystem == nil { - fsrv.fileSystem = osFS{} + fsrv.fsmap = ctx.Filesystems() + + if fsrv.FileSystem == "" { + fsrv.FileSystem = "{http.vars.fs}" } if fsrv.Root == "" { @@ -263,19 +251,26 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c filesToHide := fsrv.transformHidePaths(repl) root := repl.ReplaceAll(fsrv.Root, ".") + fsName := repl.ReplaceAll(fsrv.FileSystem, "") + + fileSystem, ok := fsrv.fsmap.Get(fsName) + if !ok { + return caddyhttp.Error(http.StatusNotFound, fmt.Errorf("filesystem not found")) + } // remove any trailing `/` as it breaks fs.ValidPath() in the stdlib filename := strings.TrimSuffix(caddyhttp.SanitizedPathJoin(root, r.URL.Path), "/") fsrv.logger.Debug("sanitized path join", zap.String("site_root", root), + zap.String("fs", fsName), zap.String("request_path", r.URL.Path), zap.String("result", filename)) // get information about the file - info, err := fs.Stat(fsrv.fileSystem, filename) + info, err := fs.Stat(fileSystem, filename) if err != nil { - err = fsrv.mapDirOpenError(err, filename) + err = fsrv.mapDirOpenError(fileSystem, err, filename) if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrInvalid) { return fsrv.notFound(w, r, next) } else if errors.Is(err, fs.ErrPermission) { @@ -299,7 +294,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c continue } - indexInfo, err := fs.Stat(fsrv.fileSystem, indexPath) + indexInfo, err := fs.Stat(fileSystem, indexPath) if err != nil { continue } @@ -327,7 +322,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c zap.String("path", filename), zap.Strings("index_filenames", fsrv.IndexNames)) if fsrv.Browse != nil && !fileHidden(filename, filesToHide) { - return fsrv.serveBrowse(root, filename, w, r, next) + return fsrv.serveBrowse(fileSystem, root, filename, w, r, next) } return fsrv.notFound(w, r, next) } @@ -381,13 +376,13 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c continue } compressedFilename := filename + precompress.Suffix() - compressedInfo, err := fs.Stat(fsrv.fileSystem, compressedFilename) + compressedInfo, err := fs.Stat(fileSystem, compressedFilename) if err != nil || compressedInfo.IsDir() { fsrv.logger.Debug("precompressed file not accessible", zap.String("filename", compressedFilename), zap.Error(err)) continue } fsrv.logger.Debug("opening compressed sidecar file", zap.String("filename", compressedFilename), zap.Error(err)) - file, err = fsrv.openFile(compressedFilename, w) + file, err = fsrv.openFile(fileSystem, compressedFilename, w) if err != nil { fsrv.logger.Warn("opening precompressed file failed", zap.String("filename", compressedFilename), zap.Error(err)) if caddyErr, ok := err.(caddyhttp.HandlerError); ok && caddyErr.StatusCode == http.StatusServiceUnavailable { @@ -416,7 +411,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c fsrv.logger.Debug("opening file", zap.String("filename", filename)) // open the file - file, err = fsrv.openFile(filename, w) + file, err = fsrv.openFile(fileSystem, filename, w) if err != nil { if herr, ok := err.(caddyhttp.HandlerError); ok && herr.StatusCode == http.StatusNotFound { @@ -502,14 +497,14 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c // the response is configured to inform the client how to best handle it // and a well-described handler error is returned (do not wrap the // returned error value). -func (fsrv *FileServer) openFile(filename string, w http.ResponseWriter) (fs.File, error) { - file, err := fsrv.fileSystem.Open(filename) +func (fsrv *FileServer) openFile(fileSystem fs.FS, filename string, w http.ResponseWriter) (fs.File, error) { + file, err := fileSystem.Open(filename) if err != nil { - err = fsrv.mapDirOpenError(err, filename) - if os.IsNotExist(err) { + err = fsrv.mapDirOpenError(fileSystem, err, filename) + if errors.Is(err, fs.ErrNotExist) { fsrv.logger.Debug("file not found", zap.String("filename", filename), zap.Error(err)) return nil, caddyhttp.Error(http.StatusNotFound, err) - } else if os.IsPermission(err) { + } else if errors.Is(err, fs.ErrPermission) { fsrv.logger.Debug("permission denied", zap.String("filename", filename), zap.Error(err)) return nil, caddyhttp.Error(http.StatusForbidden, err) } @@ -530,7 +525,7 @@ func (fsrv *FileServer) openFile(filename string, w http.ResponseWriter) (fs.Fil // Adapted from the Go standard library; originally written by Nathaniel Caza. // https://go-review.googlesource.com/c/go/+/36635/ // https://go-review.googlesource.com/c/go/+/36804/ -func (fsrv *FileServer) mapDirOpenError(originalErr error, name string) error { +func (fsrv *FileServer) mapDirOpenError(fileSystem fs.FS, originalErr error, name string) error { if errors.Is(originalErr, fs.ErrNotExist) || errors.Is(originalErr, fs.ErrPermission) { return originalErr } @@ -540,7 +535,7 @@ func (fsrv *FileServer) mapDirOpenError(originalErr error, name string) error { if parts[i] == "" { continue } - fi, err := fs.Stat(fsrv.fileSystem, strings.Join(parts[:i+1], separator)) + fi, err := fs.Stat(fileSystem, strings.Join(parts[:i+1], separator)) if err != nil { return originalErr } @@ -644,12 +639,18 @@ func calculateEtag(d os.FileInfo) string { return `"` + t + s + `"` } -func redirect(w http.ResponseWriter, r *http.Request, to string) error { - for strings.HasPrefix(to, "//") { +// redirect performs a redirect to a given path. The 'toPath' parameter +// MUST be solely a path, and MUST NOT include a query. +func redirect(w http.ResponseWriter, r *http.Request, toPath string) error { + for strings.HasPrefix(toPath, "//") { // prevent path-based open redirects - to = strings.TrimPrefix(to, "/") + toPath = strings.TrimPrefix(toPath, "/") } - http.Redirect(w, r, to, http.StatusPermanentRedirect) + // preserve the query string if present + if r.URL.RawQuery != "" { + toPath += "?" + r.URL.RawQuery + } + http.Redirect(w, r, toPath, http.StatusPermanentRedirect) return nil } @@ -673,21 +674,6 @@ func (wr statusOverrideResponseWriter) Unwrap() http.ResponseWriter { return wr.ResponseWriter } -// osFS is a simple fs.FS implementation that uses the local -// file system. (We do not use os.DirFS because we do our own -// rooting or path prefixing without being constrained to a single -// root folder. The standard os.DirFS implementation is problematic -// since roots can be dynamic in our application.) -// -// osFS also implements fs.StatFS, fs.GlobFS, fs.ReadDirFS, and fs.ReadFileFS. -type osFS struct{} - -func (osFS) Open(name string) (fs.File, error) { return os.Open(name) } -func (osFS) Stat(name string) (fs.FileInfo, error) { return os.Stat(name) } -func (osFS) Glob(pattern string) ([]string, error) { return filepath.Glob(pattern) } -func (osFS) ReadDir(name string) ([]fs.DirEntry, error) { return os.ReadDir(name) } -func (osFS) ReadFile(name string) ([]byte, error) { return os.ReadFile(name) } - var defaultIndexNames = []string{"index.html", "index.txt"} const ( @@ -699,9 +685,4 @@ const ( var ( _ caddy.Provisioner = (*FileServer)(nil) _ caddyhttp.MiddlewareHandler = (*FileServer)(nil) - - _ fs.StatFS = (*osFS)(nil) - _ fs.GlobFS = (*osFS)(nil) - _ fs.ReadDirFS = (*osFS)(nil) - _ fs.ReadFileFS = (*osFS)(nil) ) diff --git a/modules/caddyhttp/headers/caddyfile.go b/modules/caddyhttp/headers/caddyfile.go index 2b069100b..821c0d830 100644 --- a/modules/caddyhttp/headers/caddyfile.go +++ b/modules/caddyhttp/headers/caddyfile.go @@ -47,14 +47,12 @@ func init() { // ? conditionally sets a value only if the header field is not already set, // and > sets a field with defer enabled. func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) { - if !h.Next() { - return nil, h.ArgErr() - } - + h.Next() // consume directive name matcherSet, err := h.ExtractMatcherSet() if err != nil { return nil, err } + h.Next() // consume the directive name again (matcher parsing resets) makeHandler := func() Handler { return Handler{ @@ -65,73 +63,71 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) } handler, handlerWithRequire := makeHandler(), makeHandler() - for h.Next() { - // first see if headers are in the initial line - var hasArgs bool + // first see if headers are in the initial line + var hasArgs bool + if h.NextArg() { + hasArgs = true + field := h.Val() + var value, replacement string if h.NextArg() { - hasArgs = true - field := h.Val() - var value, replacement string - if h.NextArg() { - value = h.Val() - } - if h.NextArg() { - replacement = h.Val() - } - err := applyHeaderOp( - handler.Response.HeaderOps, - handler.Response, - field, - value, - replacement, - ) - if err != nil { - return nil, h.Err(err.Error()) - } - if len(handler.Response.HeaderOps.Delete) > 0 { - handler.Response.Deferred = true - } + value = h.Val() + } + if h.NextArg() { + replacement = h.Val() + } + err := applyHeaderOp( + handler.Response.HeaderOps, + handler.Response, + field, + value, + replacement, + ) + if err != nil { + return nil, h.Err(err.Error()) + } + if len(handler.Response.HeaderOps.Delete) > 0 { + handler.Response.Deferred = true + } + } + + // if not, they should be in a block + for h.NextBlock(0) { + field := h.Val() + if field == "defer" { + handler.Response.Deferred = true + continue + } + if hasArgs { + return nil, h.Err("cannot specify headers in both arguments and block") // because it would be weird } - // if not, they should be in a block - for h.NextBlock(0) { - field := h.Val() - if field == "defer" { - handler.Response.Deferred = true - continue - } - if hasArgs { - return nil, h.Err("cannot specify headers in both arguments and block") // because it would be weird - } + // sometimes it is habitual for users to suffix a field name with a colon, + // as if they were writing a curl command or something; see + // https://caddy.community/t/v2-reverse-proxy-please-add-cors-example-to-the-docs/7349/19 + field = strings.TrimSuffix(field, ":") - // sometimes it is habitual for users to suffix a field name with a colon, - // as if they were writing a curl command or something; see - // https://caddy.community/t/v2-reverse-proxy-please-add-cors-example-to-the-docs/7349/19 - field = strings.TrimSuffix(field, ":") + var value, replacement string + if h.NextArg() { + value = h.Val() + } + if h.NextArg() { + replacement = h.Val() + } - var value, replacement string - if h.NextArg() { - value = h.Val() - } - if h.NextArg() { - replacement = h.Val() - } + handlerToUse := handler + if strings.HasPrefix(field, "?") { + handlerToUse = handlerWithRequire + } - handlerToUse := handler - if strings.HasPrefix(field, "?") { - handlerToUse = handlerWithRequire - } - - err := applyHeaderOp( - handlerToUse.Response.HeaderOps, - handlerToUse.Response, - field, - value, - replacement, - ) - if err != nil { - return nil, h.Err(err.Error()) - } + err := applyHeaderOp( + handlerToUse.Response.HeaderOps, + handlerToUse.Response, + field, + value, + replacement, + ) + if err != nil { + return nil, h.Err(err.Error()) } } @@ -151,56 +147,52 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) // // request_header [] [[+|-] [] []] func parseReqHdrCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) { - if !h.Next() { - return nil, h.ArgErr() - } - + h.Next() // consume directive name matcherSet, err := h.ExtractMatcherSet() if err != nil { return nil, err } + h.Next() // consume the directive name again (matcher parsing resets) configValues := []httpcaddyfile.ConfigValue{} - for h.Next() { - if !h.NextArg() { - return nil, h.ArgErr() - } - field := h.Val() + if !h.NextArg() { + return nil, h.ArgErr() + } + field := h.Val() - hdr := Handler{ - Request: &HeaderOps{}, - } + hdr := Handler{ + Request: &HeaderOps{}, + } - // sometimes it is habitual for users to suffix a field name with a colon, - // as if they were writing a curl command or something; see - // https://caddy.community/t/v2-reverse-proxy-please-add-cors-example-to-the-docs/7349/19 - field = strings.TrimSuffix(field, ":") - - var value, replacement string - if h.NextArg() { - value = h.Val() - } - if h.NextArg() { - replacement = h.Val() - if h.NextArg() { - return nil, h.ArgErr() - } - } - - if hdr.Request == nil { - hdr.Request = new(HeaderOps) - } - if err := CaddyfileHeaderOp(hdr.Request, field, value, replacement); err != nil { - return nil, h.Err(err.Error()) - } - - configValues = append(configValues, h.NewRoute(matcherSet, hdr)...) + // sometimes it is habitual for users to suffix a field name with a colon, + // as if they were writing a curl command or something; see + // https://caddy.community/t/v2-reverse-proxy-please-add-cors-example-to-the-docs/7349/19 + field = strings.TrimSuffix(field, ":") + var value, replacement string + if h.NextArg() { + value = h.Val() + } + if h.NextArg() { + replacement = h.Val() if h.NextArg() { return nil, h.ArgErr() } } + + if hdr.Request == nil { + hdr.Request = new(HeaderOps) + } + if err := CaddyfileHeaderOp(hdr.Request, field, value, replacement); err != nil { + return nil, h.Err(err.Error()) + } + + configValues = append(configValues, h.NewRoute(matcherSet, hdr)...) + + if h.NextArg() { + return nil, h.ArgErr() + } return configValues, nil } diff --git a/modules/caddyhttp/httpredirectlistener.go b/modules/caddyhttp/httpredirectlistener.go index 3ff79ff8c..ce9ac0308 100644 --- a/modules/caddyhttp/httpredirectlistener.go +++ b/modules/caddyhttp/httpredirectlistener.go @@ -16,10 +16,11 @@ package caddyhttp import ( "bufio" + "bytes" "fmt" + "io" "net" "net/http" - "sync" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" @@ -42,7 +43,11 @@ func init() { // // This listener wrapper must be placed BEFORE the "tls" listener // wrapper, for it to work properly. -type HTTPRedirectListenerWrapper struct{} +type HTTPRedirectListenerWrapper struct { + // MaxHeaderBytes is the maximum size to parse from a client's + // HTTP request headers. Default: 1 MB + MaxHeaderBytes int64 `json:"max_header_bytes,omitempty"` +} func (HTTPRedirectListenerWrapper) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ @@ -56,7 +61,7 @@ func (h *HTTPRedirectListenerWrapper) UnmarshalCaddyfile(d *caddyfile.Dispenser) } func (h *HTTPRedirectListenerWrapper) WrapListener(l net.Listener) net.Listener { - return &httpRedirectListener{l} + return &httpRedirectListener{l, h.MaxHeaderBytes} } // httpRedirectListener is listener that checks the first few bytes @@ -64,6 +69,7 @@ func (h *HTTPRedirectListenerWrapper) WrapListener(l net.Listener) net.Listener // to respond to an HTTP request with a redirect. type httpRedirectListener struct { net.Listener + maxHeaderBytes int64 } // Accept waits for and returns the next connection to the listener, @@ -74,16 +80,23 @@ func (l *httpRedirectListener) Accept() (net.Conn, error) { return nil, err } + maxHeaderBytes := l.maxHeaderBytes + if maxHeaderBytes == 0 { + maxHeaderBytes = 1024 * 1024 + } + return &httpRedirectConn{ - Conn: c, - r: bufio.NewReader(c), + Conn: c, + limit: maxHeaderBytes, + r: bufio.NewReader(c), }, nil } type httpRedirectConn struct { net.Conn - once sync.Once - r *bufio.Reader + once bool + limit int64 + r *bufio.Reader } // Read tries to peek at the first few bytes of the request, and if we get @@ -91,53 +104,58 @@ type httpRedirectConn struct { // like an HTTP request, then we perform a HTTP->HTTPS redirect on the same // port as the original connection. func (c *httpRedirectConn) Read(p []byte) (int, error) { - var errReturn error - c.once.Do(func() { - firstBytes, err := c.r.Peek(5) - if err != nil { - return - } + if c.once { + return c.r.Read(p) + } + // no need to use sync.Once - net.Conn is not read from concurrently. + c.once = true - // If the request doesn't look like HTTP, then it's probably - // TLS bytes and we don't need to do anything. - if !firstBytesLookLikeHTTP(firstBytes) { - return - } - - // Parse the HTTP request, so we can get the Host and URL to redirect to. - req, err := http.ReadRequest(c.r) - if err != nil { - return - } - - // Build the redirect response, using the same Host and URL, - // but replacing the scheme with https. - headers := make(http.Header) - headers.Add("Location", "https://"+req.Host+req.URL.String()) - resp := &http.Response{ - Proto: "HTTP/1.0", - Status: "308 Permanent Redirect", - StatusCode: 308, - ProtoMajor: 1, - ProtoMinor: 0, - Header: headers, - } - - err = resp.Write(c.Conn) - if err != nil { - errReturn = fmt.Errorf("couldn't write HTTP->HTTPS redirect") - return - } - - errReturn = fmt.Errorf("redirected HTTP request on HTTPS port") - c.Conn.Close() - }) - - if errReturn != nil { - return 0, errReturn + firstBytes, err := c.r.Peek(5) + if err != nil { + return 0, err } - return c.r.Read(p) + // If the request doesn't look like HTTP, then it's probably + // TLS bytes, and we don't need to do anything. + if !firstBytesLookLikeHTTP(firstBytes) { + return c.r.Read(p) + } + + // From now on, we can be almost certain the request is HTTP. + // The returned error will be non nil and caller are expected to + // close the connection. + + // Set the read limit, io.MultiReader is needed because + // when resetting, *bufio.Reader discards buffered data. + buffered, _ := c.r.Peek(c.r.Buffered()) + mr := io.MultiReader(bytes.NewReader(buffered), c.Conn) + c.r.Reset(io.LimitReader(mr, c.limit)) + + // Parse the HTTP request, so we can get the Host and URL to redirect to. + req, err := http.ReadRequest(c.r) + if err != nil { + return 0, fmt.Errorf("couldn't read HTTP request") + } + + // Build the redirect response, using the same Host and URL, + // but replacing the scheme with https. + headers := make(http.Header) + headers.Add("Location", "https://"+req.Host+req.URL.String()) + resp := &http.Response{ + Proto: "HTTP/1.0", + Status: "308 Permanent Redirect", + StatusCode: 308, + ProtoMajor: 1, + ProtoMinor: 0, + Header: headers, + } + + err = resp.Write(c.Conn) + if err != nil { + return 0, fmt.Errorf("couldn't write HTTP->HTTPS redirect") + } + + return 0, fmt.Errorf("redirected HTTP request on HTTPS port") } // firstBytesLookLikeHTTP reports whether a TLS record header diff --git a/modules/caddyhttp/ip_matchers.go b/modules/caddyhttp/ip_matchers.go index 57a229578..d5571802d 100644 --- a/modules/caddyhttp/ip_matchers.go +++ b/modules/caddyhttp/ip_matchers.go @@ -37,13 +37,6 @@ type MatchRemoteIP struct { // The IPs or CIDR ranges to match. Ranges []string `json:"ranges,omitempty"` - // If true, prefer the first IP in the request's X-Forwarded-For - // header, if present, rather than the immediate peer's IP, as - // the reference IP against which to match. Note that it is easy - // to spoof request headers. Default: false - // DEPRECATED: This is insecure, MatchClientIP should be used instead. - Forwarded bool `json:"forwarded,omitempty"` - // cidrs and zones vars should aligned always in the same // length and indexes for matching later cidrs []*netip.Prefix @@ -79,24 +72,19 @@ func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo { // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - for d.NextArg() { - if d.Val() == "forwarded" { - if len(m.Ranges) > 0 { - return d.Err("if used, 'forwarded' must be first argument") - } - m.Forwarded = true - continue - } - if d.Val() == "private_ranges" { - m.Ranges = append(m.Ranges, PrivateRangesCIDR()...) - continue - } - m.Ranges = append(m.Ranges, d.Val()) + d.Next() // consume matcher name + for d.NextArg() { + if d.Val() == "forwarded" { + return d.Err("the 'forwarded' option is no longer supported; use the 'client_ip' matcher instead") } - if d.NextBlock(0) { - return d.Err("malformed remote_ip matcher: blocks are not supported") + if d.Val() == "private_ranges" { + m.Ranges = append(m.Ranges, PrivateRangesCIDR()...) + continue } + m.Ranges = append(m.Ranges, d.Val()) + } + if d.NextBlock(0) { + return d.Err("malformed remote_ip matcher: blocks are not supported") } return nil } @@ -106,7 +94,7 @@ func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // // Example: // -// expression remote_ip('forwarded', '192.168.0.0/16', '172.16.0.0/12', '10.0.0.0/8') +// expression remote_ip('192.168.0.0/16', '172.16.0.0/12', '10.0.0.0/8') func (MatchRemoteIP) CELLibrary(ctx caddy.Context) (cel.Library, error) { return CELMatcherImpl( // name of the macro, this is the function name that users see when writing expressions. @@ -127,11 +115,7 @@ func (MatchRemoteIP) CELLibrary(ctx caddy.Context) (cel.Library, error) { for _, input := range strList.([]string) { if input == "forwarded" { - if len(m.Ranges) > 0 { - return nil, errors.New("if used, 'forwarded' must be first argument") - } - m.Forwarded = true - continue + return nil, errors.New("the 'forwarded' option is no longer supported; use the 'client_ip' matcher instead") } m.Ranges = append(m.Ranges, input) } @@ -152,21 +136,12 @@ func (m *MatchRemoteIP) Provision(ctx caddy.Context) error { m.cidrs = cidrs m.zones = zones - if m.Forwarded { - m.logger.Warn("remote_ip's forwarded mode is deprecated; use the 'client_ip' matcher instead") - } - return nil } // Match returns true if r matches m. func (m MatchRemoteIP) Match(r *http.Request) bool { address := r.RemoteAddr - if m.Forwarded { - if fwdFor := r.Header.Get("X-Forwarded-For"); fwdFor != "" { - address = strings.TrimSpace(strings.Split(fwdFor, ",")[0]) - } - } clientIP, zoneID, err := parseIPZoneFromString(address) if err != nil { m.logger.Error("getting remote IP", zap.Error(err)) @@ -189,17 +164,16 @@ func (MatchClientIP) CaddyModule() caddy.ModuleInfo { // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchClientIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - for d.NextArg() { - if d.Val() == "private_ranges" { - m.Ranges = append(m.Ranges, PrivateRangesCIDR()...) - continue - } - m.Ranges = append(m.Ranges, d.Val()) - } - if d.NextBlock(0) { - return d.Err("malformed client_ip matcher: blocks are not supported") + d.Next() // consume matcher name + for d.NextArg() { + if d.Val() == "private_ranges" { + m.Ranges = append(m.Ranges, PrivateRangesCIDR()...) + continue } + m.Ranges = append(m.Ranges, d.Val()) + } + if d.NextBlock(0) { + return d.Err("malformed client_ip matcher: blocks are not supported") } return nil } diff --git a/modules/caddyhttp/logging.go b/modules/caddyhttp/logging.go index 8ecc49a63..81b2830fc 100644 --- a/modules/caddyhttp/logging.go +++ b/modules/caddyhttp/logging.go @@ -151,10 +151,22 @@ func (e *ExtraLogFields) Add(field zap.Field) { e.fields = append(e.fields, field) } +// Set sets a field in the list of extra fields to log. +// If the field already exists, it is replaced. +func (e *ExtraLogFields) Set(field zap.Field) { + for i := range e.fields { + if e.fields[i].Key == field.Key { + e.fields[i] = field + return + } + } + e.fields = append(e.fields, field) +} + const ( // Variable name used to indicate that this request // should be omitted from the access logs - SkipLogVar string = "skip_log" + LogSkipVar string = "log_skip" // For adding additional fields to the access logs ExtraLogFieldsCtxKey caddy.CtxKey = "extra_log_fields" diff --git a/modules/caddyhttp/logging/caddyfile.go b/modules/caddyhttp/logging/caddyfile.go new file mode 100644 index 000000000..010b48919 --- /dev/null +++ b/modules/caddyhttp/logging/caddyfile.go @@ -0,0 +1,53 @@ +// 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 logging + +import ( + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" +) + +func init() { + httpcaddyfile.RegisterHandlerDirective("log_append", parseCaddyfile) +} + +// parseCaddyfile sets up the log_append handler from Caddyfile tokens. Syntax: +// +// log_append [] +func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + handler := new(LogAppend) + err := handler.UnmarshalCaddyfile(h.Dispenser) + return handler, err +} + +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (h *LogAppend) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.Next() // consume directive name + if !d.NextArg() { + return d.ArgErr() + } + h.Key = d.Val() + if !d.NextArg() { + return d.ArgErr() + } + h.Value = d.Val() + return nil +} + +// Interface guards +var ( + _ caddyfile.Unmarshaler = (*LogAppend)(nil) +) diff --git a/modules/caddyhttp/logging/logadd.go b/modules/caddyhttp/logging/logadd.go new file mode 100644 index 000000000..3b554367f --- /dev/null +++ b/modules/caddyhttp/logging/logadd.go @@ -0,0 +1,94 @@ +// 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 logging + +import ( + "net/http" + "strings" + + "go.uber.org/zap" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" +) + +func init() { + caddy.RegisterModule(LogAppend{}) +} + +// LogAppend implements a middleware that takes a key and value, where +// the key is the name of a log field and the value is a placeholder, +// or variable key, or constant value to use for that field. +type LogAppend struct { + // Key is the name of the log field. + Key string `json:"key,omitempty"` + + // Value is the value to use for the log field. + // If it is a placeholder (with surrounding `{}`), + // it will be evaluated when the log is written. + // If the value is a key that exists in the `vars` + // map, the value of that key will be used. Otherwise + // the value will be used as-is as a constant string. + Value string `json:"value,omitempty"` +} + +// CaddyModule returns the Caddy module information. +func (LogAppend) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "http.handlers.log_append", + New: func() caddy.Module { return new(LogAppend) }, + } +} + +func (h LogAppend) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { + // Run the next handler in the chain first. + // If an error occurs, we still want to add + // any extra log fields that we can, so we + // hold onto the error and return it later. + handlerErr := next.ServeHTTP(w, r) + + // On the way back up the chain, add the extra log field + ctx := r.Context() + + vars := ctx.Value(caddyhttp.VarsCtxKey).(map[string]any) + repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + extra := ctx.Value(caddyhttp.ExtraLogFieldsCtxKey).(*caddyhttp.ExtraLogFields) + + var varValue any + if strings.HasPrefix(h.Value, "{") && + strings.HasSuffix(h.Value, "}") && + strings.Count(h.Value, "{") == 1 { + // the value looks like a placeholder, so get its value + varValue, _ = repl.Get(strings.Trim(h.Value, "{}")) + } else if val, ok := vars[h.Value]; ok { + // the value is a key in the vars map + varValue = val + } else { + // the value is a constant string + varValue = h.Value + } + + // Add the field to the extra log fields. + // We use zap.Any because it will reflect + // to the correct type for us. + extra.Add(zap.Any(h.Key, varValue)) + + return handlerErr +} + +// Interface guards +var ( + _ caddyhttp.MiddlewareHandler = (*LogAppend)(nil) +) diff --git a/modules/caddyhttp/map/caddyfile.go b/modules/caddyhttp/map/caddyfile.go index 9cc7d8c6d..8f7b5d34e 100644 --- a/modules/caddyhttp/map/caddyfile.go +++ b/modules/caddyhttp/map/caddyfile.go @@ -42,74 +42,73 @@ func init() { // However, for convenience, there may be fewer outputs than destinations and any missing // outputs will be filled in implicitly. func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + h.Next() // consume directive name + var handler Handler - for h.Next() { - // source - if !h.NextArg() { - return nil, h.ArgErr() - } - handler.Source = h.Val() + // source + if !h.NextArg() { + return nil, h.ArgErr() + } + handler.Source = h.Val() - // destinations - handler.Destinations = h.RemainingArgs() - if len(handler.Destinations) == 0 { - return nil, h.Err("missing destination argument(s)") - } - for _, dest := range handler.Destinations { - if shorthand := httpcaddyfile.WasReplacedPlaceholderShorthand(dest); shorthand != "" { - return nil, h.Errf("destination %s conflicts with a Caddyfile placeholder shorthand", shorthand) - } - } - - // mappings - for h.NextBlock(0) { - // defaults are a special case - if h.Val() == "default" { - if len(handler.Defaults) > 0 { - return nil, h.Err("defaults already defined") - } - handler.Defaults = h.RemainingArgs() - for len(handler.Defaults) < len(handler.Destinations) { - handler.Defaults = append(handler.Defaults, "") - } - continue - } - - // every line maps an input value to one or more outputs - in := h.Val() - var outs []any - for h.NextArg() { - val := h.ScalarVal() - if val == "-" { - outs = append(outs, nil) - } else { - outs = append(outs, val) - } - } - - // cannot have more outputs than destinations - if len(outs) > len(handler.Destinations) { - return nil, h.Err("too many outputs") - } - - // for convenience, can have fewer outputs than destinations, but the - // underlying handler won't accept that, so we fill in nil values - for len(outs) < len(handler.Destinations) { - outs = append(outs, nil) - } - - // create the mapping - mapping := Mapping{Outputs: outs} - if strings.HasPrefix(in, "~") { - mapping.InputRegexp = in[1:] - } else { - mapping.Input = in - } - - handler.Mappings = append(handler.Mappings, mapping) + // destinations + handler.Destinations = h.RemainingArgs() + if len(handler.Destinations) == 0 { + return nil, h.Err("missing destination argument(s)") + } + for _, dest := range handler.Destinations { + if shorthand := httpcaddyfile.WasReplacedPlaceholderShorthand(dest); shorthand != "" { + return nil, h.Errf("destination %s conflicts with a Caddyfile placeholder shorthand", shorthand) } } + // mappings + for h.NextBlock(0) { + // defaults are a special case + if h.Val() == "default" { + if len(handler.Defaults) > 0 { + return nil, h.Err("defaults already defined") + } + handler.Defaults = h.RemainingArgs() + for len(handler.Defaults) < len(handler.Destinations) { + handler.Defaults = append(handler.Defaults, "") + } + continue + } + + // every line maps an input value to one or more outputs + in := h.Val() + var outs []any + for h.NextArg() { + val := h.ScalarVal() + if val == "-" { + outs = append(outs, nil) + } else { + outs = append(outs, val) + } + } + + // cannot have more outputs than destinations + if len(outs) > len(handler.Destinations) { + return nil, h.Err("too many outputs") + } + + // for convenience, can have fewer outputs than destinations, but the + // underlying handler won't accept that, so we fill in nil values + for len(outs) < len(handler.Destinations) { + outs = append(outs, nil) + } + + // create the mapping + mapping := Mapping{Outputs: outs} + if strings.HasPrefix(in, "~") { + mapping.InputRegexp = in[1:] + } else { + mapping.Input = in + } + + handler.Mappings = append(handler.Mappings, mapping) + } return handler, nil } diff --git a/modules/caddyhttp/marshalers.go b/modules/caddyhttp/marshalers.go index 9a955e3b6..8f4472a27 100644 --- a/modules/caddyhttp/marshalers.go +++ b/modules/caddyhttp/marshalers.go @@ -40,7 +40,9 @@ func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error { enc.AddString("remote_ip", ip) enc.AddString("remote_port", port) - enc.AddString("client_ip", GetVar(r.Context(), ClientIPVarKey).(string)) + if ip, ok := GetVar(r.Context(), ClientIPVarKey).(string); ok { + enc.AddString("client_ip", ip) + } enc.AddString("proto", r.Proto) enc.AddString("method", r.Method) enc.AddString("host", r.Host) diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index b38597970..f01f187fa 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -33,6 +33,7 @@ import ( "github.com/google/cel-go/cel" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" + "golang.org/x/exp/slices" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" @@ -224,6 +225,7 @@ func (MatchHost) CaddyModule() caddy.ModuleInfo { // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchHost) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + // iterate to merge multiple matchers into one for d.Next() { *m = append(*m, d.RemainingArgs()...) if d.NextBlock(0) { @@ -631,6 +633,7 @@ func (MatchPath) CELLibrary(ctx caddy.Context) (cel.Library, error) { // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchPath) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + // iterate to merge multiple matchers into one for d.Next() { *m = append(*m, d.RemainingArgs()...) if d.NextBlock(0) { @@ -715,6 +718,7 @@ func (MatchMethod) CaddyModule() caddy.ModuleInfo { // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchMethod) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + // iterate to merge multiple matchers into one for d.Next() { *m = append(*m, d.RemainingArgs()...) if d.NextBlock(0) { @@ -769,6 +773,7 @@ func (m *MatchQuery) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if *m == nil { *m = make(map[string][]string) } + // iterate to merge multiple matchers into one for d.Next() { for _, query := range d.RemainingArgs() { if query == "" { @@ -789,6 +794,12 @@ func (m *MatchQuery) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // Match returns true if r matches m. An empty m matches an empty query string. func (m MatchQuery) Match(r *http.Request) bool { + // If no query keys are configured, this only + // matches an empty query string. + if len(m) == 0 { + return len(r.URL.Query()) == 0 + } + repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) // parse query string just once, for efficiency @@ -806,19 +817,25 @@ func (m MatchQuery) Match(r *http.Request) bool { return false } + // Count the amount of matched keys, to ensure we AND + // between all configured query keys; all keys must + // match at least one value. + matchedKeys := 0 for param, vals := range m { param = repl.ReplaceAll(param, "") paramVal, found := parsed[param] - if found { - for _, v := range vals { - v = repl.ReplaceAll(v, "") - if paramVal[0] == v || v == "*" { - return true - } + if !found { + return false + } + for _, v := range vals { + v = repl.ReplaceAll(v, "") + if slices.Contains(paramVal, v) || v == "*" { + matchedKeys++ + break } } } - return len(m) == 0 && len(r.URL.Query()) == 0 + return matchedKeys == len(m) } // CELLibrary produces options that expose this matcher for use in CEL @@ -855,6 +872,7 @@ func (m *MatchHeader) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if *m == nil { *m = make(map[string][]string) } + // iterate to merge multiple matchers into one for d.Next() { var field, val string if !d.Args(&field) { @@ -989,6 +1007,7 @@ func (m *MatchHeaderRE) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if *m == nil { *m = make(map[string]*MatchRegexp) } + // iterate to merge multiple matchers into one for d.Next() { var first, second, third string if !d.Args(&first, &second) { @@ -1153,6 +1172,7 @@ func (m MatchProtocol) Match(r *http.Request) bool { // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchProtocol) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + // iterate to merge multiple matchers into one for d.Next() { var proto string if !d.Args(&proto) { @@ -1194,6 +1214,7 @@ func (MatchNot) CaddyModule() caddy.ModuleInfo { // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchNot) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + // iterate to merge multiple matchers into one for d.Next() { matcherSet, err := ParseCaddyfileNestedMatcherSet(d) if err != nil { @@ -1318,6 +1339,7 @@ func (mre *MatchRegexp) Match(input string, repl *caddy.Replacer) bool { // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + // iterate to merge multiple matchers into one for d.Next() { // If this is the second iteration of the loop // then there's more than one path_regexp matcher diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go index 041975d80..5f76a36b1 100644 --- a/modules/caddyhttp/matchers_test.go +++ b/modules/caddyhttp/matchers_test.go @@ -763,6 +763,42 @@ func TestQueryMatcher(t *testing.T) { input: "/?somekey=1", expect: true, }, + { + scenario: "do not match when not all query params are present", + match: MatchQuery{"debug": []string{"1"}, "foo": []string{"bar"}}, + input: "/?debug=1", + expect: false, + }, + { + scenario: "match when all query params are present", + match: MatchQuery{"debug": []string{"1"}, "foo": []string{"bar"}}, + input: "/?debug=1&foo=bar", + expect: true, + }, + { + scenario: "do not match when the value of a query param does not match", + match: MatchQuery{"debug": []string{"1"}, "foo": []string{"bar"}}, + input: "/?debug=2&foo=bar", + expect: false, + }, + { + scenario: "do not match when all the values the query params do not match", + match: MatchQuery{"debug": []string{"1"}, "foo": []string{"bar"}}, + input: "/?debug=2&foo=baz", + expect: false, + }, + { + scenario: "match against two values for the same key", + match: MatchQuery{"debug": []string{"1"}}, + input: "/?debug=1&debug=2", + expect: true, + }, + { + scenario: "match against two values for the same key", + match: MatchQuery{"debug": []string{"2", "1"}}, + input: "/?debug=2&debug=1", + expect: true, + }, } { u, _ := url.Parse(tc.input) @@ -862,7 +898,6 @@ func TestHeaderREMatcher(t *testing.T) { } func BenchmarkHeaderREMatcher(b *testing.B) { - i := 0 match := MatchHeaderRE{"Field": &MatchRegexp{Pattern: "^foo(.*)$", Name: "name"}} input := http.Header{"Field": []string{"foobar"}} @@ -1086,6 +1121,7 @@ func TestNotMatcher(t *testing.T) { } } } + func BenchmarkLargeHostMatcher(b *testing.B) { // this benchmark simulates a large host matcher (thousands of entries) where each // value is an exact hostname (not a placeholder or wildcard) - compare the results diff --git a/modules/caddyhttp/metrics.go b/modules/caddyhttp/metrics.go index 9c0f961e5..111389218 100644 --- a/modules/caddyhttp/metrics.go +++ b/modules/caddyhttp/metrics.go @@ -2,6 +2,7 @@ package caddyhttp import ( "context" + "errors" "net/http" "sync" "time" @@ -137,22 +138,33 @@ func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Re err := h.mh.ServeHTTP(wrec, r, next) dur := time.Since(start).Seconds() httpMetrics.requestCount.With(labels).Inc() + + observeRequest := func(status int) { + // If the code hasn't been set yet, and we didn't encounter an error, we're + // probably falling through with an empty handler. + if statusLabels["code"] == "" { + // we still sanitize it, even though it's likely to be 0. A 200 is + // returned on fallthrough so we want to reflect that. + statusLabels["code"] = metrics.SanitizeCode(status) + } + + httpMetrics.requestDuration.With(statusLabels).Observe(dur) + httpMetrics.requestSize.With(statusLabels).Observe(float64(computeApproximateRequestSize(r))) + httpMetrics.responseSize.With(statusLabels).Observe(float64(wrec.Size())) + } + if err != nil { + var handlerErr HandlerError + if errors.As(err, &handlerErr) { + observeRequest(handlerErr.StatusCode) + } + httpMetrics.requestErrors.With(labels).Inc() + return err } - // If the code hasn't been set yet, and we didn't encounter an error, we're - // probably falling through with an empty handler. - if statusLabels["code"] == "" { - // we still sanitize it, even though it's likely to be 0. A 200 is - // returned on fallthrough so we want to reflect that. - statusLabels["code"] = metrics.SanitizeCode(wrec.Status()) - } - - httpMetrics.requestDuration.With(statusLabels).Observe(dur) - httpMetrics.requestSize.With(statusLabels).Observe(float64(computeApproximateRequestSize(r))) - httpMetrics.responseSize.With(statusLabels).Observe(float64(wrec.Size())) + observeRequest(wrec.Status()) return nil } diff --git a/modules/caddyhttp/metrics_test.go b/modules/caddyhttp/metrics_test.go index 6311935ae..8f88549d5 100644 --- a/modules/caddyhttp/metrics_test.go +++ b/modules/caddyhttp/metrics_test.go @@ -5,8 +5,10 @@ import ( "errors" "net/http" "net/http/httptest" + "strings" "testing" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" ) @@ -75,6 +77,123 @@ func TestMetricsInstrumentedHandler(t *testing.T) { if actual := w.Result().Header; len(actual) != 0 { t.Errorf("Not empty: expected headers to be empty, but got %#v", actual) } + + // handler returning an error with an HTTP status + mh = middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error { + return Error(http.StatusTooManyRequests, nil) + }) + + ih = newMetricsInstrumentedHandler("foo", mh) + + r = httptest.NewRequest("GET", "/", nil) + w = httptest.NewRecorder() + + if err := ih.ServeHTTP(w, r, nil); err == nil { + t.Errorf("expected error to be propagated") + } + + expected := ` + # HELP caddy_http_request_duration_seconds Histogram of round-trip request durations. + # TYPE caddy_http_request_duration_seconds histogram + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="0.005"} 1 + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="0.01"} 1 + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="0.025"} 1 + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="0.05"} 1 + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="0.1"} 1 + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="0.25"} 1 + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="0.5"} 1 + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="1"} 1 + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="2.5"} 1 + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="5"} 1 + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="10"} 1 + caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="+Inf"} 1 + caddy_http_request_duration_seconds_count{code="429",handler="foo",method="GET",server="UNKNOWN"} 1 + # HELP caddy_http_request_size_bytes Total size of the request. Includes body + # TYPE caddy_http_request_size_bytes histogram + caddy_http_request_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="256"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="1024"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="4096"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="16384"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="65536"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="262144"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="1.048576e+06"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="4.194304e+06"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="+Inf"} 1 + caddy_http_request_size_bytes_sum{code="200",handler="bar",method="GET",server="UNKNOWN"} 23 + caddy_http_request_size_bytes_count{code="200",handler="bar",method="GET",server="UNKNOWN"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="256"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="1024"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="4096"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="16384"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="65536"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="262144"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="1.048576e+06"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="4.194304e+06"} 1 + caddy_http_request_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="+Inf"} 1 + caddy_http_request_size_bytes_sum{code="200",handler="empty",method="GET",server="UNKNOWN"} 23 + caddy_http_request_size_bytes_count{code="200",handler="empty",method="GET",server="UNKNOWN"} 1 + caddy_http_request_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="256"} 1 + caddy_http_request_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="1024"} 1 + caddy_http_request_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="4096"} 1 + caddy_http_request_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="16384"} 1 + caddy_http_request_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="65536"} 1 + caddy_http_request_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="262144"} 1 + caddy_http_request_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="1.048576e+06"} 1 + caddy_http_request_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="4.194304e+06"} 1 + caddy_http_request_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="+Inf"} 1 + caddy_http_request_size_bytes_sum{code="429",handler="foo",method="GET",server="UNKNOWN"} 23 + caddy_http_request_size_bytes_count{code="429",handler="foo",method="GET",server="UNKNOWN"} 1 + # HELP caddy_http_response_size_bytes Size of the returned response. + # TYPE caddy_http_response_size_bytes histogram + caddy_http_response_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="256"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="1024"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="4096"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="16384"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="65536"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="262144"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="1.048576e+06"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="4.194304e+06"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="+Inf"} 1 + caddy_http_response_size_bytes_sum{code="200",handler="bar",method="GET",server="UNKNOWN"} 12 + caddy_http_response_size_bytes_count{code="200",handler="bar",method="GET",server="UNKNOWN"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="256"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="1024"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="4096"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="16384"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="65536"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="262144"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="1.048576e+06"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="4.194304e+06"} 1 + caddy_http_response_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="+Inf"} 1 + caddy_http_response_size_bytes_sum{code="200",handler="empty",method="GET",server="UNKNOWN"} 0 + caddy_http_response_size_bytes_count{code="200",handler="empty",method="GET",server="UNKNOWN"} 1 + caddy_http_response_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="256"} 1 + caddy_http_response_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="1024"} 1 + caddy_http_response_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="4096"} 1 + caddy_http_response_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="16384"} 1 + caddy_http_response_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="65536"} 1 + caddy_http_response_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="262144"} 1 + caddy_http_response_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="1.048576e+06"} 1 + caddy_http_response_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="4.194304e+06"} 1 + caddy_http_response_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="+Inf"} 1 + caddy_http_response_size_bytes_sum{code="429",handler="foo",method="GET",server="UNKNOWN"} 0 + caddy_http_response_size_bytes_count{code="429",handler="foo",method="GET",server="UNKNOWN"} 1 + # HELP caddy_http_request_errors_total Number of requests resulting in middleware errors. + # TYPE caddy_http_request_errors_total counter + caddy_http_request_errors_total{handler="bar",server="UNKNOWN"} 1 + caddy_http_request_errors_total{handler="foo",server="UNKNOWN"} 1 + ` + if err := testutil.GatherAndCompare(prometheus.DefaultGatherer, strings.NewReader(expected), + "caddy_http_request_size_bytes", + "caddy_http_response_size_bytes", + // caddy_http_request_duration_seconds_sum will vary based on how long the test took to run, + // so we check just the _bucket and _count metrics + "caddy_http_request_duration_seconds_bucket", + "caddy_http_request_duration_seconds_count", + "caddy_http_request_errors_total", + ); err != nil { + t.Errorf("received unexpected error: %s", err) + } } type middlewareHandlerFunc func(http.ResponseWriter, *http.Request, Handler) error diff --git a/modules/caddyhttp/proxyprotocol/listenerwrapper.go b/modules/caddyhttp/proxyprotocol/listenerwrapper.go index f404c06fe..e0d9b86ce 100644 --- a/modules/caddyhttp/proxyprotocol/listenerwrapper.go +++ b/modules/caddyhttp/proxyprotocol/listenerwrapper.go @@ -15,11 +15,11 @@ package proxyprotocol import ( - "fmt" "net" + "net/netip" "time" - "github.com/mastercactapus/proxyprotocol" + goproxy "github.com/pires/go-proxyproto" "github.com/caddyserver/caddy/v2" ) @@ -38,32 +38,74 @@ type ListenerWrapper struct { // Allow is an optional list of CIDR ranges to // allow/require PROXY headers from. Allow []string `json:"allow,omitempty"` + allow []netip.Prefix - rules []proxyprotocol.Rule + // Denby is an optional list of CIDR ranges to + // deny PROXY headers from. + Deny []string `json:"deny,omitempty"` + deny []netip.Prefix + + // Accepted values are: ignore, use, reject, require, skip + // default: ignore + // Policy definitions are here: https://pkg.go.dev/github.com/pires/go-proxyproto@v0.7.0#Policy + FallbackPolicy Policy `json:"fallback_policy,omitempty"` + + policy goproxy.PolicyFunc } // Provision sets up the listener wrapper. func (pp *ListenerWrapper) Provision(ctx caddy.Context) error { - rules := make([]proxyprotocol.Rule, 0, len(pp.Allow)) - for _, s := range pp.Allow { - _, n, err := net.ParseCIDR(s) + for _, cidr := range pp.Allow { + ipnet, err := netip.ParsePrefix(cidr) if err != nil { - return fmt.Errorf("invalid subnet '%s': %w", s, err) + return err } - rules = append(rules, proxyprotocol.Rule{ - Timeout: time.Duration(pp.Timeout), - Subnet: n, - }) + pp.allow = append(pp.allow, ipnet) } + for _, cidr := range pp.Deny { + ipnet, err := netip.ParsePrefix(cidr) + if err != nil { + return err + } + pp.deny = append(pp.deny, ipnet) + } + pp.policy = func(upstream net.Addr) (goproxy.Policy, error) { + // trust unix sockets + if network := upstream.Network(); caddy.IsUnixNetwork(network) { + return goproxy.USE, nil + } + ret := pp.FallbackPolicy + host, _, err := net.SplitHostPort(upstream.String()) + if err != nil { + return goproxy.REJECT, err + } - pp.rules = rules - + ip, err := netip.ParseAddr(host) + if err != nil { + return goproxy.REJECT, err + } + for _, ipnet := range pp.deny { + if ipnet.Contains(ip) { + return goproxy.REJECT, nil + } + } + for _, ipnet := range pp.allow { + if ipnet.Contains(ip) { + ret = PolicyUSE + break + } + } + return policyToGoProxyPolicy[ret], nil + } return nil } // WrapListener adds PROXY protocol support to the listener. func (pp *ListenerWrapper) WrapListener(l net.Listener) net.Listener { - pl := proxyprotocol.NewListener(l, time.Duration(pp.Timeout)) - pl.SetFilter(pp.rules) + pl := &goproxy.Listener{ + Listener: l, + ReadHeaderTimeout: time.Duration(pp.Timeout), + } + pl.Policy = pp.policy return pl } diff --git a/modules/caddyhttp/proxyprotocol/module.go b/modules/caddyhttp/proxyprotocol/module.go index 78f89c650..75a156a20 100644 --- a/modules/caddyhttp/proxyprotocol/module.go +++ b/modules/caddyhttp/proxyprotocol/module.go @@ -35,32 +35,44 @@ func (ListenerWrapper) CaddyModule() caddy.ModuleInfo { // proxy_protocol { // timeout // allow +// deny +// fallback_policy // } func (w *ListenerWrapper) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - // No same-line options are supported - if d.NextArg() { - return d.ArgErr() - } + d.Next() // consume wrapper name - for d.NextBlock(0) { - switch d.Val() { - case "timeout": - if !d.NextArg() { - return d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return d.Errf("parsing proxy_protocol timeout duration: %v", err) - } - w.Timeout = caddy.Duration(dur) + // No same-line options are supported + if d.NextArg() { + return d.ArgErr() + } - case "allow": - w.Allow = append(w.Allow, d.RemainingArgs()...) - - default: + for d.NextBlock(0) { + switch d.Val() { + case "timeout": + if !d.NextArg() { return d.ArgErr() } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return d.Errf("parsing proxy_protocol timeout duration: %v", err) + } + w.Timeout = caddy.Duration(dur) + + case "allow": + w.Allow = append(w.Allow, d.RemainingArgs()...) + case "deny": + w.Deny = append(w.Deny, d.RemainingArgs()...) + case "fallback_policy": + if !d.NextArg() { + return d.ArgErr() + } + p, err := parsePolicy(d.Val()) + if err != nil { + return d.WrapErr(err) + } + w.FallbackPolicy = p + default: + return d.ArgErr() } } return nil diff --git a/modules/caddyhttp/proxyprotocol/policy.go b/modules/caddyhttp/proxyprotocol/policy.go new file mode 100644 index 000000000..6dc8beb45 --- /dev/null +++ b/modules/caddyhttp/proxyprotocol/policy.go @@ -0,0 +1,82 @@ +package proxyprotocol + +import ( + "errors" + "fmt" + "strings" + + goproxy "github.com/pires/go-proxyproto" +) + +type Policy int + +// as defined in: https://pkg.go.dev/github.com/pires/go-proxyproto@v0.7.0#Policy +const ( + // IGNORE address from PROXY header, but accept connection + PolicyIGNORE Policy = iota + // USE address from PROXY header + PolicyUSE + // REJECT connection when PROXY header is sent + // Note: even though the first read on the connection returns an error if + // a PROXY header is present, subsequent reads do not. It is the task of + // the code using the connection to handle that case properly. + PolicyREJECT + // REQUIRE connection to send PROXY header, reject if not present + // Note: even though the first read on the connection returns an error if + // a PROXY header is not present, subsequent reads do not. It is the task + // of the code using the connection to handle that case properly. + PolicyREQUIRE + // SKIP accepts a connection without requiring the PROXY header + // Note: an example usage can be found in the SkipProxyHeaderForCIDR + // function. + PolicySKIP +) + +var policyToGoProxyPolicy = map[Policy]goproxy.Policy{ + PolicyUSE: goproxy.USE, + PolicyIGNORE: goproxy.IGNORE, + PolicyREJECT: goproxy.REJECT, + PolicyREQUIRE: goproxy.REQUIRE, + PolicySKIP: goproxy.SKIP, +} + +var policyMap = map[Policy]string{ + PolicyUSE: "USE", + PolicyIGNORE: "IGNORE", + PolicyREJECT: "REJECT", + PolicyREQUIRE: "REQUIRE", + PolicySKIP: "SKIP", +} + +var policyMapRev = map[string]Policy{ + "USE": PolicyUSE, + "IGNORE": PolicyIGNORE, + "REJECT": PolicyREJECT, + "REQUIRE": PolicyREQUIRE, + "SKIP": PolicySKIP, +} + +// MarshalText implements the text marshaller method. +func (x Policy) MarshalText() ([]byte, error) { + return []byte(policyMap[x]), nil +} + +// UnmarshalText implements the text unmarshaller method. +func (x *Policy) UnmarshalText(text []byte) error { + name := string(text) + tmp, err := parsePolicy(name) + if err != nil { + return err + } + *x = tmp + return nil +} + +func parsePolicy(name string) (Policy, error) { + if x, ok := policyMapRev[strings.ToUpper(name)]; ok { + return x, nil + } + return Policy(0), fmt.Errorf("%s is %w", name, errInvalidPolicy) +} + +var errInvalidPolicy = errors.New("invalid policy") diff --git a/modules/caddyhttp/push/caddyfile.go b/modules/caddyhttp/push/caddyfile.go index 963117c1c..07f6ccea7 100644 --- a/modules/caddyhttp/push/caddyfile.go +++ b/modules/caddyhttp/push/caddyfile.go @@ -44,63 +44,63 @@ func init() { // Placeholders are accepted in resource and header field // name and value and replacement tokens. func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + h.Next() // consume directive name + handler := new(Handler) - for h.Next() { - if h.NextArg() { - handler.Resources = append(handler.Resources, Resource{Target: h.Val()}) - } - - // optional block - for outerNesting := h.Nesting(); h.NextBlock(outerNesting); { - switch h.Val() { - case "headers": - if h.NextArg() { - return nil, h.ArgErr() - } - for innerNesting := h.Nesting(); h.NextBlock(innerNesting); { - var err error - - // include current token, which we treat as an argument here - args := []string{h.Val()} - args = append(args, h.RemainingArgs()...) - - if handler.Headers == nil { - handler.Headers = new(HeaderConfig) - } - - switch len(args) { - case 1: - err = headers.CaddyfileHeaderOp(&handler.Headers.HeaderOps, args[0], "", "") - case 2: - err = headers.CaddyfileHeaderOp(&handler.Headers.HeaderOps, args[0], args[1], "") - case 3: - err = headers.CaddyfileHeaderOp(&handler.Headers.HeaderOps, args[0], args[1], args[2]) - default: - return nil, h.ArgErr() - } - - if err != nil { - return nil, h.Err(err.Error()) - } - } - - case "GET", "HEAD": - method := h.Val() - if !h.NextArg() { - return nil, h.ArgErr() - } - target := h.Val() - handler.Resources = append(handler.Resources, Resource{ - Method: method, - Target: target, - }) - - default: - handler.Resources = append(handler.Resources, Resource{Target: h.Val()}) - } - } + // inline resources + if h.NextArg() { + handler.Resources = append(handler.Resources, Resource{Target: h.Val()}) } + // optional block + for h.NextBlock(0) { + switch h.Val() { + case "headers": + if h.NextArg() { + return nil, h.ArgErr() + } + for nesting := h.Nesting(); h.NextBlock(nesting); { + var err error + + // include current token, which we treat as an argument here + args := []string{h.Val()} + args = append(args, h.RemainingArgs()...) + + if handler.Headers == nil { + handler.Headers = new(HeaderConfig) + } + + switch len(args) { + case 1: + err = headers.CaddyfileHeaderOp(&handler.Headers.HeaderOps, args[0], "", "") + case 2: + err = headers.CaddyfileHeaderOp(&handler.Headers.HeaderOps, args[0], args[1], "") + case 3: + err = headers.CaddyfileHeaderOp(&handler.Headers.HeaderOps, args[0], args[1], args[2]) + default: + return nil, h.ArgErr() + } + + if err != nil { + return nil, h.Err(err.Error()) + } + } + + case "GET", "HEAD": + method := h.Val() + if !h.NextArg() { + return nil, h.ArgErr() + } + target := h.Val() + handler.Resources = append(handler.Resources, Resource{ + Method: method, + Target: target, + }) + + default: + handler.Resources = append(handler.Resources, Resource{Target: h.Val()}) + } + } return handler, nil } diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go index f6b042ce5..10258da01 100644 --- a/modules/caddyhttp/replacer.go +++ b/modules/caddyhttp/replacer.go @@ -19,7 +19,6 @@ import ( "context" "crypto/ecdsa" "crypto/ed25519" - "crypto/elliptic" "crypto/rsa" "crypto/sha256" "crypto/tls" @@ -40,6 +39,7 @@ import ( "time" "github.com/google/uuid" + "go.uber.org/zap" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddytls" @@ -157,9 +157,17 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo case "http.request.duration_ms": start := GetVar(req.Context(), "start_time").(time.Time) return time.Since(start).Seconds() * 1e3, true // multiply seconds to preserve decimal (see #4666) + case "http.request.uuid": + // fetch the UUID for this request id := GetVar(req.Context(), "uuid").(*requestID) + + // set it to this request's access log + extra := req.Context().Value(ExtraLogFieldsCtxKey).(*ExtraLogFields) + extra.Set(zap.String("uuid", id.String())) + return id.String(), true + case "http.request.body": if req.Body == nil { return "", true @@ -461,7 +469,11 @@ func marshalPublicKey(pubKey any) ([]byte, error) { case *rsa.PublicKey: return asn1.Marshal(key) case *ecdsa.PublicKey: - return elliptic.Marshal(key.Curve, key.X, key.Y), nil + e, err := key.ECDH() + if err != nil { + return nil, err + } + return e.Bytes(), nil case ed25519.PublicKey: return key, nil } diff --git a/modules/caddyhttp/requestbody/caddyfile.go b/modules/caddyhttp/requestbody/caddyfile.go index 8a92909fb..d2956cae0 100644 --- a/modules/caddyhttp/requestbody/caddyfile.go +++ b/modules/caddyhttp/requestbody/caddyfile.go @@ -26,25 +26,26 @@ func init() { } func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + h.Next() // consume directive name + rb := new(RequestBody) - for h.Next() { - // configuration should be in a block - for h.NextBlock(0) { - switch h.Val() { - case "max_size": - var sizeStr string - if !h.AllArgs(&sizeStr) { - return nil, h.ArgErr() - } - size, err := humanize.ParseBytes(sizeStr) - if err != nil { - return nil, h.Errf("parsing max_size: %v", err) - } - rb.MaxSize = int64(size) - default: - return nil, h.Errf("unrecognized servers option '%s'", h.Val()) + // configuration should be in a block + for h.NextBlock(0) { + switch h.Val() { + case "max_size": + var sizeStr string + if !h.AllArgs(&sizeStr) { + return nil, h.ArgErr() } + size, err := humanize.ParseBytes(sizeStr) + if err != nil { + return nil, h.Errf("parsing max_size: %v", err) + } + rb.MaxSize = int64(size) + + default: + return nil, h.Errf("unrecognized servers option '%s'", h.Val()) } } diff --git a/modules/caddyhttp/responsematchers.go b/modules/caddyhttp/responsematchers.go index 6bac7800b..cf96b00cd 100644 --- a/modules/caddyhttp/responsematchers.go +++ b/modules/caddyhttp/responsematchers.go @@ -67,55 +67,53 @@ func (rm ResponseMatcher) matchStatusCode(statusCode int) bool { // // @name [header []] | [status ] func ParseNamedResponseMatcher(d *caddyfile.Dispenser, matchers map[string]ResponseMatcher) error { - for d.Next() { - definitionName := d.Val() + d.Next() // consume matcher name + definitionName := d.Val() - if _, ok := matchers[definitionName]; ok { - return d.Errf("matcher is defined more than once: %s", definitionName) - } - - matcher := ResponseMatcher{} - for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); { - switch d.Val() { - case "header": - if matcher.Headers == nil { - matcher.Headers = http.Header{} - } - - // reuse the header request matcher's unmarshaler - headerMatcher := MatchHeader(matcher.Headers) - err := headerMatcher.UnmarshalCaddyfile(d.NewFromNextSegment()) - if err != nil { - return err - } - - matcher.Headers = http.Header(headerMatcher) - case "status": - if matcher.StatusCode == nil { - matcher.StatusCode = []int{} - } - - args := d.RemainingArgs() - if len(args) == 0 { - return d.ArgErr() - } - - for _, arg := range args { - if len(arg) == 3 && strings.HasSuffix(arg, "xx") { - arg = arg[:1] - } - statusNum, err := strconv.Atoi(arg) - if err != nil { - return d.Errf("bad status value '%s': %v", arg, err) - } - matcher.StatusCode = append(matcher.StatusCode, statusNum) - } - default: - return d.Errf("unrecognized response matcher %s", d.Val()) - } - } - - matchers[definitionName] = matcher + if _, ok := matchers[definitionName]; ok { + return d.Errf("matcher is defined more than once: %s", definitionName) } + + matcher := ResponseMatcher{} + for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); { + switch d.Val() { + case "header": + if matcher.Headers == nil { + matcher.Headers = http.Header{} + } + + // reuse the header request matcher's unmarshaler + headerMatcher := MatchHeader(matcher.Headers) + err := headerMatcher.UnmarshalCaddyfile(d.NewFromNextSegment()) + if err != nil { + return err + } + + matcher.Headers = http.Header(headerMatcher) + case "status": + if matcher.StatusCode == nil { + matcher.StatusCode = []int{} + } + + args := d.RemainingArgs() + if len(args) == 0 { + return d.ArgErr() + } + + for _, arg := range args { + if len(arg) == 3 && strings.HasSuffix(arg, "xx") { + arg = arg[:1] + } + statusNum, err := strconv.Atoi(arg) + if err != nil { + return d.Errf("bad status value '%s': %v", arg, err) + } + matcher.StatusCode = append(matcher.StatusCode, statusNum) + } + default: + return d.Errf("unrecognized response matcher %s", d.Val()) + } + } + matchers[definitionName] = matcher return nil } diff --git a/modules/caddyhttp/reverseproxy/admin.go b/modules/caddyhttp/reverseproxy/admin.go index f64d1ecf0..7e72a4cdb 100644 --- a/modules/caddyhttp/reverseproxy/admin.go +++ b/modules/caddyhttp/reverseproxy/admin.go @@ -32,7 +32,7 @@ func init() { // reverse proxy upstreams in the pool. type adminUpstreams struct{} -// upstreamResults holds the status of a particular upstream +// upstreamStatus holds the status of a particular upstream type upstreamStatus struct { Address string `json:"address"` NumRequests int `json:"num_requests"` diff --git a/modules/caddyhttp/reverseproxy/ascii_test.go b/modules/caddyhttp/reverseproxy/ascii_test.go index d1f0c1da9..de67963bd 100644 --- a/modules/caddyhttp/reverseproxy/ascii_test.go +++ b/modules/caddyhttp/reverseproxy/ascii_test.go @@ -26,7 +26,7 @@ package reverseproxy import "testing" func TestEqualFold(t *testing.T) { - var tests = []struct { + tests := []struct { name string a, b string want bool @@ -64,7 +64,7 @@ func TestEqualFold(t *testing.T) { } func TestIsPrint(t *testing.T) { - var tests = []struct { + tests := []struct { name string in string want bool diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go index 674776f87..42fd6f99d 100644 --- a/modules/caddyhttp/reverseproxy/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/caddyfile.go @@ -90,6 +90,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) // max_buffer_size // stream_timeout // stream_close_delay +// trace_logs // // # request manipulation // trusted_proxies [private_ranges] @@ -209,7 +210,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } } - for nesting := d.Nesting(); d.NextBlock(nesting); { + for d.NextBlock(0) { // if the subdirective has an "@" prefix then we // parse it as a response matcher for use with "handle_response" if strings.HasPrefix(d.Val(), matcherPrefix) { @@ -372,7 +373,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if len(values) == 0 { values = append(values, "") } - healthHeaders[key] = values + healthHeaders[key] = append(healthHeaders[key], values...) } if h.HealthChecks == nil { h.HealthChecks = new(HealthChecks) @@ -551,17 +552,24 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if !d.NextArg() { return d.ArgErr() } - size, err := humanize.ParseBytes(d.Val()) - if err != nil { - return d.Errf("invalid byte size '%s': %v", d.Val(), err) + val := d.Val() + var size int64 + if val == "unlimited" { + size = -1 + } else { + usize, err := humanize.ParseBytes(val) + if err != nil { + return d.Errf("invalid byte size '%s': %v", val, err) + } + size = int64(usize) } if d.NextArg() { return d.ArgErr() } if subdir == "request_buffers" { - h.RequestBuffers = int64(size) + h.RequestBuffers = size } else if subdir == "response_buffers" { - h.ResponseBuffers = int64(size) + h.ResponseBuffers = size } // TODO: These three properties are deprecated; remove them sometime after v2.6.4 @@ -766,7 +774,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } // make sure there's no block, cause it doesn't make sense - if d.NextBlock(1) { + if nesting := d.Nesting(); d.NextBlock(nesting) { return d.Errf("cannot define routes for 'replace_status', use 'handle_response' instead.") } @@ -775,6 +783,12 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { responseHandler, ) + case "verbose_logs": + if h.VerboseLogs { + return d.Err("verbose_logs already specified") + } + h.VerboseLogs = true + default: return d.Errf("unrecognized subdirective %s", d.Val()) } @@ -916,287 +930,296 @@ func (h *Handler) FinalizeUnmarshalCaddyfile(helper httpcaddyfile.Helper) error // max_idle_conns_per_host // } func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - for d.NextBlock(0) { - switch d.Val() { - case "read_buffer": - if !d.NextArg() { - return d.ArgErr() - } - size, err := humanize.ParseBytes(d.Val()) - if err != nil { - return d.Errf("invalid read buffer size '%s': %v", d.Val(), err) - } - h.ReadBufferSize = int(size) + d.Next() // consume transport name + for d.NextBlock(0) { + switch d.Val() { + case "read_buffer": + if !d.NextArg() { + return d.ArgErr() + } + size, err := humanize.ParseBytes(d.Val()) + if err != nil { + return d.Errf("invalid read buffer size '%s': %v", d.Val(), err) + } + h.ReadBufferSize = int(size) - case "write_buffer": - if !d.NextArg() { - return d.ArgErr() - } - size, err := humanize.ParseBytes(d.Val()) - if err != nil { - return d.Errf("invalid write buffer size '%s': %v", d.Val(), err) - } - h.WriteBufferSize = int(size) + case "write_buffer": + if !d.NextArg() { + return d.ArgErr() + } + size, err := humanize.ParseBytes(d.Val()) + if err != nil { + return d.Errf("invalid write buffer size '%s': %v", d.Val(), err) + } + h.WriteBufferSize = int(size) - case "read_timeout": - if !d.NextArg() { - return d.ArgErr() - } - timeout, err := caddy.ParseDuration(d.Val()) - if err != nil { - return d.Errf("invalid read timeout duration '%s': %v", d.Val(), err) - } - h.ReadTimeout = caddy.Duration(timeout) + case "read_timeout": + if !d.NextArg() { + return d.ArgErr() + } + timeout, err := caddy.ParseDuration(d.Val()) + if err != nil { + return d.Errf("invalid read timeout duration '%s': %v", d.Val(), err) + } + h.ReadTimeout = caddy.Duration(timeout) - case "write_timeout": - if !d.NextArg() { - return d.ArgErr() - } - timeout, err := caddy.ParseDuration(d.Val()) - if err != nil { - return d.Errf("invalid write timeout duration '%s': %v", d.Val(), err) - } - h.WriteTimeout = caddy.Duration(timeout) + case "write_timeout": + if !d.NextArg() { + return d.ArgErr() + } + timeout, err := caddy.ParseDuration(d.Val()) + if err != nil { + return d.Errf("invalid write timeout duration '%s': %v", d.Val(), err) + } + h.WriteTimeout = caddy.Duration(timeout) - case "max_response_header": - if !d.NextArg() { - return d.ArgErr() - } - size, err := humanize.ParseBytes(d.Val()) - if err != nil { - return d.Errf("invalid max response header size '%s': %v", d.Val(), err) - } - h.MaxResponseHeaderSize = int64(size) + case "max_response_header": + if !d.NextArg() { + return d.ArgErr() + } + size, err := humanize.ParseBytes(d.Val()) + if err != nil { + return d.Errf("invalid max response header size '%s': %v", d.Val(), err) + } + h.MaxResponseHeaderSize = int64(size) - case "proxy_protocol": - if !d.NextArg() { - return d.ArgErr() - } - switch proxyProtocol := d.Val(); proxyProtocol { - case "v1", "v2": - h.ProxyProtocol = proxyProtocol - default: - return d.Errf("invalid proxy protocol version '%s'", proxyProtocol) - } + case "proxy_protocol": + if !d.NextArg() { + return d.ArgErr() + } + switch proxyProtocol := d.Val(); proxyProtocol { + case "v1", "v2": + h.ProxyProtocol = proxyProtocol + default: + return d.Errf("invalid proxy protocol version '%s'", proxyProtocol) + } - case "dial_timeout": - if !d.NextArg() { - return d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return d.Errf("bad timeout value '%s': %v", d.Val(), err) - } - h.DialTimeout = caddy.Duration(dur) + case "dial_timeout": + if !d.NextArg() { + return d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return d.Errf("bad timeout value '%s': %v", d.Val(), err) + } + h.DialTimeout = caddy.Duration(dur) - case "dial_fallback_delay": - if !d.NextArg() { - return d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return d.Errf("bad fallback delay value '%s': %v", d.Val(), err) - } - h.FallbackDelay = caddy.Duration(dur) + case "dial_fallback_delay": + if !d.NextArg() { + return d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return d.Errf("bad fallback delay value '%s': %v", d.Val(), err) + } + h.FallbackDelay = caddy.Duration(dur) - case "response_header_timeout": - if !d.NextArg() { - return d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return d.Errf("bad timeout value '%s': %v", d.Val(), err) - } - h.ResponseHeaderTimeout = caddy.Duration(dur) + case "response_header_timeout": + if !d.NextArg() { + return d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return d.Errf("bad timeout value '%s': %v", d.Val(), err) + } + h.ResponseHeaderTimeout = caddy.Duration(dur) - case "expect_continue_timeout": - if !d.NextArg() { - return d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return d.Errf("bad timeout value '%s': %v", d.Val(), err) - } - h.ExpectContinueTimeout = caddy.Duration(dur) + case "expect_continue_timeout": + if !d.NextArg() { + return d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return d.Errf("bad timeout value '%s': %v", d.Val(), err) + } + h.ExpectContinueTimeout = caddy.Duration(dur) - case "resolvers": - if h.Resolver == nil { - h.Resolver = new(UpstreamResolver) - } - h.Resolver.Addresses = d.RemainingArgs() - if len(h.Resolver.Addresses) == 0 { - return d.Errf("must specify at least one resolver address") - } + case "resolvers": + if h.Resolver == nil { + h.Resolver = new(UpstreamResolver) + } + h.Resolver.Addresses = d.RemainingArgs() + if len(h.Resolver.Addresses) == 0 { + return d.Errf("must specify at least one resolver address") + } - case "tls": - if h.TLS == nil { - h.TLS = new(TLSConfig) - } + case "tls": + if h.TLS == nil { + h.TLS = new(TLSConfig) + } - case "tls_client_auth": - if h.TLS == nil { - h.TLS = new(TLSConfig) - } - args := d.RemainingArgs() - switch len(args) { - case 1: - h.TLS.ClientCertificateAutomate = args[0] - case 2: - h.TLS.ClientCertificateFile = args[0] - h.TLS.ClientCertificateKeyFile = args[1] - default: - return d.ArgErr() - } + case "tls_client_auth": + if h.TLS == nil { + h.TLS = new(TLSConfig) + } + args := d.RemainingArgs() + switch len(args) { + case 1: + h.TLS.ClientCertificateAutomate = args[0] + case 2: + h.TLS.ClientCertificateFile = args[0] + h.TLS.ClientCertificateKeyFile = args[1] + default: + return d.ArgErr() + } - case "tls_insecure_skip_verify": - if d.NextArg() { - return d.ArgErr() - } - if h.TLS == nil { - h.TLS = new(TLSConfig) - } - h.TLS.InsecureSkipVerify = true + case "tls_insecure_skip_verify": + if d.NextArg() { + return d.ArgErr() + } + if h.TLS == nil { + h.TLS = new(TLSConfig) + } + h.TLS.InsecureSkipVerify = true - case "tls_timeout": - if !d.NextArg() { - return d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return d.Errf("bad timeout value '%s': %v", d.Val(), err) - } - if h.TLS == nil { - h.TLS = new(TLSConfig) - } - h.TLS.HandshakeTimeout = caddy.Duration(dur) + case "tls_curves": + args := d.RemainingArgs() + if len(args) == 0 { + return d.ArgErr() + } + if h.TLS == nil { + h.TLS = new(TLSConfig) + } + h.TLS.Curves = args - case "tls_trusted_ca_certs": - args := d.RemainingArgs() - if len(args) == 0 { - return d.ArgErr() - } - if h.TLS == nil { - h.TLS = new(TLSConfig) - } - h.TLS.RootCAPEMFiles = args + case "tls_timeout": + if !d.NextArg() { + return d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return d.Errf("bad timeout value '%s': %v", d.Val(), err) + } + if h.TLS == nil { + h.TLS = new(TLSConfig) + } + h.TLS.HandshakeTimeout = caddy.Duration(dur) - case "tls_server_name": - if !d.NextArg() { - return d.ArgErr() - } - if h.TLS == nil { - h.TLS = new(TLSConfig) - } - h.TLS.ServerName = d.Val() + case "tls_trusted_ca_certs": + args := d.RemainingArgs() + if len(args) == 0 { + return d.ArgErr() + } + if h.TLS == nil { + h.TLS = new(TLSConfig) + } + h.TLS.RootCAPEMFiles = args - case "tls_renegotiation": - if h.TLS == nil { - h.TLS = new(TLSConfig) - } - if !d.NextArg() { - return d.ArgErr() - } - switch renegotiation := d.Val(); renegotiation { - case "never", "once", "freely": - h.TLS.Renegotiation = renegotiation - default: - return d.ArgErr() - } + case "tls_server_name": + if !d.NextArg() { + return d.ArgErr() + } + if h.TLS == nil { + h.TLS = new(TLSConfig) + } + h.TLS.ServerName = d.Val() - case "tls_except_ports": - if h.TLS == nil { - h.TLS = new(TLSConfig) - } - h.TLS.ExceptPorts = d.RemainingArgs() - if len(h.TLS.ExceptPorts) == 0 { - return d.ArgErr() - } + case "tls_renegotiation": + if h.TLS == nil { + h.TLS = new(TLSConfig) + } + if !d.NextArg() { + return d.ArgErr() + } + switch renegotiation := d.Val(); renegotiation { + case "never", "once", "freely": + h.TLS.Renegotiation = renegotiation + default: + return d.ArgErr() + } - case "keepalive": - if !d.NextArg() { - return d.ArgErr() - } - if h.KeepAlive == nil { - h.KeepAlive = new(KeepAlive) - } + case "tls_except_ports": + if h.TLS == nil { + h.TLS = new(TLSConfig) + } + h.TLS.ExceptPorts = d.RemainingArgs() + if len(h.TLS.ExceptPorts) == 0 { + return d.ArgErr() + } + + case "keepalive": + if !d.NextArg() { + return d.ArgErr() + } + if h.KeepAlive == nil { + h.KeepAlive = new(KeepAlive) + } + if d.Val() == "off" { + var disable bool + h.KeepAlive.Enabled = &disable + break + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return d.Errf("bad duration value '%s': %v", d.Val(), err) + } + h.KeepAlive.IdleConnTimeout = caddy.Duration(dur) + + case "keepalive_interval": + if !d.NextArg() { + return d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return d.Errf("bad interval value '%s': %v", d.Val(), err) + } + if h.KeepAlive == nil { + h.KeepAlive = new(KeepAlive) + } + h.KeepAlive.ProbeInterval = caddy.Duration(dur) + + case "keepalive_idle_conns": + if !d.NextArg() { + return d.ArgErr() + } + num, err := strconv.Atoi(d.Val()) + if err != nil { + return d.Errf("bad integer value '%s': %v", d.Val(), err) + } + if h.KeepAlive == nil { + h.KeepAlive = new(KeepAlive) + } + h.KeepAlive.MaxIdleConns = num + + case "keepalive_idle_conns_per_host": + if !d.NextArg() { + return d.ArgErr() + } + num, err := strconv.Atoi(d.Val()) + if err != nil { + return d.Errf("bad integer value '%s': %v", d.Val(), err) + } + if h.KeepAlive == nil { + h.KeepAlive = new(KeepAlive) + } + h.KeepAlive.MaxIdleConnsPerHost = num + + case "versions": + h.Versions = d.RemainingArgs() + if len(h.Versions) == 0 { + return d.ArgErr() + } + + case "compression": + if d.NextArg() { if d.Val() == "off" { var disable bool - h.KeepAlive.Enabled = &disable - break + h.Compression = &disable } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return d.Errf("bad duration value '%s': %v", d.Val(), err) - } - h.KeepAlive.IdleConnTimeout = caddy.Duration(dur) - - case "keepalive_interval": - if !d.NextArg() { - return d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return d.Errf("bad interval value '%s': %v", d.Val(), err) - } - if h.KeepAlive == nil { - h.KeepAlive = new(KeepAlive) - } - h.KeepAlive.ProbeInterval = caddy.Duration(dur) - - case "keepalive_idle_conns": - if !d.NextArg() { - return d.ArgErr() - } - num, err := strconv.Atoi(d.Val()) - if err != nil { - return d.Errf("bad integer value '%s': %v", d.Val(), err) - } - if h.KeepAlive == nil { - h.KeepAlive = new(KeepAlive) - } - h.KeepAlive.MaxIdleConns = num - - case "keepalive_idle_conns_per_host": - if !d.NextArg() { - return d.ArgErr() - } - num, err := strconv.Atoi(d.Val()) - if err != nil { - return d.Errf("bad integer value '%s': %v", d.Val(), err) - } - if h.KeepAlive == nil { - h.KeepAlive = new(KeepAlive) - } - h.KeepAlive.MaxIdleConnsPerHost = num - - case "versions": - h.Versions = d.RemainingArgs() - if len(h.Versions) == 0 { - return d.ArgErr() - } - - case "compression": - if d.NextArg() { - if d.Val() == "off" { - var disable bool - h.Compression = &disable - } - } - - case "max_conns_per_host": - if !d.NextArg() { - return d.ArgErr() - } - num, err := strconv.Atoi(d.Val()) - if err != nil { - return d.Errf("bad integer value '%s': %v", d.Val(), err) - } - h.MaxConnsPerHost = num - - default: - return d.Errf("unrecognized subdirective %s", d.Val()) } + + case "max_conns_per_host": + if !d.NextArg() { + return d.ArgErr() + } + num, err := strconv.Atoi(d.Val()) + if err != nil { + return d.Errf("bad integer value '%s': %v", d.Val(), err) + } + h.MaxConnsPerHost = num + + default: + return d.Errf("unrecognized subdirective %s", d.Val()) } } return nil @@ -1217,25 +1240,25 @@ func parseCopyResponseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHan // status // } func (h *CopyResponseHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - args := d.RemainingArgs() - if len(args) == 1 { - if num, err := strconv.Atoi(args[0]); err == nil && num > 0 { - h.StatusCode = caddyhttp.WeakString(args[0]) - break - } - } + d.Next() // consume directive name - for d.NextBlock(0) { - switch d.Val() { - case "status": - if !d.NextArg() { - return d.ArgErr() - } - h.StatusCode = caddyhttp.WeakString(d.Val()) - default: - return d.Errf("unrecognized subdirective '%s'", d.Val()) + args := d.RemainingArgs() + if len(args) == 1 { + if num, err := strconv.Atoi(args[0]); err == nil && num > 0 { + h.StatusCode = caddyhttp.WeakString(args[0]) + return nil + } + } + + for d.NextBlock(0) { + switch d.Val() { + case "status": + if !d.NextArg() { + return d.ArgErr() } + h.StatusCode = caddyhttp.WeakString(d.Val()) + default: + return d.Errf("unrecognized subdirective '%s'", d.Val()) } } return nil @@ -1257,23 +1280,23 @@ func parseCopyResponseHeadersCaddyfile(h httpcaddyfile.Helper) (caddyhttp.Middle // exclude // } func (h *CopyResponseHeadersHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - args := d.RemainingArgs() - if len(args) > 0 { - return d.ArgErr() - } + d.Next() // consume directive name - for d.NextBlock(0) { - switch d.Val() { - case "include": - h.Include = append(h.Include, d.RemainingArgs()...) + args := d.RemainingArgs() + if len(args) > 0 { + return d.ArgErr() + } - case "exclude": - h.Exclude = append(h.Exclude, d.RemainingArgs()...) + for d.NextBlock(0) { + switch d.Val() { + case "include": + h.Include = append(h.Include, d.RemainingArgs()...) - default: - return d.Errf("unrecognized subdirective '%s'", d.Val()) - } + case "exclude": + h.Exclude = append(h.Exclude, d.RemainingArgs()...) + + default: + return d.Errf("unrecognized subdirective '%s'", d.Val()) } } return nil @@ -1291,89 +1314,88 @@ func (h *CopyResponseHeadersHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) // dial_fallback_delay // } func (u *SRVUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - args := d.RemainingArgs() - if len(args) > 1 { - return d.ArgErr() - } - if len(args) > 0 { - u.Name = args[0] - } + d.Next() // consume upstream source name - for d.NextBlock(0) { - switch d.Val() { - case "service": - if !d.NextArg() { - return d.ArgErr() - } - if u.Service != "" { - return d.Errf("srv service has already been specified") - } - u.Service = d.Val() - - case "proto": - if !d.NextArg() { - return d.ArgErr() - } - if u.Proto != "" { - return d.Errf("srv proto has already been specified") - } - u.Proto = d.Val() - - case "name": - if !d.NextArg() { - return d.ArgErr() - } - if u.Name != "" { - return d.Errf("srv name has already been specified") - } - u.Name = d.Val() - - case "refresh": - if !d.NextArg() { - return d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return d.Errf("parsing refresh interval duration: %v", err) - } - u.Refresh = caddy.Duration(dur) - - case "resolvers": - if u.Resolver == nil { - u.Resolver = new(UpstreamResolver) - } - u.Resolver.Addresses = d.RemainingArgs() - if len(u.Resolver.Addresses) == 0 { - return d.Errf("must specify at least one resolver address") - } - - case "dial_timeout": - if !d.NextArg() { - return d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return d.Errf("bad timeout value '%s': %v", d.Val(), err) - } - u.DialTimeout = caddy.Duration(dur) - - case "dial_fallback_delay": - if !d.NextArg() { - return d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return d.Errf("bad delay value '%s': %v", d.Val(), err) - } - u.FallbackDelay = caddy.Duration(dur) - - default: - return d.Errf("unrecognized srv option '%s'", d.Val()) - } - } + args := d.RemainingArgs() + if len(args) > 1 { + return d.ArgErr() + } + if len(args) > 0 { + u.Name = args[0] } + for d.NextBlock(0) { + switch d.Val() { + case "service": + if !d.NextArg() { + return d.ArgErr() + } + if u.Service != "" { + return d.Errf("srv service has already been specified") + } + u.Service = d.Val() + + case "proto": + if !d.NextArg() { + return d.ArgErr() + } + if u.Proto != "" { + return d.Errf("srv proto has already been specified") + } + u.Proto = d.Val() + + case "name": + if !d.NextArg() { + return d.ArgErr() + } + if u.Name != "" { + return d.Errf("srv name has already been specified") + } + u.Name = d.Val() + + case "refresh": + if !d.NextArg() { + return d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return d.Errf("parsing refresh interval duration: %v", err) + } + u.Refresh = caddy.Duration(dur) + + case "resolvers": + if u.Resolver == nil { + u.Resolver = new(UpstreamResolver) + } + u.Resolver.Addresses = d.RemainingArgs() + if len(u.Resolver.Addresses) == 0 { + return d.Errf("must specify at least one resolver address") + } + + case "dial_timeout": + if !d.NextArg() { + return d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return d.Errf("bad timeout value '%s': %v", d.Val(), err) + } + u.DialTimeout = caddy.Duration(dur) + + case "dial_fallback_delay": + if !d.NextArg() { + return d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return d.Errf("bad delay value '%s': %v", d.Val(), err) + } + u.FallbackDelay = caddy.Duration(dur) + + default: + return d.Errf("unrecognized srv option '%s'", d.Val()) + } + } return nil } @@ -1389,105 +1411,104 @@ func (u *SRVUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // versions ipv4|ipv6 // } func (u *AUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - args := d.RemainingArgs() - if len(args) > 2 { - return d.ArgErr() - } - if len(args) > 0 { - u.Name = args[0] - if len(args) == 2 { - u.Port = args[1] - } - } + d.Next() // consume upstream source name - for d.NextBlock(0) { - switch d.Val() { - case "name": - if !d.NextArg() { - return d.ArgErr() - } - if u.Name != "" { - return d.Errf("a name has already been specified") - } - u.Name = d.Val() - - case "port": - if !d.NextArg() { - return d.ArgErr() - } - if u.Port != "" { - return d.Errf("a port has already been specified") - } - u.Port = d.Val() - - case "refresh": - if !d.NextArg() { - return d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return d.Errf("parsing refresh interval duration: %v", err) - } - u.Refresh = caddy.Duration(dur) - - case "resolvers": - if u.Resolver == nil { - u.Resolver = new(UpstreamResolver) - } - u.Resolver.Addresses = d.RemainingArgs() - if len(u.Resolver.Addresses) == 0 { - return d.Errf("must specify at least one resolver address") - } - - case "dial_timeout": - if !d.NextArg() { - return d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return d.Errf("bad timeout value '%s': %v", d.Val(), err) - } - u.DialTimeout = caddy.Duration(dur) - - case "dial_fallback_delay": - if !d.NextArg() { - return d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return d.Errf("bad delay value '%s': %v", d.Val(), err) - } - u.FallbackDelay = caddy.Duration(dur) - - case "versions": - args := d.RemainingArgs() - if len(args) == 0 { - return d.Errf("must specify at least one version") - } - - if u.Versions == nil { - u.Versions = &IPVersions{} - } - - trueBool := true - for _, arg := range args { - switch arg { - case "ipv4": - u.Versions.IPv4 = &trueBool - case "ipv6": - u.Versions.IPv6 = &trueBool - default: - return d.Errf("unsupported version: '%s'", arg) - } - } - - default: - return d.Errf("unrecognized a option '%s'", d.Val()) - } + args := d.RemainingArgs() + if len(args) > 2 { + return d.ArgErr() + } + if len(args) > 0 { + u.Name = args[0] + if len(args) == 2 { + u.Port = args[1] } } + for d.NextBlock(0) { + switch d.Val() { + case "name": + if !d.NextArg() { + return d.ArgErr() + } + if u.Name != "" { + return d.Errf("a name has already been specified") + } + u.Name = d.Val() + + case "port": + if !d.NextArg() { + return d.ArgErr() + } + if u.Port != "" { + return d.Errf("a port has already been specified") + } + u.Port = d.Val() + + case "refresh": + if !d.NextArg() { + return d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return d.Errf("parsing refresh interval duration: %v", err) + } + u.Refresh = caddy.Duration(dur) + + case "resolvers": + if u.Resolver == nil { + u.Resolver = new(UpstreamResolver) + } + u.Resolver.Addresses = d.RemainingArgs() + if len(u.Resolver.Addresses) == 0 { + return d.Errf("must specify at least one resolver address") + } + + case "dial_timeout": + if !d.NextArg() { + return d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return d.Errf("bad timeout value '%s': %v", d.Val(), err) + } + u.DialTimeout = caddy.Duration(dur) + + case "dial_fallback_delay": + if !d.NextArg() { + return d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return d.Errf("bad delay value '%s': %v", d.Val(), err) + } + u.FallbackDelay = caddy.Duration(dur) + + case "versions": + args := d.RemainingArgs() + if len(args) == 0 { + return d.Errf("must specify at least one version") + } + + if u.Versions == nil { + u.Versions = &IPVersions{} + } + + trueBool := true + for _, arg := range args { + switch arg { + case "ipv4": + u.Versions.IPv4 = &trueBool + case "ipv6": + u.Versions.IPv6 = &trueBool + default: + return d.Errf("unsupported version: '%s'", arg) + } + } + + default: + return d.Errf("unrecognized a option '%s'", d.Val()) + } + } return nil } @@ -1497,26 +1518,25 @@ func (u *AUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // [...] // } func (u *MultiUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if d.NextArg() { - return d.ArgErr() - } + d.Next() // consume upstream source name - for nesting := d.Nesting(); d.NextBlock(nesting); { - dynModule := d.Val() - modID := "http.reverse_proxy.upstreams." + dynModule - unm, err := caddyfile.UnmarshalModule(d, modID) - if err != nil { - return err - } - source, ok := unm.(UpstreamSource) - if !ok { - return d.Errf("module %s (%T) is not an UpstreamSource", modID, unm) - } - u.SourcesRaw = append(u.SourcesRaw, caddyconfig.JSONModuleObject(source, "source", dynModule, nil)) - } + if d.NextArg() { + return d.ArgErr() } + for d.NextBlock(0) { + dynModule := d.Val() + modID := "http.reverse_proxy.upstreams." + dynModule + unm, err := caddyfile.UnmarshalModule(d, modID) + if err != nil { + return err + } + source, ok := unm.(UpstreamSource) + if !ok { + return d.Errf("module %s (%T) is not an UpstreamSource", modID, unm) + } + u.SourcesRaw = append(u.SourcesRaw, caddyconfig.JSONModuleObject(source, "source", dynModule, nil)) + } return nil } diff --git a/modules/caddyhttp/reverseproxy/command.go b/modules/caddyhttp/reverseproxy/command.go index 11f935cf9..59fa67df0 100644 --- a/modules/caddyhttp/reverseproxy/command.go +++ b/modules/caddyhttp/reverseproxy/command.go @@ -308,11 +308,9 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) { return caddy.ExitCodeFailedStartup, err } - for _, to := range toAddresses { - fmt.Printf("Caddy proxying %s -> %s\n", fromAddr.String(), to) - } + caddy.Log().Info("caddy proxying", zap.String("from", fromAddr.String()), zap.Strings("to", toAddresses)) if len(toAddresses) > 1 { - fmt.Println("Load balancing policy: random") + caddy.Log().Info("using default load balancing policy", zap.String("policy", "random")) } select {} diff --git a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go index a24a3ed69..68eee32be 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go @@ -46,76 +46,75 @@ func init() { // capture_stderr // } func (t *Transport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - for d.NextBlock(0) { - switch d.Val() { - case "root": - if !d.NextArg() { - return d.ArgErr() - } - t.Root = d.Val() - - case "split": - t.SplitPath = d.RemainingArgs() - if len(t.SplitPath) == 0 { - return d.ArgErr() - } - - case "env": - args := d.RemainingArgs() - if len(args) != 2 { - return d.ArgErr() - } - if t.EnvVars == nil { - t.EnvVars = make(map[string]string) - } - t.EnvVars[args[0]] = args[1] - - case "resolve_root_symlink": - if d.NextArg() { - return d.ArgErr() - } - t.ResolveRootSymlink = true - - case "dial_timeout": - if !d.NextArg() { - return d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return d.Errf("bad timeout value %s: %v", d.Val(), err) - } - t.DialTimeout = caddy.Duration(dur) - - case "read_timeout": - if !d.NextArg() { - return d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return d.Errf("bad timeout value %s: %v", d.Val(), err) - } - t.ReadTimeout = caddy.Duration(dur) - - case "write_timeout": - if !d.NextArg() { - return d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return d.Errf("bad timeout value %s: %v", d.Val(), err) - } - t.WriteTimeout = caddy.Duration(dur) - - case "capture_stderr": - if d.NextArg() { - return d.ArgErr() - } - t.CaptureStderr = true - - default: - return d.Errf("unrecognized subdirective %s", d.Val()) + d.Next() // consume transport name + for d.NextBlock(0) { + switch d.Val() { + case "root": + if !d.NextArg() { + return d.ArgErr() } + t.Root = d.Val() + + case "split": + t.SplitPath = d.RemainingArgs() + if len(t.SplitPath) == 0 { + return d.ArgErr() + } + + case "env": + args := d.RemainingArgs() + if len(args) != 2 { + return d.ArgErr() + } + if t.EnvVars == nil { + t.EnvVars = make(map[string]string) + } + t.EnvVars[args[0]] = args[1] + + case "resolve_root_symlink": + if d.NextArg() { + return d.ArgErr() + } + t.ResolveRootSymlink = true + + case "dial_timeout": + if !d.NextArg() { + return d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return d.Errf("bad timeout value %s: %v", d.Val(), err) + } + t.DialTimeout = caddy.Duration(dur) + + case "read_timeout": + if !d.NextArg() { + return d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return d.Errf("bad timeout value %s: %v", d.Val(), err) + } + t.ReadTimeout = caddy.Duration(dur) + + case "write_timeout": + if !d.NextArg() { + return d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return d.Errf("bad timeout value %s: %v", d.Val(), err) + } + t.WriteTimeout = caddy.Duration(dur) + + case "capture_stderr": + if d.NextArg() { + return d.ArgErr() + } + t.CaptureStderr = true + + default: + return d.Errf("unrecognized subdirective %s", d.Val()) } } return nil diff --git a/modules/caddyhttp/reverseproxy/fastcgi/client.go b/modules/caddyhttp/reverseproxy/fastcgi/client.go index 04513dd85..d944c5778 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/client.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/client.go @@ -221,7 +221,6 @@ func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Respons if statusIsCut { resp.Status = statusInfo } - } else { resp.StatusCode = http.StatusOK } diff --git a/modules/caddyhttp/reverseproxy/fastcgi/client_test.go b/modules/caddyhttp/reverseproxy/fastcgi/client_test.go index 29bb5785b..a2227a653 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/client_test.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/client_test.go @@ -48,7 +48,7 @@ import ( // and output "FAILED" in response const ( scriptFile = "/tank/www/fcgic_test.php" - //ipPort = "remote-php-serv:59000" + // ipPort = "remote-php-serv:59000" ipPort = "127.0.0.1:59000" ) @@ -57,7 +57,6 @@ var globalt *testing.T type FastCGIServer struct{} func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { - if err := req.ParseMultipartForm(100000000); err != nil { log.Printf("[ERROR] failed to parse: %v", err) } @@ -84,7 +83,7 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { if req.MultipartForm != nil { fileNum = len(req.MultipartForm.File) for kn, fns := range req.MultipartForm.File { - //fmt.Fprintln(resp, "server:filekey ", kn ) + // fmt.Fprintln(resp, "server:filekey ", kn ) length += len(kn) for _, f := range fns { fd, err := f.Open() @@ -101,13 +100,13 @@ func (s FastCGIServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { length += int(l0) defer fd.Close() md5 := fmt.Sprintf("%x", h.Sum(nil)) - //fmt.Fprintln(resp, "server:filemd5 ", md5 ) + // fmt.Fprintln(resp, "server:filemd5 ", md5 ) if kn != md5 { fmt.Fprintln(resp, "server:err ", md5, kn) stat = "FAILED" } - //fmt.Fprintln(resp, "server:filename ", f.Filename ) + // fmt.Fprintln(resp, "server:filename ", f.Filename ) } } } @@ -181,7 +180,6 @@ func sendFcgi(reqType int, fcgiParams map[string]string, data []byte, posts map[ } func generateRandFile(size int) (p string, m string) { - p = filepath.Join(os.TempDir(), "fcgict"+strconv.Itoa(rand.Int())) // open output file @@ -236,7 +234,7 @@ func DisabledTest(t *testing.T) { fcgiParams := make(map[string]string) fcgiParams["REQUEST_METHOD"] = "GET" fcgiParams["SERVER_PROTOCOL"] = "HTTP/1.1" - //fcgi_params["GATEWAY_INTERFACE"] = "CGI/1.1" + // fcgi_params["GATEWAY_INTERFACE"] = "CGI/1.1" fcgiParams["SCRIPT_FILENAME"] = scriptFile // simple GET diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go index 1b4f2d055..ad21ccb5c 100644 --- a/modules/caddyhttp/reverseproxy/healthchecks.go +++ b/modules/caddyhttp/reverseproxy/healthchecks.go @@ -358,11 +358,17 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre } ctx = context.WithValue(ctx, caddyhttp.OriginalRequestCtxKey, *req) req = req.WithContext(ctx) - for key, hdrs := range h.HealthChecks.Active.Headers { + + // set headers, using a replacer with only globals (env vars, system info, etc.) + repl := caddy.NewReplacer() + for key, vals := range h.HealthChecks.Active.Headers { + key = repl.ReplaceAll(key, "") if key == "Host" { - req.Host = h.HealthChecks.Active.Headers.Get(key) - } else { - req.Header[key] = hdrs + req.Host = repl.ReplaceAll(h.HealthChecks.Active.Headers.Get(key), "") + continue + } + for _, val := range vals { + req.Header.Add(key, repl.ReplaceKnown(val, "")) } } diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index 9290f7ecb..0a803a83a 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -28,7 +28,7 @@ import ( "strings" "time" - "github.com/mastercactapus/proxyprotocol" + "github.com/pires/go-proxyproto" "go.uber.org/zap" "golang.org/x/net/http2" @@ -207,44 +207,42 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e if !ok { return nil, fmt.Errorf("failed to get proxy protocol info from context") } - - // The src and dst have to be of the some address family. As we don't know the original - // dst address (it's kind of impossible to know) and this address is generelly of very + header := proxyproto.Header{ + SourceAddr: &net.TCPAddr{ + IP: proxyProtocolInfo.AddrPort.Addr().AsSlice(), + Port: int(proxyProtocolInfo.AddrPort.Port()), + Zone: proxyProtocolInfo.AddrPort.Addr().Zone(), + }, + } + // The src and dst have to be of the same address family. As we don't know the original + // dst address (it's kind of impossible to know) and this address is generally of very // little interest, we just set it to all zeros. - var destIP net.IP switch { case proxyProtocolInfo.AddrPort.Addr().Is4(): - destIP = net.IPv4zero + header.TransportProtocol = proxyproto.TCPv4 + header.DestinationAddr = &net.TCPAddr{ + IP: net.IPv4zero, + } case proxyProtocolInfo.AddrPort.Addr().Is6(): - destIP = net.IPv6zero + header.TransportProtocol = proxyproto.TCPv6 + header.DestinationAddr = &net.TCPAddr{ + IP: net.IPv6zero, + } default: return nil, fmt.Errorf("unexpected remote addr type in proxy protocol info") } - // TODO: We should probably migrate away from net.IP to use netip.Addr, - // but due to the upstream dependency, we can't do that yet. switch h.ProxyProtocol { case "v1": - header := proxyprotocol.HeaderV1{ - SrcIP: net.IP(proxyProtocolInfo.AddrPort.Addr().AsSlice()), - SrcPort: int(proxyProtocolInfo.AddrPort.Port()), - DestIP: destIP, - DestPort: 0, - } + header.Version = 1 caddyCtx.Logger().Debug("sending proxy protocol header v1", zap.Any("header", header)) - _, err = header.WriteTo(conn) case "v2": - header := proxyprotocol.HeaderV2{ - Command: proxyprotocol.CmdProxy, - Src: &net.TCPAddr{IP: net.IP(proxyProtocolInfo.AddrPort.Addr().AsSlice()), Port: int(proxyProtocolInfo.AddrPort.Port())}, - Dest: &net.TCPAddr{IP: destIP, Port: 0}, - } + header.Version = 2 caddyCtx.Logger().Debug("sending proxy protocol header v2", zap.Any("header", header)) - _, err = header.WriteTo(conn) default: return nil, fmt.Errorf("unexpected proxy protocol version") } - + _, err = header.WriteTo(conn) if err != nil { // identify this error as one that occurred during // dialing, which can be important when trying to @@ -493,6 +491,10 @@ type TLSConfig struct { // When specified, TLS will automatically be configured on the transport. // The value can be a list of any valid tcp port numbers, default empty. ExceptPorts []string `json:"except_ports,omitempty"` + + // The list of elliptic curves to support. Caddy's + // defaults are modern and secure. + Curves []string `json:"curves,omitempty"` } // MakeTLSClientConfig returns a tls.Config usable by a client to a backend. @@ -558,7 +560,6 @@ func (t TLSConfig) MakeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) { return nil, fmt.Errorf("failed reading ca cert: %v", err) } rootPool.AppendCertsFromPEM(pemData) - } cfg.RootCAs = rootPool } @@ -581,6 +582,15 @@ func (t TLSConfig) MakeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) { // throw all security out the window cfg.InsecureSkipVerify = t.InsecureSkipVerify + curvesAdded := make(map[tls.CurveID]struct{}) + for _, curveName := range t.Curves { + curveID := caddytls.SupportedCurves[curveName] + if _, ok := curvesAdded[curveID]; !ok { + curvesAdded[curveID] = struct{}{} + cfg.CurvePreferences = append(cfg.CurvePreferences, curveID) + } + } + // only return a config if it's not empty if reflect.DeepEqual(cfg, new(tls.Config)) { return nil, nil diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index f11c8e33a..1648b7791 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -191,6 +191,13 @@ type Handler struct { // - `{http.reverse_proxy.header.*}` The headers from the response HandleResponse []caddyhttp.ResponseHandler `json:"handle_response,omitempty"` + // If set, the proxy will write very detailed logs about its + // inner workings. Enable this only when debugging, as it + // will produce a lot of output. + // + // EXPERIMENTAL: This feature is subject to change or removal. + VerboseLogs bool `json:"verbose_logs,omitempty"` + Transport http.RoundTripper `json:"-"` CB CircuitBreaker `json:"-"` DynamicUpstreams UpstreamSource `json:"-"` @@ -357,7 +364,7 @@ func (h *Handler) Provision(ctx caddy.Context) error { // set defaults on passive health checks, if necessary if h.HealthChecks.Passive != nil { h.HealthChecks.Passive.logger = h.logger.Named("health_checker.passive") - if h.HealthChecks.Passive.FailDuration > 0 && h.HealthChecks.Passive.MaxFails == 0 { + if h.HealthChecks.Passive.MaxFails == 0 { h.HealthChecks.Passive.MaxFails = 1 } } @@ -480,7 +487,7 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h upstream := h.LoadBalancing.SelectionPolicy.Select(upstreams, r, w) if upstream == nil { if proxyErr == nil { - proxyErr = caddyhttp.Error(http.StatusServiceUnavailable, fmt.Errorf("no upstreams available")) + proxyErr = caddyhttp.Error(http.StatusServiceUnavailable, errNoUpstream) } if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r) { return true, proxyErr @@ -582,8 +589,12 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http. // feature if absolutely required, if read timeouts are // set, and if body size is limited if h.RequestBuffers != 0 && req.Body != nil { - req.Body, req.ContentLength = h.bufferedBody(req.Body, h.RequestBuffers) - req.Header.Set("Content-Length", strconv.FormatInt(req.ContentLength, 10)) + var readBytes int64 + req.Body, readBytes = h.bufferedBody(req.Body, h.RequestBuffers) + if h.RequestBuffers == -1 { + req.ContentLength = readBytes + req.Header.Set("Content-Length", strconv.FormatInt(req.ContentLength, 10)) + } } if req.ContentLength == 0 { @@ -772,7 +783,7 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe // regardless, and we should expect client disconnection in low-latency streaming // scenarios (see issue #4922) if h.FlushInterval == -1 { - req = req.WithContext(ignoreClientGoneContext{req.Context()}) + req = req.WithContext(context.WithoutCancel(req.Context())) } // do the round-trip; emit debug log with values we know are @@ -848,12 +859,6 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe break } - // otherwise, if there are any routes configured, execute those as the - // actual response instead of what we got from the proxy backend - if len(rh.Routes) == 0 { - continue - } - // set up the replacer so that parts of the original response can be // used for routing decisions for field, value := range res.Header { @@ -949,16 +954,24 @@ func (h *Handler) finalizeResponse( } rw.WriteHeader(res.StatusCode) + if h.VerboseLogs { + logger.Debug("wrote header") + } - err := h.copyResponse(rw, res.Body, h.flushInterval(req, res)) - res.Body.Close() // close now, instead of defer, to populate res.Trailer + err := h.copyResponse(rw, res.Body, h.flushInterval(req, res), logger) + errClose := res.Body.Close() // close now, instead of defer, to populate res.Trailer + if h.VerboseLogs || errClose != nil { + logger.Debug("closed response body from upstream", zap.Error(errClose)) + } if err != nil { // we're streaming the response and we've already written headers, so // there's nothing an error handler can do to recover at this point; - // the standard lib's proxy panics at this point, but we'll just log - // the error and abort the stream here + // we'll just log the error and abort the stream here and panic just as + // the standard lib's proxy to propagate the stream error. + // see issue https://github.com/caddyserver/caddy/issues/5951 logger.Error("aborting with incomplete response", zap.Error(err)) - return nil + // no extra logging from stdlib + panic(http.ErrAbortHandler) } if len(res.Trailer) > 0 { @@ -985,6 +998,10 @@ func (h *Handler) finalizeResponse( } } + if h.VerboseLogs { + logger.Debug("response finalized") + } + return nil } @@ -1016,17 +1033,23 @@ func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int // should be safe to retry, since without a connection, no // HTTP request can be transmitted; but if the error is not // specifically a dialer error, we need to be careful - if _, ok := proxyErr.(DialError); proxyErr != nil && !ok { + if proxyErr != nil { + _, isDialError := proxyErr.(DialError) + herr, isHandlerError := proxyErr.(caddyhttp.HandlerError) + // if the error occurred after a connection was established, // we have to assume the upstream received the request, and // retries need to be carefully decided, because some requests // are not idempotent - if lb.RetryMatch == nil && req.Method != "GET" { - // by default, don't retry requests if they aren't GET - return false - } - if !lb.RetryMatch.AnyMatch(req) { - return false + if !isDialError && !(isHandlerError && errors.Is(herr, errNoUpstream)) { + if lb.RetryMatch == nil && req.Method != "GET" { + // by default, don't retry requests if they aren't GET + return false + } + + if !lb.RetryMatch.AnyMatch(req) { + return false + } } } @@ -1074,7 +1097,7 @@ func (h Handler) provisionUpstream(upstream *Upstream) { // if the passive health checker has a non-zero UnhealthyRequestCount // but the upstream has no MaxRequests set (they are the same thing, - // but the passive health checker is a default value for for upstreams + // but the passive health checker is a default value for upstreams // without MaxRequests), copy the value into this upstream, since the // value in the upstream (MaxRequests) is what is used during // availability checks @@ -1396,37 +1419,14 @@ type handleResponseContext struct { isFinalized bool } -// ignoreClientGoneContext is a special context.Context type -// intended for use when doing a RoundTrip where you don't -// want a client disconnection to cancel the request during -// the roundtrip. -// This context clears cancellation, error, and deadline methods, -// but still allows values to pass through from its embedded -// context. -// -// TODO: This can be replaced with context.WithoutCancel once -// the minimum required version of Go is 1.21. -type ignoreClientGoneContext struct { - context.Context -} - -func (c ignoreClientGoneContext) Deadline() (deadline time.Time, ok bool) { - return -} - -func (c ignoreClientGoneContext) Done() <-chan struct{} { - return nil -} - -func (c ignoreClientGoneContext) Err() error { - return nil -} - // proxyHandleResponseContextCtxKey is the context key for the active proxy handler // so that handle_response routes can inherit some config options // from the proxy handler. const proxyHandleResponseContextCtxKey caddy.CtxKey = "reverse_proxy_handle_response_context" +// errNoUpstream occurs when there are no upstream available. +var errNoUpstream = fmt.Errorf("no upstreams available") + // Interface guards var ( _ caddy.Provisioner = (*Handler)(nil) diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies.go b/modules/caddyhttp/reverseproxy/selectionpolicies.go index bc6de3516..b6f807c16 100644 --- a/modules/caddyhttp/reverseproxy/selectionpolicies.go +++ b/modules/caddyhttp/reverseproxy/selectionpolicies.go @@ -68,10 +68,9 @@ func (r RandomSelection) Select(pool UpstreamPool, request *http.Request, _ http // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (r *RandomSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if d.NextArg() { - return d.ArgErr() - } + d.Next() // consume policy name + if d.NextArg() { + return d.ArgErr() } return nil } @@ -98,22 +97,22 @@ func (WeightedRoundRobinSelection) CaddyModule() caddy.ModuleInfo { // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (r *WeightedRoundRobinSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - args := d.RemainingArgs() - if len(args) == 0 { - return d.ArgErr() - } + d.Next() // consume policy name - for _, weight := range args { - weightInt, err := strconv.Atoi(weight) - if err != nil { - return d.Errf("invalid weight value '%s': %v", weight, err) - } - if weightInt < 1 { - return d.Errf("invalid weight value '%s': weight should be non-zero and positive", weight) - } - r.Weights = append(r.Weights, weightInt) + args := d.RemainingArgs() + if len(args) == 0 { + return d.ArgErr() + } + + for _, weight := range args { + weightInt, err := strconv.Atoi(weight) + if err != nil { + return d.Errf("invalid weight value '%s': %v", weight, err) } + if weightInt < 1 { + return d.Errf("invalid weight value '%s': weight should be non-zero and positive", weight) + } + r.Weights = append(r.Weights, weightInt) } return nil } @@ -179,17 +178,17 @@ func (RandomChoiceSelection) CaddyModule() caddy.ModuleInfo { // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (r *RandomChoiceSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if !d.NextArg() { - return d.ArgErr() - } - chooseStr := d.Val() - choose, err := strconv.Atoi(chooseStr) - if err != nil { - return d.Errf("invalid choice value '%s': %v", chooseStr, err) - } - r.Choose = choose + d.Next() // consume policy name + + if !d.NextArg() { + return d.ArgErr() } + chooseStr := d.Val() + choose, err := strconv.Atoi(chooseStr) + if err != nil { + return d.Errf("invalid choice value '%s': %v", chooseStr, err) + } + r.Choose = choose return nil } @@ -269,7 +268,7 @@ func (LeastConnSelection) Select(pool UpstreamPool, _ *http.Request, _ http.Resp // sample: https://en.wikipedia.org/wiki/Reservoir_sampling if numReqs == leastReqs { count++ - if count > 1 || (weakrand.Int()%count) == 0 { //nolint:gosec + if count == 1 || (weakrand.Int()%count) == 0 { //nolint:gosec bestHost = host } } @@ -280,10 +279,9 @@ func (LeastConnSelection) Select(pool UpstreamPool, _ *http.Request, _ http.Resp // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (r *LeastConnSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if d.NextArg() { - return d.ArgErr() - } + d.Next() // consume policy name + if d.NextArg() { + return d.ArgErr() } return nil } @@ -320,10 +318,9 @@ func (r *RoundRobinSelection) Select(pool UpstreamPool, _ *http.Request, _ http. // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (r *RoundRobinSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if d.NextArg() { - return d.ArgErr() - } + d.Next() // consume policy name + if d.NextArg() { + return d.ArgErr() } return nil } @@ -352,10 +349,9 @@ func (FirstSelection) Select(pool UpstreamPool, _ *http.Request, _ http.Response // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (r *FirstSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if d.NextArg() { - return d.ArgErr() - } + d.Next() // consume policy name + if d.NextArg() { + return d.ArgErr() } return nil } @@ -383,10 +379,9 @@ func (IPHashSelection) Select(pool UpstreamPool, req *http.Request, _ http.Respo // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (r *IPHashSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if d.NextArg() { - return d.ArgErr() - } + d.Next() // consume policy name + if d.NextArg() { + return d.ArgErr() } return nil } @@ -416,10 +411,9 @@ func (ClientIPHashSelection) Select(pool UpstreamPool, req *http.Request, _ http // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (r *ClientIPHashSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if d.NextArg() { - return d.ArgErr() - } + d.Next() // consume policy name + if d.NextArg() { + return d.ArgErr() } return nil } @@ -443,10 +437,9 @@ func (URIHashSelection) Select(pool UpstreamPool, req *http.Request, _ http.Resp // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (r *URIHashSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if d.NextArg() { - return d.ArgErr() - } + d.Next() // consume policy name + if d.NextArg() { + return d.ArgErr() } return nil } @@ -504,13 +497,14 @@ func (s QueryHashSelection) Select(pool UpstreamPool, req *http.Request, _ http. // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (s *QueryHashSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if !d.NextArg() { - return d.ArgErr() - } - s.Key = d.Val() + d.Next() // consume policy name + + if !d.NextArg() { + return d.ArgErr() } - for nesting := d.Nesting(); d.NextBlock(nesting); { + s.Key = d.Val() + + for d.NextBlock(0) { switch d.Val() { case "fallback": if !d.NextArg() { @@ -583,13 +577,14 @@ func (s HeaderHashSelection) Select(pool UpstreamPool, req *http.Request, _ http // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (s *HeaderHashSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if !d.NextArg() { - return d.ArgErr() - } - s.Field = d.Val() + d.Next() // consume policy name + + if !d.NextArg() { + return d.ArgErr() } - for nesting := d.Nesting(); d.NextBlock(nesting); { + s.Field = d.Val() + + for d.NextBlock(0) { switch d.Val() { case "fallback": if !d.NextArg() { @@ -660,12 +655,22 @@ func (s CookieHashSelection) Select(pool UpstreamPool, req *http.Request, w http if err != nil { return upstream } - http.SetCookie(w, &http.Cookie{ + cookie := &http.Cookie{ Name: s.Name, Value: sha, Path: "/", Secure: false, - }) + } + isProxyHttps := false + if trusted, ok := caddyhttp.GetVar(req.Context(), caddyhttp.TrustedProxyVarKey).(bool); ok && trusted { + xfp, xfpOk, _ := lastHeaderValue(req.Header, "X-Forwarded-Proto") + isProxyHttps = xfpOk && xfp == "https" + } + if req.TLS != nil || isProxyHttps { + cookie.Secure = true + cookie.SameSite = http.SameSiteNoneMode + } + http.SetCookie(w, cookie) return upstream } @@ -708,7 +713,7 @@ func (s *CookieHashSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { default: return d.ArgErr() } - for nesting := d.Nesting(); d.NextBlock(nesting); { + for d.NextBlock(0) { switch d.Val() { case "fallback": if !d.NextArg() { diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies_test.go b/modules/caddyhttp/reverseproxy/selectionpolicies_test.go index dc613a538..d7e79626c 100644 --- a/modules/caddyhttp/reverseproxy/selectionpolicies_test.go +++ b/modules/caddyhttp/reverseproxy/selectionpolicies_test.go @@ -629,7 +629,6 @@ func TestRandomChoicePolicy(t *testing.T) { if h == pool[0] { t.Error("RandomChoicePolicy should not choose pool[0]") } - } func TestCookieHashPolicy(t *testing.T) { @@ -659,6 +658,9 @@ func TestCookieHashPolicy(t *testing.T) { if cookieServer1.Name != "lb" { t.Error("cookieHashPolicy should set a cookie with name lb") } + if cookieServer1.Secure { + t.Error("cookieHashPolicy should set cookie Secure attribute to false when request is not secure") + } if h != pool[0] { t.Error("Expected cookieHashPolicy host to be the first only available host.") } @@ -688,6 +690,57 @@ func TestCookieHashPolicy(t *testing.T) { } } +func TestCookieHashPolicyWithSecureRequest(t *testing.T) { + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + cookieHashPolicy := CookieHashSelection{} + if err := cookieHashPolicy.Provision(ctx); err != nil { + t.Errorf("Provision error: %v", err) + t.FailNow() + } + + pool := testPool() + pool[0].Dial = "localhost:8080" + pool[1].Dial = "localhost:8081" + pool[2].Dial = "localhost:8082" + pool[0].setHealthy(true) + pool[1].setHealthy(false) + pool[2].setHealthy(false) + + // Create a test server that serves HTTPS requests + ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h := cookieHashPolicy.Select(pool, r, w) + if h != pool[0] { + t.Error("Expected cookieHashPolicy host to be the first only available host.") + } + })) + defer ts.Close() + + // Make a new HTTPS request to the test server + client := ts.Client() + request, err := http.NewRequest(http.MethodGet, ts.URL+"/test", nil) + if err != nil { + t.Fatal(err) + } + response, err := client.Do(request) + if err != nil { + t.Fatal(err) + } + + // Check if the cookie set is Secure and has SameSiteNone mode + cookies := response.Cookies() + if len(cookies) == 0 { + t.Fatal("Expected a cookie to be set") + } + cookie := cookies[0] + if !cookie.Secure { + t.Error("Expected cookie Secure attribute to be true when request is secure") + } + if cookie.SameSite != http.SameSiteNoneMode { + t.Error("Expected cookie SameSite attribute to be None when request is secure") + } +} + func TestCookieHashPolicyWithFirstFallback(t *testing.T) { ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) defer cancel() diff --git a/modules/caddyhttp/reverseproxy/streaming.go b/modules/caddyhttp/reverseproxy/streaming.go index c5369d8c3..2a5b5286a 100644 --- a/modules/caddyhttp/reverseproxy/streaming.go +++ b/modules/caddyhttp/reverseproxy/streaming.go @@ -68,7 +68,7 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, rw http.ResponseWrit //nolint:bodyclose conn, brw, hijackErr := http.NewResponseController(rw).Hijack() if errors.Is(hijackErr, http.ErrNotSupported) { - h.logger.Sugar().Errorf("can't switch protocols using non-Hijacker ResponseWriter type %T", rw) + h.logger.Error("can't switch protocols using non-Hijacker ResponseWriter", zap.String("type", fmt.Sprintf("%T", rw))) return } @@ -184,15 +184,22 @@ func (h Handler) isBidirectionalStream(req *http.Request, res *http.Response) bo (ae == "identity" || ae == "") } -func (h Handler) copyResponse(dst http.ResponseWriter, src io.Reader, flushInterval time.Duration) error { +func (h Handler) copyResponse(dst http.ResponseWriter, src io.Reader, flushInterval time.Duration, logger *zap.Logger) error { var w io.Writer = dst if flushInterval != 0 { + var mlwLogger *zap.Logger + if h.VerboseLogs { + mlwLogger = logger.Named("max_latency_writer") + } else { + mlwLogger = zap.NewNop() + } mlw := &maxLatencyWriter{ dst: dst, //nolint:bodyclose flush: http.NewResponseController(dst).Flush, latency: flushInterval, + logger: mlwLogger, } defer mlw.stop() @@ -205,19 +212,30 @@ func (h Handler) copyResponse(dst http.ResponseWriter, src io.Reader, flushInter buf := streamingBufPool.Get().(*[]byte) defer streamingBufPool.Put(buf) - _, err := h.copyBuffer(w, src, *buf) + + var copyLogger *zap.Logger + if h.VerboseLogs { + copyLogger = logger + } else { + copyLogger = zap.NewNop() + } + + _, err := h.copyBuffer(w, src, *buf, copyLogger) return err } // copyBuffer returns any write errors or non-EOF read errors, and the amount // of bytes written. -func (h Handler) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int64, error) { +func (h Handler) copyBuffer(dst io.Writer, src io.Reader, buf []byte, logger *zap.Logger) (int64, error) { if len(buf) == 0 { buf = make([]byte, defaultBufferSize) } var written int64 for { + logger.Debug("waiting to read from upstream") nr, rerr := src.Read(buf) + logger := logger.With(zap.Int("read", nr)) + logger.Debug("read from upstream", zap.Error(rerr)) if rerr != nil && rerr != io.EOF && rerr != context.Canceled { // TODO: this could be useful to know (indeed, it revealed an error in our // fastcgi PoC earlier; but it's this single error report here that necessitates @@ -229,10 +247,15 @@ func (h Handler) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int64, er h.logger.Error("reading from backend", zap.Error(rerr)) } if nr > 0 { + logger.Debug("writing to downstream") nw, werr := dst.Write(buf[:nr]) if nw > 0 { written += int64(nw) } + logger.Debug("wrote to downstream", + zap.Int("written", nw), + zap.Int64("written_total", written), + zap.Error(werr)) if werr != nil { return written, fmt.Errorf("writing: %w", werr) } @@ -452,18 +475,22 @@ type maxLatencyWriter struct { mu sync.Mutex // protects t, flushPending, and dst.Flush t *time.Timer flushPending bool + logger *zap.Logger } func (m *maxLatencyWriter) Write(p []byte) (n int, err error) { m.mu.Lock() defer m.mu.Unlock() n, err = m.dst.Write(p) + m.logger.Debug("wrote bytes", zap.Int("n", n), zap.Error(err)) if m.latency < 0 { + m.logger.Debug("flushing immediately") //nolint:errcheck m.flush() return } if m.flushPending { + m.logger.Debug("delayed flush already pending") return } if m.t == nil { @@ -471,6 +498,7 @@ func (m *maxLatencyWriter) Write(p []byte) (n int, err error) { } else { m.t.Reset(m.latency) } + m.logger.Debug("timer set for delayed flush", zap.Duration("duration", m.latency)) m.flushPending = true return } @@ -479,8 +507,10 @@ func (m *maxLatencyWriter) delayedFlush() { m.mu.Lock() defer m.mu.Unlock() if !m.flushPending { // if stop was called but AfterFunc already started this goroutine + m.logger.Debug("delayed flush is not pending") return } + m.logger.Debug("delayed flush") //nolint:errcheck m.flush() m.flushPending = false diff --git a/modules/caddyhttp/reverseproxy/streaming_test.go b/modules/caddyhttp/reverseproxy/streaming_test.go index 919538fea..3f6da2ffa 100644 --- a/modules/caddyhttp/reverseproxy/streaming_test.go +++ b/modules/caddyhttp/reverseproxy/streaming_test.go @@ -5,6 +5,8 @@ import ( "net/http/httptest" "strings" "testing" + + "github.com/caddyserver/caddy/v2" ) func TestHandlerCopyResponse(t *testing.T) { @@ -22,7 +24,7 @@ func TestHandlerCopyResponse(t *testing.T) { for _, d := range testdata { src := bytes.NewBuffer([]byte(d)) dst.Reset() - err := h.copyResponse(recorder, src, 0) + err := h.copyResponse(recorder, src, 0, caddy.Log()) if err != nil { t.Errorf("failed with error: %v", err) } diff --git a/modules/caddyhttp/reverseproxy/upstreams.go b/modules/caddyhttp/reverseproxy/upstreams.go index 09efdd518..46e45c646 100644 --- a/modules/caddyhttp/reverseproxy/upstreams.go +++ b/modules/caddyhttp/reverseproxy/upstreams.go @@ -49,6 +49,13 @@ type SRVUpstreams struct { // Results are cached between lookups. Default: 1m Refresh caddy.Duration `json:"refresh,omitempty"` + // If > 0 and there is an error with the lookup, + // continue to use the cached results for up to + // this long before trying again, (even though they + // are stale) instead of returning an error to the + // client. Default: 0s. + GracePeriod caddy.Duration `json:"grace_period,omitempty"` + // Configures the DNS resolver used to resolve the // SRV address to SRV records. Resolver *UpstreamResolver `json:"resolver,omitempty"` @@ -140,6 +147,12 @@ func (su SRVUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) { // out and an error will be returned alongside the remaining results, if any." Thus, we // only return an error if no records were also returned. if len(records) == 0 { + if su.GracePeriod > 0 { + su.logger.Error("SRV lookup failed; using previously cached", zap.Error(err)) + cached.freshness = time.Now().Add(time.Duration(su.GracePeriod) - time.Duration(su.Refresh)) + srvs[suAddr] = cached + return allNew(cached.upstreams), nil + } return nil, err } su.logger.Warn("SRV records filtered", zap.Error(err)) @@ -251,6 +264,8 @@ type AUpstreams struct { Versions *IPVersions `json:"versions,omitempty"` resolver *net.Resolver + + logger *zap.Logger } // CaddyModule returns the Caddy module information. @@ -261,7 +276,8 @@ func (AUpstreams) CaddyModule() caddy.ModuleInfo { } } -func (au *AUpstreams) Provision(_ caddy.Context) error { +func (au *AUpstreams) Provision(ctx caddy.Context) error { + au.logger = ctx.Logger() if au.Refresh == 0 { au.Refresh = caddy.Duration(time.Minute) } @@ -297,8 +313,8 @@ func (au *AUpstreams) Provision(_ caddy.Context) error { func (au AUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) { repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) - resolveIpv4 := au.Versions.IPv4 == nil || *au.Versions.IPv4 - resolveIpv6 := au.Versions.IPv6 == nil || *au.Versions.IPv6 + resolveIpv4 := au.Versions == nil || au.Versions.IPv4 == nil || *au.Versions.IPv4 + resolveIpv6 := au.Versions == nil || au.Versions.IPv6 == nil || *au.Versions.IPv6 // Map ipVersion early, so we can use it as part of the cache-key. // This should be fairly inexpensive and comes and the upside of @@ -343,6 +359,11 @@ func (au AUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) { name := repl.ReplaceAll(au.Name, "") port := repl.ReplaceAll(au.Port, "") + au.logger.Debug("refreshing A upstreams", + zap.String("version", ipVersion), + zap.String("name", name), + zap.String("port", port)) + ips, err := au.resolver.LookupIP(r.Context(), ipVersion, name) if err != nil { return nil, err @@ -350,6 +371,8 @@ func (au AUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) { upstreams := make([]Upstream, len(ips)) for i, ip := range ips { + au.logger.Debug("discovered A record", + zap.String("ip", ip.String())) upstreams[i] = Upstream{ Dial: net.JoinHostPort(ip.String(), port), } diff --git a/modules/caddyhttp/rewrite/caddyfile.go b/modules/caddyhttp/rewrite/caddyfile.go index a34c1bb08..31f7e9b48 100644 --- a/modules/caddyhttp/rewrite/caddyfile.go +++ b/modules/caddyhttp/rewrite/caddyfile.go @@ -26,7 +26,7 @@ import ( ) func init() { - httpcaddyfile.RegisterHandlerDirective("rewrite", parseCaddyfileRewrite) + httpcaddyfile.RegisterDirective("rewrite", parseCaddyfileRewrite) httpcaddyfile.RegisterHandlerDirective("method", parseCaddyfileMethod) httpcaddyfile.RegisterHandlerDirective("uri", parseCaddyfileURI) httpcaddyfile.RegisterDirective("handle_path", parseCaddyfileHandlePath) @@ -38,35 +38,49 @@ func init() { // // Only URI components which are given in will be set in the resulting URI. // See the docs for the rewrite handler for more information. -func parseCaddyfileRewrite(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { - var rewr Rewrite - for h.Next() { +func parseCaddyfileRewrite(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) { + h.Next() // consume directive name + + // count the tokens to determine what to do + argsCount := h.CountRemainingArgs() + if argsCount == 0 { + return nil, h.Errf("too few arguments; must have at least a rewrite URI") + } + if argsCount > 2 { + return nil, h.Errf("too many arguments; should only be a matcher and a URI") + } + + // with only one arg, assume it's a rewrite URI with no matcher token + if argsCount == 1 { if !h.NextArg() { return nil, h.ArgErr() } - rewr.URI = h.Val() - if h.NextArg() { - return nil, h.ArgErr() - } + return h.NewRoute(nil, Rewrite{URI: h.Val()}), nil } - return rewr, nil + + // parse the matcher token into a matcher set + userMatcherSet, err := h.ExtractMatcherSet() + if err != nil { + return nil, err + } + h.Next() // consume directive name again, matcher parsing does a reset + h.Next() // advance to the rewrite URI + + return h.NewRoute(userMatcherSet, Rewrite{URI: h.Val()}), nil } // parseCaddyfileMethod sets up a basic method rewrite handler from Caddyfile tokens. Syntax: // // method [] func parseCaddyfileMethod(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { - var rewr Rewrite - for h.Next() { - if !h.NextArg() { - return nil, h.ArgErr() - } - rewr.Method = h.Val() - if h.NextArg() { - return nil, h.ArgErr() - } + h.Next() // consume directive name + if !h.NextArg() { + return nil, h.ArgErr() } - return rewr, nil + if h.NextArg() { + return nil, h.ArgErr() + } + return Rewrite{Method: h.Val()}, nil } // parseCaddyfileURI sets up a handler for manipulating (but not "rewriting") the @@ -81,69 +95,133 @@ func parseCaddyfileMethod(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, // path_regexp is used, then regular expression replacements will be performed // on the path portion of the URI (and a limit cannot be set). func parseCaddyfileURI(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + h.Next() // consume directive name + + args := h.RemainingArgs() + if len(args) < 1 { + return nil, h.ArgErr() + } + var rewr Rewrite - for h.Next() { - args := h.RemainingArgs() - if len(args) < 2 { + + switch args[0] { + case "strip_prefix": + if len(args) > 2 { return nil, h.ArgErr() } - switch args[0] { - case "strip_prefix": - if len(args) > 2 { - return nil, h.ArgErr() - } - rewr.StripPathPrefix = args[1] - if !strings.HasPrefix(rewr.StripPathPrefix, "/") { - rewr.StripPathPrefix = "/" + rewr.StripPathPrefix - } - case "strip_suffix": - if len(args) > 2 { - return nil, h.ArgErr() - } - rewr.StripPathSuffix = args[1] - case "replace": - var find, replace, lim string - switch len(args) { - case 4: - lim = args[3] - fallthrough - case 3: - find = args[1] - replace = args[2] - default: - return nil, h.ArgErr() - } - - var limInt int - if lim != "" { - var err error - limInt, err = strconv.Atoi(lim) - if err != nil { - return nil, h.Errf("limit must be an integer; invalid: %v", err) - } - } - - rewr.URISubstring = append(rewr.URISubstring, substrReplacer{ - Find: find, - Replace: replace, - Limit: limInt, - }) - case "path_regexp": - if len(args) != 3 { - return nil, h.ArgErr() - } - find, replace := args[1], args[2] - rewr.PathRegexp = append(rewr.PathRegexp, ®exReplacer{ - Find: find, - Replace: replace, - }) - default: - return nil, h.Errf("unrecognized URI manipulation '%s'", args[0]) + rewr.StripPathPrefix = args[1] + if !strings.HasPrefix(rewr.StripPathPrefix, "/") { + rewr.StripPathPrefix = "/" + rewr.StripPathPrefix } + + case "strip_suffix": + if len(args) > 2 { + return nil, h.ArgErr() + } + rewr.StripPathSuffix = args[1] + + case "replace": + var find, replace, lim string + switch len(args) { + case 4: + lim = args[3] + fallthrough + case 3: + find = args[1] + replace = args[2] + default: + return nil, h.ArgErr() + } + + var limInt int + if lim != "" { + var err error + limInt, err = strconv.Atoi(lim) + if err != nil { + return nil, h.Errf("limit must be an integer; invalid: %v", err) + } + } + + rewr.URISubstring = append(rewr.URISubstring, substrReplacer{ + Find: find, + Replace: replace, + Limit: limInt, + }) + + case "path_regexp": + if len(args) != 3 { + return nil, h.ArgErr() + } + find, replace := args[1], args[2] + rewr.PathRegexp = append(rewr.PathRegexp, ®exReplacer{ + Find: find, + Replace: replace, + }) + + case "query": + if len(args) > 4 { + return nil, h.ArgErr() + } + rewr.Query = &queryOps{} + var hasArgs bool + if len(args) > 1 { + hasArgs = true + err := applyQueryOps(h, rewr.Query, args[1:]) + if err != nil { + return nil, err + } + } + + for h.NextBlock(0) { + if hasArgs { + return nil, h.Err("Cannot specify uri query rewrites in both argument and block") + } + queryArgs := []string{h.Val()} + queryArgs = append(queryArgs, h.RemainingArgs()...) + err := applyQueryOps(h, rewr.Query, queryArgs) + if err != nil { + return nil, err + } + } + + default: + return nil, h.Errf("unrecognized URI manipulation '%s'", args[0]) } return rewr, nil } +func applyQueryOps(h httpcaddyfile.Helper, qo *queryOps, args []string) error { + key := args[0] + switch { + case strings.HasPrefix(key, "-"): + if len(args) != 1 { + return h.ArgErr() + } + qo.Delete = append(qo.Delete, strings.TrimLeft(key, "-")) + + case strings.HasPrefix(key, "+"): + if len(args) != 2 { + return h.ArgErr() + } + param := strings.TrimLeft(key, "+") + qo.Add = append(qo.Add, queryOpsArguments{Key: param, Val: args[1]}) + + case strings.Contains(key, ">"): + if len(args) != 1 { + return h.ArgErr() + } + renameValKey := strings.Split(key, ">") + qo.Rename = append(qo.Rename, queryOpsArguments{Key: renameValKey[0], Val: renameValKey[1]}) + + default: + if len(args) != 2 { + return h.ArgErr() + } + qo.Set = append(qo.Set, queryOpsArguments{Key: key, Val: args[1]}) + } + return nil +} + // parseCaddyfileHandlePath parses the handle_path directive. Syntax: // // handle_path [] { @@ -153,9 +231,9 @@ func parseCaddyfileURI(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, err // Only path matchers (with a `/` prefix) are supported as this is a shortcut // for the handle directive with a strip_prefix rewrite. func parseCaddyfileHandlePath(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) { - if !h.Next() { - return nil, h.ArgErr() - } + h.Next() // consume directive name + + // there must be a path matcher if !h.NextArg() { return nil, h.ArgErr() } diff --git a/modules/caddyhttp/rewrite/rewrite.go b/modules/caddyhttp/rewrite/rewrite.go index 77ef668bf..1859f9df2 100644 --- a/modules/caddyhttp/rewrite/rewrite.go +++ b/modules/caddyhttp/rewrite/rewrite.go @@ -89,6 +89,9 @@ type Rewrite struct { // Performs regular expression replacements on the URI path. PathRegexp []*regexReplacer `json:"path_regexp,omitempty"` + // Mutates the query string of the URI. + Query *queryOps `json:"query,omitempty"` + logger *zap.Logger } @@ -269,6 +272,11 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool { rep.do(r, repl) } + // apply query operations + if rewr.Query != nil { + rewr.Query.do(r, repl) + } + // update the encoded copy of the URI r.RequestURI = r.URL.RequestURI() @@ -470,5 +478,73 @@ func changePath(req *http.Request, newVal func(pathOrRawPath string) string) { } } +// queryOps describes the operations to perform on query keys: add, set, rename and delete. +type queryOps struct { + // Renames a query key from Key to Val, without affecting the value. + Rename []queryOpsArguments `json:"rename,omitempty"` + + // Sets query parameters; overwrites a query key with the given value. + Set []queryOpsArguments `json:"set,omitempty"` + + // Adds query parameters; does not overwrite an existing query field, + // and only appends an additional value for that key if any already exist. + Add []queryOpsArguments `json:"add,omitempty"` + + // Deletes a given query key by name. + Delete []string `json:"delete,omitempty"` +} + +func (q *queryOps) do(r *http.Request, repl *caddy.Replacer) { + query := r.URL.Query() + + for _, renameParam := range q.Rename { + key := repl.ReplaceAll(renameParam.Key, "") + val := repl.ReplaceAll(renameParam.Val, "") + if key == "" || val == "" { + continue + } + query[val] = query[key] + delete(query, key) + } + + for _, setParam := range q.Set { + key := repl.ReplaceAll(setParam.Key, "") + if key == "" { + continue + } + val := repl.ReplaceAll(setParam.Val, "") + query[key] = []string{val} + } + + for _, addParam := range q.Add { + key := repl.ReplaceAll(addParam.Key, "") + if key == "" { + continue + } + val := repl.ReplaceAll(addParam.Val, "") + query[key] = append(query[key], val) + } + + for _, deleteParam := range q.Delete { + param := repl.ReplaceAll(deleteParam, "") + if param == "" { + continue + } + delete(query, param) + } + + r.URL.RawQuery = query.Encode() +} + +type queryOpsArguments struct { + // A key in the query string. Note that query string keys may appear multiple times. + Key string `json:"key,omitempty"` + + // The value for the given operation; for add and set, this is + // simply the value of the query, and for rename this is the + // query key to rename to. + Val string `json:"val,omitempty"` +} + // Interface guard var _ caddyhttp.MiddlewareHandler = (*Rewrite)(nil) diff --git a/modules/caddyhttp/rewrite/rewrite_test.go b/modules/caddyhttp/rewrite/rewrite_test.go index bb937ec5b..aaa142bc2 100644 --- a/modules/caddyhttp/rewrite/rewrite_test.go +++ b/modules/caddyhttp/rewrite/rewrite_test.go @@ -333,7 +333,7 @@ func TestRewrite(t *testing.T) { input: newRequest(t, "GET", "/foo/findme%2Fbar"), expect: newRequest(t, "GET", "/foo/replaced%2Fbar"), }, - + { rule: Rewrite{PathRegexp: []*regexReplacer{{Find: "/{2,}", Replace: "/"}}}, input: newRequest(t, "GET", "/foo//bar///baz?a=b//c"), diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index cf1760947..04ae003a7 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -173,6 +173,19 @@ type Server struct { // remote IP address. ClientIPHeaders []string `json:"client_ip_headers,omitempty"` + // If greater than zero, enables strict ClientIPHeaders + // (default X-Forwarded-For) parsing. If enabled, the + // ClientIPHeaders will be parsed from right to left, and + // the first value that is both valid and doesn't match the + // trusted proxy list will be used as client IP. If zero, + // the ClientIPHeaders will be parsed from left to right, + // and the first value that is a valid IP address will be + // used as client IP. + // + // This depends on `trusted_proxies` being configured. + // This option is disabled by default. + TrustedProxiesStrict int `json:"trusted_proxies_strict,omitempty"` + // Enables access logging and configures how access logs are handled // in this server. To minimally enable access logs, simply set this // to a non-null, empty struct. @@ -228,7 +241,6 @@ type Server struct { server *http.Server h3server *http3.Server - h3listeners []net.PacketConn // TODO: we have to hold these because quic-go won't close listeners it didn't create h2listeners []*http2Listener addresses []caddy.NetworkAddress @@ -241,6 +253,7 @@ type Server struct { connStateFuncs []func(net.Conn, http.ConnState) connContextFuncs []func(ctx context.Context, c net.Conn) context.Context onShutdownFuncs []func() + onStopFuncs []func(context.Context) error // TODO: Experimental (Nov. 2023) } // ServeHTTP is the entry point for all HTTP requests. @@ -289,12 +302,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // enable full-duplex for HTTP/1, ensuring the entire // request body gets consumed before writing the response - if s.EnableFullDuplex { - // TODO: Remove duplex_go12*.go abstraction once our - // minimum Go version is 1.21 or later - err := enableFullDuplex(w) + if s.EnableFullDuplex && r.ProtoMajor == 1 { + //nolint:bodyclose + err := http.NewResponseController(w).EnableFullDuplex() if err != nil { - s.accessLogger.Warn("failed to enable full duplex", zap.Error(err)) + s.logger.Warn("failed to enable full duplex", zap.Error(err)) } } @@ -555,13 +567,7 @@ func (s *Server) findLastRouteWithHostMatcher() int { // the listener, with Server s as the handler. func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error { addr.Network = getHTTP3Network(addr.Network) - lnAny, err := addr.Listen(s.ctx, 0, net.ListenConfig{}) - if err != nil { - return err - } - ln := lnAny.(net.PacketConn) - - h3ln, err := caddy.ListenQUIC(ln, tlsCfg, &s.activeRequests) + h3ln, err := addr.ListenQUIC(s.ctx, 0, net.ListenConfig{}, tlsCfg, &s.activeRequests) if err != nil { return fmt.Errorf("starting HTTP/3 QUIC listener: %v", err) } @@ -579,8 +585,6 @@ func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error } } - s.h3listeners = append(s.h3listeners, ln) - //nolint:errcheck go s.h3server.ServeListener(h3ln) @@ -627,11 +631,18 @@ func (s *Server) RegisterConnContext(f func(ctx context.Context, c net.Conn) con s.connContextFuncs = append(s.connContextFuncs, f) } -// RegisterOnShutdown registers f to be invoked on server shutdown. +// RegisterOnShutdown registers f to be invoked when the server begins to shut down. func (s *Server) RegisterOnShutdown(f func()) { s.onShutdownFuncs = append(s.onShutdownFuncs, f) } +// RegisterOnStop registers f to be invoked after the server has shut down completely. +// +// EXPERIMENTAL: Subject to change or removal. +func (s *Server) RegisterOnStop(f func(context.Context) error) { + s.onStopFuncs = append(s.onStopFuncs, f) +} + // HTTPErrorConfig determines how to handle errors // from the HTTP handlers. type HTTPErrorConfig struct { @@ -704,7 +715,7 @@ func (s *Server) logRequest( repl *caddy.Replacer, bodyReader *lengthReader, shouldLogCredentials bool, ) { // this request may be flagged as omitted from the logs - if skipLog, ok := GetVar(r.Context(), SkipLogVar).(bool); ok && skipLog { + if skip, ok := GetVar(r.Context(), LogSkipVar).(bool); ok && skip { return } @@ -848,17 +859,28 @@ func determineTrustedProxy(r *http.Request, s *Server) (bool, string) { if s.trustedProxies == nil { return false, ipAddr.String() } - for _, ipRange := range s.trustedProxies.GetIPRanges(r) { - if ipRange.Contains(ipAddr) { - // We trust the proxy, so let's try to - // determine the real client IP - return true, trustedRealClientIP(r, s.ClientIPHeaders, ipAddr.String()) + + if isTrustedClientIP(ipAddr, s.trustedProxies.GetIPRanges(r)) { + if s.TrustedProxiesStrict > 0 { + return true, strictUntrustedClientIp(r, s.ClientIPHeaders, s.trustedProxies.GetIPRanges(r), ipAddr.String()) } + return true, trustedRealClientIP(r, s.ClientIPHeaders, ipAddr.String()) } return false, ipAddr.String() } +// isTrustedClientIP returns true if the given IP address is +// in the list of trusted IP ranges. +func isTrustedClientIP(ipAddr netip.Addr, trusted []netip.Prefix) bool { + for _, ipRange := range trusted { + if ipRange.Contains(ipAddr) { + return true + } + } + return false +} + // trustedRealClientIP finds the client IP from the request assuming it is // from a trusted client. If there is no client IP headers, then the // direct remote address is returned. If there are client IP headers, @@ -893,6 +915,29 @@ func trustedRealClientIP(r *http.Request, headers []string, clientIP string) str return clientIP } +// strictUntrustedClientIp iterates through the list of client IP headers, +// parses them from right-to-left, and returns the first valid IP address +// that is untrusted. If no valid IP address is found, then the direct +// remote address is returned. +func strictUntrustedClientIp(r *http.Request, headers []string, trusted []netip.Prefix, clientIP string) string { + for _, headerName := range headers { + ips := strings.Split(strings.Join(r.Header.Values(headerName), ","), ",") + + for i := len(ips) - 1; i >= 0; i-- { + ip, _, _ := strings.Cut(strings.TrimSpace(ips[i]), "%") + ipAddr, err := netip.ParseAddr(ip) + if err != nil { + continue + } + if !isTrustedClientIP(ipAddr, trusted) { + return ipAddr.String() + } + } + } + + return clientIP +} + // cloneURL makes a copy of r.URL and returns a // new value that doesn't reference the original. func cloneURL(from, to *url.URL) { diff --git a/modules/caddyhttp/server_test.go b/modules/caddyhttp/server_test.go index 96a241ba8..fd0e1e349 100644 --- a/modules/caddyhttp/server_test.go +++ b/modules/caddyhttp/server_test.go @@ -6,6 +6,7 @@ import ( "io" "net/http" "net/http/httptest" + "net/netip" "testing" "time" @@ -144,3 +145,294 @@ func BenchmarkServer_LogRequest_WithTraceID(b *testing.B) { s.logRequest(accLog, req, wrec, &duration, repl, bodyReader, false) } } +func TestServer_TrustedRealClientIP_NoTrustedHeaders(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "192.0.2.1:12345" + ip := trustedRealClientIP(req, []string{}, "192.0.2.1") + + assert.Equal(t, ip, "192.0.2.1") +} + +func TestServer_TrustedRealClientIP_OneTrustedHeaderEmpty(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "192.0.2.1:12345" + ip := trustedRealClientIP(req, []string{"X-Forwarded-For"}, "192.0.2.1") + + assert.Equal(t, ip, "192.0.2.1") +} + +func TestServer_TrustedRealClientIP_OneTrustedHeaderInvalid(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "192.0.2.1:12345" + req.Header.Set("X-Forwarded-For", "not, an, ip") + ip := trustedRealClientIP(req, []string{"X-Forwarded-For"}, "192.0.2.1") + + assert.Equal(t, ip, "192.0.2.1") +} + +func TestServer_TrustedRealClientIP_OneTrustedHeaderValid(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "192.0.2.1:12345" + req.Header.Set("X-Forwarded-For", "10.0.0.1") + ip := trustedRealClientIP(req, []string{"X-Forwarded-For"}, "192.0.2.1") + + assert.Equal(t, ip, "10.0.0.1") +} + +func TestServer_TrustedRealClientIP_OneTrustedHeaderValidArray(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "192.0.2.1:12345" + req.Header.Set("X-Forwarded-For", "1.1.1.1, 2.2.2.2, 3.3.3.3") + ip := trustedRealClientIP(req, []string{"X-Forwarded-For"}, "192.0.2.1") + + assert.Equal(t, ip, "1.1.1.1") +} + +func TestServer_TrustedRealClientIP_SkipsInvalidIps(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "192.0.2.1:12345" + req.Header.Set("X-Forwarded-For", "not an ip, bad bad, 10.0.0.1") + ip := trustedRealClientIP(req, []string{"X-Forwarded-For"}, "192.0.2.1") + + assert.Equal(t, ip, "10.0.0.1") +} + +func TestServer_TrustedRealClientIP_MultipleTrustedHeaderValidArray(t *testing.T) { + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "192.0.2.1:12345" + req.Header.Set("Real-Client-IP", "1.1.1.1, 2.2.2.2, 3.3.3.3") + req.Header.Set("X-Forwarded-For", "3.3.3.3, 4.4.4.4") + ip1 := trustedRealClientIP(req, []string{"X-Forwarded-For", "Real-Client-IP"}, "192.0.2.1") + ip2 := trustedRealClientIP(req, []string{"Real-Client-IP", "X-Forwarded-For"}, "192.0.2.1") + ip3 := trustedRealClientIP(req, []string{"Missing-Header-IP", "Real-Client-IP", "X-Forwarded-For"}, "192.0.2.1") + + assert.Equal(t, ip1, "3.3.3.3") + assert.Equal(t, ip2, "1.1.1.1") + assert.Equal(t, ip3, "1.1.1.1") +} + +func TestServer_DetermineTrustedProxy_NoConfig(t *testing.T) { + server := &Server{} + + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "192.0.2.1:12345" + + trusted, clientIP := determineTrustedProxy(req, server) + + assert.False(t, trusted) + assert.Equal(t, clientIP, "192.0.2.1") +} + +func TestServer_DetermineTrustedProxy_NoConfigIpv6(t *testing.T) { + server := &Server{} + + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "[::1]:12345" + + trusted, clientIP := determineTrustedProxy(req, server) + + assert.False(t, trusted) + assert.Equal(t, clientIP, "::1") +} + +func TestServer_DetermineTrustedProxy_NoConfigIpv6Zones(t *testing.T) { + server := &Server{} + + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "[::1%eth2]:12345" + + trusted, clientIP := determineTrustedProxy(req, server) + + assert.False(t, trusted) + assert.Equal(t, clientIP, "::1") +} + +func TestServer_DetermineTrustedProxy_TrustedLoopback(t *testing.T) { + loopbackPrefix, _ := netip.ParsePrefix("127.0.0.1/8") + + server := &Server{ + trustedProxies: &StaticIPRange{ + ranges: []netip.Prefix{loopbackPrefix}, + }, + ClientIPHeaders: []string{"X-Forwarded-For"}, + } + + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "127.0.0.1:12345" + req.Header.Set("X-Forwarded-For", "31.40.0.10") + + trusted, clientIP := determineTrustedProxy(req, server) + + assert.True(t, trusted) + assert.Equal(t, clientIP, "31.40.0.10") +} + +func TestServer_DetermineTrustedProxy_UntrustedPrefix(t *testing.T) { + loopbackPrefix, _ := netip.ParsePrefix("127.0.0.1/8") + + server := &Server{ + trustedProxies: &StaticIPRange{ + ranges: []netip.Prefix{loopbackPrefix}, + }, + ClientIPHeaders: []string{"X-Forwarded-For"}, + } + + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "10.0.0.1:12345" + req.Header.Set("X-Forwarded-For", "31.40.0.10") + + trusted, clientIP := determineTrustedProxy(req, server) + + assert.False(t, trusted) + assert.Equal(t, clientIP, "10.0.0.1") +} + +func TestServer_DetermineTrustedProxy_MultipleTrustedPrefixes(t *testing.T) { + loopbackPrefix, _ := netip.ParsePrefix("127.0.0.1/8") + localPrivatePrefix, _ := netip.ParsePrefix("10.0.0.0/8") + + server := &Server{ + trustedProxies: &StaticIPRange{ + ranges: []netip.Prefix{loopbackPrefix, localPrivatePrefix}, + }, + ClientIPHeaders: []string{"X-Forwarded-For"}, + } + + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "10.0.0.1:12345" + req.Header.Set("X-Forwarded-For", "31.40.0.10") + + trusted, clientIP := determineTrustedProxy(req, server) + + assert.True(t, trusted) + assert.Equal(t, clientIP, "31.40.0.10") +} + +func TestServer_DetermineTrustedProxy_MultipleTrustedClientHeaders(t *testing.T) { + loopbackPrefix, _ := netip.ParsePrefix("127.0.0.1/8") + localPrivatePrefix, _ := netip.ParsePrefix("10.0.0.0/8") + + server := &Server{ + trustedProxies: &StaticIPRange{ + ranges: []netip.Prefix{loopbackPrefix, localPrivatePrefix}, + }, + ClientIPHeaders: []string{"CF-Connecting-IP", "X-Forwarded-For"}, + } + + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "10.0.0.1:12345" + req.Header.Set("CF-Connecting-IP", "1.1.1.1, 2.2.2.2") + req.Header.Set("X-Forwarded-For", "3.3.3.3, 4.4.4.4") + + trusted, clientIP := determineTrustedProxy(req, server) + + assert.True(t, trusted) + assert.Equal(t, clientIP, "1.1.1.1") +} + +func TestServer_DetermineTrustedProxy_MatchLeftMostValidIp(t *testing.T) { + localPrivatePrefix, _ := netip.ParsePrefix("10.0.0.0/8") + + server := &Server{ + trustedProxies: &StaticIPRange{ + ranges: []netip.Prefix{localPrivatePrefix}, + }, + ClientIPHeaders: []string{"X-Forwarded-For"}, + } + + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "10.0.0.1:12345" + req.Header.Set("X-Forwarded-For", "30.30.30.30, 45.54.45.54, 10.0.0.1") + + trusted, clientIP := determineTrustedProxy(req, server) + + assert.True(t, trusted) + assert.Equal(t, clientIP, "30.30.30.30") +} + +func TestServer_DetermineTrustedProxy_MatchRightMostUntrusted(t *testing.T) { + localPrivatePrefix, _ := netip.ParsePrefix("10.0.0.0/8") + + server := &Server{ + trustedProxies: &StaticIPRange{ + ranges: []netip.Prefix{localPrivatePrefix}, + }, + ClientIPHeaders: []string{"X-Forwarded-For"}, + TrustedProxiesStrict: 1, + } + + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "10.0.0.1:12345" + req.Header.Set("X-Forwarded-For", "30.30.30.30, 45.54.45.54, 10.0.0.1") + + trusted, clientIP := determineTrustedProxy(req, server) + + assert.True(t, trusted) + assert.Equal(t, clientIP, "45.54.45.54") +} + +func TestServer_DetermineTrustedProxy_MatchRightMostUntrustedSkippingEmpty(t *testing.T) { + localPrivatePrefix, _ := netip.ParsePrefix("10.0.0.0/8") + + server := &Server{ + trustedProxies: &StaticIPRange{ + ranges: []netip.Prefix{localPrivatePrefix}, + }, + ClientIPHeaders: []string{"Missing-Header", "CF-Connecting-IP", "X-Forwarded-For"}, + TrustedProxiesStrict: 1, + } + + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "10.0.0.1:12345" + req.Header.Set("CF-Connecting-IP", "not a real IP") + req.Header.Set("X-Forwarded-For", "30.30.30.30, bad, 45.54.45.54, not real") + + trusted, clientIP := determineTrustedProxy(req, server) + + assert.True(t, trusted) + assert.Equal(t, clientIP, "45.54.45.54") +} + +func TestServer_DetermineTrustedProxy_MatchRightMostUntrustedSkippingTrusted(t *testing.T) { + localPrivatePrefix, _ := netip.ParsePrefix("10.0.0.0/8") + + server := &Server{ + trustedProxies: &StaticIPRange{ + ranges: []netip.Prefix{localPrivatePrefix}, + }, + ClientIPHeaders: []string{"CF-Connecting-IP", "X-Forwarded-For"}, + TrustedProxiesStrict: 1, + } + + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "10.0.0.1:12345" + req.Header.Set("CF-Connecting-IP", "10.0.0.1, 10.0.0.2, 10.0.0.3") + req.Header.Set("X-Forwarded-For", "30.30.30.30, 45.54.45.54, 10.0.0.4") + + trusted, clientIP := determineTrustedProxy(req, server) + + assert.True(t, trusted) + assert.Equal(t, clientIP, "45.54.45.54") +} + +func TestServer_DetermineTrustedProxy_MatchRightMostUntrustedFirst(t *testing.T) { + localPrivatePrefix, _ := netip.ParsePrefix("10.0.0.0/8") + + server := &Server{ + trustedProxies: &StaticIPRange{ + ranges: []netip.Prefix{localPrivatePrefix}, + }, + ClientIPHeaders: []string{"CF-Connecting-IP", "X-Forwarded-For"}, + TrustedProxiesStrict: 1, + } + + req := httptest.NewRequest("GET", "/", nil) + req.RemoteAddr = "10.0.0.1:12345" + req.Header.Set("CF-Connecting-IP", "10.0.0.1, 90.100.110.120, 10.0.0.2, 10.0.0.3") + req.Header.Set("X-Forwarded-For", "30.30.30.30, 45.54.45.54, 10.0.0.4") + + trusted, clientIP := determineTrustedProxy(req, server) + + assert.True(t, trusted) + assert.Equal(t, clientIP, "90.100.110.120") +} diff --git a/modules/caddyhttp/standard/imports.go b/modules/caddyhttp/standard/imports.go index d7bb2800c..236e7be1e 100644 --- a/modules/caddyhttp/standard/imports.go +++ b/modules/caddyhttp/standard/imports.go @@ -10,6 +10,7 @@ import ( _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/zstd" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" + _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/logging" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/map" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/proxyprotocol" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/push" diff --git a/modules/caddyhttp/staticerror.go b/modules/caddyhttp/staticerror.go index 7cf1a3e95..b6e10ff32 100644 --- a/modules/caddyhttp/staticerror.go +++ b/modules/caddyhttp/staticerror.go @@ -60,36 +60,35 @@ func (StaticError) CaddyModule() caddy.ModuleInfo { // If there is just one argument (other than the matcher), it is considered // to be a status code if it's a valid positive integer of 3 digits. func (e *StaticError) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - args := d.RemainingArgs() - switch len(args) { - case 1: - if len(args[0]) == 3 { - if num, err := strconv.Atoi(args[0]); err == nil && num > 0 { - e.StatusCode = WeakString(args[0]) - break - } + d.Next() // consume directive name + args := d.RemainingArgs() + switch len(args) { + case 1: + if len(args[0]) == 3 { + if num, err := strconv.Atoi(args[0]); err == nil && num > 0 { + e.StatusCode = WeakString(args[0]) + break } - e.Error = args[0] - case 2: - e.Error = args[0] - e.StatusCode = WeakString(args[1]) - default: - return d.ArgErr() } + e.Error = args[0] + case 2: + e.Error = args[0] + e.StatusCode = WeakString(args[1]) + default: + return d.ArgErr() + } - for d.NextBlock(0) { - switch d.Val() { - case "message": - if e.Error != "" { - return d.Err("message already specified") - } - if !d.AllArgs(&e.Error) { - return d.ArgErr() - } - default: - return d.Errf("unrecognized subdirective '%s'", d.Val()) + for d.NextBlock(0) { + switch d.Val() { + case "message": + if e.Error != "" { + return d.Err("message already specified") } + if !d.AllArgs(&e.Error) { + return d.ArgErr() + } + default: + return d.Errf("unrecognized subdirective '%s'", d.Val()) } } return nil diff --git a/modules/caddyhttp/staticresp.go b/modules/caddyhttp/staticresp.go index 4fe5910e4..3ec76f29f 100644 --- a/modules/caddyhttp/staticresp.go +++ b/modules/caddyhttp/staticresp.go @@ -138,41 +138,40 @@ func (StaticResponse) CaddyModule() caddy.ModuleInfo { // If there is just one argument (other than the matcher), it is considered // to be a status code if it's a valid positive integer of 3 digits. func (s *StaticResponse) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - args := d.RemainingArgs() - switch len(args) { - case 1: - if len(args[0]) == 3 { - if num, err := strconv.Atoi(args[0]); err == nil && num > 0 { - s.StatusCode = WeakString(args[0]) - break - } + d.Next() // consume directive name + args := d.RemainingArgs() + switch len(args) { + case 1: + if len(args[0]) == 3 { + if num, err := strconv.Atoi(args[0]); err == nil && num > 0 { + s.StatusCode = WeakString(args[0]) + break } - s.Body = args[0] - case 2: - s.Body = args[0] - s.StatusCode = WeakString(args[1]) - default: - return d.ArgErr() } + s.Body = args[0] + case 2: + s.Body = args[0] + s.StatusCode = WeakString(args[1]) + default: + return d.ArgErr() + } - for d.NextBlock(0) { - switch d.Val() { - case "body": - if s.Body != "" { - return d.Err("body already specified") - } - if !d.AllArgs(&s.Body) { - return d.ArgErr() - } - case "close": - if s.Close { - return d.Err("close already specified") - } - s.Close = true - default: - return d.Errf("unrecognized subdirective '%s'", d.Val()) + for d.NextBlock(0) { + switch d.Val() { + case "body": + if s.Body != "" { + return d.Err("body already specified") } + if !d.AllArgs(&s.Body) { + return d.ArgErr() + } + case "close": + if s.Close { + return d.Err("close already specified") + } + s.Close = true + default: + return d.Errf("unrecognized subdirective '%s'", d.Val()) } } return nil @@ -257,6 +256,53 @@ func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, next H return nil } +func buildHTTPServer(i int, port uint, addr string, statusCode int, hdr http.Header, body string, accessLog bool) (*Server, error) { + var handlers []json.RawMessage + + // response body supports a basic template; evaluate it + tplCtx := struct { + N int // server number + Port uint // only the port + Address string // listener address + }{ + N: i, + Port: port, + Address: addr, + } + tpl, err := template.New("body").Parse(body) + if err != nil { + return nil, err + } + buf := new(bytes.Buffer) + err = tpl.Execute(buf, tplCtx) + if err != nil { + return nil, err + } + + // create route with handler + handler := StaticResponse{ + StatusCode: WeakString(fmt.Sprintf("%d", statusCode)), + Headers: hdr, + Body: buf.String(), + } + handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "static_response", nil)) + route := Route{HandlersRaw: handlers} + + server := &Server{ + Listen: []string{addr}, + ReadHeaderTimeout: caddy.Duration(10 * time.Second), + IdleTimeout: caddy.Duration(30 * time.Second), + MaxHeaderBytes: 1024 * 10, + Routes: RouteList{route}, + AutoHTTPS: &AutoHTTPSConfig{DisableRedir: true}, + } + if accessLog { + server.Logs = new(ServerLogConfig) + } + + return server, nil +} + func cmdRespond(fl caddycmd.Flags) (int, error) { caddy.TrapSignals() @@ -332,65 +378,38 @@ func cmdRespond(fl caddycmd.Flags) (int, error) { hdr.Set(key, val) } + // build each HTTP server + httpApp := App{Servers: make(map[string]*Server)} + // expand listen address, if more than one port listenAddr, err := caddy.ParseNetworkAddress(listen) if err != nil { return caddy.ExitCodeFailedStartup, err } - listenAddrs := make([]string, 0, listenAddr.PortRangeSize()) - for offset := uint(0); offset < listenAddr.PortRangeSize(); offset++ { - listenAddrs = append(listenAddrs, listenAddr.JoinHostPort(offset)) - } - // build each HTTP server - httpApp := App{Servers: make(map[string]*Server)} - - for i, addr := range listenAddrs { - var handlers []json.RawMessage - - // response body supports a basic template; evaluate it - tplCtx := struct { - N int // server number - Port uint // only the port - Address string // listener address - }{ - N: i, - Port: listenAddr.StartPort + uint(i), - Address: addr, + if !listenAddr.IsUnixNetwork() { + listenAddrs := make([]string, 0, listenAddr.PortRangeSize()) + for offset := uint(0); offset < listenAddr.PortRangeSize(); offset++ { + listenAddrs = append(listenAddrs, listenAddr.JoinHostPort(offset)) } - tpl, err := template.New("body").Parse(body) + + for i, addr := range listenAddrs { + server, err := buildHTTPServer(i, listenAddr.StartPort+uint(i), addr, statusCode, hdr, body, accessLog) + if err != nil { + return caddy.ExitCodeFailedStartup, err + } + + // save server + httpApp.Servers[fmt.Sprintf("static%d", i)] = server + } + } else { + server, err := buildHTTPServer(0, 0, listen, statusCode, hdr, body, accessLog) if err != nil { return caddy.ExitCodeFailedStartup, err } - buf := new(bytes.Buffer) - err = tpl.Execute(buf, tplCtx) - if err != nil { - return caddy.ExitCodeFailedStartup, err - } - - // create route with handler - handler := StaticResponse{ - StatusCode: WeakString(fmt.Sprintf("%d", statusCode)), - Headers: hdr, - Body: buf.String(), - } - handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "static_response", nil)) - route := Route{HandlersRaw: handlers} - - server := &Server{ - Listen: []string{addr}, - ReadHeaderTimeout: caddy.Duration(10 * time.Second), - IdleTimeout: caddy.Duration(30 * time.Second), - MaxHeaderBytes: 1024 * 10, - Routes: RouteList{route}, - AutoHTTPS: &AutoHTTPSConfig{DisableRedir: true}, - } - if accessLog { - server.Logs = new(ServerLogConfig) - } // save server - httpApp.Servers[fmt.Sprintf("static%d", i)] = server + httpApp.Servers[fmt.Sprintf("static%d", 0)] = server } // finish building the config diff --git a/modules/caddyhttp/templates/caddyfile.go b/modules/caddyhttp/templates/caddyfile.go index 06ca3e260..d23493483 100644 --- a/modules/caddyhttp/templates/caddyfile.go +++ b/modules/caddyhttp/templates/caddyfile.go @@ -15,6 +15,9 @@ package templates import ( + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) @@ -31,24 +34,46 @@ func init() { // root // } func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + h.Next() // consume directive name t := new(Templates) - for h.Next() { - for h.NextBlock(0) { - switch h.Val() { - case "mime": - t.MIMETypes = h.RemainingArgs() - if len(t.MIMETypes) == 0 { - return nil, h.ArgErr() + for h.NextBlock(0) { + switch h.Val() { + case "mime": + t.MIMETypes = h.RemainingArgs() + if len(t.MIMETypes) == 0 { + return nil, h.ArgErr() + } + case "between": + t.Delimiters = h.RemainingArgs() + if len(t.Delimiters) != 2 { + return nil, h.ArgErr() + } + case "root": + if !h.Args(&t.FileRoot) { + return nil, h.ArgErr() + } + case "extensions": + if h.NextArg() { + return nil, h.ArgErr() + } + if t.ExtensionsRaw != nil { + return nil, h.Err("extensions already specified") + } + for nesting := h.Nesting(); h.NextBlock(nesting); { + extensionModuleName := h.Val() + modID := "http.handlers.templates.functions." + extensionModuleName + unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID) + if err != nil { + return nil, err } - case "between": - t.Delimiters = h.RemainingArgs() - if len(t.Delimiters) != 2 { - return nil, h.ArgErr() + cf, ok := unm.(CustomFunctions) + if !ok { + return nil, h.Errf("module %s (%T) does not provide template functions", modID, unm) } - case "root": - if !h.Args(&t.FileRoot) { - return nil, h.ArgErr() + if t.ExtensionsRaw == nil { + t.ExtensionsRaw = make(caddy.ModuleMap) } + t.ExtensionsRaw[extensionModuleName] = caddyconfig.JSON(cf, nil) } } } diff --git a/modules/caddyhttp/templates/templates.go b/modules/caddyhttp/templates/templates.go index 65359d9e9..b97093b6f 100644 --- a/modules/caddyhttp/templates/templates.go +++ b/modules/caddyhttp/templates/templates.go @@ -23,6 +23,8 @@ import ( "strings" "text/template" + "go.uber.org/zap" + "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) @@ -46,7 +48,8 @@ func init() { // // ##### `.Args` // -// A slice of arguments passed to this page/context, for example as the result of a `include`. +// A slice of arguments passed to this page/context, for example +// as the result of a [`include`](#include). // // ``` // {{index .Args 0}} // first argument @@ -103,8 +106,8 @@ func init() { // Reads and returns the contents of another file, and parses it // as a template, adding any template definitions to the template // stack. If there are no definitions, the filepath will be the -// definition name. Any {{ define }} blocks will be accessible by -// {{ template }} or {{ block }}. Imports must happen before the +// definition name. Any `{{ define }}` blocks will be accessible by +// `{{ template }}` or `{{ block }}`. Imports must happen before the // template or block action is called. Note that the contents are // NOT escaped, so you should only import trusted template files. // @@ -125,12 +128,13 @@ func init() { // // Includes the contents of another file, rendering it in-place. // Optionally can pass key-value pairs as arguments to be accessed -// by the included file. Note that the contents are NOT escaped, -// so you should only include trusted template files. +// by the included file. Use [`.Args N`](#args) to access the N-th +// argument, 0-indexed. Note that the contents are NOT escaped, so +// you should only include trusted template files. // // ``` // {{include "path/to/file.html"}} // no arguments -// {{include "path/to/file.html" "arg1" 2 "value 3"}} // with arguments +// {{include "path/to/file.html" "arg0" 1 "value 2"}} // with arguments // ``` // // ##### `readFile` @@ -145,7 +149,8 @@ func init() { // // ##### `listFiles` // -// Returns a list of the files in the given directory, which is relative to the template context's file root. +// Returns a list of the files in the given directory, which is relative +// to the template context's file root. // // ``` // {{listFiles "/mydir"}} @@ -165,12 +170,21 @@ func init() { // // ##### `.RemoteIP` // -// Returns the client's IP address. +// Returns the connection's IP address. // // ``` // {{.RemoteIP}} // ``` // +// ##### `.ClientIP` +// +// Returns the real client's IP address, if `trusted_proxies` was configured, +// otherwise returns the connection's IP address. +// +// ``` +// {{.ClientIP}} +// ``` +// // ##### `.Req` // // Accesses the current HTTP request, which has various fields, including: @@ -186,7 +200,8 @@ func init() { // // ##### `.OriginalReq` // -// Like .Req, except it accesses the original HTTP request before rewrites or other internal modifications. +// Like [`.Req`](#req), except it accesses the original HTTP +// request before rewrites or other internal modifications. // // ##### `.RespHeader.Add` // @@ -222,11 +237,13 @@ func init() { // // ##### `splitFrontMatter` // -// Splits front matter out from the body. Front matter is metadata that appears at the very beginning of a file or string. Front matter can be in YAML, TOML, or JSON formats: +// Splits front matter out from the body. Front matter is metadata that +// appears at the very beginning of a file or string. Front matter can +// be in YAML, TOML, or JSON formats: // // **TOML** front matter starts and ends with `+++`: // -// ``` +// ```toml // +++ // template = "blog" // title = "Blog Homepage" @@ -236,7 +253,7 @@ func init() { // // **YAML** is surrounded by `---`: // -// ``` +// ```yaml // --- // template: blog // title: Blog Homepage @@ -246,14 +263,12 @@ func init() { // // **JSON** is simply `{` and `}`: // -// ``` -// -// { -// "template": "blog", -// "title": "Blog Homepage", -// "sitename": "A Caddy site" -// } -// +// ```json +// { +// "template": "blog", +// "title": "Blog Homepage", +// "sitename": "A Caddy site" +// } // ``` // // The resulting front matter will be made available like so: @@ -306,10 +321,15 @@ type Templates struct { // the opening and closing delimiters. Default: `["{{", "}}"]` Delimiters []string `json:"delimiters,omitempty"` + // Extensions adds functions to the template's func map. These often + // act as components on web pages, for example. + ExtensionsRaw caddy.ModuleMap `json:"match,omitempty" caddy:"namespace=http.handlers.templates.functions"` + customFuncs []template.FuncMap + logger *zap.Logger } -// Customfunctions is the interface for registering custom template functions. +// CustomFunctions is the interface for registering custom template functions. type CustomFunctions interface { // CustomTemplateFunctions should return the mapping from custom function names to implementations. CustomTemplateFunctions() template.FuncMap @@ -325,17 +345,14 @@ func (Templates) CaddyModule() caddy.ModuleInfo { // Provision provisions t. func (t *Templates) Provision(ctx caddy.Context) error { - fnModInfos := caddy.GetModules("http.handlers.templates.functions") - customFuncs := make([]template.FuncMap, 0, len(fnModInfos)) - for _, modInfo := range fnModInfos { - mod := modInfo.New() - fnMod, ok := mod.(CustomFunctions) - if !ok { - return fmt.Errorf("module %q does not satisfy the CustomFunctions interface", modInfo.ID) - } - customFuncs = append(customFuncs, fnMod.CustomTemplateFunctions()) + t.logger = ctx.Logger() + mods, err := ctx.LoadModule(t, "ExtensionsRaw") + if err != nil { + return fmt.Errorf("loading template extensions: %v", err) + } + for _, modIface := range mods.(map[string]any) { + t.customFuncs = append(t.customFuncs, modIface.(CustomFunctions).CustomTemplateFunctions()) } - t.customFuncs = customFuncs if t.MIMETypes == nil { t.MIMETypes = defaultMIMETypes diff --git a/modules/caddyhttp/templates/tplcontext.go b/modules/caddyhttp/templates/tplcontext.go index 6b2b4954d..a66a0c305 100644 --- a/modules/caddyhttp/templates/tplcontext.go +++ b/modules/caddyhttp/templates/tplcontext.go @@ -23,6 +23,7 @@ import ( "net/http" "os" "path" + "reflect" "strconv" "strings" "sync" @@ -37,6 +38,7 @@ import ( "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" gmhtml "github.com/yuin/goldmark/renderer/html" + "go.uber.org/zap" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" @@ -57,7 +59,7 @@ type TemplateContext struct { // NewTemplate returns a new template intended to be evaluated with this // context, as it is initialized with configuration from this context. func (c *TemplateContext) NewTemplate(tplName string) *template.Template { - c.tpl = template.New(tplName) + c.tpl = template.New(tplName).Option("missingkey=zero") // customize delimiters, if applicable if c.config != nil && len(c.config.Delimiters) == 2 { @@ -88,6 +90,7 @@ func (c *TemplateContext) NewTemplate(tplName string) *template.Template { "fileExists": c.funcFileExists, "httpError": c.funcHTTPError, "humanize": c.funcHumanize, + "maybe": c.funcMaybe, }) return c.tpl } @@ -188,6 +191,7 @@ func (c TemplateContext) funcHTTPInclude(uri string) (string, error) { return "", err } virtReq.Host = c.Req.Host + virtReq.RemoteAddr = "127.0.0.1:10000" // https://github.com/caddyserver/caddy/issues/5835 virtReq.Header = c.Req.Header.Clone() virtReq.Header.Set("Accept-Encoding", "identity") // https://github.com/caddyserver/caddy/issues/4352 virtReq.Trailer = c.Req.Trailer.Clone() @@ -264,7 +268,7 @@ func (c TemplateContext) Cookie(name string) string { return "" } -// RemoteIP gets the IP address of the client making the request. +// RemoteIP gets the IP address of the connection's remote IP. func (c TemplateContext) RemoteIP() string { ip, _, err := net.SplitHostPort(c.Req.RemoteAddr) if err != nil { @@ -273,6 +277,18 @@ func (c TemplateContext) RemoteIP() string { return ip } +// ClientIP gets the IP address of the real client making the request +// if the request is trusted (see trusted_proxies), otherwise returns +// the connection's remote IP. +func (c TemplateContext) ClientIP() string { + address := caddyhttp.GetVar(c.Req.Context(), caddyhttp.ClientIPVarKey).(string) + clientIP, _, err := net.SplitHostPort(address) + if err != nil { + clientIP = address // no port + } + return clientIP +} + // Host returns the hostname portion of the Host header // from the HTTP request. func (c TemplateContext) Host() (string, error) { @@ -431,6 +447,14 @@ func (c TemplateContext) funcFileStat(filename string) (fs.FileInfo, error) { // funcHTTPError returns a structured HTTP handler error. EXPERIMENTAL; SUBJECT TO CHANGE. // Example usage: `{{if not (fileExists $includeFile)}}{{httpError 404}}{{end}}` func (c TemplateContext) funcHTTPError(statusCode int) (bool, error) { + // Delete some headers that may have been set by the underlying + // handler (such as file_server) which may break the error response. + c.RespHeader.Header.Del("Content-Length") + c.RespHeader.Header.Del("Content-Type") + c.RespHeader.Header.Del("Etag") + c.RespHeader.Header.Del("Last-Modified") + c.RespHeader.Header.Del("Accept-Ranges") + return false, caddyhttp.Error(statusCode, nil) } @@ -471,6 +495,51 @@ func (c TemplateContext) funcHumanize(formatType, data string) (string, error) { return "", fmt.Errorf("no know function was given") } +// funcMaybe invokes the plugged-in function named functionName if it is plugged in +// (is a module in the 'http.handlers.templates.functions' namespace). If it is not +// available, a log message is emitted. +// +// The first argument is the function name, and the rest of the arguments are +// passed on to the actual function. +// +// This function is useful for executing templates that use components that may be +// considered as optional in some cases (like during local development) where you do +// not want to require everyone to have a custom Caddy build to be able to execute +// your template. +// +// NOTE: This function is EXPERIMENTAL and subject to change or removal. +func (c TemplateContext) funcMaybe(functionName string, args ...any) (any, error) { + for _, funcMap := range c.CustomFuncs { + if fn, ok := funcMap[functionName]; ok { + val := reflect.ValueOf(fn) + if val.Kind() != reflect.Func { + continue + } + argVals := make([]reflect.Value, len(args)) + for i, arg := range args { + argVals[i] = reflect.ValueOf(arg) + } + returnVals := val.Call(argVals) + switch len(returnVals) { + case 0: + return "", nil + case 1: + return returnVals[0].Interface(), nil + case 2: + var err error + if !returnVals[1].IsNil() { + err = returnVals[1].Interface().(error) + } + return returnVals[0].Interface(), err + default: + return nil, fmt.Errorf("maybe %s: invalid number of return values: %d", functionName, len(returnVals)) + } + } + } + c.config.logger.Named("maybe").Warn("template function could not be found; ignoring invocation", zap.String("name", functionName)) + return "", nil +} + // WrappedHeader wraps niladic functions so that they // can be used in templates. (Template functions must // return a value.) diff --git a/modules/caddyhttp/templates/tplcontext_test.go b/modules/caddyhttp/templates/tplcontext_test.go index fdf2c1065..67ebbac70 100644 --- a/modules/caddyhttp/templates/tplcontext_test.go +++ b/modules/caddyhttp/templates/tplcontext_test.go @@ -17,7 +17,9 @@ package templates import ( "bytes" "context" + "errors" "fmt" + "io/fs" "net/http" "os" "path/filepath" @@ -30,8 +32,7 @@ import ( "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) -type handle struct { -} +type handle struct{} func (h *handle) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Accept-Encoding") == "identity" { @@ -176,11 +177,10 @@ func TestImport(t *testing.T) { } else if !templateWasDefined && actual != "" { // template should be defined, return value should be an empty string t.Errorf("Test %d: Expected template %s to be define but got %s", i, test.expect, tplContext.tpl.DefinedTemplates()) - } if absFilePath != "" { - if err := os.Remove(absFilePath); err != nil && !os.IsNotExist(err) { + if err := os.Remove(absFilePath); err != nil && !errors.Is(err, fs.ErrNotExist) { t.Fatalf("Test %d: Expected no error removing temporary test file, got: %v", i, err) } } @@ -255,21 +255,20 @@ func TestNestedInclude(t *testing.T) { } else if buf.String() != test.expect { // t.Errorf("Test %d: Expected '%s' but got '%s'", i, test.expect, buf.String()) - } if absFilePath != "" { - if err := os.Remove(absFilePath); err != nil && !os.IsNotExist(err) { + if err := os.Remove(absFilePath); err != nil && !errors.Is(err, fs.ErrNotExist) { t.Fatalf("Test %d: Expected no error removing temporary test file, got: %v", i, err) } } if absFilePath0 != "" { - if err := os.Remove(absFilePath0); err != nil && !os.IsNotExist(err) { + if err := os.Remove(absFilePath0); err != nil && !errors.Is(err, fs.ErrNotExist) { t.Fatalf("Test %d: Expected no error removing temporary test file, got: %v", i, err) } } if absFilePath1 != "" { - if err := os.Remove(absFilePath1); err != nil && !os.IsNotExist(err) { + if err := os.Remove(absFilePath1); err != nil && !errors.Is(err, fs.ErrNotExist) { t.Fatalf("Test %d: Expected no error removing temporary test file, got: %v", i, err) } } @@ -342,11 +341,10 @@ func TestInclude(t *testing.T) { t.Errorf("Test %d: Expected error but had none", i) } else if actual != test.expect { t.Errorf("Test %d: Expected %s but got %s", i, test.expect, actual) - } if absFilePath != "" { - if err := os.Remove(absFilePath); err != nil && !os.IsNotExist(err) { + if err := os.Remove(absFilePath); err != nil && !errors.Is(err, fs.ErrNotExist) { t.Fatalf("Test %d: Expected no error removing temporary test file, got: %v", i, err) } } @@ -460,7 +458,7 @@ func TestFileListing(t *testing.T) { fileNames: nil, inputBase: "doesNotExist", shouldErr: true, - verifyErr: os.IsNotExist, + verifyErr: func(err error) bool { return errors.Is(err, fs.ErrNotExist) }, }, { // directory and files exist, but path to a file @@ -476,7 +474,7 @@ func TestFileListing(t *testing.T) { fileNames: nil, inputBase: filepath.Join("..", "..", "..", "..", "..", "etc"), shouldErr: true, - verifyErr: os.IsNotExist, + verifyErr: func(err error) bool { return errors.Is(err, fs.ErrNotExist) }, }, } { tplContext := getContextOrFail(t) @@ -525,7 +523,7 @@ func TestFileListing(t *testing.T) { } if dirPath != "" { - if err := os.RemoveAll(dirPath); err != nil && !os.IsNotExist(err) { + if err := os.RemoveAll(dirPath); err != nil && !errors.Is(err, fs.ErrNotExist) { t.Fatalf("Test %d: Expected no error removing temporary test directory, got: %v", i, err) } } @@ -602,7 +600,6 @@ title = "Welcome" t.Errorf("Test %d: Expected body %s, found %s. Input was SplitFrontMatter(%s)", i, test.body, result.Body, test.input) } } - } func TestHumanize(t *testing.T) { diff --git a/modules/caddyhttp/tracing/module.go b/modules/caddyhttp/tracing/module.go index fd117c537..85fd63002 100644 --- a/modules/caddyhttp/tracing/module.go +++ b/modules/caddyhttp/tracing/module.go @@ -88,20 +88,18 @@ func (ot *Tracing) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { "span": &ot.SpanName, } - for d.Next() { - args := d.RemainingArgs() - if len(args) > 0 { - return d.ArgErr() - } + d.Next() // consume directive name + if d.NextArg() { + return d.ArgErr() + } - for d.NextBlock(0) { - if dst, ok := paramsMap[d.Val()]; ok { - if err := setParameter(d, dst); err != nil { - return err - } - } else { - return d.ArgErr() + for d.NextBlock(0) { + if dst, ok := paramsMap[d.Val()]; ok { + if err := setParameter(d, dst); err != nil { + return err } + } else { + return d.ArgErr() } } return nil diff --git a/modules/caddyhttp/tracing/tracerprovider_test.go b/modules/caddyhttp/tracing/tracerprovider_test.go index cb2e5936f..5a5df0a23 100644 --- a/modules/caddyhttp/tracing/tracerprovider_test.go +++ b/modules/caddyhttp/tracing/tracerprovider_test.go @@ -28,7 +28,6 @@ func Test_tracersProvider_cleanupTracerProvider(t *testing.T) { tp.getTracerProvider() err := tp.cleanupTracerProvider(zap.NewNop()) - if err != nil { t.Errorf("There should be no error: %v", err) } diff --git a/modules/caddyhttp/vars.go b/modules/caddyhttp/vars.go index d2d7f62c5..f5afe264a 100644 --- a/modules/caddyhttp/vars.go +++ b/modules/caddyhttp/vars.go @@ -57,6 +57,12 @@ func (m VarsMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next H v = repl.ReplaceAll(valStr, "") } vars[keyExpanded] = v + + // Special case: the user ID is in the replacer, pulled from there + // for access logs. Allow users to override it with the vars handler. + if keyExpanded == "http.auth.user.id" { + repl.Set(keyExpanded, v) + } } return next.ServeHTTP(w, r) } @@ -68,6 +74,8 @@ func (m VarsMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next H // ... // } func (m *VarsMiddleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.Next() // consume directive name + if *m == nil { *m = make(VarsMiddleware) } @@ -94,15 +102,13 @@ func (m *VarsMiddleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return nil } - for d.Next() { - if err := nextVar(true); err != nil { + if err := nextVar(true); err != nil { + return err + } + for d.NextBlock(0) { + if err := nextVar(false); err != nil { return err } - for nesting := d.Nesting(); d.NextBlock(nesting); { - if err := nextVar(false); err != nil { - return err - } - } } return nil @@ -135,6 +141,7 @@ func (m *VarsMatcher) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if *m == nil { *m = make(map[string][]string) } + // iterate to merge multiple matchers into one for d.Next() { var field string if !d.Args(&field) { @@ -216,6 +223,7 @@ func (m *MatchVarsRE) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if *m == nil { *m = make(map[string]*MatchRegexp) } + // iterate to merge multiple matchers into one for d.Next() { var first, second, third string if !d.Args(&first, &second) { diff --git a/modules/caddypki/acmeserver/acmeserver.go b/modules/caddypki/acmeserver/acmeserver.go index 86896150c..eaf6e930a 100644 --- a/modules/caddypki/acmeserver/acmeserver.go +++ b/modules/caddypki/acmeserver/acmeserver.go @@ -26,7 +26,7 @@ import ( "strings" "time" - "github.com/go-chi/chi" + "github.com/go-chi/chi/v5" "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/acme/api" acmeNoSQL "github.com/smallstep/certificates/acme/db/nosql" @@ -91,6 +91,14 @@ type Handler struct { // than 1 resolver address, one is chosen at random. Resolvers []string `json:"resolvers,omitempty"` + // Specify the set of enabled ACME challenges. An empty or absent value + // means all challenges are enabled. Accepted values are: + // "http-01", "dns-01", "tls-alpn-01" + Challenges ACMEChallenges `json:"challenges,omitempty" ` + + // The policy to use for issuing certificates + Policy *Policy `json:"policy,omitempty"` + logger *zap.Logger resolvers []caddy.NetworkAddress ctx caddy.Context @@ -125,6 +133,11 @@ func (ash *Handler) Provision(ctx caddy.Context) error { if ash.Lifetime == 0 { ash.Lifetime = caddy.Duration(12 * time.Hour) } + if len(ash.Challenges) > 0 { + if err := ash.Challenges.validate(); err != nil { + return err + } + } // get a reference to the configured CA appModule, err := ctx.App("pki") @@ -153,7 +166,11 @@ func (ash *Handler) Provision(ctx caddy.Context) error { AuthConfig: &authority.AuthConfig{ Provisioners: provisioner.List{ &provisioner.ACME{ - Name: ash.CA, + Name: ash.CA, + Challenges: ash.Challenges.toSmallstepType(), + Options: &provisioner.Options{ + X509: ash.Policy.normalizeRules(), + }, Type: provisioner.TypeACME.String(), Claims: &provisioner.Claims{ MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, diff --git a/modules/caddypki/acmeserver/caddyfile.go b/modules/caddypki/acmeserver/caddyfile.go index 3b52113b5..7eaaec49a 100644 --- a/modules/caddypki/acmeserver/caddyfile.go +++ b/modules/caddypki/acmeserver/caddyfile.go @@ -32,56 +32,112 @@ func init() { // ca // lifetime // resolvers +// challenges +// allow_wildcard_names +// allow { +// domains +// ip_ranges +// } +// deny { +// domains +// ip_ranges +// } // } func parseACMEServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) { - if !h.Next() { - return nil, h.ArgErr() - } - + h.Next() // consume directive name matcherSet, err := h.ExtractMatcherSet() if err != nil { return nil, err } + h.Next() // consume the directive name again (matcher parsing resets) + + // no inline args allowed + if h.NextArg() { + return nil, h.ArgErr() + } var acmeServer Handler var ca *caddypki.CA - for h.Next() { - if h.NextArg() { - return nil, h.ArgErr() - } - for h.NextBlock(0) { - switch h.Val() { - case "ca": - if !h.AllArgs(&acmeServer.CA) { - return nil, h.ArgErr() - } - if ca == nil { - ca = new(caddypki.CA) - } - ca.ID = acmeServer.CA - case "lifetime": - if !h.NextArg() { - return nil, h.ArgErr() - } + for h.NextBlock(0) { + switch h.Val() { + case "ca": + if !h.AllArgs(&acmeServer.CA) { + return nil, h.ArgErr() + } + if ca == nil { + ca = new(caddypki.CA) + } + ca.ID = acmeServer.CA + case "lifetime": + if !h.NextArg() { + return nil, h.ArgErr() + } - dur, err := caddy.ParseDuration(h.Val()) - if err != nil { - return nil, err - } - - if d := time.Duration(ca.IntermediateLifetime); d > 0 && dur > d { - return nil, h.Errf("certificate lifetime (%s) exceeds intermediate certificate lifetime (%s)", dur, d) - } - - acmeServer.Lifetime = caddy.Duration(dur) - - case "resolvers": - acmeServer.Resolvers = h.RemainingArgs() - if len(acmeServer.Resolvers) == 0 { - return nil, h.Errf("must specify at least one resolver address") + dur, err := caddy.ParseDuration(h.Val()) + if err != nil { + return nil, err + } + if d := time.Duration(ca.IntermediateLifetime); d > 0 && dur > d { + return nil, h.Errf("certificate lifetime (%s) exceeds intermediate certificate lifetime (%s)", dur, d) + } + acmeServer.Lifetime = caddy.Duration(dur) + case "resolvers": + acmeServer.Resolvers = h.RemainingArgs() + if len(acmeServer.Resolvers) == 0 { + return nil, h.Errf("must specify at least one resolver address") + } + case "challenges": + acmeServer.Challenges = append(acmeServer.Challenges, stringToChallenges(h.RemainingArgs())...) + case "allow_wildcard_names": + if acmeServer.Policy == nil { + acmeServer.Policy = &Policy{} + } + acmeServer.Policy.AllowWildcardNames = true + case "allow": + r := &RuleSet{} + for h.Next() { + for h.NextBlock(h.Nesting() - 1) { + if h.CountRemainingArgs() == 0 { + return nil, h.ArgErr() // TODO: + } + switch h.Val() { + case "domains": + r.Domains = append(r.Domains, h.RemainingArgs()...) + case "ip_ranges": + r.IPRanges = append(r.IPRanges, h.RemainingArgs()...) + default: + return nil, h.Errf("unrecognized 'allow' subdirective: %s", h.Val()) + } } } + if acmeServer.Policy == nil { + acmeServer.Policy = &Policy{} + } + acmeServer.Policy.Allow = r + case "deny": + r := &RuleSet{} + for h.Next() { + for h.NextBlock(h.Nesting() - 1) { + if h.CountRemainingArgs() == 0 { + return nil, h.ArgErr() // TODO: + } + switch h.Val() { + case "domains": + r.Domains = append(r.Domains, h.RemainingArgs()...) + case "ip_ranges": + r.IPRanges = append(r.IPRanges, h.RemainingArgs()...) + default: + return nil, h.Errf("unrecognized 'deny' subdirective: %s", h.Val()) + } + } + } + if acmeServer.Policy == nil { + acmeServer.Policy = &Policy{} + } + acmeServer.Policy.Deny = r + default: + return nil, h.Errf("unrecognized ACME server directive: %s", h.Val()) } } diff --git a/modules/caddypki/acmeserver/challenges.go b/modules/caddypki/acmeserver/challenges.go new file mode 100644 index 000000000..728a74928 --- /dev/null +++ b/modules/caddypki/acmeserver/challenges.go @@ -0,0 +1,77 @@ +package acmeserver + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/smallstep/certificates/authority/provisioner" +) + +// ACMEChallenge is an opaque string that represents supported ACME challenges. +type ACMEChallenge string + +const ( + HTTP_01 ACMEChallenge = "http-01" + DNS_01 ACMEChallenge = "dns-01" + TLS_ALPN_01 ACMEChallenge = "tls-alpn-01" +) + +// validate checks if the given challenge is supported. +func (c ACMEChallenge) validate() error { + switch c { + case HTTP_01, DNS_01, TLS_ALPN_01: + return nil + default: + return fmt.Errorf("acme challenge %q is not supported", c) + } +} + +// The unmarshaller first marshals the value into a string. Then it +// trims any space around it and lowercase it for normaliztion. The +// method does not and should not validate the value within accepted enums. +func (c *ACMEChallenge) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + *c = ACMEChallenge(strings.ToLower(strings.TrimSpace(s))) + return nil +} + +// String returns a string representation of the challenge. +func (c ACMEChallenge) String() string { + return strings.ToLower(string(c)) +} + +// ACMEChallenges is a list of ACME challenges. +type ACMEChallenges []ACMEChallenge + +// validate checks if the given challenges are supported. +func (c ACMEChallenges) validate() error { + for _, ch := range c { + if err := ch.validate(); err != nil { + return err + } + } + return nil +} + +func (c ACMEChallenges) toSmallstepType() []provisioner.ACMEChallenge { + if len(c) == 0 { + return nil + } + ac := make([]provisioner.ACMEChallenge, len(c)) + for i, ch := range c { + ac[i] = provisioner.ACMEChallenge(ch) + } + return ac +} + +func stringToChallenges(chs []string) ACMEChallenges { + challenges := make(ACMEChallenges, len(chs)) + for i, ch := range chs { + challenges[i] = ACMEChallenge(ch) + } + return challenges +} diff --git a/modules/caddypki/acmeserver/policy.go b/modules/caddypki/acmeserver/policy.go new file mode 100644 index 000000000..96137e6e6 --- /dev/null +++ b/modules/caddypki/acmeserver/policy.go @@ -0,0 +1,83 @@ +package acmeserver + +import ( + "github.com/smallstep/certificates/authority/policy" + "github.com/smallstep/certificates/authority/provisioner" +) + +// Policy defines the criteria for the ACME server +// of when to issue a certificate. Refer to the +// [Certificate Issuance Policy](https://smallstep.com/docs/step-ca/policies/) +// on Smallstep website for the evaluation criteria. +type Policy struct { + // If a rule set is configured to allow a certain type of name, + // all other types of names are automatically denied. + Allow *RuleSet `json:"allow,omitempty"` + + // If a rule set is configured to deny a certain type of name, + // all other types of names are still allowed. + Deny *RuleSet `json:"deny,omitempty"` + + // If set to true, the ACME server will allow issuing wildcard certificates. + AllowWildcardNames bool `json:"allow_wildcard_names,omitempty"` +} + +// RuleSet is the specific set of SAN criteria for a certificate +// to be issued or denied. +type RuleSet struct { + // Domains is a list of DNS domains that are allowed to be issued. + // It can be in the form of FQDN for specific domain name, or + // a wildcard domain name format, e.g. *.example.com, to allow + // sub-domains of a domain. + Domains []string `json:"domains,omitempty"` + + // IP ranges in the form of CIDR notation or specific IP addresses + // to be approved or denied for certificates. Non-CIDR IP addresses + // are matched exactly. + IPRanges []string `json:"ip_ranges,omitempty"` +} + +// normalizeAllowRules returns `nil` if policy is nil, the `Allow` rule is `nil`, +// or all rules within the `Allow` rule are empty. Otherwise, it returns the X509NameOptions +// with the content of the `Allow` rule. +func (p *Policy) normalizeAllowRules() *policy.X509NameOptions { + if (p == nil) || (p.Allow == nil) || (len(p.Allow.Domains) == 0 && len(p.Allow.IPRanges) == 0) { + return nil + } + return &policy.X509NameOptions{ + DNSDomains: p.Allow.Domains, + IPRanges: p.Allow.IPRanges, + } +} + +// normalizeDenyRules returns `nil` if policy is nil, the `Deny` rule is `nil`, +// or all rules within the `Deny` rule are empty. Otherwise, it returns the X509NameOptions +// with the content of the `Deny` rule. +func (p *Policy) normalizeDenyRules() *policy.X509NameOptions { + if (p == nil) || (p.Deny == nil) || (len(p.Deny.Domains) == 0 && len(p.Deny.IPRanges) == 0) { + return nil + } + return &policy.X509NameOptions{ + DNSDomains: p.Deny.Domains, + IPRanges: p.Deny.IPRanges, + } +} + +// normalizeRules returns `nil` if policy is nil, the `Allow` and `Deny` rules are `nil`, +func (p *Policy) normalizeRules() *provisioner.X509Options { + if p == nil { + return nil + } + + allow := p.normalizeAllowRules() + deny := p.normalizeDenyRules() + if allow == nil && deny == nil && !p.AllowWildcardNames { + return nil + } + + return &provisioner.X509Options{ + AllowedNames: allow, + DeniedNames: deny, + AllowWildcardNames: p.AllowWildcardNames, + } +} diff --git a/modules/caddypki/acmeserver/policy_test.go b/modules/caddypki/acmeserver/policy_test.go new file mode 100644 index 000000000..02d7856d9 --- /dev/null +++ b/modules/caddypki/acmeserver/policy_test.go @@ -0,0 +1,176 @@ +package acmeserver + +import ( + "reflect" + "testing" + + "github.com/smallstep/certificates/authority/policy" + "github.com/smallstep/certificates/authority/provisioner" +) + +func TestPolicyNormalizeAllowRules(t *testing.T) { + type fields struct { + Allow *RuleSet + Deny *RuleSet + AllowWildcardNames bool + } + tests := []struct { + name string + fields fields + want *policy.X509NameOptions + }{ + { + name: "providing no rules results in 'nil'", + fields: fields{}, + want: nil, + }, + { + name: "providing 'nil' Allow rules results in 'nil', regardless of Deny rules", + fields: fields{ + Allow: nil, + Deny: &RuleSet{}, + AllowWildcardNames: true, + }, + want: nil, + }, + { + name: "providing empty Allow rules results in 'nil', regardless of Deny rules", + fields: fields{ + Allow: &RuleSet{ + Domains: []string{}, + IPRanges: []string{}, + }, + }, + want: nil, + }, + { + name: "rules configured in Allow are returned in X509NameOptions", + fields: fields{ + Allow: &RuleSet{ + Domains: []string{"example.com"}, + IPRanges: []string{"127.0.0.1/32"}, + }, + }, + want: &policy.X509NameOptions{ + DNSDomains: []string{"example.com"}, + IPRanges: []string{"127.0.0.1/32"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &Policy{ + Allow: tt.fields.Allow, + Deny: tt.fields.Deny, + AllowWildcardNames: tt.fields.AllowWildcardNames, + } + if got := p.normalizeAllowRules(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Policy.normalizeAllowRules() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPolicy_normalizeDenyRules(t *testing.T) { + type fields struct { + Allow *RuleSet + Deny *RuleSet + AllowWildcardNames bool + } + tests := []struct { + name string + fields fields + want *policy.X509NameOptions + }{ + { + name: "providing no rules results in 'nil'", + fields: fields{}, + want: nil, + }, + { + name: "providing 'nil' Deny rules results in 'nil', regardless of Allow rules", + fields: fields{ + Deny: nil, + Allow: &RuleSet{}, + AllowWildcardNames: true, + }, + want: nil, + }, + { + name: "providing empty Deny rules results in 'nil', regardless of Allow rules", + fields: fields{ + Deny: &RuleSet{ + Domains: []string{}, + IPRanges: []string{}, + }, + }, + want: nil, + }, + { + name: "rules configured in Deny are returned in X509NameOptions", + fields: fields{ + Deny: &RuleSet{ + Domains: []string{"example.com"}, + IPRanges: []string{"127.0.0.1/32"}, + }, + }, + want: &policy.X509NameOptions{ + DNSDomains: []string{"example.com"}, + IPRanges: []string{"127.0.0.1/32"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &Policy{ + Allow: tt.fields.Allow, + Deny: tt.fields.Deny, + AllowWildcardNames: tt.fields.AllowWildcardNames, + } + if got := p.normalizeDenyRules(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Policy.normalizeDenyRules() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPolicy_normalizeRules(t *testing.T) { + tests := []struct { + name string + policy *Policy + want *provisioner.X509Options + }{ + { + name: "'nil' policy results in 'nil' options", + policy: nil, + want: nil, + }, + { + name: "'nil' Allow/Deny rules and disallowing wildcard names result in 'nil' X509Options", + policy: &Policy{ + Allow: nil, + Deny: nil, + AllowWildcardNames: false, + }, + want: nil, + }, + { + name: "'nil' Allow/Deny rules and allowing wildcard names result in 'nil' Allow/Deny rules in X509Options but allowing wildcard names in X509Options", + policy: &Policy{ + Allow: nil, + Deny: nil, + AllowWildcardNames: true, + }, + want: &provisioner.X509Options{ + AllowWildcardNames: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.policy.normalizeRules(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Policy.normalizeRules() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index 5e79c2d2e..8a7f8b499 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -17,9 +17,7 @@ package caddytls import ( "context" "crypto/x509" - "errors" "fmt" - "net/url" "os" "strconv" "time" @@ -275,256 +273,224 @@ func (iss *ACMEIssuer) GetACMEIssuer() *ACMEIssuer { return iss } // } // } func (iss *ACMEIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { + d.Next() // consume issuer name + + if d.NextArg() { + iss.CA = d.Val() if d.NextArg() { - iss.CA = d.Val() + return d.ArgErr() + } + } + + for d.NextBlock(0) { + switch d.Val() { + case "dir": + if iss.CA != "" { + return d.Errf("directory is already specified: %s", iss.CA) + } + if !d.AllArgs(&iss.CA) { + return d.ArgErr() + } + + case "test_dir": + if !d.AllArgs(&iss.TestCA) { + return d.ArgErr() + } + + case "email": + if !d.AllArgs(&iss.Email) { + return d.ArgErr() + } + + case "timeout": + var timeoutStr string + if !d.AllArgs(&timeoutStr) { + return d.ArgErr() + } + timeout, err := caddy.ParseDuration(timeoutStr) + if err != nil { + return d.Errf("invalid timeout duration %s: %v", timeoutStr, err) + } + iss.ACMETimeout = caddy.Duration(timeout) + + case "disable_http_challenge": if d.NextArg() { return d.ArgErr() } - } - for nesting := d.Nesting(); d.NextBlock(nesting); { - switch d.Val() { - case "dir": - if iss.CA != "" { - return d.Errf("directory is already specified: %s", iss.CA) - } - if !d.AllArgs(&iss.CA) { - return d.ArgErr() - } - - case "test_dir": - if !d.AllArgs(&iss.TestCA) { - return d.ArgErr() - } - - case "email": - if !d.AllArgs(&iss.Email) { - return d.ArgErr() - } - - case "timeout": - var timeoutStr string - if !d.AllArgs(&timeoutStr) { - return d.ArgErr() - } - timeout, err := caddy.ParseDuration(timeoutStr) - if err != nil { - return d.Errf("invalid timeout duration %s: %v", timeoutStr, err) - } - iss.ACMETimeout = caddy.Duration(timeout) - - case "disable_http_challenge": - if d.NextArg() { - return d.ArgErr() - } - if iss.Challenges == nil { - iss.Challenges = new(ChallengesConfig) - } - if iss.Challenges.HTTP == nil { - iss.Challenges.HTTP = new(HTTPChallengeConfig) - } - iss.Challenges.HTTP.Disabled = true - - case "disable_tlsalpn_challenge": - if d.NextArg() { - return d.ArgErr() - } - if iss.Challenges == nil { - iss.Challenges = new(ChallengesConfig) - } - if iss.Challenges.TLSALPN == nil { - iss.Challenges.TLSALPN = new(TLSALPNChallengeConfig) - } - iss.Challenges.TLSALPN.Disabled = true - - case "alt_http_port": - if !d.NextArg() { - return d.ArgErr() - } - port, err := strconv.Atoi(d.Val()) - if err != nil { - return d.Errf("invalid port %s: %v", d.Val(), err) - } - if iss.Challenges == nil { - iss.Challenges = new(ChallengesConfig) - } - if iss.Challenges.HTTP == nil { - iss.Challenges.HTTP = new(HTTPChallengeConfig) - } - iss.Challenges.HTTP.AlternatePort = port - - case "alt_tlsalpn_port": - if !d.NextArg() { - return d.ArgErr() - } - port, err := strconv.Atoi(d.Val()) - if err != nil { - return d.Errf("invalid port %s: %v", d.Val(), err) - } - if iss.Challenges == nil { - iss.Challenges = new(ChallengesConfig) - } - if iss.Challenges.TLSALPN == nil { - iss.Challenges.TLSALPN = new(TLSALPNChallengeConfig) - } - iss.Challenges.TLSALPN.AlternatePort = port - - case "eab": - iss.ExternalAccount = new(acme.EAB) - if !d.AllArgs(&iss.ExternalAccount.KeyID, &iss.ExternalAccount.MACKey) { - return d.ArgErr() - } - - case "trusted_roots": - iss.TrustedRootsPEMFiles = d.RemainingArgs() - - case "dns": - if !d.NextArg() { - return d.ArgErr() - } - provName := d.Val() - if iss.Challenges == nil { - iss.Challenges = new(ChallengesConfig) - } - if iss.Challenges.DNS == nil { - iss.Challenges.DNS = new(DNSChallengeConfig) - } - unm, err := caddyfile.UnmarshalModule(d, "dns.providers."+provName) - if err != nil { - return err - } - iss.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, nil) - - case "propagation_delay": - if !d.NextArg() { - return d.ArgErr() - } - delayStr := d.Val() - delay, err := caddy.ParseDuration(delayStr) - if err != nil { - return d.Errf("invalid propagation_delay duration %s: %v", delayStr, err) - } - if iss.Challenges == nil { - iss.Challenges = new(ChallengesConfig) - } - if iss.Challenges.DNS == nil { - iss.Challenges.DNS = new(DNSChallengeConfig) - } - iss.Challenges.DNS.PropagationDelay = caddy.Duration(delay) - - case "propagation_timeout": - if !d.NextArg() { - return d.ArgErr() - } - timeoutStr := d.Val() - var timeout time.Duration - if timeoutStr == "-1" { - timeout = time.Duration(-1) - } else { - var err error - timeout, err = caddy.ParseDuration(timeoutStr) - if err != nil { - return d.Errf("invalid propagation_timeout duration %s: %v", timeoutStr, err) - } - } - if iss.Challenges == nil { - iss.Challenges = new(ChallengesConfig) - } - if iss.Challenges.DNS == nil { - iss.Challenges.DNS = new(DNSChallengeConfig) - } - iss.Challenges.DNS.PropagationTimeout = caddy.Duration(timeout) - - case "resolvers": - if iss.Challenges == nil { - iss.Challenges = new(ChallengesConfig) - } - if iss.Challenges.DNS == nil { - iss.Challenges.DNS = new(DNSChallengeConfig) - } - iss.Challenges.DNS.Resolvers = d.RemainingArgs() - if len(iss.Challenges.DNS.Resolvers) == 0 { - return d.ArgErr() - } - - case "dns_ttl": - if !d.NextArg() { - return d.ArgErr() - } - ttlStr := d.Val() - ttl, err := caddy.ParseDuration(ttlStr) - if err != nil { - return d.Errf("invalid dns_ttl duration %s: %v", ttlStr, err) - } - if iss.Challenges == nil { - iss.Challenges = new(ChallengesConfig) - } - if iss.Challenges.DNS == nil { - iss.Challenges.DNS = new(DNSChallengeConfig) - } - iss.Challenges.DNS.TTL = caddy.Duration(ttl) - - case "dns_challenge_override_domain": - arg := d.RemainingArgs() - if len(arg) != 1 { - return d.ArgErr() - } - if iss.Challenges == nil { - iss.Challenges = new(ChallengesConfig) - } - if iss.Challenges.DNS == nil { - iss.Challenges.DNS = new(DNSChallengeConfig) - } - iss.Challenges.DNS.OverrideDomain = arg[0] - - case "preferred_chains": - chainPref, err := ParseCaddyfilePreferredChainsOptions(d) - if err != nil { - return err - } - iss.PreferredChains = chainPref - - default: - return d.Errf("unrecognized ACME issuer property: %s", d.Val()) + if iss.Challenges == nil { + iss.Challenges = new(ChallengesConfig) } + if iss.Challenges.HTTP == nil { + iss.Challenges.HTTP = new(HTTPChallengeConfig) + } + iss.Challenges.HTTP.Disabled = true + + case "disable_tlsalpn_challenge": + if d.NextArg() { + return d.ArgErr() + } + if iss.Challenges == nil { + iss.Challenges = new(ChallengesConfig) + } + if iss.Challenges.TLSALPN == nil { + iss.Challenges.TLSALPN = new(TLSALPNChallengeConfig) + } + iss.Challenges.TLSALPN.Disabled = true + + case "alt_http_port": + if !d.NextArg() { + return d.ArgErr() + } + port, err := strconv.Atoi(d.Val()) + if err != nil { + return d.Errf("invalid port %s: %v", d.Val(), err) + } + if iss.Challenges == nil { + iss.Challenges = new(ChallengesConfig) + } + if iss.Challenges.HTTP == nil { + iss.Challenges.HTTP = new(HTTPChallengeConfig) + } + iss.Challenges.HTTP.AlternatePort = port + + case "alt_tlsalpn_port": + if !d.NextArg() { + return d.ArgErr() + } + port, err := strconv.Atoi(d.Val()) + if err != nil { + return d.Errf("invalid port %s: %v", d.Val(), err) + } + if iss.Challenges == nil { + iss.Challenges = new(ChallengesConfig) + } + if iss.Challenges.TLSALPN == nil { + iss.Challenges.TLSALPN = new(TLSALPNChallengeConfig) + } + iss.Challenges.TLSALPN.AlternatePort = port + + case "eab": + iss.ExternalAccount = new(acme.EAB) + if !d.AllArgs(&iss.ExternalAccount.KeyID, &iss.ExternalAccount.MACKey) { + return d.ArgErr() + } + + case "trusted_roots": + iss.TrustedRootsPEMFiles = d.RemainingArgs() + + case "dns": + if !d.NextArg() { + return d.ArgErr() + } + provName := d.Val() + if iss.Challenges == nil { + iss.Challenges = new(ChallengesConfig) + } + if iss.Challenges.DNS == nil { + iss.Challenges.DNS = new(DNSChallengeConfig) + } + unm, err := caddyfile.UnmarshalModule(d, "dns.providers."+provName) + if err != nil { + return err + } + iss.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, nil) + + case "propagation_delay": + if !d.NextArg() { + return d.ArgErr() + } + delayStr := d.Val() + delay, err := caddy.ParseDuration(delayStr) + if err != nil { + return d.Errf("invalid propagation_delay duration %s: %v", delayStr, err) + } + if iss.Challenges == nil { + iss.Challenges = new(ChallengesConfig) + } + if iss.Challenges.DNS == nil { + iss.Challenges.DNS = new(DNSChallengeConfig) + } + iss.Challenges.DNS.PropagationDelay = caddy.Duration(delay) + + case "propagation_timeout": + if !d.NextArg() { + return d.ArgErr() + } + timeoutStr := d.Val() + var timeout time.Duration + if timeoutStr == "-1" { + timeout = time.Duration(-1) + } else { + var err error + timeout, err = caddy.ParseDuration(timeoutStr) + if err != nil { + return d.Errf("invalid propagation_timeout duration %s: %v", timeoutStr, err) + } + } + if iss.Challenges == nil { + iss.Challenges = new(ChallengesConfig) + } + if iss.Challenges.DNS == nil { + iss.Challenges.DNS = new(DNSChallengeConfig) + } + iss.Challenges.DNS.PropagationTimeout = caddy.Duration(timeout) + + case "resolvers": + if iss.Challenges == nil { + iss.Challenges = new(ChallengesConfig) + } + if iss.Challenges.DNS == nil { + iss.Challenges.DNS = new(DNSChallengeConfig) + } + iss.Challenges.DNS.Resolvers = d.RemainingArgs() + if len(iss.Challenges.DNS.Resolvers) == 0 { + return d.ArgErr() + } + + case "dns_ttl": + if !d.NextArg() { + return d.ArgErr() + } + ttlStr := d.Val() + ttl, err := caddy.ParseDuration(ttlStr) + if err != nil { + return d.Errf("invalid dns_ttl duration %s: %v", ttlStr, err) + } + if iss.Challenges == nil { + iss.Challenges = new(ChallengesConfig) + } + if iss.Challenges.DNS == nil { + iss.Challenges.DNS = new(DNSChallengeConfig) + } + iss.Challenges.DNS.TTL = caddy.Duration(ttl) + + case "dns_challenge_override_domain": + arg := d.RemainingArgs() + if len(arg) != 1 { + return d.ArgErr() + } + if iss.Challenges == nil { + iss.Challenges = new(ChallengesConfig) + } + if iss.Challenges.DNS == nil { + iss.Challenges.DNS = new(DNSChallengeConfig) + } + iss.Challenges.DNS.OverrideDomain = arg[0] + + case "preferred_chains": + chainPref, err := ParseCaddyfilePreferredChainsOptions(d) + if err != nil { + return err + } + iss.PreferredChains = chainPref + + default: + return d.Errf("unrecognized ACME issuer property: %s", d.Val()) } } return nil } -// onDemandAskRequest makes a request to the ask URL -// to see if a certificate can be obtained for name. -// The certificate request should be denied if this -// returns an error. -func onDemandAskRequest(logger *zap.Logger, ask string, name string) error { - askURL, err := url.Parse(ask) - if err != nil { - return fmt.Errorf("parsing ask URL: %v", err) - } - qs := askURL.Query() - qs.Set("domain", name) - askURL.RawQuery = qs.Encode() - - askURLString := askURL.String() - resp, err := onDemandAskClient.Get(askURLString) - if err != nil { - return fmt.Errorf("error checking %v to determine if certificate for hostname '%s' should be allowed: %v", - ask, name, err) - } - resp.Body.Close() - - logger.Debug("response from ask endpoint", - zap.String("domain", name), - zap.String("url", askURLString), - zap.Int("status", resp.StatusCode)) - - if resp.StatusCode < 200 || resp.StatusCode > 299 { - return fmt.Errorf("%s: %w %s - non-2xx status code %d", name, errAskDenied, ask, resp.StatusCode) - } - - return nil -} - func ParseCaddyfilePreferredChainsOptions(d *caddyfile.Dispenser) (*ChainPreference, error) { chainPref := new(ChainPreference) if d.NextArg() { @@ -592,11 +558,6 @@ type ChainPreference struct { AnyCommonName []string `json:"any_common_name,omitempty"` } -// errAskDenied is an error that should be wrapped or returned when the -// configured "ask" endpoint does not allow a certificate to be issued, -// to distinguish that from other errors such as connection failure. -var errAskDenied = errors.New("certificate not allowed by ask endpoint") - // Interface guards var ( _ certmagic.PreChecker = (*ACMEIssuer)(nil) diff --git a/modules/caddytls/automation.go b/modules/caddytls/automation.go index ee25400a6..a90e5ded8 100644 --- a/modules/caddytls/automation.go +++ b/modules/caddytls/automation.go @@ -15,12 +15,13 @@ package caddytls import ( + "context" + "crypto/tls" "encoding/json" "errors" "fmt" - "net/http" + "net" "strings" - "time" "github.com/caddyserver/certmagic" "github.com/mholt/acmez" @@ -137,6 +138,15 @@ type AutomationPolicy struct { // load. This enables On-Demand TLS for this policy. OnDemand bool `json:"on_demand,omitempty"` + // If true, private keys already existing in storage + // will be reused. Otherwise, a new key will be + // created for every new certificate to mitigate + // pinning and reduce the scope of key compromise. + // TEMPORARY: Key pinning is against industry best practices. + // This property will likely be removed in the future. + // Do not rely on it forever; watch the release notes. + ReusePrivateKeys bool `json:"reuse_private_keys,omitempty"` + // Disables OCSP stapling. Disabling OCSP stapling puts clients at // greater risk, reduces their privacy, and usually lowers client // performance. It is NOT recommended to disable this unless you @@ -244,37 +254,52 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { // on-demand TLS var ond *certmagic.OnDemandConfig if ap.OnDemand || len(ap.Managers) > 0 { - // ask endpoint is now required after a number of negligence cases causing abuse; - // but is still allowed for explicit subjects (non-wildcard, non-unbounded), - // for the internal issuer since it doesn't cause ACME issuer pressure - if ap.isWildcardOrDefault() && !ap.onlyInternalIssuer() && (tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil || tlsApp.Automation.OnDemand.Ask == "") { - return fmt.Errorf("on-demand TLS cannot be enabled without an 'ask' endpoint to prevent abuse; please refer to documentation for details") + // permission module is now required after a number of negligence cases that allowed abuse; + // but it may still be optional for explicit subjects (bounded, non-wildcard), for the + // internal issuer since it doesn't cause public PKI pressure on ACME servers + if ap.isWildcardOrDefault() && !ap.onlyInternalIssuer() && (tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil || tlsApp.Automation.OnDemand.permission == nil) { + return fmt.Errorf("on-demand TLS cannot be enabled without a permission module to prevent abuse; please refer to documentation for details") } ond = &certmagic.OnDemandConfig{ - DecisionFunc: func(name string) error { + DecisionFunc: func(ctx context.Context, name string) error { if tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil { return nil } - if err := onDemandAskRequest(tlsApp.logger, tlsApp.Automation.OnDemand.Ask, name); err != nil { + + // logging the remote IP can be useful for servers that want to count + // attempts from clients to detect patterns of abuse -- it should NOT be + // used solely for decision making, however + var remoteIP string + if hello, ok := ctx.Value(certmagic.ClientHelloInfoCtxKey).(*tls.ClientHelloInfo); ok && hello != nil { + if remote := hello.Conn.RemoteAddr(); remote != nil { + remoteIP, _, _ = net.SplitHostPort(remote.String()) + } + } + tlsApp.logger.Debug("asking for permission for on-demand certificate", + zap.String("remote_ip", remoteIP), + zap.String("domain", name)) + + // ask the permission module if this cert is allowed + if err := tlsApp.Automation.OnDemand.permission.CertificateAllowed(ctx, name); err != nil { // distinguish true errors from denials, because it's important to elevate actual errors - if errors.Is(err, errAskDenied) { - tlsApp.logger.Debug("certificate issuance denied", - zap.String("ask_endpoint", tlsApp.Automation.OnDemand.Ask), + if errors.Is(err, ErrPermissionDenied) { + tlsApp.logger.Debug("on-demand certificate issuance denied", zap.String("domain", name), zap.Error(err)) } else { - tlsApp.logger.Error("request to 'ask' endpoint failed", - zap.String("ask_endpoint", tlsApp.Automation.OnDemand.Ask), + tlsApp.logger.Error("failed to get permission for on-demand certificate", zap.String("domain", name), zap.Error(err)) } return err } + // check the rate limiter last because // doing so makes a reservation if !onDemandRateLimiter.Allow() { return fmt.Errorf("on-demand rate limit exceeded") } + return nil }, Managers: ap.Managers, @@ -287,6 +312,7 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { KeySource: keySource, OnEvent: tlsApp.onEvent, OnDemand: ond, + ReusePrivateKeys: ap.ReusePrivateKeys, OCSP: certmagic.OCSPConfig{ DisableStapling: ap.DisableOCSPStapling, ResponderOverrides: ap.OCSPOverrides, @@ -453,42 +479,6 @@ type DNSChallengeConfig struct { solver acmez.Solver } -// OnDemandConfig configures on-demand TLS, for obtaining -// needed certificates at handshake-time. Because this -// feature can easily be abused, you should use this to -// establish rate limits and/or an internal endpoint that -// Caddy can "ask" if it should be allowed to manage -// certificates for a given hostname. -type OnDemandConfig struct { - // REQUIRED. If Caddy needs to load a certificate from - // storage or obtain/renew a certificate during a TLS - // handshake, it will perform a quick HTTP request to - // this URL to check if it should be allowed to try to - // get a certificate for the name in the "domain" query - // string parameter, like so: `?domain=example.com`. - // The endpoint must return a 200 OK status if a certificate - // is allowed; anything else will cause it to be denied. - // Redirects are not followed. - Ask string `json:"ask,omitempty"` - - // DEPRECATED. An optional rate limit to throttle - // the checking of storage and the issuance of - // certificates from handshakes if not already in - // storage. WILL BE REMOVED IN A FUTURE RELEASE. - RateLimit *RateLimit `json:"rate_limit,omitempty"` -} - -// DEPRECATED. RateLimit specifies an interval with optional burst size. -type RateLimit struct { - // A duration value. Storage may be checked and a certificate may be - // obtained 'burst' times during this interval. - Interval caddy.Duration `json:"interval,omitempty"` - - // How many times during an interval storage can be checked or a - // certificate can be obtained. - Burst int `json:"burst,omitempty"` -} - // ConfigSetter is implemented by certmagic.Issuers that // need access to a parent certmagic.Config as part of // their provisioning phase. For example, the ACMEIssuer @@ -497,14 +487,3 @@ type RateLimit struct { type ConfigSetter interface { SetConfig(cfg *certmagic.Config) } - -// These perpetual values are used for on-demand TLS. -var ( - onDemandRateLimiter = certmagic.NewRateLimiter(0, 0) - onDemandAskClient = &http.Client{ - Timeout: 10 * time.Second, - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return fmt.Errorf("following http redirects is not allowed") - }, - } -) diff --git a/modules/caddytls/capools.go b/modules/caddytls/capools.go new file mode 100644 index 000000000..44a2fa2c2 --- /dev/null +++ b/modules/caddytls/capools.go @@ -0,0 +1,820 @@ +package caddytls + +import ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "reflect" + + "github.com/caddyserver/certmagic" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/modules/caddypki" +) + +func init() { + caddy.RegisterModule(InlineCAPool{}) + caddy.RegisterModule(FileCAPool{}) + caddy.RegisterModule(PKIRootCAPool{}) + caddy.RegisterModule(PKIIntermediateCAPool{}) + caddy.RegisterModule(StoragePool{}) + caddy.RegisterModule(HTTPCertPool{}) + caddy.RegisterModule(LazyCertPool{}) +} + +// The interface to be implemented by all guest modules part of +// the namespace 'tls.ca_pool.source.' +type CA interface { + CertPool() *x509.CertPool +} + +// InlineCAPool is a certificate authority pool provider coming from +// a DER-encoded certificates in the config +type InlineCAPool struct { + // A list of base64 DER-encoded CA certificates + // against which to validate client certificates. + // Client certs which are not signed by any of + // these CAs will be rejected. + TrustedCACerts []string `json:"trusted_ca_certs,omitempty"` + + pool *x509.CertPool +} + +// CaddyModule implements caddy.Module. +func (icp InlineCAPool) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.ca_pool.source.inline", + New: func() caddy.Module { + return new(InlineCAPool) + }, + } +} + +// Provision implements caddy.Provisioner. +func (icp *InlineCAPool) Provision(ctx caddy.Context) error { + caPool := x509.NewCertPool() + for i, clientCAString := range icp.TrustedCACerts { + clientCA, err := decodeBase64DERCert(clientCAString) + if err != nil { + return fmt.Errorf("parsing certificate at index %d: %v", i, err) + } + caPool.AddCert(clientCA) + } + icp.pool = caPool + + return nil +} + +// Syntax: +// +// trust_pool inline { +// trust_der ... +// } +// +// The 'trust_der' directive can be specified multiple times. +func (icp *InlineCAPool) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.Next() // consume module name + if d.CountRemainingArgs() > 0 { + return d.ArgErr() + } + for d.NextBlock(0) { + switch d.Val() { + case "trust_der": + icp.TrustedCACerts = append(icp.TrustedCACerts, d.RemainingArgs()...) + default: + return d.Errf("unrecognized directive: %s", d.Val()) + } + } + if len(icp.TrustedCACerts) == 0 { + return d.Err("no certificates specified") + } + return nil +} + +// CertPool implements CA. +func (icp InlineCAPool) CertPool() *x509.CertPool { + return icp.pool +} + +// FileCAPool generates trusted root certificates pool from the designated DER and PEM file +type FileCAPool struct { + // TrustedCACertPEMFiles is a list of PEM file names + // from which to load certificates of trusted CAs. + // Client certificates which are not signed by any of + // these CA certificates will be rejected. + TrustedCACertPEMFiles []string `json:"pem_files,omitempty"` + + pool *x509.CertPool +} + +// CaddyModule implements caddy.Module. +func (FileCAPool) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.ca_pool.source.file", + New: func() caddy.Module { + return new(FileCAPool) + }, + } +} + +// Loads and decodes the DER and pem files to generate the certificate pool +func (f *FileCAPool) Provision(ctx caddy.Context) error { + caPool := x509.NewCertPool() + for _, pemFile := range f.TrustedCACertPEMFiles { + pemContents, err := os.ReadFile(pemFile) + if err != nil { + return fmt.Errorf("reading %s: %v", pemFile, err) + } + caPool.AppendCertsFromPEM(pemContents) + } + f.pool = caPool + return nil +} + +// Syntax: +// +// trust_pool file [...] { +// pem_file ... +// } +// +// The 'pem_file' directive can be specified multiple times. +func (fcap *FileCAPool) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.Next() // consume module name + fcap.TrustedCACertPEMFiles = append(fcap.TrustedCACertPEMFiles, d.RemainingArgs()...) + for d.NextBlock(0) { + switch d.Val() { + case "pem_file": + fcap.TrustedCACertPEMFiles = append(fcap.TrustedCACertPEMFiles, d.RemainingArgs()...) + default: + return d.Errf("unrecognized directive: %s", d.Val()) + } + } + if len(fcap.TrustedCACertPEMFiles) == 0 { + return d.Err("no certificates specified") + } + return nil +} + +func (f FileCAPool) CertPool() *x509.CertPool { + return f.pool +} + +// PKIRootCAPool extracts the trusted root certificates from Caddy's native 'pki' app +type PKIRootCAPool struct { + // List of the Authority names that are configured in the `pki` app whose root certificates are trusted + Authority []string `json:"authority,omitempty"` + + ca []*caddypki.CA + pool *x509.CertPool +} + +// CaddyModule implements caddy.Module. +func (PKIRootCAPool) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.ca_pool.source.pki_root", + New: func() caddy.Module { + return new(PKIRootCAPool) + }, + } +} + +// Loads the PKI app and load the root certificates into the certificate pool +func (p *PKIRootCAPool) Provision(ctx caddy.Context) error { + pkiApp := ctx.AppIfConfigured("pki") + if pkiApp == nil { + return fmt.Errorf("PKI app not configured") + } + pki := pkiApp.(*caddypki.PKI) + for _, caID := range p.Authority { + c, err := pki.GetCA(ctx, caID) + if err != nil || c == nil { + return fmt.Errorf("getting CA %s: %v", caID, err) + } + p.ca = append(p.ca, c) + } + + caPool := x509.NewCertPool() + for _, ca := range p.ca { + caPool.AddCert(ca.RootCertificate()) + } + p.pool = caPool + + return nil +} + +// Syntax: +// +// trust_pool pki_root [...] { +// authority ... +// } +// +// The 'authority' directive can be specified multiple times. +func (pkir *PKIRootCAPool) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.Next() // consume module name + pkir.Authority = append(pkir.Authority, d.RemainingArgs()...) + for nesting := d.Nesting(); d.NextBlock(nesting); { + switch d.Val() { + case "authority": + pkir.Authority = append(pkir.Authority, d.RemainingArgs()...) + default: + return d.Errf("unrecognized directive: %s", d.Val()) + } + } + if len(pkir.Authority) == 0 { + return d.Err("no authorities specified") + } + return nil +} + +// return the certificate pool generated with root certificates from the PKI app +func (p PKIRootCAPool) CertPool() *x509.CertPool { + return p.pool +} + +// PKIIntermediateCAPool extracts the trusted intermediate certificates from Caddy's native 'pki' app +type PKIIntermediateCAPool struct { + // List of the Authority names that are configured in the `pki` app whose intermediate certificates are trusted + Authority []string `json:"authority,omitempty"` + + ca []*caddypki.CA + pool *x509.CertPool +} + +// CaddyModule implements caddy.Module. +func (PKIIntermediateCAPool) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.ca_pool.source.pki_intermediate", + New: func() caddy.Module { + return new(PKIIntermediateCAPool) + }, + } +} + +// Loads the PKI app and load the intermediate certificates into the certificate pool +func (p *PKIIntermediateCAPool) Provision(ctx caddy.Context) error { + pkiApp := ctx.AppIfConfigured("pki") + if pkiApp == nil { + return fmt.Errorf("PKI app not configured") + } + pki := pkiApp.(*caddypki.PKI) + for _, caID := range p.Authority { + c, err := pki.GetCA(ctx, caID) + if err != nil || c == nil { + return fmt.Errorf("getting CA %s: %v", caID, err) + } + p.ca = append(p.ca, c) + } + + caPool := x509.NewCertPool() + for _, ca := range p.ca { + caPool.AddCert(ca.IntermediateCertificate()) + } + p.pool = caPool + return nil +} + +// Syntax: +// +// trust_pool pki_intermediate [...] { +// authority ... +// } +// +// The 'authority' directive can be specified multiple times. +func (pic *PKIIntermediateCAPool) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.Next() // consume module name + pic.Authority = append(pic.Authority, d.RemainingArgs()...) + for nesting := d.Nesting(); d.NextBlock(nesting); { + switch d.Val() { + case "authority": + pic.Authority = append(pic.Authority, d.RemainingArgs()...) + default: + return d.Errf("unrecognized directive: %s", d.Val()) + } + } + if len(pic.Authority) == 0 { + return d.Err("no authorities specified") + } + return nil +} + +// return the certificate pool generated with intermediate certificates from the PKI app +func (p PKIIntermediateCAPool) CertPool() *x509.CertPool { + return p.pool +} + +// StoragePool extracts the trusted certificates root from Caddy storage +type StoragePool struct { + // The storage module where the trusted root certificates are stored. Absent + // explicit storage implies the use of Caddy default storage. + StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"` + + // The storage key/index to the location of the certificates + PEMKeys []string `json:"pem_keys,omitempty"` + + storage certmagic.Storage + pool *x509.CertPool +} + +// CaddyModule implements caddy.Module. +func (StoragePool) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.ca_pool.source.storage", + New: func() caddy.Module { + return new(StoragePool) + }, + } +} + +// Provision implements caddy.Provisioner. +func (ca *StoragePool) Provision(ctx caddy.Context) error { + if ca.StorageRaw != nil { + val, err := ctx.LoadModule(ca, "StorageRaw") + if err != nil { + return fmt.Errorf("loading storage module: %v", err) + } + cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage() + if err != nil { + return fmt.Errorf("creating storage configuration: %v", err) + } + ca.storage = cmStorage + } + if ca.storage == nil { + ca.storage = ctx.Storage() + } + if len(ca.PEMKeys) == 0 { + return fmt.Errorf("no PEM keys specified") + } + caPool := x509.NewCertPool() + for _, caID := range ca.PEMKeys { + bs, err := ca.storage.Load(ctx, caID) + if err != nil { + return fmt.Errorf("error loading cert '%s' from storage: %s", caID, err) + } + if !caPool.AppendCertsFromPEM(bs) { + return fmt.Errorf("failed to add certificate '%s' to pool", caID) + } + } + ca.pool = caPool + + return nil +} + +// Syntax: +// +// trust_pool storage [...] { +// storage +// keys ... +// } +// +// The 'keys' directive can be specified multiple times. +// The'storage' directive is optional and defaults to the default storage module. +func (sp *StoragePool) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.Next() // consume module name + sp.PEMKeys = append(sp.PEMKeys, d.RemainingArgs()...) + for nesting := d.Nesting(); d.NextBlock(nesting); { + switch d.Val() { + case "storage": + if sp.StorageRaw != nil { + return d.Err("storage module already set") + } + if !d.NextArg() { + return d.ArgErr() + } + modStem := d.Val() + modID := "caddy.storage." + modStem + unm, err := caddyfile.UnmarshalModule(d, modID) + if err != nil { + return err + } + storage, ok := unm.(caddy.StorageConverter) + if !ok { + return d.Errf("module %s is not a caddy.StorageConverter", modID) + } + sp.StorageRaw = caddyconfig.JSONModuleObject(storage, "module", modStem, nil) + case "keys": + sp.PEMKeys = append(sp.PEMKeys, d.RemainingArgs()...) + default: + return d.Errf("unrecognized directive: %s", d.Val()) + } + } + return nil +} + +func (p StoragePool) CertPool() *x509.CertPool { + return p.pool +} + +// TLSConfig holds configuration related to the TLS configuration for the +// transport/client. +// copied from with minor modifications: modules/caddyhttp/reverseproxy/httptransport.go +type TLSConfig struct { + // Provides the guest module that provides the trusted certificate authority (CA) certificates + CARaw json.RawMessage `json:"ca,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"` + + // If true, TLS verification of server certificates will be disabled. + // This is insecure and may be removed in the future. Do not use this + // option except in testing or local development environments. + InsecureSkipVerify bool `json:"insecure_skip_verify,omitempty"` + + // The duration to allow a TLS handshake to a server. Default: No timeout. + HandshakeTimeout caddy.Duration `json:"handshake_timeout,omitempty"` + + // The server name used when verifying the certificate received in the TLS + // handshake. By default, this will use the upstream address' host part. + // You only need to override this if your upstream address does not match the + // certificate the upstream is likely to use. For example if the upstream + // address is an IP address, then you would need to configure this to the + // hostname being served by the upstream server. Currently, this does not + // support placeholders because the TLS config is not provisioned on each + // connection, so a static value must be used. + ServerName string `json:"server_name,omitempty"` + + // TLS renegotiation level. TLS renegotiation is the act of performing + // subsequent handshakes on a connection after the first. + // The level can be: + // - "never": (the default) disables renegotiation. + // - "once": allows a remote server to request renegotiation once per connection. + // - "freely": allows a remote server to repeatedly request renegotiation. + Renegotiation string `json:"renegotiation,omitempty"` +} + +func (t *TLSConfig) unmarshalCaddyfile(d *caddyfile.Dispenser) error { + for nesting := d.Nesting(); d.NextBlock(nesting); { + switch d.Val() { + case "ca": + if !d.NextArg() { + return d.ArgErr() + } + modStem := d.Val() + modID := "tls.ca_pool.source." + modStem + unm, err := caddyfile.UnmarshalModule(d, modID) + if err != nil { + return err + } + ca, ok := unm.(CA) + if !ok { + return d.Errf("module %s is not a caddytls.CA", modID) + } + t.CARaw = caddyconfig.JSONModuleObject(ca, "provider", modStem, nil) + case "insecure_skip_verify": + t.InsecureSkipVerify = true + case "handshake_timeout": + if !d.NextArg() { + return d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return d.Errf("bad timeout value '%s': %v", d.Val(), err) + } + t.HandshakeTimeout = caddy.Duration(dur) + case "server_name": + if !d.Args(&t.ServerName) { + return d.ArgErr() + } + case "renegotiation": + if !d.Args(&t.Renegotiation) { + return d.ArgErr() + } + switch t.Renegotiation { + case "never", "once", "freely": + continue + default: + t.Renegotiation = "" + return d.Errf("unrecognized renegotiation level: %s", t.Renegotiation) + } + default: + return d.Errf("unrecognized directive: %s", d.Val()) + } + } + return nil +} + +// MakeTLSClientConfig returns a tls.Config usable by a client to a backend. +// If there is no custom TLS configuration, a nil config may be returned. +// copied from with minor modifications: modules/caddyhttp/reverseproxy/httptransport.go +func (t TLSConfig) makeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) { + repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if repl == nil { + repl = caddy.NewReplacer() + } + cfg := new(tls.Config) + + if t.CARaw != nil { + caRaw, err := ctx.LoadModule(t, "CARaw") + if err != nil { + return nil, err + } + ca := caRaw.(CA) + cfg.RootCAs = ca.CertPool() + } + + // Renegotiation + switch t.Renegotiation { + case "never", "": + cfg.Renegotiation = tls.RenegotiateNever + case "once": + cfg.Renegotiation = tls.RenegotiateOnceAsClient + case "freely": + cfg.Renegotiation = tls.RenegotiateFreelyAsClient + default: + return nil, fmt.Errorf("invalid TLS renegotiation level: %v", t.Renegotiation) + } + + // override for the server name used verify the TLS handshake + cfg.ServerName = repl.ReplaceKnown(cfg.ServerName, "") + + // throw all security out the window + cfg.InsecureSkipVerify = t.InsecureSkipVerify + + // only return a config if it's not empty + if reflect.DeepEqual(cfg, new(tls.Config)) { + return nil, nil + } + + return cfg, nil +} + +// The HTTPCertPool fetches the trusted root certificates from HTTP(S) +// endpoints. The TLS connection properties can be customized, including custom +// trusted root certificate. One example usage of this module is to get the trusted +// certificates from another Caddy instance that is running the PKI app and ACME server. +type HTTPCertPool struct { + // the list of URLs that respond with PEM-encoded certificates to trust. + Endpoints []string `json:"endpoints,omitempty"` + + // Customize the TLS connection knobs to used during the HTTP call + TLS *TLSConfig `json:"tls,omitempty"` + + pool *x509.CertPool +} + +// CaddyModule implements caddy.Module. +func (HTTPCertPool) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.ca_pool.source.http", + New: func() caddy.Module { + return new(HTTPCertPool) + }, + } +} + +// Provision implements caddy.Provisioner. +func (hcp *HTTPCertPool) Provision(ctx caddy.Context) error { + caPool := x509.NewCertPool() + + customTransport := http.DefaultTransport.(*http.Transport).Clone() + if hcp.TLS != nil { + tlsConfig, err := hcp.TLS.makeTLSClientConfig(ctx) + if err != nil { + return err + } + customTransport.TLSClientConfig = tlsConfig + } + + var httpClient *http.Client + *httpClient = *http.DefaultClient + httpClient.Transport = customTransport + + for _, uri := range hcp.Endpoints { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) + if err != nil { + return err + } + res, err := httpClient.Do(req) + if err != nil { + return err + } + pembs, err := io.ReadAll(res.Body) + res.Body.Close() + if err != nil { + return err + } + if !caPool.AppendCertsFromPEM(pembs) { + return fmt.Errorf("failed to add certs from URL: %s", uri) + } + } + hcp.pool = caPool + return nil +} + +// Syntax: +// +// trust_pool http [] { +// endpoints +// tls +// } +// +// tls_config: +// +// ca +// insecure_skip_verify +// handshake_timeout +// server_name +// renegotiation +// +// is the name of the CA module to source the trust +// +// certificate pool and follows the syntax of the named CA module. +func (hcp *HTTPCertPool) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.Next() // consume module name + hcp.Endpoints = append(hcp.Endpoints, d.RemainingArgs()...) + for nesting := d.Nesting(); d.NextBlock(nesting); { + switch d.Val() { + case "endpoints": + if d.CountRemainingArgs() == 0 { + return d.ArgErr() + } + hcp.Endpoints = append(hcp.Endpoints, d.RemainingArgs()...) + case "tls": + if hcp.TLS != nil { + return d.Err("tls block already defined") + } + hcp.TLS = new(TLSConfig) + if err := hcp.TLS.unmarshalCaddyfile(d); err != nil { + return err + } + default: + return d.Errf("unrecognized directive: %s", d.Val()) + } + } + + return nil +} + +// report error if the endpoints are not valid URLs +func (hcp HTTPCertPool) Validate() (err error) { + for _, u := range hcp.Endpoints { + _, e := url.Parse(u) + if e != nil { + err = errors.Join(err, e) + } + } + return err +} + +// CertPool return the certificate pool generated from the HTTP responses +func (hcp HTTPCertPool) CertPool() *x509.CertPool { + return hcp.pool +} + +// LazyCertPool defers the generation of the certificate pool from the +// guest module to demand-time rather than at provisionig time. The gain of the +// lazy load adds a risk of failure to load the certificates at demand time +// because the validation that's typically done at provisioning is deferred. +// The validation can be enforced to run before runtime by setting +// `EagerValidation`/`eager_validation` to `true`. It is the operator's responsibility +// to ensure the resources are available if `EagerValidation`/`eager_validation` +// is set to `true`. The module also incurs performance cost at every demand. +type LazyCertPool struct { + // Provides the guest module that provides the trusted certificate authority (CA) certificates + CARaw json.RawMessage `json:"ca,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"` + + // Whether the validation step should try to load and provision the guest module to validate + // the correctness of the configuration. Depeneding on the type of the guest module, + // the resources may not be available at validation time. It is the + // operator's responsibility to ensure the resources are available if `EagerValidation`/`eager_validation` + // is set to `true`. + EagerValidation bool `json:"eager_validation,omitempty"` + + ctx caddy.Context +} + +// CaddyModule implements caddy.Module. +func (LazyCertPool) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.ca_pool.source.lazy", + New: func() caddy.Module { + return new(LazyCertPool) + }, + } +} + +// Provision implements caddy.Provisioner. +func (lcp *LazyCertPool) Provision(ctx caddy.Context) error { + if len(lcp.CARaw) == 0 { + return fmt.Errorf("missing backing CA source") + } + lcp.ctx = ctx + return nil +} + +// Syntax: +// +// trust_pool lazy { +// backend +// eager_validation +// } +// +// The `backend` directive specifies the CA module to use to provision the +// certificate pool. The `eager_validation` directive specifies that the +// validation step should try to load and provision the guest module to validate +// the correctness of the configuration. Depeneding on the type of the guest module, +// the resources may not be available at validation time. It is the +// operator's responsibility to ensure the resources are available if `EagerValidation`/`eager_validation` +// is set to `true`. +// +// The `backend` directive is required. +func (lcp *LazyCertPool) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.Next() // consume module name + for nesting := d.Nesting(); d.NextBlock(nesting); { + switch d.Val() { + case "backend": + if lcp.CARaw != nil { + return d.Err("backend block already defined") + } + if !d.NextArg() { + return d.ArgErr() + } + modStem := d.Val() + modID := "tls.ca_pool.source." + modStem + unm, err := caddyfile.UnmarshalModule(d, modID) + if err != nil { + return err + } + backend, ok := unm.(CA) + if !ok { + return d.Errf("module %s is not a caddytls.CA", modID) + } + lcp.CARaw = caddyconfig.JSONModuleObject(backend, "provider", modStem, nil) + case "eager_validation": + lcp.EagerValidation = true + default: + return d.Errf("unrecognized directive: %s", d.Val()) + } + } + if lcp.CARaw == nil { + return d.Err("backend block is required") + } + return nil +} + +// If EagerValidation is `true`, it attempts to load and provision the guest module +// to ensure the guesst module's configuration is correct. Depeneding on the type of the +// guest module, the resources may not be available at validation time. It is the +// operator's responsibility to ensure the resources are available if `EagerValidation` is +// set to `true`. +func (lcp LazyCertPool) Validate() error { + if lcp.EagerValidation { + _, err := lcp.ctx.LoadModule(lcp, "CARaw") + return err + } + return nil +} + +// CertPool loads the guest module and returns the CertPool from there +// TODO: Cache? +func (lcp LazyCertPool) CertPool() *x509.CertPool { + caRaw, err := lcp.ctx.LoadModule(lcp, "CARaw") + if err != nil { + return nil + } + ca := caRaw.(CA) + return ca.CertPool() +} + +var ( + _ caddy.Module = (*InlineCAPool)(nil) + _ caddy.Provisioner = (*InlineCAPool)(nil) + _ CA = (*InlineCAPool)(nil) + _ caddyfile.Unmarshaler = (*InlineCAPool)(nil) + + _ caddy.Module = (*FileCAPool)(nil) + _ caddy.Provisioner = (*FileCAPool)(nil) + _ CA = (*FileCAPool)(nil) + _ caddyfile.Unmarshaler = (*FileCAPool)(nil) + + _ caddy.Module = (*PKIRootCAPool)(nil) + _ caddy.Provisioner = (*PKIRootCAPool)(nil) + _ CA = (*PKIRootCAPool)(nil) + _ caddyfile.Unmarshaler = (*PKIRootCAPool)(nil) + + _ caddy.Module = (*PKIIntermediateCAPool)(nil) + _ caddy.Provisioner = (*PKIIntermediateCAPool)(nil) + _ CA = (*PKIIntermediateCAPool)(nil) + _ caddyfile.Unmarshaler = (*PKIIntermediateCAPool)(nil) + + _ caddy.Module = (*StoragePool)(nil) + _ caddy.Provisioner = (*StoragePool)(nil) + _ CA = (*StoragePool)(nil) + _ caddyfile.Unmarshaler = (*StoragePool)(nil) + + _ caddy.Module = (*HTTPCertPool)(nil) + _ caddy.Provisioner = (*HTTPCertPool)(nil) + _ caddy.Validator = (*HTTPCertPool)(nil) + _ CA = (*HTTPCertPool)(nil) + _ caddyfile.Unmarshaler = (*HTTPCertPool)(nil) + + _ caddy.Module = (*LazyCertPool)(nil) + _ caddy.Provisioner = (*LazyCertPool)(nil) + _ caddy.Validator = (*LazyCertPool)(nil) + _ CA = (*LazyCertPool)(nil) + _ caddyfile.Unmarshaler = (*LazyCertPool)(nil) +) diff --git a/modules/caddytls/capools_test.go b/modules/caddytls/capools_test.go new file mode 100644 index 000000000..d04354a18 --- /dev/null +++ b/modules/caddytls/capools_test.go @@ -0,0 +1,892 @@ +package caddytls + +import ( + "encoding/json" + "fmt" + "reflect" + "testing" + "time" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + _ "github.com/caddyserver/caddy/v2/modules/filestorage" +) + +const ( + test_der_1 = `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==` + test_cert_file_1 = "../../caddytest/caddy.ca.cer" +) + +func TestInlineCAPoolUnmarshalCaddyfile(t *testing.T) { + type args struct { + d *caddyfile.Dispenser + } + tests := []struct { + name string + args args + expected InlineCAPool + wantErr bool + }{ + { + name: "configuring no certificatest produces an error", + args: args{ + d: caddyfile.NewTestDispenser(` + inline { + } + `), + }, + wantErr: true, + }, + { + name: "configuring certificates as arguments in-line produces an error", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf(` + inline %s + `, test_der_1)), + }, + wantErr: true, + }, + { + name: "single cert", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf(` + inline { + trust_der %s + } + `, test_der_1)), + }, + expected: InlineCAPool{ + TrustedCACerts: []string{test_der_1}, + }, + wantErr: false, + }, + { + name: "multiple certs in one line", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf(` + inline { + trust_der %s %s + } + `, test_der_1, test_der_1), + ), + }, + expected: InlineCAPool{ + TrustedCACerts: []string{test_der_1, test_der_1}, + }, + }, + { + name: "multiple certs in multiple lines", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf(` + inline { + trust_der %s + trust_der %s + } + `, test_der_1, test_der_1)), + }, + expected: InlineCAPool{ + TrustedCACerts: []string{test_der_1, test_der_1}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + icp := &InlineCAPool{} + if err := icp.UnmarshalCaddyfile(tt.args.d); (err != nil) != tt.wantErr { + t.Errorf("InlineCAPool.UnmarshalCaddyfile() error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr && !reflect.DeepEqual(&tt.expected, icp) { + t.Errorf("InlineCAPool.UnmarshalCaddyfile() = %v, want %v", icp, tt.expected) + } + }) + } +} + +func TestFileCAPoolUnmarshalCaddyfile(t *testing.T) { + type args struct { + d *caddyfile.Dispenser + } + tests := []struct { + name string + expected FileCAPool + args args + wantErr bool + }{ + { + name: "configuring no certificatest produces an error", + args: args{ + d: caddyfile.NewTestDispenser(` + file { + } + `), + }, + wantErr: true, + }, + { + name: "configuring certificates as arguments in-line produces an error", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf(` + file %s + `, test_cert_file_1)), + }, + expected: FileCAPool{ + TrustedCACertPEMFiles: []string{test_cert_file_1}, + }, + }, + { + name: "single cert", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf(` + file { + pem_file %s + } + `, test_cert_file_1)), + }, + expected: FileCAPool{ + TrustedCACertPEMFiles: []string{test_cert_file_1}, + }, + wantErr: false, + }, + { + name: "multiple certs inline and in-block are merged", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf(` + file %s { + pem_file %s + } + `, test_cert_file_1, test_cert_file_1)), + }, + expected: FileCAPool{ + TrustedCACertPEMFiles: []string{test_cert_file_1, test_cert_file_1}, + }, + wantErr: false, + }, + { + name: "multiple certs in one line", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf(` + file { + pem_file %s %s + } + `, test_der_1, test_der_1), + ), + }, + expected: FileCAPool{ + TrustedCACertPEMFiles: []string{test_der_1, test_der_1}, + }, + }, + { + name: "multiple certs in multiple lines", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf(` + file { + pem_file %s + pem_file %s + } + `, test_cert_file_1, test_cert_file_1)), + }, + expected: FileCAPool{ + TrustedCACertPEMFiles: []string{test_cert_file_1, test_cert_file_1}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fcap := &FileCAPool{} + if err := fcap.UnmarshalCaddyfile(tt.args.d); (err != nil) != tt.wantErr { + t.Errorf("FileCAPool.UnmarshalCaddyfile() error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr && !reflect.DeepEqual(&tt.expected, fcap) { + t.Errorf("FileCAPool.UnmarshalCaddyfile() = %v, want %v", fcap, tt.expected) + } + }) + } +} + +func TestPKIRootCAPoolUnmarshalCaddyfile(t *testing.T) { + type args struct { + d *caddyfile.Dispenser + } + tests := []struct { + name string + expected PKIRootCAPool + args args + wantErr bool + }{ + { + name: "configuring no certificatest produces an error", + args: args{ + d: caddyfile.NewTestDispenser(` + pki_root { + } + `), + }, + wantErr: true, + }, + { + name: "single authority as arguments in-line", + args: args{ + d: caddyfile.NewTestDispenser(` + pki_root ca_1 + `), + }, + expected: PKIRootCAPool{ + Authority: []string{"ca_1"}, + }, + }, + { + name: "multiple authorities as arguments in-line", + args: args{ + d: caddyfile.NewTestDispenser(` + pki_root ca_1 ca_2 + `), + }, + expected: PKIRootCAPool{ + Authority: []string{"ca_1", "ca_2"}, + }, + }, + { + name: "single authority in block", + args: args{ + d: caddyfile.NewTestDispenser(` + pki_root { + authority ca_1 + }`), + }, + expected: PKIRootCAPool{ + Authority: []string{"ca_1"}, + }, + wantErr: false, + }, + { + name: "multiple authorities in one line", + args: args{ + d: caddyfile.NewTestDispenser(` + pki_root { + authority ca_1 ca_2 + }`), + }, + expected: PKIRootCAPool{ + Authority: []string{"ca_1", "ca_2"}, + }, + }, + { + name: "multiple authorities in multiple lines", + args: args{ + d: caddyfile.NewTestDispenser(` + pki_root { + authority ca_1 + authority ca_2 + }`), + }, + expected: PKIRootCAPool{ + Authority: []string{"ca_1", "ca_2"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pkir := &PKIRootCAPool{} + if err := pkir.UnmarshalCaddyfile(tt.args.d); (err != nil) != tt.wantErr { + t.Errorf("PKIRootCAPool.UnmarshalCaddyfile() error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr && !reflect.DeepEqual(&tt.expected, pkir) { + t.Errorf("PKIRootCAPool.UnmarshalCaddyfile() = %v, want %v", pkir, tt.expected) + } + }) + } +} + +func TestPKIIntermediateCAPoolUnmarshalCaddyfile(t *testing.T) { + type args struct { + d *caddyfile.Dispenser + } + tests := []struct { + name string + expected PKIIntermediateCAPool + args args + wantErr bool + }{ + { + name: "configuring no certificatest produces an error", + args: args{ + d: caddyfile.NewTestDispenser(` + pki_intermediate { + }`), + }, + wantErr: true, + }, + { + name: "single authority as arguments in-line", + args: args{ + d: caddyfile.NewTestDispenser(`pki_intermediate ca_1`), + }, + expected: PKIIntermediateCAPool{ + Authority: []string{"ca_1"}, + }, + }, + { + name: "multiple authorities as arguments in-line", + args: args{ + d: caddyfile.NewTestDispenser(`pki_intermediate ca_1 ca_2`), + }, + expected: PKIIntermediateCAPool{ + Authority: []string{"ca_1", "ca_2"}, + }, + }, + { + name: "single authority in block", + args: args{ + d: caddyfile.NewTestDispenser(` + pki_intermediate { + authority ca_1 + }`), + }, + expected: PKIIntermediateCAPool{ + Authority: []string{"ca_1"}, + }, + wantErr: false, + }, + { + name: "multiple authorities in one line", + args: args{ + d: caddyfile.NewTestDispenser(` + pki_intermediate { + authority ca_1 ca_2 + }`), + }, + expected: PKIIntermediateCAPool{ + Authority: []string{"ca_1", "ca_2"}, + }, + }, + { + name: "multiple authorities in multiple lines", + args: args{ + d: caddyfile.NewTestDispenser(` + pki_intermediate { + authority ca_1 + authority ca_2 + }`), + }, + expected: PKIIntermediateCAPool{ + Authority: []string{"ca_1", "ca_2"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pic := &PKIIntermediateCAPool{} + if err := pic.UnmarshalCaddyfile(tt.args.d); (err != nil) != tt.wantErr { + t.Errorf("PKIIntermediateCAPool.UnmarshalCaddyfile() error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr && !reflect.DeepEqual(&tt.expected, pic) { + t.Errorf("PKIIntermediateCAPool.UnmarshalCaddyfile() = %v, want %v", pic, tt.expected) + } + }) + } +} + +func TestStoragePoolUnmarshalCaddyfile(t *testing.T) { + type args struct { + d *caddyfile.Dispenser + } + tests := []struct { + name string + args args + expected StoragePool + wantErr bool + }{ + { + name: "empty block", + args: args{ + d: caddyfile.NewTestDispenser(`storage { + }`), + }, + expected: StoragePool{}, + wantErr: false, + }, + { + name: "providing single storage key inline", + args: args{ + d: caddyfile.NewTestDispenser(`storage key-1`), + }, + expected: StoragePool{ + PEMKeys: []string{"key-1"}, + }, + wantErr: false, + }, + { + name: "providing multiple storage keys inline", + args: args{ + d: caddyfile.NewTestDispenser(`storage key-1 key-2`), + }, + expected: StoragePool{ + PEMKeys: []string{"key-1", "key-2"}, + }, + wantErr: false, + }, + { + name: "providing keys inside block without specifying storage type", + args: args{ + d: caddyfile.NewTestDispenser(` + storage { + keys key-1 key-2 + } + `), + }, + expected: StoragePool{ + PEMKeys: []string{"key-1", "key-2"}, + }, + wantErr: false, + }, + { + name: "providing keys in-line and inside block merges them", + args: args{ + d: caddyfile.NewTestDispenser(`storage key-1 key-2 key-3 { + keys key-4 key-5 + }`), + }, + expected: StoragePool{ + PEMKeys: []string{"key-1", "key-2", "key-3", "key-4", "key-5"}, + }, + wantErr: false, + }, + { + name: "specifying storage type in block", + args: args{ + d: caddyfile.NewTestDispenser(`storage { + storage file_system /var/caddy/storage + }`), + }, + expected: StoragePool{ + StorageRaw: json.RawMessage(`{"module":"file_system","root":"/var/caddy/storage"}`), + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sp := &StoragePool{} + if err := sp.UnmarshalCaddyfile(tt.args.d); (err != nil) != tt.wantErr { + t.Errorf("StoragePool.UnmarshalCaddyfile() error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr && !reflect.DeepEqual(&tt.expected, sp) { + t.Errorf("StoragePool.UnmarshalCaddyfile() = %s, want %s", sp.StorageRaw, tt.expected.StorageRaw) + } + }) + } +} + +func TestTLSConfig_unmarshalCaddyfile(t *testing.T) { + type args struct { + d *caddyfile.Dispenser + } + tests := []struct { + name string + args args + expected TLSConfig + wantErr bool + }{ + { + name: "no arguments is valid", + args: args{ + d: caddyfile.NewTestDispenser(` { + }`), + }, + expected: TLSConfig{}, + }, + { + name: "setting 'renegotiation' to 'never' is valid", + args: args{ + d: caddyfile.NewTestDispenser(` { + renegotiation never + }`), + }, + expected: TLSConfig{ + Renegotiation: "never", + }, + }, + { + name: "setting 'renegotiation' to 'once' is valid", + args: args{ + d: caddyfile.NewTestDispenser(` { + renegotiation once + }`), + }, + expected: TLSConfig{ + Renegotiation: "once", + }, + }, + { + name: "setting 'renegotiation' to 'freely' is valid", + args: args{ + d: caddyfile.NewTestDispenser(` { + renegotiation freely + }`), + }, + expected: TLSConfig{ + Renegotiation: "freely", + }, + }, + { + name: "setting 'renegotiation' to other than 'none', 'once, or 'freely' is invalid", + args: args{ + d: caddyfile.NewTestDispenser(` { + renegotiation foo + }`), + }, + wantErr: true, + }, + { + name: "setting 'renegotiation' without argument is invalid", + args: args{ + d: caddyfile.NewTestDispenser(` { + renegotiation + }`), + }, + wantErr: true, + }, + { + name: "setting 'ca' without arguemnt is an error", + args: args{ + d: caddyfile.NewTestDispenser(`{ + ca + }`), + }, + wantErr: true, + }, + { + name: "setting 'ca' to 'file' with in-line cert is valid", + args: args{ + d: caddyfile.NewTestDispenser(`{ + ca file /var/caddy/ca.pem + }`), + }, + expected: TLSConfig{ + CARaw: []byte(`{"pem_files":["/var/caddy/ca.pem"],"provider":"file"}`), + }, + }, + { + name: "setting 'ca' to 'lazy' with appropriate block is valid", + args: args{ + d: caddyfile.NewTestDispenser(`{ + ca lazy { + backend file { + pem_file /var/caddy/ca.pem + } + } + }`), + }, + expected: TLSConfig{ + CARaw: []byte(`{"ca":{"pem_files":["/var/caddy/ca.pem"],"provider":"file"},"provider":"lazy"}`), + }, + }, + { + name: "setting 'ca' to 'file' with appropriate block is valid", + args: args{ + d: caddyfile.NewTestDispenser(`{ + ca file /var/caddy/ca.pem { + pem_file /var/caddy/ca.pem + } + }`), + }, + expected: TLSConfig{ + CARaw: []byte(`{"pem_files":["/var/caddy/ca.pem","/var/caddy/ca.pem"],"provider":"file"}`), + }, + }, + { + name: "setting 'ca' multiple times is an error", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf(`{ + ca file /var/caddy/ca.pem { + pem_file /var/caddy/ca.pem + } + ca inline %s + }`, test_der_1)), + }, + wantErr: true, + }, + { + name: "setting 'handshake_timeout' without value is an error", + args: args{ + d: caddyfile.NewTestDispenser(`{ + handshake_timeout + }`), + }, + wantErr: true, + }, + { + name: "setting 'handshake_timeout' properly is successful", + args: args{ + d: caddyfile.NewTestDispenser(`{ + handshake_timeout 42m + }`), + }, + expected: TLSConfig{ + HandshakeTimeout: caddy.Duration(42 * time.Minute), + }, + }, + { + name: "setting 'server_name' without value is an error", + args: args{ + d: caddyfile.NewTestDispenser(`{ + server_name + }`), + }, + wantErr: true, + }, + { + name: "setting 'server_name' properly is successful", + args: args{ + d: caddyfile.NewTestDispenser(`{ + server_name example.com + }`), + }, + expected: TLSConfig{ + ServerName: "example.com", + }, + }, + { + name: "unsupported directives are errors", + args: args{ + d: caddyfile.NewTestDispenser(`{ + foo + }`), + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tr := &TLSConfig{} + if err := tr.unmarshalCaddyfile(tt.args.d); (err != nil) != tt.wantErr { + t.Errorf("TLSConfig.unmarshalCaddyfile() error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr && !reflect.DeepEqual(&tt.expected, tr) { + t.Errorf("TLSConfig.UnmarshalCaddyfile() = %v, want %v", tr, tt.expected) + } + }) + } +} + +func TestHTTPCertPoolUnmarshalCaddyfile(t *testing.T) { + type args struct { + d *caddyfile.Dispenser + } + tests := []struct { + name string + args args + expected HTTPCertPool + wantErr bool + }{ + { + name: "no block, inline http endpoint", + args: args{ + d: caddyfile.NewTestDispenser(`http http://localhost/ca-certs`), + }, + expected: HTTPCertPool{ + Endpoints: []string{"http://localhost/ca-certs"}, + }, + wantErr: false, + }, + { + name: "no block, inline https endpoint", + args: args{ + d: caddyfile.NewTestDispenser(`http https://localhost/ca-certs`), + }, + expected: HTTPCertPool{ + Endpoints: []string{"https://localhost/ca-certs"}, + }, + wantErr: false, + }, + { + name: "no block, mixed http and https endpoints inline", + args: args{ + d: caddyfile.NewTestDispenser(`http http://localhost/ca-certs https://localhost/ca-certs`), + }, + expected: HTTPCertPool{ + Endpoints: []string{"http://localhost/ca-certs", "https://localhost/ca-certs"}, + }, + wantErr: false, + }, + { + name: "multiple endpoints in separate lines in block", + args: args{ + d: caddyfile.NewTestDispenser(` + http { + endpoints http://localhost/ca-certs + endpoints http://remotehost/ca-certs + } + `), + }, + expected: HTTPCertPool{ + Endpoints: []string{"http://localhost/ca-certs", "http://remotehost/ca-certs"}, + }, + wantErr: false, + }, + { + name: "endpoints defiend inline and in block are merged", + args: args{ + d: caddyfile.NewTestDispenser(`http http://localhost/ca-certs { + endpoints http://remotehost/ca-certs + }`), + }, + expected: HTTPCertPool{ + Endpoints: []string{"http://localhost/ca-certs", "http://remotehost/ca-certs"}, + }, + wantErr: false, + }, + { + name: "multiple endpoints defiend in block on the same line", + args: args{ + d: caddyfile.NewTestDispenser(`http { + endpoints http://remotehost/ca-certs http://localhost/ca-certs + }`), + }, + expected: HTTPCertPool{ + Endpoints: []string{"http://remotehost/ca-certs", "http://localhost/ca-certs"}, + }, + wantErr: false, + }, + { + name: "declaring 'endpoints' in block without argument is an error", + args: args{ + d: caddyfile.NewTestDispenser(`http { + endpoints + }`), + }, + wantErr: true, + }, + { + name: "multiple endpoints in separate lines in block", + args: args{ + d: caddyfile.NewTestDispenser(` + http { + endpoints http://localhost/ca-certs + endpoints http://remotehost/ca-certs + tls { + renegotiation freely + } + } + `), + }, + expected: HTTPCertPool{ + Endpoints: []string{"http://localhost/ca-certs", "http://remotehost/ca-certs"}, + TLS: &TLSConfig{ + Renegotiation: "freely", + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hcp := &HTTPCertPool{} + if err := hcp.UnmarshalCaddyfile(tt.args.d); (err != nil) != tt.wantErr { + t.Errorf("HTTPCertPool.UnmarshalCaddyfile() error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr && !reflect.DeepEqual(&tt.expected, hcp) { + t.Errorf("HTTPCertPool.UnmarshalCaddyfile() = %v, want %v", hcp, tt.expected) + } + }) + } +} + +func TestLazyCertPoolUnmarshalCaddyfile(t *testing.T) { + type args struct { + d *caddyfile.Dispenser + } + tests := []struct { + name string + args args + expected LazyCertPool + wantErr bool + }{ + { + name: "no block results in error", + args: args{ + d: caddyfile.NewTestDispenser(`lazy`), + }, + wantErr: true, + }, + { + name: "empty block results in error", + args: args{ + d: caddyfile.NewTestDispenser(`lazy { + }`), + }, + wantErr: true, + }, + { + name: "defining 'backend' multiple times results in error", + args: args{ + d: caddyfile.NewTestDispenser(`lazy { + backend http { + endpoints http://localhost/ca-certs + } + backend file { + pem_file /var/caddy/certs + } + }`), + }, + wantErr: true, + }, + { + name: "defining 'backend' without argument results in error", + args: args{ + d: caddyfile.NewTestDispenser(`lazy { + backend + }`), + }, + wantErr: true, + }, + { + name: "using unrecognized directive results in error", + args: args{ + d: caddyfile.NewTestDispenser(`lazy { + foo + }`), + }, + wantErr: true, + }, + { + name: "defining single 'backend' is successful", + args: args{ + d: caddyfile.NewTestDispenser(`lazy { + backend http { + endpoints http://localhost/ca-certs + } + }`), + }, + expected: LazyCertPool{ + CARaw: []byte(`{"endpoints":["http://localhost/ca-certs"],"provider":"http"}`), + }, + }, + { + name: "defining single 'backend' with 'eager_validation' successful", + args: args{ + d: caddyfile.NewTestDispenser(`lazy { + backend file { + pem_file /var/caddy/certs + } + eager_validation + }`), + }, + expected: LazyCertPool{ + CARaw: []byte(`{"pem_files":["/var/caddy/certs"],"provider":"file"}`), + EagerValidation: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + lcp := &LazyCertPool{} + if err := lcp.UnmarshalCaddyfile(tt.args.d); (err != nil) != tt.wantErr { + t.Errorf("LazyCertPool.UnmarshalCaddyfile() error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr && !reflect.DeepEqual(&tt.expected, lcp) { + t.Errorf("LazyCertPool.UnmarshalCaddyfile() = %v, want %v", lcp, tt.expected) + } + }) + } +} diff --git a/modules/caddytls/certmanagers.go b/modules/caddytls/certmanagers.go index ad26468a9..9bb436a37 100644 --- a/modules/caddytls/certmanagers.go +++ b/modules/caddytls/certmanagers.go @@ -72,10 +72,9 @@ func (ts Tailscale) canHazCertificate(ctx context.Context, hello *tls.ClientHell // // ... tailscale func (Tailscale) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if d.NextArg() { - return d.ArgErr() - } + d.Next() // consume cert manager name + if d.NextArg() { + return d.ArgErr() } return nil } @@ -169,17 +168,18 @@ func (hcg HTTPCertGetter) GetCertificate(ctx context.Context, hello *tls.ClientH // // ... http func (hcg *HTTPCertGetter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if !d.NextArg() { - return d.ArgErr() - } - hcg.URL = d.Val() - if d.NextArg() { - return d.ArgErr() - } - for nesting := d.Nesting(); d.NextBlock(nesting); { - return d.Err("block not allowed here") - } + d.Next() // consume cert manager name + + if !d.NextArg() { + return d.ArgErr() + } + hcg.URL = d.Val() + + if d.NextArg() { + return d.ArgErr() + } + if d.NextBlock(0) { + return d.Err("block not allowed here") } return nil } diff --git a/modules/caddytls/cf.go b/modules/caddytls/cf.go new file mode 100644 index 000000000..e61a59c09 --- /dev/null +++ b/modules/caddytls/cf.go @@ -0,0 +1,24 @@ +//go:build cfgo + +package caddytls + +// This file adds support for X25519Kyber768Draft00, a post-quantum +// key agreement that is currently being rolled out by Chrome [1] +// and Cloudflare [2,3]. For more context, see the PR [4]. +// +// [1] https://blog.chromium.org/2023/08/protecting-chrome-traffic-with-hybrid.html +// [2] https://blog.cloudflare.com/post-quantum-for-all/ +// [3] https://blog.cloudflare.com/post-quantum-to-origins/ +// [4] https://github.com/caddyserver/caddy/pull/5852 + +import ( + "crypto/tls" +) + +func init() { + SupportedCurves["X25519Kyber768Draft00"] = tls.X25519Kyber768Draft00 + defaultCurves = append( + []tls.CurveID{tls.X25519Kyber768Draft00}, + defaultCurves..., + ) +} diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index 64fdd5138..49c7add49 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -19,6 +19,7 @@ import ( "crypto/x509" "encoding/base64" "encoding/json" + "encoding/pem" "fmt" "io" "os" @@ -29,6 +30,8 @@ import ( "go.uber.org/zap" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) func init() { @@ -301,8 +304,10 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error { // client authentication if p.ClientAuthentication != nil { - err := p.ClientAuthentication.ConfigureTLSConfig(cfg) - if err != nil { + if err := p.ClientAuthentication.provision(ctx); err != nil { + return fmt.Errorf("provisioning client CA: %v", err) + } + if err := p.ClientAuthentication.ConfigureTLSConfig(cfg); err != nil { return fmt.Errorf("configuring TLS client authentication: %v", err) } } @@ -354,12 +359,18 @@ func (p ConnectionPolicy) SettingsEmpty() bool { // ClientAuthentication configures TLS client auth. type ClientAuthentication struct { + // Certificate authority module which provides the certificate pool of trusted certificates + CARaw json.RawMessage `json:"ca,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"` + ca CA + + // DEPRECATED: Use the `ca` field with the `tls.ca_pool.source.inline` module instead. // A list of base64 DER-encoded CA certificates // against which to validate client certificates. // Client certs which are not signed by any of // these CAs will be rejected. TrustedCACerts []string `json:"trusted_ca_certs,omitempty"` + // DEPRECATED: Use the `ca` field with the `tls.ca_pool.source.file` module instead. // TrustedCACertPEMFiles is a list of PEM file names // from which to load certificates of trusted CAs. // Client certificates which are not signed by any of @@ -368,7 +379,7 @@ type ClientAuthentication struct { // DEPRECATED: This field is deprecated and will be removed in // a future version. Please use the `validators` field instead - // with the tls.client_auth.leaf module instead. + // with the tls.client_auth.verifier.leaf module instead. // // A list of base64 DER-encoded client leaf certs // to accept. If this list is not empty, client certs @@ -378,7 +389,7 @@ type ClientAuthentication struct { // Client certificate verification modules. These can perform // custom client authentication checks, such as ensuring the // certificate is not revoked. - VerifiersRaw []json.RawMessage `json:"verifiers,omitempty" caddy:"namespace=tls.client_auth inline_key=verifier"` + VerifiersRaw []json.RawMessage `json:"verifiers,omitempty" caddy:"namespace=tls.client_auth.verifier inline_key=verifier"` verifiers []ClientCertificateVerifier @@ -399,13 +410,194 @@ type ClientAuthentication struct { existingVerifyPeerCert func([][]byte, [][]*x509.Certificate) error } +// UnmarshalCaddyfile parses the Caddyfile segment to set up the client authentication. Syntax: +// +// client_auth { +// mode [request|require|verify_if_given|require_and_verify] +// trust_pool { +// ... +// } +// trusted_leaf_cert +// trusted_leaf_cert_file +// } +// +// If `mode` is not provided, it defaults to `require_and_verify` if any of the following are provided: +// - `trusted_leaf_certs` +// - `trusted_leaf_cert_file` +// - `trust_pool` +// +// Otherwise, it defaults to `require`. +func (ca *ClientAuthentication) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.NextArg() { + // consume any tokens on the same line, if any. + } + for nesting := d.Nesting(); d.NextBlock(nesting); { + subdir := d.Val() + switch subdir { + case "mode": + if d.CountRemainingArgs() > 1 { + return d.ArgErr() + } + if !d.Args(&ca.Mode) { + return d.ArgErr() + } + case "trusted_ca_cert": + if len(ca.CARaw) != 0 { + return d.Err("cannot specify both 'trust_pool' and 'trusted_ca_cert' or 'trusted_ca_cert_file'") + } + if !d.NextArg() { + return d.ArgErr() + } + ca.TrustedCACerts = append(ca.TrustedCACerts, d.Val()) + case "trusted_leaf_cert": + if !d.NextArg() { + return d.ArgErr() + } + ca.TrustedLeafCerts = append(ca.TrustedLeafCerts, d.Val()) + case "trusted_ca_cert_file": + if len(ca.CARaw) != 0 { + return d.Err("cannot specify both 'trust_pool' and 'trusted_ca_cert' or 'trusted_ca_cert_file'") + } + if !d.NextArg() { + return d.ArgErr() + } + filename := d.Val() + ders, err := convertPEMFilesToDER(filename) + if err != nil { + return d.WrapErr(err) + } + ca.TrustedCACerts = append(ca.TrustedCACerts, ders...) + case "trusted_leaf_cert_file": + if !d.NextArg() { + return d.ArgErr() + } + filename := d.Val() + ders, err := convertPEMFilesToDER(filename) + if err != nil { + return d.WrapErr(err) + } + ca.TrustedLeafCerts = append(ca.TrustedLeafCerts, ders...) + case "trust_pool": + if len(ca.TrustedCACerts) != 0 { + return d.Err("cannot specify both 'trust_pool' and 'trusted_ca_cert' or 'trusted_ca_cert_file'") + } + if !d.NextArg() { + return d.ArgErr() + } + modName := d.Val() + mod, err := caddyfile.UnmarshalModule(d, "tls.ca_pool.source."+modName) + if err != nil { + return d.WrapErr(err) + } + caMod, ok := mod.(CA) + if !ok { + return fmt.Errorf("trust_pool module '%s' is not a certificate pool provider", caMod) + } + ca.CARaw = caddyconfig.JSONModuleObject(caMod, "provider", modName, nil) + case "verifier": + if !d.NextArg() { + return d.ArgErr() + } + + vType := d.Val() + modID := "tls.client_auth.verifier." + vType + unm, err := caddyfile.UnmarshalModule(d, modID) + if err != nil { + return err + } + + _, ok := unm.(ClientCertificateVerifier) + if !ok { + return d.Errf("module '%s' is not a caddytls.ClientCertificatVerifier", modID) + } + ca.VerifiersRaw = append(ca.VerifiersRaw, caddyconfig.JSONModuleObject(unm, "verifier", vType, nil)) + default: + return d.Errf("unknown subdirective for client_auth: %s", subdir) + } + } + + // only trust_ca_cert or trust_ca_cert_file was specified + if len(ca.TrustedCACerts) > 0 { + fileMod := &InlineCAPool{} + fileMod.TrustedCACerts = append(fileMod.TrustedCACerts, ca.TrustedCACerts...) + ca.CARaw = caddyconfig.JSONModuleObject(fileMod, "provider", "inline", nil) + ca.TrustedCACertPEMFiles, ca.TrustedCACerts = nil, nil + } + return nil +} + +func convertPEMFilesToDER(filename string) ([]string, error) { + certDataPEM, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + var ders []string + // while block is not nil, we have more certificates in the file + for block, rest := pem.Decode(certDataPEM); block != nil; block, rest = pem.Decode(rest) { + if block.Type != "CERTIFICATE" { + return nil, fmt.Errorf("no CERTIFICATE pem block found in %s", filename) + } + ders = append( + ders, + base64.StdEncoding.EncodeToString(block.Bytes), + ) + } + // if we decoded nothing, return an error + if len(ders) == 0 { + return nil, fmt.Errorf("no CERTIFICATE pem block found in %s", filename) + } + return ders, nil +} + +func (clientauth *ClientAuthentication) provision(ctx caddy.Context) error { + if len(clientauth.CARaw) > 0 && (len(clientauth.TrustedCACerts) > 0 || len(clientauth.TrustedCACertPEMFiles) > 0) { + return fmt.Errorf("conflicting config for client authentication trust CA") + } + + // convert all named file paths to inline + if len(clientauth.TrustedCACertPEMFiles) > 0 { + for _, fpath := range clientauth.TrustedCACertPEMFiles { + ders, err := convertPEMFilesToDER(fpath) + if err != nil { + return nil + } + clientauth.TrustedCACerts = append(clientauth.TrustedCACerts, ders...) + } + } + + // if we have TrustedCACerts explicitly set, create an 'inline' CA and return + if len(clientauth.TrustedCACerts) > 0 { + clientauth.ca = InlineCAPool{ + TrustedCACerts: clientauth.TrustedCACerts, + } + return nil + } + + // if we don't have any CARaw set, there's not much work to do + if clientauth.CARaw == nil { + return nil + } + caRaw, err := ctx.LoadModule(clientauth, "CARaw") + if err != nil { + return err + } + ca, ok := caRaw.(CA) + if !ok { + return fmt.Errorf("'ca' module '%s' is not a certificate pool provider", ca) + } + clientauth.ca = ca + + return nil +} + // Active returns true if clientauth has an actionable configuration. func (clientauth ClientAuthentication) Active() bool { return len(clientauth.TrustedCACerts) > 0 || len(clientauth.TrustedCACertPEMFiles) > 0 || len(clientauth.TrustedLeafCerts) > 0 || // TODO: DEPRECATED len(clientauth.VerifiersRaw) > 0 || - len(clientauth.Mode) > 0 + len(clientauth.Mode) > 0 || + clientauth.CARaw != nil || clientauth.ca != nil } // ConfigureTLSConfig sets up cfg to enforce clientauth's configuration. @@ -434,7 +626,8 @@ func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) erro // otherwise, set a safe default mode if len(clientauth.TrustedCACerts) > 0 || len(clientauth.TrustedCACertPEMFiles) > 0 || - len(clientauth.TrustedLeafCerts) > 0 { + len(clientauth.TrustedLeafCerts) > 0 || + clientauth.CARaw != nil { cfg.ClientAuth = tls.RequireAndVerifyClientCert } else { cfg.ClientAuth = tls.RequireAnyClientCert @@ -442,23 +635,8 @@ func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) erro } // enforce CA verification by adding CA certs to the ClientCAs pool - if len(clientauth.TrustedCACerts) > 0 || len(clientauth.TrustedCACertPEMFiles) > 0 { - caPool := x509.NewCertPool() - for _, clientCAString := range clientauth.TrustedCACerts { - clientCA, err := decodeBase64DERCert(clientCAString) - if err != nil { - return fmt.Errorf("parsing certificate: %v", err) - } - caPool.AddCert(clientCA) - } - for _, pemFile := range clientauth.TrustedCACertPEMFiles { - pemContents, err := os.ReadFile(pemFile) - if err != nil { - return fmt.Errorf("reading %s: %v", pemFile, err) - } - caPool.AppendCertsFromPEM(pemContents) - } - cfg.ClientCAs = caPool + if clientauth.ca != nil { + cfg.ClientCAs = clientauth.ca.CertPool() } // TODO: DEPRECATED: Only here for backwards compatibility. @@ -473,7 +651,7 @@ func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) erro } trustedLeafCerts = append(trustedLeafCerts, clientCert) } - clientauth.verifiers = append(clientauth.verifiers, LeafCertClientAuth{TrustedLeafCerts: trustedLeafCerts}) + clientauth.verifiers = append(clientauth.verifiers, LeafCertClientAuth{trustedLeafCerts: trustedLeafCerts}) } // if a custom verification function already exists, wrap it @@ -537,17 +715,42 @@ func setDefaultTLSParams(cfg *tls.Config) { // LeafCertClientAuth verifies the client's leaf certificate. type LeafCertClientAuth struct { - TrustedLeafCerts []*x509.Certificate + LeafCertificateLoadersRaw []json.RawMessage `json:"leaf_certs_loaders,omitempty" caddy:"namespace=tls.leaf_cert_loader inline_key=loader"` + trustedLeafCerts []*x509.Certificate } // CaddyModule returns the Caddy module information. func (LeafCertClientAuth) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - ID: "tls.client_auth.leaf", + ID: "tls.client_auth.verifier.leaf", New: func() caddy.Module { return new(LeafCertClientAuth) }, } } +func (l *LeafCertClientAuth) Provision(ctx caddy.Context) error { + if l.LeafCertificateLoadersRaw == nil { + return nil + } + val, err := ctx.LoadModule(l, "LeafCertificateLoadersRaw") + if err != nil { + return fmt.Errorf("could not parse leaf certificates loaders: %s", err.Error()) + } + trustedLeafCertloaders := []LeafCertificateLoader{} + for _, loader := range val.([]any) { + trustedLeafCertloaders = append(trustedLeafCertloaders, loader.(LeafCertificateLoader)) + } + trustedLeafCertificates := []*x509.Certificate{} + for _, loader := range trustedLeafCertloaders { + certs, err := loader.LoadLeafCertificates() + if err != nil { + return fmt.Errorf("could not load leaf certificates: %s", err.Error()) + } + trustedLeafCertificates = append(trustedLeafCertificates, certs...) + } + l.trustedLeafCerts = trustedLeafCertificates + return nil +} + func (l LeafCertClientAuth) VerifyClientCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) error { if len(rawCerts) == 0 { return fmt.Errorf("no client certificate provided") @@ -558,7 +761,7 @@ func (l LeafCertClientAuth) VerifyClientCertificate(rawCerts [][]byte, _ [][]*x5 return fmt.Errorf("can't parse the given certificate: %s", err.Error()) } - for _, trustedLeafCert := range l.TrustedLeafCerts { + for _, trustedLeafCert := range l.trustedLeafCerts { if remoteLeafCert.Equal(trustedLeafCert) { return nil } @@ -587,6 +790,12 @@ type ConnectionMatcher interface { Match(*tls.ClientHelloInfo) bool } +// LeafCertificateLoader is a type that loads the trusted leaf certificates +// for the tls.leaf_cert_loader modules +type LeafCertificateLoader interface { + LoadLeafCertificates() ([]*x509.Certificate, error) +} + // ClientCertificateVerifier is a type which verifies client certificates. // It is called during verifyPeerCertificate in the TLS handshake. type ClientCertificateVerifier interface { @@ -600,3 +809,5 @@ type destructableWriter struct{ *os.File } func (d destructableWriter) Destruct() error { return d.Close() } var secretsLogPool = caddy.NewUsagePool() + +var _ caddyfile.Unmarshaler = (*ClientAuthentication)(nil) diff --git a/modules/caddytls/connpolicy_test.go b/modules/caddytls/connpolicy_test.go new file mode 100644 index 000000000..573b9c163 --- /dev/null +++ b/modules/caddytls/connpolicy_test.go @@ -0,0 +1,280 @@ +// 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 caddytls + +import ( + "encoding/json" + "fmt" + "reflect" + "testing" + + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" +) + +func TestClientAuthenticationUnmarshalCaddyfileWithDirectiveName(t *testing.T) { + const test_der_1 = `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==` + const test_cert_file_1 = "../../caddytest/caddy.ca.cer" + type args struct { + d *caddyfile.Dispenser + } + tests := []struct { + name string + args args + expected ClientAuthentication + wantErr bool + }{ + { + name: "empty client_auth block does not error", + args: args{ + d: caddyfile.NewTestDispenser( + `client_auth { + }`, + ), + }, + wantErr: false, + }, + { + name: "providing both 'trust_pool' and 'trusted_ca_cert' returns an error", + args: args{ + d: caddyfile.NewTestDispenser( + `client_auth { + trust_pool inline 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== + trusted_ca_cert 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== + }`), + }, + wantErr: true, + }, + { + name: "trust_pool without a module argument returns an error", + args: args{ + d: caddyfile.NewTestDispenser( + `client_auth { + trust_pool + }`), + }, + wantErr: true, + }, + { + name: "providing more than 1 mode produces an error", + args: args{ + d: caddyfile.NewTestDispenser(` + client_auth { + mode require request + } + `), + }, + wantErr: true, + }, + { + name: "not providing 'mode' argument produces an error", + args: args{d: caddyfile.NewTestDispenser(` + client_auth { + mode + } + `)}, + wantErr: true, + }, + { + name: "providing a single 'mode' argument sets the mode", + args: args{ + d: caddyfile.NewTestDispenser(` + client_auth { + mode require + } + `), + }, + expected: ClientAuthentication{ + Mode: "require", + }, + wantErr: false, + }, + { + name: "not providing an argument to 'trusted_ca_cert' produces an error", + args: args{ + d: caddyfile.NewTestDispenser(` + client_auth { + trusted_ca_cert + } + `), + }, + wantErr: true, + }, + { + name: "not providing an argument to 'trusted_leaf_cert' produces an error", + args: args{ + d: caddyfile.NewTestDispenser(` + client_auth { + trusted_leaf_cert + } + `), + }, + wantErr: true, + }, + { + name: "not providing an argument to 'trusted_ca_cert_file' produces an error", + args: args{ + d: caddyfile.NewTestDispenser(` + client_auth { + trusted_ca_cert_file + } + `), + }, + wantErr: true, + }, + { + name: "not providing an argument to 'trusted_leaf_cert_file' produces an error", + args: args{ + d: caddyfile.NewTestDispenser(` + client_auth { + trusted_leaf_cert_file + } + `), + }, + wantErr: true, + }, + { + name: "using 'trusted_ca_cert' adapts sucessfully", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf(` + client_auth { + trusted_ca_cert %s + }`, test_der_1)), + }, + expected: ClientAuthentication{ + CARaw: json.RawMessage(fmt.Sprintf(`{"provider":"inline","trusted_ca_certs":["%s"]}`, test_der_1)), + }, + }, + { + name: "using 'inline' trust_pool loads the module successfully", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf(` + client_auth { + trust_pool inline { + trust_der %s + } + } + `, test_der_1)), + }, + expected: ClientAuthentication{ + CARaw: json.RawMessage(fmt.Sprintf(`{"provider":"inline","trusted_ca_certs":["%s"]}`, test_der_1)), + }, + }, + { + name: "setting 'trusted_ca_cert' and 'trust_pool' produces an error", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf(` + client_auth { + trusted_ca_cert %s + trust_pool inline { + trust_der %s + } + }`, test_der_1, test_der_1)), + }, + wantErr: true, + }, + { + name: "setting 'trust_pool' and 'trusted_ca_cert' produces an error", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf(` + client_auth { + trust_pool inline { + trust_der %s + } + trusted_ca_cert %s + }`, test_der_1, test_der_1)), + }, + wantErr: true, + }, + { + name: "setting 'trust_pool' and 'trusted_ca_cert' produces an error", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf(` + client_auth { + trust_pool inline { + trust_der %s + } + trusted_ca_cert_file %s + }`, test_der_1, test_cert_file_1)), + }, + wantErr: true, + }, + { + name: "configuring 'trusted_ca_cert_file' without an argument is an error", + args: args{ + d: caddyfile.NewTestDispenser(` + client_auth { + trusted_ca_cert_file + } + `), + }, + wantErr: true, + }, + { + name: "configuring 'trusted_ca_cert_file' produces config with 'inline' provider", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf(` + client_auth { + trusted_ca_cert_file %s + }`, test_cert_file_1), + ), + }, + expected: ClientAuthentication{ + CARaw: json.RawMessage(fmt.Sprintf(`{"provider":"inline","trusted_ca_certs":["%s"]}`, test_der_1)), + }, + wantErr: false, + }, + { + name: "configuring leaf certs does not conflict with 'trust_pool'", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf(` + client_auth { + trust_pool inline { + trust_der %s + } + trusted_leaf_cert %s + }`, test_der_1, test_der_1)), + }, + expected: ClientAuthentication{ + CARaw: json.RawMessage(fmt.Sprintf(`{"provider":"inline","trusted_ca_certs":["%s"]}`, test_der_1)), + TrustedLeafCerts: []string{test_der_1}, + }, + }, + { + name: "providing trusted leaf certificate file loads the cert successfully", + args: args{ + d: caddyfile.NewTestDispenser(fmt.Sprintf(` + client_auth { + trusted_leaf_cert_file %s + }`, test_cert_file_1)), + }, + expected: ClientAuthentication{ + TrustedLeafCerts: []string{test_der_1}, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ca := &ClientAuthentication{} + if err := ca.UnmarshalCaddyfile(tt.args.d); (err != nil) != tt.wantErr { + t.Errorf("ClientAuthentication.UnmarshalCaddyfile() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && !reflect.DeepEqual(&tt.expected, ca) { + t.Errorf("ClientAuthentication.UnmarshalCaddyfile() = %v, want %v", ca, tt.expected) + } + }) + } +} diff --git a/modules/caddytls/fileloader.go b/modules/caddytls/fileloader.go index 430932b99..8603bbe65 100644 --- a/modules/caddytls/fileloader.go +++ b/modules/caddytls/fileloader.go @@ -29,6 +29,26 @@ func init() { // FileLoader loads certificates and their associated keys from disk. type FileLoader []CertKeyFilePair +// Provision implements caddy.Provisioner. +func (fl FileLoader) Provision(ctx caddy.Context) error { + repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if !ok { + repl = caddy.NewReplacer() + } + for k, pair := range fl { + for i, tag := range pair.Tags { + pair.Tags[i] = repl.ReplaceKnown(tag, "") + } + fl[k] = CertKeyFilePair{ + Certificate: repl.ReplaceKnown(pair.Certificate, ""), + Key: repl.ReplaceKnown(pair.Key, ""), + Format: repl.ReplaceKnown(pair.Format, ""), + Tags: pair.Tags, + } + } + return nil +} + // CaddyModule returns the Caddy module information. func (FileLoader) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ @@ -87,4 +107,7 @@ func (fl FileLoader) LoadCertificates() ([]Certificate, error) { } // Interface guard -var _ CertificateLoader = (FileLoader)(nil) +var ( + _ CertificateLoader = (FileLoader)(nil) + _ caddy.Provisioner = (FileLoader)(nil) +) diff --git a/modules/caddytls/folderloader.go b/modules/caddytls/folderloader.go index 33b31a54a..89e978df6 100644 --- a/modules/caddytls/folderloader.go +++ b/modules/caddytls/folderloader.go @@ -43,6 +43,18 @@ func (FolderLoader) CaddyModule() caddy.ModuleInfo { } } +// Provision implements caddy.Provisioner. +func (fl FolderLoader) Provision(ctx caddy.Context) error { + repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if !ok { + repl = caddy.NewReplacer() + } + for k, path := range fl { + fl[k] = repl.ReplaceKnown(path, "") + } + return nil +} + // LoadCertificates loads all the certificates+keys in the directories // listed in fl from all files ending with .pem. This method of loading // certificates expects the certificate and key to be bundled into the @@ -146,4 +158,7 @@ func tlsCertFromCertAndKeyPEMBundle(bundle []byte) (tls.Certificate, error) { return cert, nil } -var _ CertificateLoader = (FolderLoader)(nil) +var ( + _ CertificateLoader = (FolderLoader)(nil) + _ caddy.Provisioner = (FolderLoader)(nil) +) diff --git a/modules/caddytls/internalissuer.go b/modules/caddytls/internalissuer.go index 1cf2461ab..0d7f4157e 100644 --- a/modules/caddytls/internalissuer.go +++ b/modules/caddytls/internalissuer.go @@ -155,31 +155,29 @@ func (iss InternalIssuer) Issue(ctx context.Context, csr *x509.CertificateReques // sign_with_root // } func (iss *InternalIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - for d.NextBlock(0) { - switch d.Val() { - case "ca": - if !d.AllArgs(&iss.CA) { - return d.ArgErr() - } - - case "lifetime": - if !d.NextArg() { - return d.ArgErr() - } - dur, err := caddy.ParseDuration(d.Val()) - if err != nil { - return err - } - iss.Lifetime = caddy.Duration(dur) - - case "sign_with_root": - if d.NextArg() { - return d.ArgErr() - } - iss.SignWithRoot = true - + d.Next() // consume issuer name + for d.NextBlock(0) { + switch d.Val() { + case "ca": + if !d.AllArgs(&iss.CA) { + return d.ArgErr() } + + case "lifetime": + if !d.NextArg() { + return d.ArgErr() + } + dur, err := caddy.ParseDuration(d.Val()) + if err != nil { + return err + } + iss.Lifetime = caddy.Duration(dur) + + case "sign_with_root": + if d.NextArg() { + return d.ArgErr() + } + iss.SignWithRoot = true } } return nil diff --git a/modules/caddytls/leaffileloader.go b/modules/caddytls/leaffileloader.go new file mode 100644 index 000000000..1d3f3a3e5 --- /dev/null +++ b/modules/caddytls/leaffileloader.go @@ -0,0 +1,99 @@ +// 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 caddytls + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "os" + + "github.com/caddyserver/caddy/v2" +) + +func init() { + caddy.RegisterModule(LeafFileLoader{}) +} + +// LeafFileLoader loads leaf certificates from disk. +type LeafFileLoader struct { + Files []string `json:"files,omitempty"` +} + +// Provision implements caddy.Provisioner. +func (fl *LeafFileLoader) Provision(ctx caddy.Context) error { + repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if !ok { + repl = caddy.NewReplacer() + } + for k, path := range fl.Files { + fl.Files[k] = repl.ReplaceKnown(path, "") + } + return nil +} + +// CaddyModule returns the Caddy module information. +func (LeafFileLoader) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.leaf_cert_loader.file", + New: func() caddy.Module { return new(LeafFileLoader) }, + } +} + +// LoadLeafCertificates returns the certificates to be loaded by fl. +func (fl LeafFileLoader) LoadLeafCertificates() ([]*x509.Certificate, error) { + certificates := make([]*x509.Certificate, 0, len(fl.Files)) + for _, path := range fl.Files { + ders, err := convertPEMFilesToDERBytes(path) + if err != nil { + return nil, err + } + certs, err := x509.ParseCertificates(ders) + if err != nil { + return nil, err + } + certificates = append(certificates, certs...) + } + return certificates, nil +} + +func convertPEMFilesToDERBytes(filename string) ([]byte, error) { + certDataPEM, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + var ders []byte + // while block is not nil, we have more certificates in the file + for block, rest := pem.Decode(certDataPEM); block != nil; block, rest = pem.Decode(rest) { + if block.Type != "CERTIFICATE" { + return nil, fmt.Errorf("no CERTIFICATE pem block found in %s", filename) + } + ders = append( + ders, + block.Bytes..., + ) + } + // if we decoded nothing, return an error + if len(ders) == 0 { + return nil, fmt.Errorf("no CERTIFICATE pem block found in %s", filename) + } + return ders, nil +} + +// Interface guard +var ( + _ LeafCertificateLoader = (*LeafFileLoader)(nil) + _ caddy.Provisioner = (*LeafFileLoader)(nil) +) diff --git a/modules/caddytls/leaffileloader_test.go b/modules/caddytls/leaffileloader_test.go new file mode 100644 index 000000000..940ed78bd --- /dev/null +++ b/modules/caddytls/leaffileloader_test.go @@ -0,0 +1,38 @@ +package caddytls + +import ( + "context" + "encoding/pem" + "os" + "strings" + "testing" + + "github.com/caddyserver/caddy/v2" +) + +func TestLeafFileLoader(t *testing.T) { + fl := LeafFileLoader{Files: []string{"../../caddytest/leafcert.pem"}} + fl.Provision(caddy.Context{Context: context.Background()}) + + out, err := fl.LoadLeafCertificates() + if err != nil { + t.Errorf("Leaf certs file loading test failed: %v", err) + } + if len(out) != 1 { + t.Errorf("Error loading leaf cert in memory struct") + return + } + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: out[0].Raw}) + + pemFileBytes, err := os.ReadFile("../../caddytest/leafcert.pem") + if err != nil { + t.Errorf("Unable to read the example certificate from the file") + } + + // Remove /r because windows. + pemFileString := strings.ReplaceAll(string(pemFileBytes), "\r\n", "\n") + + if string(pemBytes) != pemFileString { + t.Errorf("Leaf Certificate File Loader: Failed to load the correct certificate") + } +} diff --git a/modules/caddytls/leaffolderloader.go b/modules/caddytls/leaffolderloader.go new file mode 100644 index 000000000..5c7b06e76 --- /dev/null +++ b/modules/caddytls/leaffolderloader.go @@ -0,0 +1,97 @@ +// 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 caddytls + +import ( + "crypto/x509" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/caddyserver/caddy/v2" +) + +func init() { + caddy.RegisterModule(LeafFolderLoader{}) +} + +// LeafFolderLoader loads certificates and their associated keys from disk +// by recursively walking the specified directories, looking for PEM +// files which contain both a certificate and a key. +type LeafFolderLoader struct { + Folders []string `json:"folders,omitempty"` +} + +// CaddyModule returns the Caddy module information. +func (LeafFolderLoader) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.leaf_cert_loader.folder", + New: func() caddy.Module { return new(LeafFolderLoader) }, + } +} + +// Provision implements caddy.Provisioner. +func (fl *LeafFolderLoader) Provision(ctx caddy.Context) error { + repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if !ok { + repl = caddy.NewReplacer() + } + for k, path := range fl.Folders { + fl.Folders[k] = repl.ReplaceKnown(path, "") + } + return nil +} + +// LoadLeafCertificates loads all the leaf certificates in the directories +// listed in fl from all files ending with .pem. +func (fl LeafFolderLoader) LoadLeafCertificates() ([]*x509.Certificate, error) { + var certs []*x509.Certificate + for _, dir := range fl.Folders { + err := filepath.Walk(dir, func(fpath string, info os.FileInfo, err error) error { + if err != nil { + return fmt.Errorf("unable to traverse into path: %s", fpath) + } + if info.IsDir() { + return nil + } + if !strings.HasSuffix(strings.ToLower(info.Name()), ".pem") { + return nil + } + + certData, err := convertPEMFilesToDERBytes(fpath) + if err != nil { + return err + } + cert, err := x509.ParseCertificate(certData) + if err != nil { + return fmt.Errorf("%s: %w", fpath, err) + } + + certs = append(certs, cert) + + return nil + }) + if err != nil { + return nil, err + } + } + return certs, nil +} + +var ( + _ LeafCertificateLoader = (*LeafFolderLoader)(nil) + _ caddy.Provisioner = (*LeafFolderLoader)(nil) +) diff --git a/modules/caddytls/leaffolderloader_test.go b/modules/caddytls/leaffolderloader_test.go new file mode 100644 index 000000000..35fecba89 --- /dev/null +++ b/modules/caddytls/leaffolderloader_test.go @@ -0,0 +1,37 @@ +package caddytls + +import ( + "context" + "encoding/pem" + "os" + "strings" + "testing" + + "github.com/caddyserver/caddy/v2" +) + +func TestLeafFolderLoader(t *testing.T) { + fl := LeafFolderLoader{Folders: []string{"../../caddytest"}} + fl.Provision(caddy.Context{Context: context.Background()}) + + out, err := fl.LoadLeafCertificates() + if err != nil { + t.Errorf("Leaf certs folder loading test failed: %v", err) + } + if len(out) != 1 { + t.Errorf("Error loading leaf cert in memory struct") + return + } + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: out[0].Raw}) + pemFileBytes, err := os.ReadFile("../../caddytest/leafcert.pem") + if err != nil { + t.Errorf("Unable to read the example certificate from the file") + } + + // Remove /r because windows. + pemFileString := strings.ReplaceAll(string(pemFileBytes), "\r\n", "\n") + + if string(pemBytes) != pemFileString { + t.Errorf("Leaf Certificate Folder Loader: Failed to load the correct certificate") + } +} diff --git a/modules/caddytls/leafpemloader.go b/modules/caddytls/leafpemloader.go new file mode 100644 index 000000000..28467ccf2 --- /dev/null +++ b/modules/caddytls/leafpemloader.go @@ -0,0 +1,76 @@ +// 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 caddytls + +import ( + "crypto/x509" + "fmt" + + "github.com/caddyserver/caddy/v2" +) + +func init() { + caddy.RegisterModule(LeafPEMLoader{}) +} + +// LeafPEMLoader loads leaf certificates by +// decoding their PEM blocks directly. This has the advantage +// of not needing to store them on disk at all. +type LeafPEMLoader struct { + Certificates []string `json:"certificates,omitempty"` +} + +// Provision implements caddy.Provisioner. +func (pl *LeafPEMLoader) Provision(ctx caddy.Context) error { + repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if !ok { + repl = caddy.NewReplacer() + } + for i, cert := range pl.Certificates { + pl.Certificates[i] = repl.ReplaceKnown(cert, "") + } + return nil +} + +// CaddyModule returns the Caddy module information. +func (LeafPEMLoader) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.leaf_cert_loader.pem", + New: func() caddy.Module { return new(LeafPEMLoader) }, + } +} + +// LoadLeafCertificates returns the certificates contained in pl. +func (pl LeafPEMLoader) LoadLeafCertificates() ([]*x509.Certificate, error) { + certs := make([]*x509.Certificate, 0, len(pl.Certificates)) + for i, cert := range pl.Certificates { + derBytes, err := convertPEMToDER([]byte(cert)) + if err != nil { + return nil, fmt.Errorf("PEM leaf certificate loader, cert %d: %v", i, err) + } + cert, err := x509.ParseCertificate(derBytes) + if err != nil { + return nil, fmt.Errorf("PEM cert %d: %v", i, err) + } + certs = append(certs, cert) + } + return certs, nil +} + +// Interface guard +var ( + _ LeafCertificateLoader = (*LeafPEMLoader)(nil) + _ caddy.Provisioner = (*LeafPEMLoader)(nil) +) diff --git a/modules/caddytls/leafpemloader_test.go b/modules/caddytls/leafpemloader_test.go new file mode 100644 index 000000000..04a9efd25 --- /dev/null +++ b/modules/caddytls/leafpemloader_test.go @@ -0,0 +1,54 @@ +package caddytls + +import ( + "context" + "encoding/pem" + "os" + "strings" + "testing" + + "github.com/caddyserver/caddy/v2" +) + +func TestLeafPEMLoader(t *testing.T) { + pl := LeafPEMLoader{Certificates: []string{` +-----BEGIN CERTIFICATE----- +MIICUTCCAfugAwIBAgIBADANBgkqhkiG9w0BAQQFADBXMQswCQYDVQQGEwJDTjEL +MAkGA1UECBMCUE4xCzAJBgNVBAcTAkNOMQswCQYDVQQKEwJPTjELMAkGA1UECxMC +VU4xFDASBgNVBAMTC0hlcm9uZyBZYW5nMB4XDTA1MDcxNTIxMTk0N1oXDTA1MDgx +NDIxMTk0N1owVzELMAkGA1UEBhMCQ04xCzAJBgNVBAgTAlBOMQswCQYDVQQHEwJD +TjELMAkGA1UEChMCT04xCzAJBgNVBAsTAlVOMRQwEgYDVQQDEwtIZXJvbmcgWWFu +ZzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQCp5hnG7ogBhtlynpOS21cBewKE/B7j +V14qeyslnr26xZUsSVko36ZnhiaO/zbMOoRcKK9vEcgMtcLFuQTWDl3RAgMBAAGj +gbEwga4wHQYDVR0OBBYEFFXI70krXeQDxZgbaCQoR4jUDncEMH8GA1UdIwR4MHaA +FFXI70krXeQDxZgbaCQoR4jUDncEoVukWTBXMQswCQYDVQQGEwJDTjELMAkGA1UE +CBMCUE4xCzAJBgNVBAcTAkNOMQswCQYDVQQKEwJPTjELMAkGA1UECxMCVU4xFDAS +BgNVBAMTC0hlcm9uZyBZYW5nggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEE +BQADQQA/ugzBrjjK9jcWnDVfGHlk3icNRq0oV7Ri32z/+HQX67aRfgZu7KWdI+Ju +Wm7DCfrPNGVwFWUQOmsPue9rZBgO +-----END CERTIFICATE----- +`}} + pl.Provision(caddy.Context{Context: context.Background()}) + + out, err := pl.LoadLeafCertificates() + if err != nil { + t.Errorf("Leaf certs pem loading test failed: %v", err) + } + if len(out) != 1 { + t.Errorf("Error loading leaf cert in memory struct") + return + } + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: out[0].Raw}) + + pemFileBytes, err := os.ReadFile("../../caddytest/leafcert.pem") + if err != nil { + t.Errorf("Unable to read the example certificate from the file") + } + + // Remove /r because windows. + pemFileString := strings.ReplaceAll(string(pemFileBytes), "\r\n", "\n") + + if string(pemBytes) != pemFileString { + t.Errorf("Leaf Certificate Folder Loader: Failed to load the correct certificate") + } +} diff --git a/modules/caddytls/leafstorageloader.go b/modules/caddytls/leafstorageloader.go new file mode 100644 index 000000000..0215c8af2 --- /dev/null +++ b/modules/caddytls/leafstorageloader.go @@ -0,0 +1,129 @@ +// 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 caddytls + +import ( + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + + "github.com/caddyserver/certmagic" + + "github.com/caddyserver/caddy/v2" +) + +func init() { + caddy.RegisterModule(LeafStorageLoader{}) +} + +// LeafStorageLoader loads leaf certificates from the +// globally configured storage module. +type LeafStorageLoader struct { + // A list of certificate file names to be loaded from storage. + Certificates []string `json:"certificates,omitempty"` + + // The storage module where the trusted leaf certificates are stored. Absent + // explicit storage implies the use of Caddy default storage. + StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"` + + // Reference to the globally configured storage module. + storage certmagic.Storage + + ctx caddy.Context +} + +// CaddyModule returns the Caddy module information. +func (LeafStorageLoader) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.leaf_cert_loader.storage", + New: func() caddy.Module { return new(LeafStorageLoader) }, + } +} + +// Provision loads the storage module for sl. +func (sl *LeafStorageLoader) Provision(ctx caddy.Context) error { + if sl.StorageRaw != nil { + val, err := ctx.LoadModule(sl, "StorageRaw") + if err != nil { + return fmt.Errorf("loading storage module: %v", err) + } + cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage() + if err != nil { + return fmt.Errorf("creating storage configuration: %v", err) + } + sl.storage = cmStorage + } + if sl.storage == nil { + sl.storage = ctx.Storage() + } + sl.ctx = ctx + + repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if !ok { + repl = caddy.NewReplacer() + } + for k, path := range sl.Certificates { + sl.Certificates[k] = repl.ReplaceKnown(path, "") + } + return nil +} + +// LoadLeafCertificates returns the certificates to be loaded by sl. +func (sl LeafStorageLoader) LoadLeafCertificates() ([]*x509.Certificate, error) { + certificates := make([]*x509.Certificate, 0, len(sl.Certificates)) + for _, path := range sl.Certificates { + certData, err := sl.storage.Load(sl.ctx, path) + if err != nil { + return nil, err + } + + ders, err := convertPEMToDER(certData) + if err != nil { + return nil, err + } + certs, err := x509.ParseCertificates(ders) + if err != nil { + return nil, err + } + certificates = append(certificates, certs...) + } + return certificates, nil +} + +func convertPEMToDER(pemData []byte) ([]byte, error) { + var ders []byte + // while block is not nil, we have more certificates in the file + for block, rest := pem.Decode(pemData); block != nil; block, rest = pem.Decode(rest) { + if block.Type != "CERTIFICATE" { + return nil, fmt.Errorf("no CERTIFICATE pem block found in the given pem data") + } + ders = append( + ders, + block.Bytes..., + ) + } + // if we decoded nothing, return an error + if len(ders) == 0 { + return nil, fmt.Errorf("no CERTIFICATE pem block found in the given pem data") + } + return ders, nil +} + +// Interface guard +var ( + _ LeafCertificateLoader = (*LeafStorageLoader)(nil) + _ caddy.Provisioner = (*LeafStorageLoader)(nil) +) diff --git a/modules/caddytls/ondemand.go b/modules/caddytls/ondemand.go new file mode 100644 index 000000000..31f6ef2dc --- /dev/null +++ b/modules/caddytls/ondemand.go @@ -0,0 +1,192 @@ +// 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 caddytls + +import ( + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/caddyserver/certmagic" + "go.uber.org/zap" + + "github.com/caddyserver/caddy/v2" +) + +func init() { + caddy.RegisterModule(PermissionByHTTP{}) +} + +// OnDemandConfig configures on-demand TLS, for obtaining +// needed certificates at handshake-time. Because this +// feature can easily be abused, you should use this to +// establish rate limits and/or an internal endpoint that +// Caddy can "ask" if it should be allowed to manage +// certificates for a given hostname. +type OnDemandConfig struct { + // DEPRECATED. WILL BE REMOVED SOON. Use 'permission' instead. + Ask string `json:"ask,omitempty"` + + // REQUIRED. A module that will determine whether a + // certificate is allowed to be loaded from storage + // or obtained from an issuer on demand. + PermissionRaw json.RawMessage `json:"permission,omitempty" caddy:"namespace=tls.permission inline_key=module"` + permission OnDemandPermission + + // DEPRECATED. An optional rate limit to throttle + // the checking of storage and the issuance of + // certificates from handshakes if not already in + // storage. WILL BE REMOVED IN A FUTURE RELEASE. + RateLimit *RateLimit `json:"rate_limit,omitempty"` +} + +// DEPRECATED. WILL LIKELY BE REMOVED SOON. +// Instead of using this rate limiter, use a proper tool such as a +// level 3 or 4 firewall and/or a permission module to apply rate limits. +type RateLimit struct { + // A duration value. Storage may be checked and a certificate may be + // obtained 'burst' times during this interval. + Interval caddy.Duration `json:"interval,omitempty"` + + // How many times during an interval storage can be checked or a + // certificate can be obtained. + Burst int `json:"burst,omitempty"` +} + +// OnDemandPermission is a type that can give permission for +// whether a certificate should be allowed to be obtained or +// loaded from storage on-demand. +// EXPERIMENTAL: This API is experimental and subject to change. +type OnDemandPermission interface { + // CertificateAllowed returns nil if a certificate for the given + // name is allowed to be either obtained from an issuer or loaded + // from storage on-demand. + // + // The context passed in has the associated *tls.ClientHelloInfo + // value available at the certmagic.ClientHelloInfoCtxKey key. + // + // In the worst case, this function may be called as frequently + // as every TLS handshake, so it should return as quick as possible + // to reduce latency. In the normal case, this function is only + // called when a certificate is needed that is not already loaded + // into memory ready to serve. + CertificateAllowed(ctx context.Context, name string) error +} + +// PermissionByHTTP determines permission for a TLS certificate by +// making a request to an HTTP endpoint. +type PermissionByHTTP struct { + // The endpoint to access. It should be a full URL. + // A query string parameter "domain" will be added to it, + // containing the domain (or IP) for the desired certificate, + // like so: `?domain=example.com`. Generally, this endpoint + // is not exposed publicly to avoid a minor information leak + // (which domains are serviced by your application). + // + // The endpoint must return a 200 OK status if a certificate + // is allowed; anything else will cause it to be denied. + // Redirects are not followed. + Endpoint string `json:"endpoint"` + + logger *zap.Logger + replacer *caddy.Replacer +} + +// CaddyModule returns the Caddy module information. +func (PermissionByHTTP) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.permission.http", + New: func() caddy.Module { return new(PermissionByHTTP) }, + } +} + +func (p *PermissionByHTTP) Provision(ctx caddy.Context) error { + p.logger = ctx.Logger() + p.replacer = caddy.NewReplacer() + return nil +} + +func (p PermissionByHTTP) CertificateAllowed(ctx context.Context, name string) error { + // run replacer on endpoint URL (for environment variables) -- return errors to prevent surprises (#5036) + askEndpoint, err := p.replacer.ReplaceOrErr(p.Endpoint, true, true) + if err != nil { + return fmt.Errorf("preparing 'ask' endpoint: %v", err) + } + + askURL, err := url.Parse(askEndpoint) + if err != nil { + return fmt.Errorf("parsing ask URL: %v", err) + } + qs := askURL.Query() + qs.Set("domain", name) + askURL.RawQuery = qs.Encode() + askURLString := askURL.String() + + var remote string + if chi, ok := ctx.Value(certmagic.ClientHelloInfoCtxKey).(*tls.ClientHelloInfo); ok && chi != nil { + remote = chi.Conn.RemoteAddr().String() + } + + p.logger.Debug("asking permission endpoint", + zap.String("remote", remote), + zap.String("domain", name), + zap.String("url", askURLString)) + + resp, err := onDemandAskClient.Get(askURLString) + if err != nil { + return fmt.Errorf("checking %v to determine if certificate for hostname '%s' should be allowed: %v", + askEndpoint, name, err) + } + resp.Body.Close() + + p.logger.Debug("response from permission endpoint", + zap.String("remote", remote), + zap.String("domain", name), + zap.String("url", askURLString), + zap.Int("status", resp.StatusCode)) + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return fmt.Errorf("%s: %w %s - non-2xx status code %d", name, ErrPermissionDenied, askEndpoint, resp.StatusCode) + } + + return nil +} + +// ErrPermissionDenied is an error that should be wrapped or returned when the +// configured permission module does not allow a certificate to be issued, +// to distinguish that from other errors such as connection failure. +var ErrPermissionDenied = errors.New("certificate not allowed by permission module") + +// These perpetual values are used for on-demand TLS. +var ( + onDemandRateLimiter = certmagic.NewRateLimiter(0, 0) + onDemandAskClient = &http.Client{ + Timeout: 10 * time.Second, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return fmt.Errorf("following http redirects is not allowed") + }, + } +) + +// Interface guards +var ( + _ OnDemandPermission = (*PermissionByHTTP)(nil) + _ caddy.Provisioner = (*PermissionByHTTP)(nil) +) diff --git a/modules/caddytls/pemloader.go b/modules/caddytls/pemloader.go index 61b08851c..9c5ec17c9 100644 --- a/modules/caddytls/pemloader.go +++ b/modules/caddytls/pemloader.go @@ -30,6 +30,25 @@ func init() { // of not needing to store them on disk at all. type PEMLoader []CertKeyPEMPair +// Provision implements caddy.Provisioner. +func (pl PEMLoader) Provision(ctx caddy.Context) error { + repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if !ok { + repl = caddy.NewReplacer() + } + for k, pair := range pl { + for i, tag := range pair.Tags { + pair.Tags[i] = repl.ReplaceKnown(tag, "") + } + pl[k] = CertKeyPEMPair{ + CertificatePEM: repl.ReplaceKnown(pair.CertificatePEM, ""), + KeyPEM: repl.ReplaceKnown(pair.KeyPEM, ""), + Tags: pair.Tags, + } + } + return nil +} + // CaddyModule returns the Caddy module information. func (PEMLoader) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ @@ -69,4 +88,7 @@ func (pl PEMLoader) LoadCertificates() ([]Certificate, error) { } // Interface guard -var _ CertificateLoader = (PEMLoader)(nil) +var ( + _ CertificateLoader = (PEMLoader)(nil) + _ caddy.Provisioner = (PEMLoader)(nil) +) diff --git a/modules/caddytls/storageloader.go b/modules/caddytls/storageloader.go index ddaaa5156..f9f0e7e68 100644 --- a/modules/caddytls/storageloader.go +++ b/modules/caddytls/storageloader.go @@ -52,6 +52,22 @@ func (StorageLoader) CaddyModule() caddy.ModuleInfo { func (sl *StorageLoader) Provision(ctx caddy.Context) error { sl.storage = ctx.Storage() sl.ctx = ctx + + repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if !ok { + repl = caddy.NewReplacer() + } + for k, pair := range sl.Pairs { + for i, tag := range pair.Tags { + pair.Tags[i] = repl.ReplaceKnown(tag, "") + } + sl.Pairs[k] = CertKeyFilePair{ + Certificate: repl.ReplaceKnown(pair.Certificate, ""), + Key: repl.ReplaceKnown(pair.Key, ""), + Format: repl.ReplaceKnown(pair.Format, ""), + Tags: pair.Tags, + } + } return nil } diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 02d5aae75..2ec7bd8fb 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -164,6 +164,36 @@ func (t *TLS) Provision(ctx caddy.Context) error { t.certificateLoaders = append(t.certificateLoaders, modIface.(CertificateLoader)) } + // on-demand permission module + if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.PermissionRaw != nil { + if t.Automation.OnDemand.Ask != "" { + return fmt.Errorf("on-demand TLS config conflict: both 'ask' endpoint and a 'permission' module are specified; 'ask' is deprecated, so use only the permission module") + } + val, err := ctx.LoadModule(t.Automation.OnDemand, "PermissionRaw") + if err != nil { + return fmt.Errorf("loading on-demand TLS permission module: %v", err) + } + t.Automation.OnDemand.permission = val.(OnDemandPermission) + } + + // on-demand rate limiting + if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.RateLimit != nil { + onDemandRateLimiter.SetMaxEvents(t.Automation.OnDemand.RateLimit.Burst) + onDemandRateLimiter.SetWindow(time.Duration(t.Automation.OnDemand.RateLimit.Interval)) + } else { + // remove any existing rate limiter + onDemandRateLimiter.SetWindow(0) + onDemandRateLimiter.SetMaxEvents(0) + } + + // run replacer on ask URL (for environment variables) -- return errors to prevent surprises (#5036) + if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.Ask != "" { + t.Automation.OnDemand.Ask, err = repl.ReplaceOrErr(t.Automation.OnDemand.Ask, true, true) + if err != nil { + return fmt.Errorf("preparing 'ask' endpoint: %v", err) + } + } + // automation/management policies if t.Automation == nil { t.Automation = new(AutomationConfig) @@ -204,24 +234,6 @@ func (t *TLS) Provision(ctx caddy.Context) error { } } - // on-demand rate limiting - if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.RateLimit != nil { - onDemandRateLimiter.SetMaxEvents(t.Automation.OnDemand.RateLimit.Burst) - onDemandRateLimiter.SetWindow(time.Duration(t.Automation.OnDemand.RateLimit.Interval)) - } else { - // remove any existing rate limiter - onDemandRateLimiter.SetWindow(0) - onDemandRateLimiter.SetMaxEvents(0) - } - - // run replacer on ask URL (for environment variables) -- return errors to prevent surprises (#5036) - if t.Automation != nil && t.Automation.OnDemand != nil && t.Automation.OnDemand.Ask != "" { - t.Automation.OnDemand.Ask, err = repl.ReplaceOrErr(t.Automation.OnDemand.Ask, true, true) - if err != nil { - return fmt.Errorf("preparing 'ask' endpoint: %v", err) - } - } - // load manual/static (unmanaged) certificates - we do this in // provision so that other apps (such as http) can know which // certificates have been manually loaded, and also so that @@ -288,8 +300,7 @@ func (t *TLS) Validate() error { // Start activates the TLS module. func (t *TLS) Start() error { // warn if on-demand TLS is enabled but no restrictions are in place - if t.Automation.OnDemand == nil || - (t.Automation.OnDemand.Ask == "" && t.Automation.OnDemand.RateLimit == nil) { + if t.Automation.OnDemand == nil || (t.Automation.OnDemand.Ask == "" && t.Automation.OnDemand.permission == nil) { for _, ap := range t.Automation.Policies { if ap.OnDemand && ap.isWildcardOrDefault() { t.logger.Warn("YOUR SERVER MAY BE VULNERABLE TO ABUSE: on-demand TLS is enabled, but no protections are in place", @@ -551,6 +562,10 @@ func (t *TLS) cleanStorageUnits() { storageCleanMu.Lock() defer storageCleanMu.Unlock() + // TODO: This check might not be needed anymore now that CertMagic syncs + // and throttles storage cleaning globally across the cluster. + // The original comment below might be outdated: + // // If storage was cleaned recently, don't do it again for now. Although the ticker // calling this function drops missed ticks for us, config reloads discard the old // ticker and replace it with a new one, possibly invoking a cleaning to happen again @@ -563,21 +578,26 @@ func (t *TLS) cleanStorageUnits() { return } + id, err := caddy.InstanceID() + if err != nil { + t.logger.Warn("unable to get instance ID; storage clean stamps will be incomplete", zap.Error(err)) + } options := certmagic.CleanStorageOptions{ + Logger: t.logger, + InstanceID: id.String(), + Interval: t.storageCleanInterval(), OCSPStaples: true, ExpiredCerts: true, ExpiredCertGracePeriod: 24 * time.Hour * 14, } - // avoid cleaning same storage more than once per cleaning cycle - storagesCleaned := make(map[string]struct{}) - // start with the default/global storage - storage := t.ctx.Storage() - storageStr := fmt.Sprintf("%v", storage) - t.logger.Info("cleaning storage unit", zap.String("description", storageStr)) - certmagic.CleanStorage(t.ctx, storage, options) - storagesCleaned[storageStr] = struct{}{} + err = certmagic.CleanStorage(t.ctx, t.ctx.Storage(), options) + if err != nil { + // probably don't want to return early, since we should still + // see if any other storages can get cleaned up + t.logger.Error("could not clean default/global storage", zap.Error(err)) + } // then clean each storage defined in ACME automation policies if t.Automation != nil { @@ -585,13 +605,9 @@ func (t *TLS) cleanStorageUnits() { if ap.storage == nil { continue } - storageStr := fmt.Sprintf("%v", ap.storage) - if _, ok := storagesCleaned[storageStr]; ok { - continue + if err := certmagic.CleanStorage(t.ctx, ap.storage, options); err != nil { + t.logger.Error("could not clean storage configured in automation policy", zap.Error(err)) } - t.logger.Info("cleaning storage unit", zap.String("description", storageStr)) - certmagic.CleanStorage(t.ctx, ap.storage, options) - storagesCleaned[storageStr] = struct{}{} } } diff --git a/modules/caddytls/zerosslissuer.go b/modules/caddytls/zerosslissuer.go index 697bab07d..1c091a076 100644 --- a/modules/caddytls/zerosslissuer.go +++ b/modules/caddytls/zerosslissuer.go @@ -208,21 +208,20 @@ func (iss *ZeroSSLIssuer) Revoke(ctx context.Context, cert certmagic.Certificate // // Any of the subdirectives for the ACME issuer can be used in the block. func (iss *ZeroSSLIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { + d.Next() // consume issuer name + if d.NextArg() { + iss.APIKey = d.Val() if d.NextArg() { - iss.APIKey = d.Val() - if d.NextArg() { - return d.ArgErr() - } + return d.ArgErr() } + } - if iss.ACMEIssuer == nil { - iss.ACMEIssuer = new(ACMEIssuer) - } - err := iss.ACMEIssuer.UnmarshalCaddyfile(d.NewFromNextSegment()) - if err != nil { - return err - } + if iss.ACMEIssuer == nil { + iss.ACMEIssuer = new(ACMEIssuer) + } + err := iss.ACMEIssuer.UnmarshalCaddyfile(d.NewFromNextSegment()) + if err != nil { + return err } return nil } diff --git a/modules/filestorage/filestorage.go b/modules/filestorage/filestorage.go index 672c66cbe..76aff4e8b 100644 --- a/modules/filestorage/filestorage.go +++ b/modules/filestorage/filestorage.go @@ -55,7 +55,7 @@ func (s *FileStorage) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if d.NextArg() { return d.ArgErr() } - for nesting := d.Nesting(); d.NextBlock(nesting); { + for d.NextBlock(0) { switch d.Val() { case "root": if !d.NextArg() { diff --git a/modules/logging/appendencoder.go b/modules/logging/appendencoder.go new file mode 100644 index 000000000..63bd532d0 --- /dev/null +++ b/modules/logging/appendencoder.go @@ -0,0 +1,357 @@ +// 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 logging + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "time" + + "go.uber.org/zap" + "go.uber.org/zap/buffer" + "go.uber.org/zap/zapcore" + "golang.org/x/term" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" +) + +func init() { + caddy.RegisterModule(AppendEncoder{}) +} + +// AppendEncoder can be used to add fields to all log entries +// that pass through it. It is a wrapper around another +// encoder, which it uses to actually encode the log entries. +// It is most useful for adding information about the Caddy +// instance that is producing the log entries, possibly via +// an environment variable. +type AppendEncoder struct { + // The underlying encoder that actually encodes the + // log entries. If not specified, defaults to "json", + // unless the output is a terminal, in which case + // it defaults to "console". + WrappedRaw json.RawMessage `json:"wrap,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"` + + // A map of field names to their values. The values + // can be global placeholders (e.g. env vars), or constants. + // Note that the encoder does not run as part of an HTTP + // request context, so request placeholders are not available. + Fields map[string]any `json:"fields,omitempty"` + + wrapped zapcore.Encoder + repl *caddy.Replacer + + wrappedIsDefault bool + ctx caddy.Context +} + +// CaddyModule returns the Caddy module information. +func (AppendEncoder) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "caddy.logging.encoders.append", + New: func() caddy.Module { return new(AppendEncoder) }, + } +} + +// Provision sets up the encoder. +func (fe *AppendEncoder) Provision(ctx caddy.Context) error { + fe.ctx = ctx + fe.repl = caddy.NewReplacer() + + if fe.WrappedRaw == nil { + // if wrap is not specified, default to JSON + fe.wrapped = &JSONEncoder{} + if p, ok := fe.wrapped.(caddy.Provisioner); ok { + if err := p.Provision(ctx); err != nil { + return fmt.Errorf("provisioning fallback encoder module: %v", err) + } + } + fe.wrappedIsDefault = true + } else { + // set up wrapped encoder + val, err := ctx.LoadModule(fe, "WrappedRaw") + if err != nil { + return fmt.Errorf("loading fallback encoder module: %v", err) + } + fe.wrapped = val.(zapcore.Encoder) + } + + return nil +} + +// ConfigureDefaultFormat will set the default format to "console" +// if the writer is a terminal. If already configured, it passes +// through the writer so a deeply nested encoder can configure +// its own default format. +func (fe *AppendEncoder) ConfigureDefaultFormat(wo caddy.WriterOpener) error { + if !fe.wrappedIsDefault { + if cfd, ok := fe.wrapped.(caddy.ConfiguresFormatterDefault); ok { + return cfd.ConfigureDefaultFormat(wo) + } + return nil + } + + if caddy.IsWriterStandardStream(wo) && term.IsTerminal(int(os.Stderr.Fd())) { + fe.wrapped = &ConsoleEncoder{} + if p, ok := fe.wrapped.(caddy.Provisioner); ok { + if err := p.Provision(fe.ctx); err != nil { + return fmt.Errorf("provisioning fallback encoder module: %v", err) + } + } + } + return nil +} + +// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: +// +// append { +// wrap +// fields { +// +// } +// +// } +func (fe *AppendEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + d.Next() // consume encoder name + + // parse a field + parseField := func() error { + if fe.Fields == nil { + fe.Fields = make(map[string]any) + } + field := d.Val() + if !d.NextArg() { + return d.ArgErr() + } + fe.Fields[field] = d.ScalarVal() + if d.NextArg() { + return d.ArgErr() + } + return nil + } + + for d.NextBlock(0) { + switch d.Val() { + case "wrap": + if !d.NextArg() { + return d.ArgErr() + } + moduleName := d.Val() + moduleID := "caddy.logging.encoders." + moduleName + unm, err := caddyfile.UnmarshalModule(d, moduleID) + if err != nil { + return err + } + enc, ok := unm.(zapcore.Encoder) + if !ok { + return d.Errf("module %s (%T) is not a zapcore.Encoder", moduleID, unm) + } + fe.WrappedRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, nil) + + case "fields": + for nesting := d.Nesting(); d.NextBlock(nesting); { + err := parseField() + if err != nil { + return err + } + } + + default: + // if unknown, assume it's a field so that + // the config can be flat + err := parseField() + if err != nil { + return err + } + } + } + return nil +} + +// AddArray is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddArray(key string, marshaler zapcore.ArrayMarshaler) error { + return fe.wrapped.AddArray(key, marshaler) +} + +// AddObject is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddObject(key string, marshaler zapcore.ObjectMarshaler) error { + return fe.wrapped.AddObject(key, marshaler) +} + +// AddBinary is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddBinary(key string, value []byte) { + fe.wrapped.AddBinary(key, value) +} + +// AddByteString is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddByteString(key string, value []byte) { + fe.wrapped.AddByteString(key, value) +} + +// AddBool is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddBool(key string, value bool) { + fe.wrapped.AddBool(key, value) +} + +// AddComplex128 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddComplex128(key string, value complex128) { + fe.wrapped.AddComplex128(key, value) +} + +// AddComplex64 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddComplex64(key string, value complex64) { + fe.wrapped.AddComplex64(key, value) +} + +// AddDuration is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddDuration(key string, value time.Duration) { + fe.wrapped.AddDuration(key, value) +} + +// AddFloat64 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddFloat64(key string, value float64) { + fe.wrapped.AddFloat64(key, value) +} + +// AddFloat32 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddFloat32(key string, value float32) { + fe.wrapped.AddFloat32(key, value) +} + +// AddInt is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddInt(key string, value int) { + fe.wrapped.AddInt(key, value) +} + +// AddInt64 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddInt64(key string, value int64) { + fe.wrapped.AddInt64(key, value) +} + +// AddInt32 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddInt32(key string, value int32) { + fe.wrapped.AddInt32(key, value) +} + +// AddInt16 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddInt16(key string, value int16) { + fe.wrapped.AddInt16(key, value) +} + +// AddInt8 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddInt8(key string, value int8) { + fe.wrapped.AddInt8(key, value) +} + +// AddString is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddString(key, value string) { + fe.wrapped.AddString(key, value) +} + +// AddTime is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddTime(key string, value time.Time) { + fe.wrapped.AddTime(key, value) +} + +// AddUint is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddUint(key string, value uint) { + fe.wrapped.AddUint(key, value) +} + +// AddUint64 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddUint64(key string, value uint64) { + fe.wrapped.AddUint64(key, value) +} + +// AddUint32 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddUint32(key string, value uint32) { + fe.wrapped.AddUint32(key, value) +} + +// AddUint16 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddUint16(key string, value uint16) { + fe.wrapped.AddUint16(key, value) +} + +// AddUint8 is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddUint8(key string, value uint8) { + fe.wrapped.AddUint8(key, value) +} + +// AddUintptr is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddUintptr(key string, value uintptr) { + fe.wrapped.AddUintptr(key, value) +} + +// AddReflected is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) AddReflected(key string, value any) error { + return fe.wrapped.AddReflected(key, value) +} + +// OpenNamespace is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) OpenNamespace(key string) { + fe.wrapped.OpenNamespace(key) +} + +// Clone is part of the zapcore.ObjectEncoder interface. +func (fe AppendEncoder) Clone() zapcore.Encoder { + return AppendEncoder{ + Fields: fe.Fields, + wrapped: fe.wrapped.Clone(), + repl: fe.repl, + } +} + +// EncodeEntry partially implements the zapcore.Encoder interface. +func (fe AppendEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) { + fe.wrapped = fe.wrapped.Clone() + for _, field := range fields { + field.AddTo(fe) + } + + // append fields from config + for key, value := range fe.Fields { + // if the value is a string + if str, ok := value.(string); ok { + isPlaceholder := strings.HasPrefix(str, "{") && + strings.HasSuffix(str, "}") && + strings.Count(str, "{") == 1 + if isPlaceholder { + // and it looks like a placeholder, evaluate it + replaced, _ := fe.repl.Get(strings.Trim(str, "{}")) + zap.Any(key, replaced).AddTo(fe) + } else { + // just use the string as-is + zap.String(key, str).AddTo(fe) + } + } else { + // not a string, so use the value as any + zap.Any(key, value).AddTo(fe) + } + } + + return fe.wrapped.EncodeEntry(ent, nil) +} + +// Interface guards +var ( + _ zapcore.Encoder = (*AppendEncoder)(nil) + _ caddyfile.Unmarshaler = (*AppendEncoder)(nil) + _ caddy.ConfiguresFormatterDefault = (*AppendEncoder)(nil) +) diff --git a/modules/logging/encoders.go b/modules/logging/encoders.go index a4409e7e4..7399423bf 100644 --- a/modules/logging/encoders.go +++ b/modules/logging/encoders.go @@ -65,14 +65,13 @@ func (ce *ConsoleEncoder) Provision(_ caddy.Context) error { // See the godoc on the LogEncoderConfig type for the syntax of // subdirectives that are common to most/all encoders. func (ce *ConsoleEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if d.NextArg() { - return d.ArgErr() - } - err := ce.LogEncoderConfig.UnmarshalCaddyfile(d) - if err != nil { - return err - } + d.Next() // consume encoder name + if d.NextArg() { + return d.ArgErr() + } + err := ce.LogEncoderConfig.UnmarshalCaddyfile(d) + if err != nil { + return err } return nil } @@ -106,14 +105,13 @@ func (je *JSONEncoder) Provision(_ caddy.Context) error { // See the godoc on the LogEncoderConfig type for the syntax of // subdirectives that are common to most/all encoders. func (je *JSONEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if d.NextArg() { - return d.ArgErr() - } - err := je.LogEncoderConfig.UnmarshalCaddyfile(d) - if err != nil { - return err - } + d.Next() // consume encoder name + if d.NextArg() { + return d.ArgErr() + } + err := je.LogEncoderConfig.UnmarshalCaddyfile(d) + if err != nil { + return err } return nil } @@ -149,7 +147,7 @@ type LogEncoderConfig struct { // level_format // } func (lec *LogEncoderConfig) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for nesting := d.Nesting(); d.NextBlock(nesting); { + for d.NextBlock(0) { subdir := d.Val() switch subdir { case "time_local": diff --git a/modules/logging/filewriter.go b/modules/logging/filewriter.go index 11c051d78..3b1001b7c 100644 --- a/modules/logging/filewriter.go +++ b/modules/logging/filewriter.go @@ -154,73 +154,72 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) { // omitted or set to a zero value, then Caddy's default value for that // subdirective is used. func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if !d.NextArg() { - return d.ArgErr() - } - fw.Filename = d.Val() - if d.NextArg() { - return d.ArgErr() - } + d.Next() // consume writer name + if !d.NextArg() { + return d.ArgErr() + } + fw.Filename = d.Val() + if d.NextArg() { + return d.ArgErr() + } - for d.NextBlock(0) { - switch d.Val() { - case "roll_disabled": - var f bool - fw.Roll = &f - if d.NextArg() { - return d.ArgErr() - } - - case "roll_size": - var sizeStr string - if !d.AllArgs(&sizeStr) { - return d.ArgErr() - } - size, err := humanize.ParseBytes(sizeStr) - if err != nil { - return d.Errf("parsing size: %v", err) - } - fw.RollSizeMB = int(math.Ceil(float64(size) / humanize.MiByte)) - - case "roll_uncompressed": - var f bool - fw.RollCompress = &f - if d.NextArg() { - return d.ArgErr() - } - - case "roll_local_time": - fw.RollLocalTime = true - if d.NextArg() { - return d.ArgErr() - } - - case "roll_keep": - var keepStr string - if !d.AllArgs(&keepStr) { - return d.ArgErr() - } - keep, err := strconv.Atoi(keepStr) - if err != nil { - return d.Errf("parsing roll_keep number: %v", err) - } - fw.RollKeep = keep - - case "roll_keep_for": - var keepForStr string - if !d.AllArgs(&keepForStr) { - return d.ArgErr() - } - keepFor, err := caddy.ParseDuration(keepForStr) - if err != nil { - return d.Errf("parsing roll_keep_for duration: %v", err) - } - if keepFor < 0 { - return d.Errf("negative roll_keep_for duration: %v", keepFor) - } - fw.RollKeepDays = int(math.Ceil(keepFor.Hours() / 24)) + for d.NextBlock(0) { + switch d.Val() { + case "roll_disabled": + var f bool + fw.Roll = &f + if d.NextArg() { + return d.ArgErr() } + + case "roll_size": + var sizeStr string + if !d.AllArgs(&sizeStr) { + return d.ArgErr() + } + size, err := humanize.ParseBytes(sizeStr) + if err != nil { + return d.Errf("parsing size: %v", err) + } + fw.RollSizeMB = int(math.Ceil(float64(size) / humanize.MiByte)) + + case "roll_uncompressed": + var f bool + fw.RollCompress = &f + if d.NextArg() { + return d.ArgErr() + } + + case "roll_local_time": + fw.RollLocalTime = true + if d.NextArg() { + return d.ArgErr() + } + + case "roll_keep": + var keepStr string + if !d.AllArgs(&keepStr) { + return d.ArgErr() + } + keep, err := strconv.Atoi(keepStr) + if err != nil { + return d.Errf("parsing roll_keep number: %v", err) + } + fw.RollKeep = keep + + case "roll_keep_for": + var keepForStr string + if !d.AllArgs(&keepForStr) { + return d.ArgErr() + } + keepFor, err := caddy.ParseDuration(keepForStr) + if err != nil { + return d.Errf("parsing roll_keep_for duration: %v", err) + } + if keepFor < 0 { + return d.Errf("negative roll_keep_for duration: %v", keepFor) + } + fw.RollKeepDays = int(math.Ceil(keepFor.Hours() / 24)) } } return nil diff --git a/modules/logging/filterencoder.go b/modules/logging/filterencoder.go index 4d51e645c..c46df0788 100644 --- a/modules/logging/filterencoder.go +++ b/modules/logging/filterencoder.go @@ -17,11 +17,13 @@ package logging import ( "encoding/json" "fmt" + "os" "time" "go.uber.org/zap" "go.uber.org/zap/buffer" "go.uber.org/zap/zapcore" + "golang.org/x/term" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" @@ -36,8 +38,10 @@ func init() { // log entries before they are actually encoded by // an underlying encoder. type FilterEncoder struct { - // The underlying encoder that actually - // encodes the log entries. Required. + // The underlying encoder that actually encodes the + // log entries. If not specified, defaults to "json", + // unless the output is a terminal, in which case + // it defaults to "console". WrappedRaw json.RawMessage `json:"wrap,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"` // A map of field names to their filters. Note that this @@ -59,6 +63,9 @@ type FilterEncoder struct { // used to keep keys unique across nested objects keyPrefix string + + wrappedIsDefault bool + ctx caddy.Context } // CaddyModule returns the Caddy module information. @@ -71,16 +78,25 @@ func (FilterEncoder) CaddyModule() caddy.ModuleInfo { // Provision sets up the encoder. func (fe *FilterEncoder) Provision(ctx caddy.Context) error { - if fe.WrappedRaw == nil { - return fmt.Errorf("missing \"wrap\" (must specify an underlying encoder)") - } + fe.ctx = ctx - // set up wrapped encoder (required) - val, err := ctx.LoadModule(fe, "WrappedRaw") - if err != nil { - return fmt.Errorf("loading fallback encoder module: %v", err) + if fe.WrappedRaw == nil { + // if wrap is not specified, default to JSON + fe.wrapped = &JSONEncoder{} + if p, ok := fe.wrapped.(caddy.Provisioner); ok { + if err := p.Provision(ctx); err != nil { + return fmt.Errorf("provisioning fallback encoder module: %v", err) + } + } + fe.wrappedIsDefault = true + } else { + // set up wrapped encoder + val, err := ctx.LoadModule(fe, "WrappedRaw") + if err != nil { + return fmt.Errorf("loading fallback encoder module: %v", err) + } + fe.wrapped = val.(zapcore.Encoder) } - fe.wrapped = val.(zapcore.Encoder) // set up each field filter if fe.Fields == nil { @@ -97,6 +113,29 @@ func (fe *FilterEncoder) Provision(ctx caddy.Context) error { return nil } +// ConfigureDefaultFormat will set the default format to "console" +// if the writer is a terminal. If already configured as a filter +// encoder, it passes through the writer so a deeply nested filter +// encoder can configure its own default format. +func (fe *FilterEncoder) ConfigureDefaultFormat(wo caddy.WriterOpener) error { + if !fe.wrappedIsDefault { + if cfd, ok := fe.wrapped.(caddy.ConfiguresFormatterDefault); ok { + return cfd.ConfigureDefaultFormat(wo) + } + return nil + } + + if caddy.IsWriterStandardStream(wo) && term.IsTerminal(int(os.Stderr.Fd())) { + fe.wrapped = &ConsoleEncoder{} + if p, ok := fe.wrapped.(caddy.Provisioner); ok { + if err := p.Provision(fe.ctx); err != nil { + return fmt.Errorf("provisioning fallback encoder module: %v", err) + } + } + } + return nil +} + // UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: // // filter { @@ -106,51 +145,68 @@ func (fe *FilterEncoder) Provision(ctx caddy.Context) error { // // } // } +// { +// +// } // } func (fe *FilterEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - for d.NextBlock(0) { - switch d.Val() { - case "wrap": - if !d.NextArg() { - return d.ArgErr() - } - moduleName := d.Val() - moduleID := "caddy.logging.encoders." + moduleName - unm, err := caddyfile.UnmarshalModule(d, moduleID) + d.Next() // consume encoder name + + // parse a field + parseField := func() error { + if fe.FieldsRaw == nil { + fe.FieldsRaw = make(map[string]json.RawMessage) + } + field := d.Val() + if !d.NextArg() { + return d.ArgErr() + } + filterName := d.Val() + moduleID := "caddy.logging.encoders.filter." + filterName + unm, err := caddyfile.UnmarshalModule(d, moduleID) + if err != nil { + return err + } + filter, ok := unm.(LogFieldFilter) + if !ok { + return d.Errf("module %s (%T) is not a logging.LogFieldFilter", moduleID, unm) + } + fe.FieldsRaw[field] = caddyconfig.JSONModuleObject(filter, "filter", filterName, nil) + return nil + } + + for d.NextBlock(0) { + switch d.Val() { + case "wrap": + if !d.NextArg() { + return d.ArgErr() + } + moduleName := d.Val() + moduleID := "caddy.logging.encoders." + moduleName + unm, err := caddyfile.UnmarshalModule(d, moduleID) + if err != nil { + return err + } + enc, ok := unm.(zapcore.Encoder) + if !ok { + return d.Errf("module %s (%T) is not a zapcore.Encoder", moduleID, unm) + } + fe.WrappedRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, nil) + + case "fields": + for nesting := d.Nesting(); d.NextBlock(nesting); { + err := parseField() if err != nil { return err } - enc, ok := unm.(zapcore.Encoder) - if !ok { - return d.Errf("module %s (%T) is not a zapcore.Encoder", moduleID, unm) - } - fe.WrappedRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, nil) + } - case "fields": - for d.NextBlock(1) { - field := d.Val() - if !d.NextArg() { - return d.ArgErr() - } - filterName := d.Val() - moduleID := "caddy.logging.encoders.filter." + filterName - unm, err := caddyfile.UnmarshalModule(d, moduleID) - if err != nil { - return err - } - filter, ok := unm.(LogFieldFilter) - if !ok { - return d.Errf("module %s (%T) is not a logging.LogFieldFilter", moduleID, unm) - } - if fe.FieldsRaw == nil { - fe.FieldsRaw = make(map[string]json.RawMessage) - } - fe.FieldsRaw[field] = caddyconfig.JSONModuleObject(filter, "filter", filterName, nil) - } - - default: - return d.Errf("unrecognized subdirective %s", d.Val()) + default: + // if unknown, assume it's a field so that + // the config can be flat + err := parseField() + if err != nil { + return err } } } @@ -391,7 +447,8 @@ func (mom logObjectMarshalerWrapper) MarshalLogObject(_ zapcore.ObjectEncoder) e // Interface guards var ( - _ zapcore.Encoder = (*FilterEncoder)(nil) - _ zapcore.ObjectMarshaler = (*logObjectMarshalerWrapper)(nil) - _ caddyfile.Unmarshaler = (*FilterEncoder)(nil) + _ zapcore.Encoder = (*FilterEncoder)(nil) + _ zapcore.ObjectMarshaler = (*logObjectMarshalerWrapper)(nil) + _ caddyfile.Unmarshaler = (*FilterEncoder)(nil) + _ caddy.ConfiguresFormatterDefault = (*FilterEncoder)(nil) ) diff --git a/modules/logging/filters.go b/modules/logging/filters.go index ed4c7affb..79d908fca 100644 --- a/modules/logging/filters.go +++ b/modules/logging/filters.go @@ -100,12 +100,15 @@ func (f *HashFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // Filter filters the input field with the replacement value. func (f *HashFilter) Filter(in zapcore.Field) zapcore.Field { if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok { + newArray := make(caddyhttp.LoggableStringArray, len(array)) for i, s := range array { - array[i] = hash(s) + newArray[i] = hash(s) } + in.Interface = newArray } else { in.String = hash(in.String) } + return in } @@ -125,10 +128,9 @@ func (ReplaceFilter) CaddyModule() caddy.ModuleInfo { // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (f *ReplaceFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if d.NextArg() { - f.Value = d.Val() - } + d.Next() // consume filter name + if d.NextArg() { + f.Value = d.Val() } return nil } @@ -166,32 +168,52 @@ func (IPMaskFilter) CaddyModule() caddy.ModuleInfo { // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (m *IPMaskFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - for d.NextBlock(0) { - switch d.Val() { - case "ipv4": - if !d.NextArg() { - return d.ArgErr() - } - val, err := strconv.Atoi(d.Val()) - if err != nil { - return d.Errf("error parsing %s: %v", d.Val(), err) - } - m.IPv4MaskRaw = val + d.Next() // consume filter name - case "ipv6": - if !d.NextArg() { - return d.ArgErr() - } - val, err := strconv.Atoi(d.Val()) - if err != nil { - return d.Errf("error parsing %s: %v", d.Val(), err) - } - m.IPv6MaskRaw = val + args := d.RemainingArgs() + if len(args) > 2 { + return d.Errf("too many arguments") + } + if len(args) > 0 { + val, err := strconv.Atoi(args[0]) + if err != nil { + return d.Errf("error parsing %s: %v", args[0], err) + } + m.IPv4MaskRaw = val - default: - return d.Errf("unrecognized subdirective %s", d.Val()) + if len(args) > 1 { + val, err := strconv.Atoi(args[1]) + if err != nil { + return d.Errf("error parsing %s: %v", args[1], err) } + m.IPv6MaskRaw = val + } + } + + for d.NextBlock(0) { + switch d.Val() { + case "ipv4": + if !d.NextArg() { + return d.ArgErr() + } + val, err := strconv.Atoi(d.Val()) + if err != nil { + return d.Errf("error parsing %s: %v", d.Val(), err) + } + m.IPv4MaskRaw = val + + case "ipv6": + if !d.NextArg() { + return d.ArgErr() + } + val, err := strconv.Atoi(d.Val()) + if err != nil { + return d.Errf("error parsing %s: %v", d.Val(), err) + } + m.IPv6MaskRaw = val + + default: + return d.Errf("unrecognized subdirective %s", d.Val()) } } return nil @@ -219,9 +241,11 @@ func (m *IPMaskFilter) Provision(ctx caddy.Context) error { // Filter filters the input field. func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field { if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok { + newArray := make(caddyhttp.LoggableStringArray, len(array)) for i, s := range array { - array[i] = m.mask(s) + newArray[i] = m.mask(s) } + in.Interface = newArray } else { in.String = m.mask(in.String) } @@ -323,54 +347,67 @@ func (QueryFilter) CaddyModule() caddy.ModuleInfo { // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (m *QueryFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - for d.NextBlock(0) { - qfa := queryFilterAction{} - switch d.Val() { - case "replace": - if !d.NextArg() { - return d.ArgErr() - } - - qfa.Type = replaceAction - qfa.Parameter = d.Val() - - if !d.NextArg() { - return d.ArgErr() - } - qfa.Value = d.Val() - - case "hash": - if !d.NextArg() { - return d.ArgErr() - } - - qfa.Type = hashAction - qfa.Parameter = d.Val() - - case "delete": - if !d.NextArg() { - return d.ArgErr() - } - - qfa.Type = deleteAction - qfa.Parameter = d.Val() - - default: - return d.Errf("unrecognized subdirective %s", d.Val()) + d.Next() // consume filter name + for d.NextBlock(0) { + qfa := queryFilterAction{} + switch d.Val() { + case "replace": + if !d.NextArg() { + return d.ArgErr() } - m.Actions = append(m.Actions, qfa) + qfa.Type = replaceAction + qfa.Parameter = d.Val() + + if !d.NextArg() { + return d.ArgErr() + } + qfa.Value = d.Val() + + case "hash": + if !d.NextArg() { + return d.ArgErr() + } + + qfa.Type = hashAction + qfa.Parameter = d.Val() + + case "delete": + if !d.NextArg() { + return d.ArgErr() + } + + qfa.Type = deleteAction + qfa.Parameter = d.Val() + + default: + return d.Errf("unrecognized subdirective %s", d.Val()) } + + m.Actions = append(m.Actions, qfa) } return nil } // Filter filters the input field. func (m QueryFilter) Filter(in zapcore.Field) zapcore.Field { - u, err := url.Parse(in.String) + if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok { + newArray := make(caddyhttp.LoggableStringArray, len(array)) + for i, s := range array { + newArray[i] = m.processQueryString(s) + } + in.Interface = newArray + } else { + in.String = m.processQueryString(in.String) + } + + return in +} + +func (m QueryFilter) processQueryString(s string) string { + u, err := url.Parse(s) if err != nil { - return in + return s } q := u.Query() @@ -392,9 +429,7 @@ func (m QueryFilter) Filter(in zapcore.Field) zapcore.Field { } u.RawQuery = q.Encode() - in.String = u.String() - - return in + return u.String() } type cookieFilterAction struct { @@ -443,45 +478,44 @@ func (CookieFilter) CaddyModule() caddy.ModuleInfo { // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (m *CookieFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - for d.NextBlock(0) { - cfa := cookieFilterAction{} - switch d.Val() { - case "replace": - if !d.NextArg() { - return d.ArgErr() - } - - cfa.Type = replaceAction - cfa.Name = d.Val() - - if !d.NextArg() { - return d.ArgErr() - } - cfa.Value = d.Val() - - case "hash": - if !d.NextArg() { - return d.ArgErr() - } - - cfa.Type = hashAction - cfa.Name = d.Val() - - case "delete": - if !d.NextArg() { - return d.ArgErr() - } - - cfa.Type = deleteAction - cfa.Name = d.Val() - - default: - return d.Errf("unrecognized subdirective %s", d.Val()) + d.Next() // consume filter name + for d.NextBlock(0) { + cfa := cookieFilterAction{} + switch d.Val() { + case "replace": + if !d.NextArg() { + return d.ArgErr() } - m.Actions = append(m.Actions, cfa) + cfa.Type = replaceAction + cfa.Name = d.Val() + + if !d.NextArg() { + return d.ArgErr() + } + cfa.Value = d.Val() + + case "hash": + if !d.NextArg() { + return d.ArgErr() + } + + cfa.Type = hashAction + cfa.Name = d.Val() + + case "delete": + if !d.NextArg() { + return d.ArgErr() + } + + cfa.Type = deleteAction + cfa.Name = d.Val() + + default: + return d.Errf("unrecognized subdirective %s", d.Val()) } + + m.Actions = append(m.Actions, cfa) } return nil } @@ -554,13 +588,12 @@ func (RegexpFilter) CaddyModule() caddy.ModuleInfo { // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (f *RegexpFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if d.NextArg() { - f.RawRegexp = d.Val() - } - if d.NextArg() { - f.Value = d.Val() - } + d.Next() // consume filter name + if d.NextArg() { + f.RawRegexp = d.Val() + } + if d.NextArg() { + f.Value = d.Val() } return nil } @@ -580,9 +613,11 @@ func (m *RegexpFilter) Provision(ctx caddy.Context) error { // Filter filters the input field with the replacement value if it matches the regexp. func (f *RegexpFilter) Filter(in zapcore.Field) zapcore.Field { if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok { + newArray := make(caddyhttp.LoggableStringArray, len(array)) for i, s := range array { - array[i] = f.regexp.ReplaceAllString(s, f.Value) + newArray[i] = f.regexp.ReplaceAllString(s, f.Value) } + in.Interface = newArray } else { in.String = f.regexp.ReplaceAllString(in.String, f.Value) } @@ -606,10 +641,9 @@ func (RenameFilter) CaddyModule() caddy.ModuleInfo { // UnmarshalCaddyfile sets up the module from Caddyfile tokens. func (f *RenameFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if d.NextArg() { - f.Name = d.Val() - } + d.Next() // consume filter name + if d.NextArg() { + f.Name = d.Val() } return nil } diff --git a/modules/logging/filters_test.go b/modules/logging/filters_test.go index e9c3e77fe..8f7ba0d70 100644 --- a/modules/logging/filters_test.go +++ b/modules/logging/filters_test.go @@ -83,7 +83,7 @@ func TestIPMaskMultiValue(t *testing.T) { } } -func TestQueryFilter(t *testing.T) { +func TestQueryFilterSingleValue(t *testing.T) { f := QueryFilter{[]queryFilterAction{ {replaceAction, "foo", "REDACTED"}, {replaceAction, "notexist", "REDACTED"}, @@ -102,6 +102,40 @@ func TestQueryFilter(t *testing.T) { } } +func TestQueryFilterMultiValue(t *testing.T) { + f := QueryFilter{ + Actions: []queryFilterAction{ + {Type: replaceAction, Parameter: "foo", Value: "REDACTED"}, + {Type: replaceAction, Parameter: "notexist", Value: "REDACTED"}, + {Type: deleteAction, Parameter: "bar"}, + {Type: deleteAction, Parameter: "notexist"}, + {Type: hashAction, Parameter: "hash"}, + }, + } + + if f.Validate() != nil { + t.Fatalf("the filter must be valid") + } + + out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{ + "/path1?foo=a&foo=b&bar=c&bar=d&baz=e&hash=hashed", + "/path2?foo=c&foo=d&bar=e&bar=f&baz=g&hash=hashed", + }}) + arr, ok := out.Interface.(caddyhttp.LoggableStringArray) + if !ok { + t.Fatalf("field is wrong type: %T", out.Interface) + } + + expected1 := "/path1?baz=e&foo=REDACTED&foo=REDACTED&hash=e3b0c442" + expected2 := "/path2?baz=g&foo=REDACTED&foo=REDACTED&hash=e3b0c442" + if arr[0] != expected1 { + t.Fatalf("query parameters in entry 0 have not been filtered correctly: got %s, expected %s", arr[0], expected1) + } + if arr[1] != expected2 { + t.Fatalf("query parameters in entry 1 have not been filtered correctly: got %s, expected %s", arr[1], expected2) + } +} + func TestValidateQueryFilter(t *testing.T) { f := QueryFilter{[]queryFilterAction{ {}, diff --git a/modules/logging/netwriter.go b/modules/logging/netwriter.go index 1939cb711..dc2b0922c 100644 --- a/modules/logging/netwriter.go +++ b/modules/logging/netwriter.go @@ -117,35 +117,34 @@ func (nw NetWriter) OpenWriter() (io.WriteCloser, error) { // soft_start // } func (nw *NetWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if !d.NextArg() { - return d.ArgErr() - } - nw.Address = d.Val() - if d.NextArg() { - return d.ArgErr() - } - for nesting := d.Nesting(); d.NextBlock(nesting); { - switch d.Val() { - case "dial_timeout": - if !d.NextArg() { - return d.ArgErr() - } - timeout, err := caddy.ParseDuration(d.Val()) - if err != nil { - return d.Errf("invalid duration: %s", d.Val()) - } - if d.NextArg() { - return d.ArgErr() - } - nw.DialTimeout = caddy.Duration(timeout) - - case "soft_start": - if d.NextArg() { - return d.ArgErr() - } - nw.SoftStart = true + d.Next() // consume writer name + if !d.NextArg() { + return d.ArgErr() + } + nw.Address = d.Val() + if d.NextArg() { + return d.ArgErr() + } + for d.NextBlock(0) { + switch d.Val() { + case "dial_timeout": + if !d.NextArg() { + return d.ArgErr() } + timeout, err := caddy.ParseDuration(d.Val()) + if err != nil { + return d.Errf("invalid duration: %s", d.Val()) + } + if d.NextArg() { + return d.ArgErr() + } + nw.DialTimeout = caddy.Duration(timeout) + + case "soft_start": + if d.NextArg() { + return d.ArgErr() + } + nw.SoftStart = true } } return nil diff --git a/modules/metrics/metrics.go b/modules/metrics/metrics.go index a9e0f0efa..dc6196a15 100644 --- a/modules/metrics/metrics.go +++ b/modules/metrics/metrics.go @@ -78,19 +78,18 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) // disable_openmetrics // } func (m *Metrics) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - args := d.RemainingArgs() - if len(args) > 0 { - return d.ArgErr() - } + d.Next() // consume directive name + args := d.RemainingArgs() + if len(args) > 0 { + return d.ArgErr() + } - for d.NextBlock(0) { - switch d.Val() { - case "disable_openmetrics": - m.DisableOpenMetrics = true - default: - return d.Errf("unrecognized subdirective %q", d.Val()) - } + for d.NextBlock(0) { + switch d.Val() { + case "disable_openmetrics": + m.DisableOpenMetrics = true + default: + return d.Errf("unrecognized subdirective %q", d.Val()) } } return nil diff --git a/modules/standard/imports.go b/modules/standard/imports.go index a9d0b3968..813c63d6c 100644 --- a/modules/standard/imports.go +++ b/modules/standard/imports.go @@ -5,6 +5,7 @@ import ( _ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" _ "github.com/caddyserver/caddy/v2/modules/caddyevents" _ "github.com/caddyserver/caddy/v2/modules/caddyevents/eventsconfig" + _ "github.com/caddyserver/caddy/v2/modules/caddyfs" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/standard" _ "github.com/caddyserver/caddy/v2/modules/caddypki" _ "github.com/caddyserver/caddy/v2/modules/caddypki/acmeserver" diff --git a/replacer.go b/replacer.go index 5d33b7f18..046be867a 100644 --- a/replacer.go +++ b/replacer.go @@ -133,7 +133,7 @@ func (r *Replacer) replace(input, empty string, treatUnknownAsEmpty, errOnEmpty, errOnUnknown bool, f ReplacementFunc, ) (string, error) { - if !strings.Contains(input, string(phOpen)) { + if !strings.Contains(input, string(phOpen)) && !strings.Contains(input, string(phClose)) { return input, nil } diff --git a/replacer_test.go b/replacer_test.go index 41ada7d6d..c88683947 100644 --- a/replacer_test.go +++ b/replacer_test.go @@ -69,7 +69,7 @@ func TestReplacer(t *testing.T) { }, { input: `\}`, - expect: `\}`, + expect: `}`, }, { input: "{}", @@ -164,6 +164,10 @@ func TestReplacer(t *testing.T) { input: string([]byte{0x26, 0x00, 0x83, 0x7B, 0x84, 0x07, 0x5C, 0x7D, 0x84}), expect: string([]byte{0x26, 0x00, 0x83, 0x7B, 0x84, 0x07, 0x7D, 0x84}), }, + { + input: `\\}`, + expect: `\}`, + }, } { actual := rep.ReplaceAll(tc.input, tc.empty) if actual != tc.expect { diff --git a/usagepool.go b/usagepool.go index 7007849fb..e011be961 100644 --- a/usagepool.go +++ b/usagepool.go @@ -112,7 +112,7 @@ func (up *UsagePool) LoadOrNew(key any, construct Constructor) (value any, loade // LoadOrStore loads the value associated with key from the pool if it // already exists, or stores it if it does not exist. It returns the // value that was either loaded or stored, and true if the value already -// existed and was +// existed and was loaded, false if the value didn't exist and was stored. func (up *UsagePool) LoadOrStore(key, val any) (value any, loaded bool) { var upv *usagePoolVal up.Lock()