diff --git a/.editorconfig b/.editorconfig index 7134bf7ec3a..96948b9dbe7 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 ed8374433bb..309ef79357f 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@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 @@ -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 @@ -168,5 +173,3 @@ jobs: 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 13e215b757c..676607d0e6b 100644 --- a/.github/workflows/cross-build.yml +++ b/.github/workflows/cross-build.yml @@ -11,7 +11,7 @@ on: - 2.* jobs: - cross-build-test: + build: strategy: fail-fast: false matrix: @@ -29,13 +29,13 @@ 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 @@ -44,7 +44,7 @@ jobs: 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 @@ -68,7 +68,7 @@ jobs: continue-on-error: true working-directory: ./cmd/caddy run: | - GOOS=$GOOS GOARCH=$GOARCH go build -trimpath -o caddy-"$GOOS"-$GOARCH 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 e636e07aa75..bfb91dc66fe 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@v4 - - uses: actions/setup-go@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 @@ -57,5 +66,5 @@ jobs: - name: govulncheck uses: golang/govulncheck-action@v1 with: - go-version-input: '~1.21.0' + go-version-input: '~1.22.1' check-latest: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 184662f7fa1..b11686626cd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,7 @@ jobs: 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 diff --git a/.github/workflows/release_published.yml b/.github/workflows/release_published.yml index f304888e8ae..491dae75db4 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/.golangci.yml b/.golangci.yml index 5f018970eb7..d144395dbda 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 dfd6589dfe1..0fba8e2c435 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 6dd04688797..e5f99efdff2 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/caddy.go b/caddy.go index 4a7bd8b6d05..1e0eacd2b07 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`; @@ -828,7 +864,7 @@ func InstanceID() (uuid.UUID, error) { 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 diff --git a/caddyconfig/caddyfile/adapter.go b/caddyconfig/caddyfile/adapter.go index d6ef602dca5..da4f98337fb 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 b64a97354d4..0f6ee5043f4 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 82581a3d31e..d506219c40a 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 8e5b3686069..6eec822fe59 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 92acc4da9f3..7389af79b40 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,390 +105,349 @@ func parseTLS(h Helper) ([]ConfigValue, error) { var issuers []certmagic.Issuer var certManagers []certmagic.Manager var onDemand 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) - } - 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 + var reusePrivateKeys bool + + // 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] + } - 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) + 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 + } - default: - return nil, h.ArgErr() + 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) - var hasBlock bool - for nesting := h.Nesting(); h.NextBlock(nesting); { - hasBlock = true + default: + return nil, h.ArgErr() + } - 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] - } + var hasBlock bool + for h.NextBlock(0) { + hasBlock = true - 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()) + 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]) } - - 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()) + 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 "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 "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 "alpn": - args := h.RemainingArgs() - if len(args) == 0 { - return nil, h.ArgErr() + 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.ALPN = args + cp.Curves = append(cp.Curves, h.Val()) + } - case "load": - folderLoader = append(folderLoader, h.RemainingArgs()...) + 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 "ca": - arg := h.RemainingArgs() - if len(arg) != 1 { - return nil, h.ArgErr() - } - if acmeIssuer == nil { - acmeIssuer = new(caddytls.ACMEIssuer) - } - acmeIssuer.CA = arg[0] + case "load": + folderLoader = append(folderLoader, h.RemainingArgs()...) - case "key_type": - arg := h.RemainingArgs() - if len(arg) != 1 { - return nil, h.ArgErr() - } - keyType = arg[0] + 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 "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 "key_type": + arg := h.RemainingArgs() + if len(arg) != 1 { + return nil, h.ArgErr() + } + keyType = arg[0] - 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 "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 "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 "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 "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 "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 "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 "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 "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 "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_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 "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 "dns_ttl": - arg := h.RemainingArgs() - if len(arg) != 1 { - return nil, h.ArgErr() - } - ttlStr := arg[0] - ttl, err := caddy.ParseDuration(ttlStr) + 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 dns_ttl duration %s: %v", ttlStr, err) - } - if acmeIssuer == nil { - acmeIssuer = new(caddytls.ACMEIssuer) + return nil, h.Errf("invalid propagation_timeout duration %s: %v", timeoutStr, err) } - 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) + } + 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_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 "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 "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 "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 "on_demand": - if h.NextArg() { - return nil, h.ArgErr() - } - onDemand = true + 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 "insecure_secrets_log": - if !h.NextArg() { - return nil, h.ArgErr() - } - cp.InsecureSecretsLog = h.Val() + case "on_demand": + if h.NextArg() { + return nil, h.ArgErr() + } + onDemand = true - default: - return nil, h.Errf("unknown subdirective: %s", h.Val()) + case "reuse_private_keys": + if h.NextArg() { + return nil, h.ArgErr() } - } + reusePrivateKeys = true - // a naked tls directive is not allowed - if len(firstLine) == 0 && !hasBlock { - return nil, h.ArgErr() + 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() + } + // begin building the final config values configVals := []ConfigValue{} @@ -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 + } + + // parse the matcher token into a matcher set + userMatcherSet, err := h.ExtractMatcherSet() + if err != nil { + return nil, err } - return caddyhttp.VarsMiddleware{"root": root}, nil + 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 { - if h.NextArg() { - logName = h.Val() + // 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 - // 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 - } + if parseAsGlobalOption { + if h.NextArg() { + logName = h.Val() - // Verify this name is unused. - _, used := globalLogNames[logName] - if used { - return nil, h.Err("duplicate global log option for: " + logName) + // Only a single argument is supported. + if h.NextArg() { + return nil, h.ArgErr() } - 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() + // 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 + } - // Only a single argument is supported. - if h.NextArg() { - 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() - cl := new(caddy.CustomLog) + // Only a single argument is supported. + if h.NextArg() { + return nil, h.ArgErr() + } + } + } - // 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{} + cl := new(caddy.CustomLog) - 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...) + // 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{} - 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 - } - 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) + } + cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings) - case "level": - if !h.NextArg() { - return nil, h.ArgErr() - } - cl.Level = h.Val() - if h.NextArg() { - return nil, h.ArgErr() - } + case "format": + if !h.NextArg() { + return nil, h.ArgErr() + } + 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 "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 "level": + if !h.NextArg() { + return nil, h.ArgErr() + } + cl.Level = h.Val() + if h.NextArg() { + return nil, h.ArgErr() + } - 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()) - } + 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()) + } - default: - return nil, h.Errf("unrecognized subdirective: %s", 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()) } + } - var val namedCustomLog - val.hostnames = customHostnames + var val namedCustomLog + val.hostnames = customHostnames - isEmptyConfig := reflect.DeepEqual(cl, new(caddy.CustomLog)) + isEmptyConfig := reflect.DeepEqual(cl, new(caddy.CustomLog)) - // Skip handling of empty logging configs + // Skip handling of empty logging configs - if parseAsGlobalOption { - // Use indicated name for global log options + if parseAsGlobalOption { + // Use indicated name for global log options + val.name = logName + } else { + if logName != "" { 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} + } 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 !isEmptyConfig { - val.log = cl + if val.name != "" { + cl.Include = []string{"http.log.access." + val.name} } - configValues = append(configValues, ConfigValue{ - Class: "custom_log", - Value: val, - }) } + 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!") + } + + if h.NextArg() { + return nil, h.ArgErr() } - return caddyhttp.VarsMiddleware{"skip_log": true}, nil + return caddyhttp.VarsMiddleware{"log_skip": true}, nil } diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go index 13229ed5cd4..bde25031a84 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 e46a6d2af7b..db028229950 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 c82d92efc62..da5557aa804 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -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{ @@ -271,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", @@ -280,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) @@ -485,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 @@ -523,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) } } @@ -769,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 @@ -820,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) @@ -1362,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 + + // 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) - if _, ok := matchers[definitionName]; ok { - return fmt.Errorf("matcher is defined more than once: %s", definitionName) + // 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 err != nil { + return err } - matchers[definitionName] = make(caddy.ModuleMap) + 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 diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go index ba1896b6b84..70d475d6b28 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) { - newOrder := directiveOrder + d.Next() // consume option name - 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) - } + // 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 := d.Val() + // get positional token + if !d.Next() { + return nil, d.ArgErr() + } + pos := Positional(d.Val()) - // if directive exists, first remove it - for i, d := range newOrder { - if d == dirName { - newOrder = append(newOrder[:i], newOrder[i+1:]...) - break - } - } + newOrder := directiveOrder - // 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) + // if directive exists, first remove it + for i, d := range newOrder { + if d == dirName { + newOrder = append(newOrder[:i], newOrder[i+1:]...) + break } + } - // get name of other directive - if !d.NextArg() { + // act on the positional + switch pos { + case First: + newOrder = append([]string{dirName}, newOrder...) + if d.NextArg() { return nil, d.ArgErr() } - otherDir := d.Val() + 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() + 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 + // 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 b5c682187d9..c57263baf92 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 "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_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 "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 "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 "key": + if !d.NextArg() { + return nil, d.ArgErr() } - } + pkiCa.Root.PrivateKey = 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()) + 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()) } + } - default: - return nil, d.Errf("unrecognized pki ca 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() - pki.CAs[pkiCa.ID] = pkiCa + case "key": + if !d.NextArg() { + return nil, d.ArgErr() + } + pkiCa.Intermediate.PrivateKey = d.Val() - default: - return nil, d.Errf("unrecognized pki option '%s'", 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()) } } - return pki, nil } diff --git a/caddyconfig/httpcaddyfile/serveroptions.go b/caddyconfig/httpcaddyfile/serveroptions.go index 6d7c6787f37..62902b96458 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") + 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 } - if !d.NextArg() { - return nil, d.ArgErr() + listenerWrapper, ok := unm.(caddy.ListenerWrapper) + if !ok { + return nil, fmt.Errorf("module %s (%T) is not a listener wrapper", modID, unm) } - serverOpts.Name = d.Val() + jsonListenerWrapper := caddyconfig.JSONModuleObject( + listenerWrapper, + "wrapper", + listenerWrapper.(caddy.Module).CaddyModule().ID.Name(), + nil, + ) + serverOpts.ListenerWrappersRaw = append(serverOpts.ListenerWrappersRaw, jsonListenerWrapper) + } - case "listener_wrappers": - for nesting := d.Nesting(); d.NextBlock(nesting); { - modID := "caddy.listeners." + d.Val() - unm, err := caddyfile.UnmarshalModule(d, modID) + 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, err + return nil, d.Errf("parsing read_body timeout duration: %v", err) } - listenerWrapper, ok := unm.(caddy.ListenerWrapper) - if !ok { - return nil, fmt.Errorf("module %s (%T) is not a listener wrapper", modID, unm) + serverOpts.ReadTimeout = caddy.Duration(dur) + + case "read_header": + if !d.NextArg() { + return nil, d.ArgErr() } - jsonListenerWrapper := caddyconfig.JSONModuleObject( - listenerWrapper, - "wrapper", - listenerWrapper.(caddy.Module).CaddyModule().ID.Name(), - nil, - ) - serverOpts.ListenerWrappersRaw = append(serverOpts.ListenerWrappersRaw, jsonListenerWrapper) - } + 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 "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 "write": + if !d.NextArg() { + return nil, d.ArgErr() } - } - 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) + 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 "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 "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) - case "enable_full_duplex": - if d.NextArg() { - return nil, d.ArgErr() + default: + return nil, d.Errf("unrecognized timeouts option '%s'", d.Val()) } - serverOpts.EnableFullDuplex = true + } + 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 "log_credentials": - if d.NextArg() { - return nil, d.ArgErr() - } - serverOpts.ShouldLogCredentials = true + 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 "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 "enable_full_duplex": + if d.NextArg() { + return nil, d.ArgErr() + } + serverOpts.EnableFullDuplex = true - 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 "log_credentials": + if d.NextArg() { + return nil, d.ArgErr() + } + serverOpts.ShouldLogCredentials = true - case "trusted_proxies": - if !d.NextArg() { - return nil, d.Err("trusted_proxies expects an IP range source module name as its first argument") + 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) } - modID := "http.ip_sources." + d.Val() - unm, err := caddyfile.UnmarshalModule(d, modID) - if err != nil { - return nil, err + if sliceContains(serverOpts.Protocols, proto) { + return nil, d.Errf("protocol %s specified more than once", proto) } - source, ok := unm.(caddyhttp.IPRangeSource) - if !ok { - return nil, fmt.Errorf("module %s (%T) is not an IP range source", modID, unm) + 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) } - jsonSource := caddyconfig.JSONModuleObject( - source, - "source", - source.(caddy.Module).CaddyModule().ID.Name(), - nil, - ) - serverOpts.TrustedProxiesRaw = jsonSource + 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") - 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) + 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() } - serverOpts.ClientIPHeaders = append(serverOpts.ClientIPHeaders, header) - } - if nesting := d.Nesting(); d.NextBlock(nesting) { - return nil, d.ArgErr() - } + if sliceContains(serverOpts.Protocols, "h2c") { + return nil, d.Errf("protocol h2c already specified") + } + serverOpts.Protocols = append(serverOpts.Protocols, "h2c") - 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()) + 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 servers option '%s'", d.Val()) + 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 index 75795ae61ee..5855d127c8a 100644 --- a/caddyconfig/httpcaddyfile/shorthands.go +++ b/caddyconfig/httpcaddyfile/shorthands.go @@ -33,7 +33,7 @@ func NewShorthandReplacer() ShorthandReplacer { {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(`{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}"}, diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go index cb947a6e412..1adb2b6e027 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) } @@ -587,6 +592,7 @@ outer: 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 39aca23476e..deb567b3b52 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) diff --git a/caddytest/integration/acme_test.go b/caddytest/integration/acme_test.go new file mode 100644 index 00000000000..840af023f67 --- /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 index 0323d5cdd54..435bfc7b48e 100644 --- a/caddytest/integration/acmeserver_test.go +++ b/caddytest/integration/acmeserver_test.go @@ -1,9 +1,17 @@ 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) { @@ -31,3 +39,171 @@ func TestACMEServerDirectory(t *testing.T) { `{"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 00000000000..2a7a5149243 --- /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 00000000000..26d345047c9 --- /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 00000000000..7fe3ca663db --- /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.txt b/caddytest/integration/caddyfile_adapt/enable_tls_for_catch_all_site.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/enable_tls_for_catch_all_site.txt rename to caddytest/integration/caddyfile_adapt/enable_tls_for_catch_all_site.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/encode_options.txt b/caddytest/integration/caddyfile_adapt/encode_options.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/encode_options.txt rename to caddytest/integration/caddyfile_adapt/encode_options.caddyfiletest 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 00000000000..0e84a13c2d3 --- /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 00000000000..46b70c8e384 --- /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 00000000000..70158830c26 --- /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 00000000000..5ac5863e32d --- /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 00000000000..63701cccba9 --- /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 603209802e1..88729c51287 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 03aee2cec3c..f51779253e3 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 2b90d6de7e1..cfc5788267f 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 00000000000..675523a57d3 --- /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 00000000000..4f91e4644cb --- /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 00000000000..88a6cd6be7a --- /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 93% rename from caddytest/integration/caddyfile_adapt/log_except_catchall_blocks.txt rename to caddytest/integration/caddyfile_adapt/log_except_catchall_blocks.caddyfiletest index 6fbc6c7c839..e1362f8fbbb 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": [ 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 00000000000..f63a1d92580 --- /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 776fa68d34c..1b2fc2e502e 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 100% rename from caddytest/integration/caddyfile_adapt/log_override_hostname.txt rename to caddytest/integration/caddyfile_adapt/log_override_hostname.caddyfiletest diff --git a/caddytest/integration/caddyfile_adapt/log_override_name_multiaccess.txt b/caddytest/integration/caddyfile_adapt/log_override_name_multiaccess.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/log_override_name_multiaccess.txt rename to caddytest/integration/caddyfile_adapt/log_override_name_multiaccess.caddyfiletest 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 100% rename from caddytest/integration/caddyfile_adapt/log_override_name_multiaccess_debug.txt rename to caddytest/integration/caddyfile_adapt/log_override_name_multiaccess_debug.caddyfiletest 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 25e4bd1c206..8fdd5715612 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 00000000000..1fe344e0978 --- /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.txt b/caddytest/integration/caddyfile_adapt/reverse_proxy_buffers.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/reverse_proxy_buffers.txt rename to caddytest/integration/caddyfile_adapt/reverse_proxy_buffers.caddyfiletest 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 100% rename from caddytest/integration/caddyfile_adapt/reverse_proxy_health_headers.txt rename to caddytest/integration/caddyfile_adapt/reverse_proxy_health_headers.caddyfiletest 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 00000000000..870e82afdcc --- /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 00000000000..b2ef86c45ac --- /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 d5c35b3c3fb..30bc2c12866 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.txt b/caddytest/integration/caddyfile_adapt/tls_automation_policies_11.caddyfiletest similarity index 100% rename from caddytest/integration/caddyfile_adapt/tls_automation_policies_11.txt rename to caddytest/integration/caddyfile_adapt/tls_automation_policies_11.caddyfiletest 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 00000000000..302d8fd1e11 --- /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 00000000000..36fd978ee1b --- /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 00000000000..dbf408fa13f --- /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 aaa5abffc0f..00000000000 --- 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 4cd45813b7f..3a91e832bf7 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 00000000000..7b8e5a206da --- /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 00000000000..66c3a3c36a7 --- /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 00000000000..860b8a8df4c --- /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 5b052df49e2..0d9f0fa471d 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 205bc5b914b..5d1fa3f0898 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 00000000000..4399902eaee --- /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/pki_test.go b/caddytest/integration/pki_test.go index 5e9928c0cba..8467982092f 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 4f4261b87d0..0beb71afcac 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 24dddc59afe..188f9354135 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 6bc612d3695..ef3ea498d33 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 00000000000..03febfd3ae1 --- /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 d777e5a62f7..ac5335c5601 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 d43b4372906..1a2509206a8 100644 --- a/cmd/cobra.go +++ b/cmd/cobra.go @@ -117,7 +117,7 @@ func onlyVersionText() string { func caddyCmdToCobra(caddyCmd Command) *cobra.Command { cmd := &cobra.Command{ - Use: caddyCmd.Name, + Use: caddyCmd.Name + " " + caddyCmd.Usage, Short: caddyCmd.Short, Long: caddyCmd.Long, } diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index b0c576a3d4f..746cf3da6b6 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -190,7 +190,7 @@ func cmdRun(fl Flags) (int, error) { var config []byte 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)) resumeFlag = false diff --git a/cmd/main.go b/cmd/main.go index b6d36d0b9e1..9be3585b955 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -17,10 +17,12 @@ package caddycmd import ( "bufio" "bytes" + "encoding/json" "errors" "flag" "fmt" "io" + "io/fs" "log" "net" "os" @@ -33,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" @@ -63,6 +66,12 @@ 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) { @@ -99,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)") @@ -111,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 @@ -128,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 { @@ -137,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") } } } @@ -174,16 +187,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 diff --git a/context.go b/context.go index 85978d49cd8..d73af770285 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 00000000000..9785f57d45b --- /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 dde4a27e38c..8760d835112 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,13 @@ 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.9.1 + 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.20.0 github.com/dustin/go-humanize v1.0.1 @@ -15,29 +17,31 @@ require ( github.com/klauspost/compress v1.17.0 github.com/klauspost/cpuid/v2 v2.2.5 github.com/mholt/acmez v1.2.0 - github.com/prometheus/client_golang v1.15.1 - github.com/quic-go/quic-go v0.40.0 + 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-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.45.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.21.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 go.opentelemetry.io/otel/sdk v1.21.0 - go.uber.org/zap v1.25.0 - golang.org/x/crypto v0.14.0 - golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 - golang.org/x/net v0.17.0 - golang.org/x/sync v0.4.0 - golang.org/x/term v0.13.0 - google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b + 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 ) @@ -49,15 +53,15 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/fxamacker/cbor/v2 v2.5.0 // indirect github.com/golang/glog v1.1.2 // indirect - github.com/google/certificate-transparency-go v1.1.6 // 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/google/pprof v0.0.0-20231212022811-ec68065c825e // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect - github.com/onsi/ginkgo/v2 v2.9.5 // 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.4.1 // 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 @@ -66,7 +70,7 @@ require ( go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect go.opentelemetry.io/contrib/propagators/ot v1.17.0 // indirect go.uber.org/mock v0.3.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect ) require ( @@ -79,13 +83,13 @@ 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.10.0 // indirect - github.com/felixge/httpsnoop v1.0.3 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-kit/kit v0.10.0 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-logr/logr v1.3.0 // indirect @@ -109,7 +113,6 @@ 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.4 // 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 @@ -118,9 +121,9 @@ require ( 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.4.0 // indirect - github.com/prometheus/common v0.42.0 // indirect - github.com/prometheus/procfs v0.9.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,7 +133,7 @@ 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/otlptrace v1.21.0 // indirect go.opentelemetry.io/otel/metric v1.21.0 // indirect @@ -140,12 +143,12 @@ require ( 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.14.0 - golang.org/x/text v0.13.0 // indirect - golang.org/x/tools v0.10.0 // indirect - google.golang.org/grpc v1.59.0 // 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 adcdf2402c0..1c7d91d07f5 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,14 @@ 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.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o= -cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= +cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/iam v1.1.2 h1:gacbrBdWcoVmGLozRuStX45YKvJtzIjJdAolzUs1sm4= cloud.google.com/go/iam v1.1.2/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU= cloud.google.com/go/kms v1.15.2 h1:lh6qra6oC4AyWe5fUUUBe/S27k12OHAleOOOw6KakdE= +cloud.google.com/go/kms v1.15.2/go.mod h1:3hopT4+7ooWRCjc2DxgnpESFxhIraaI2IpAVUEhbT/w= filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= @@ -30,11 +33,15 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMx github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alecthomas/assert/v2 v2.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.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= @@ -52,9 +59,9 @@ github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.45.12 h1:+bKbbesGNPp+TeGrcqfrWuZoqcIEhjwKyBMHQPp80Jo= +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/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= @@ -97,8 +104,9 @@ github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7 github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.2 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/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= @@ -131,8 +139,8 @@ github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4s github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= -github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +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= @@ -177,6 +185,7 @@ github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/ github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -191,32 +200,38 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.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.6 h1:SW5K3sr7ptST/pIvNkSVWMiJqemRmkjJPPT0jzXdOOY= -github.com/google/certificate-transparency-go v1.1.6/go.mod h1:0OJjOsOk+wj6aYQgP7FU0ioQ0AJUmnWPFMqTjQeazPQ= +github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo= +github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.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/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/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvkiqTYKBCKLNmlge2eVjoZfySzM= +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/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= @@ -252,11 +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.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-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 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= @@ -315,6 +330,7 @@ github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dv github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -335,15 +351,18 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.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/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.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= @@ -366,8 +385,8 @@ github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peK 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/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +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= @@ -410,10 +429,11 @@ github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQ github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/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.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= @@ -429,6 +449,7 @@ github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtP 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= @@ -441,40 +462,41 @@ github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6J github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= -github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= -github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +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.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= -github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= -github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= -github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= -github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +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.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= -github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= -github.com/quic-go/quic-go v0.40.0 h1:GYd1iznlKm7dpHD7pOVpUvItgMPo/jrMgDWZhMCecqw= -github.com/quic-go/quic-go v0.40.0/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c= +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/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/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= @@ -488,6 +510,7 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E= +github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= @@ -503,6 +526,7 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/slackhq/nebula v1.6.1 h1:/OCTR3abj0Sbf2nGoLUrdDXImrCv0ZVFpVPP5qa0DsM= github.com/slackhq/nebula v1.6.1/go.mod h1:UmkqnXe4O53QwToSl/gG7sM4BroQwAB7dd4hUaT6MlI= github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= +github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= github.com/smallstep/certificates v0.25.0 h1:WWihtjQ7SprnRxDV44mBp8t5SMsNO5EWsQaEwy1rgFg= github.com/smallstep/certificates v0.25.0/go.mod h1:thJmekMKUplKYip+la99Lk4IwQej/oVH/zS9PVMagEE= github.com/smallstep/go-attestation v0.4.4-0.20230627102604-cf579e53cbd2 h1:UIAS8DTWkeclraEGH2aiJPyNPu16VbT41w4JoBlyFfU= @@ -525,8 +549,8 @@ 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.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= @@ -580,8 +604,8 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.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.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= @@ -590,8 +614,11 @@ go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q= +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= @@ -626,7 +653,10 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= +go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= go.uber.org/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= @@ -638,8 +668,10 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= -go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= +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= @@ -656,11 +688,11 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +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-20230310171629-522b1b587ee0 h1:LGJsf5LRplCck6jUCH3dBL2dmycNruWNF5xugkSlfXw= -golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +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= @@ -669,8 +701,8 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.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= @@ -692,11 +724,12 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.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.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= +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= @@ -704,8 +737,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= -golang.org/x/sync v0.4.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= @@ -727,7 +760,6 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -741,15 +773,15 @@ 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.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -758,10 +790,12 @@ 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.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.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-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/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= @@ -779,8 +813,8 @@ golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.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= @@ -788,21 +822,24 @@ 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.142.0 h1:mf+7EJ94fi5ZcnpPy+m0Yv2dkz8bKm+UL0snTCuwXlY= +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.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20231012201019-e917dd12ba7a h1:fwgW9j3vHirt4ObdHoYNwuO24BEZjSzbh+zPaNWoiY8= -google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b h1:CIC2YMXmIhYw6evmhPxBKJ4fmLbOFtXQN/GV3XOZR8k= -google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:IBQ646DjkDkvUIsVq/cc03FUFQ9wbZu7yE396YcL870= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b h1:ZlWIi1wSK56/8hn4QcBp/j9M7Gt3U/3hZw3mC7vDICo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc= +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= @@ -812,17 +849,20 @@ google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= -google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +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.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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= diff --git a/internal/filesystems/map.go b/internal/filesystems/map.go new file mode 100644 index 00000000000..e795ed1fec1 --- /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 00000000000..04b4d5b4079 --- /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 00000000000..1fbef32bfe2 --- /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 0cd3fabb7d5..34812b54f1a 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 diff --git a/listen_unix.go b/listen_unix.go index 34cd76c5c58..9ec65c39960 100644 --- a/listen_unix.go +++ b/listen_unix.go @@ -15,7 +15,7 @@ // 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 diff --git a/listen_unix_setopt.go b/listen_unix_setopt.go index c9675f928ff..13ee7b83096 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/logging.go b/logging.go index 98b031c6a2a..a076f062c7d 100644 --- a/logging.go +++ b/logging.go @@ -150,12 +150,18 @@ func (logging *Logging) setupNewDefault(ctx Context) error { logging.Logs[DefaultLoggerName] = newDefault.CustomLog } + // options for the default logger + options, err := newDefault.CustomLog.buildOptions() + if err != nil { + return fmt.Errorf("setting up default log: %v", err) + } + // set up this new log - err := newDefault.CustomLog.provision(ctx, logging) + 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) + 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 9c3fae78cc3..93a4c3d388f 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 00000000000..b3361df20de --- /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 46846d4f8a7..cdb368c4fc6 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" @@ -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 { @@ -650,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 f30a8691aeb..52a5a08c187 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 05c02320927..cc92477e5ab 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 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() - } + var cmp Comparer + args := h.RemainingArgs() - switch hashName { - case "bcrypt": - cmp = BcryptHash{} - case "scrypt": - cmp = ScryptHash{} - default: - return nil, h.Errf("unrecognized hash algorithm: %s", hashName) - } + 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() + } - ba.HashRaw = caddyconfig.JSONModuleObject(cmp, "algorithm", hashName, nil) + switch hashName { + case "bcrypt": + cmp = BcryptHash{} + default: + return nil, h.Errf("unrecognized hash algorithm: %s", hashName) + } - for h.NextBlock(0) { - username := h.Val() + ba.HashRaw = caddyconfig.JSONModuleObject(cmp, "algorithm", hashName, nil) - var b64Pwd, b64Salt string - h.Args(&b64Pwd, &b64Salt) - if h.NextArg() { - return nil, h.ArgErr() - } + for h.NextBlock(0) { + username := h.Val() - if username == "" || b64Pwd == "" { - return nil, h.Err("username and password cannot be empty or missing") - } + var b64Pwd string + h.Args(&b64Pwd) + if h.NextArg() { + return nil, h.ArgErr() + } - ba.AccountList = append(ba.AccountList, Account{ - Username: username, - Password: b64Pwd, - Salt: b64Salt, - }) + 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, + }) } return Authentication{ diff --git a/modules/caddyhttp/caddyauth/command.go b/modules/caddyhttp/caddyauth/command.go index b93b7a402f3..c9f44006037 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 324cf1e1ec3..ce3df901e68 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 a14de781429..84c0271f184 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 e997336ff8a..3761353643f 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 3604562b312..e67b87c77f9 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 065ccf2824e..00000000000 --- 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 a17d3afe738..00000000000 --- 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 25d13f7c668..e8ea4b807b2 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 - responseMatchers := make(map[string]caddyhttp.ResponseMatcher) + 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) + } - for d.Next() { - for _, arg := range d.RemainingArgs() { - mod, err := caddy.GetModule("http.encoders." + arg) + responseMatchers := make(map[string]caddyhttp.ResponseMatcher) + 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 d.Errf("finding encoder module '%s': %v", mod, err) + return err } - encoding, ok := mod.New().(Encoding) + 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 dc35fa245fc..bd3d84bc8a3 100644 --- a/modules/caddyhttp/encode/encode.go +++ b/modules/caddyhttp/encode/encode.go @@ -84,17 +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/*", }, }, } @@ -311,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 3374ee3b5c0..d76945498dd 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 0af38b92776..40f37ab8e35 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 81eb0859299..86adc7e3930 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" @@ -49,9 +50,11 @@ var BrowseTemplate string type Browse struct { // Filename of the template to use instead of the embedded browse template. TemplateFile string `json:"template_file,omitempty"` + // Determines whether or not targets of symlinks should be revealed. + RevealSymlinks bool `json:"reveal_symlinks,omitempty"` } -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)) @@ -81,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 } @@ -90,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) @@ -144,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 @@ -153,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. @@ -222,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 e0e12969e99..484d900205f 100644 --- a/modules/caddyhttp/fileserver/browse.html +++ b/modules/caddyhttp/fileserver/browse.html @@ -790,6 +790,9 @@

{{.NumFiles}} file{{if ne 1 .NumFiles}}s{{end}} + + {{.HumanTotalFileSize}} total + {{- if ne 0 .Limit}} (of which only {{.Limit}} are displayed) @@ -959,7 +962,15 @@

{{template "icon" .}} + {{- if not .SymlinkPath}} {{html .Name}} + {{- else}} + {{html .Name}} + + + + {{html .SymlinkPath}} + {{- end}} {{- if .IsDir}} diff --git a/modules/caddyhttp/fileserver/browsetplcontext.go b/modules/caddyhttp/fileserver/browsetplcontext.go index 682273c0a20..5456f0597f0 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 df56092b031..6ad9190f3c2 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() - } - fsrv.Browse = new(Browse) - default: - 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 h.NextBlock(0) { - switch h.Val() { - case "fs": - if !h.NextArg() { - return nil, h.ArgErr() - } - if fsrv.FileSystemRaw != nil { - return nil, h.Err("file system module already specified") - } - 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) + 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 = h.RemainingArgs() - if len(fsrv.Hide) == 0 { - return nil, h.ArgErr() - } + case "hide": + fsrv.Hide = d.RemainingArgs() + if len(fsrv.Hide) == 0 { + return d.ArgErr() + } - case "index": - fsrv.IndexNames = h.RemainingArgs() - if len(fsrv.IndexNames) == 0 { - return nil, h.ArgErr() - } + case "index": + fsrv.IndexNames = d.RemainingArgs() + if len(fsrv.IndexNames) == 0 { + return d.ArgErr() + } - case "root": - if !h.Args(&fsrv.Root) { - return nil, h.ArgErr() - } + case "root": + if !d.Args(&fsrv.Root) { + return d.ArgErr() + } - case "browse": - if fsrv.Browse != nil { - return nil, h.Err("browsing is already configured") + case "browse": + if fsrv.Browse != nil { + return d.Err("browsing is already configured") + } + fsrv.Browse = new(Browse) + 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()) } - 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()) + if fsrv.Browse.RevealSymlinks { + return d.Err("Symlinks path reveal is already enabled") } - fsrv.PrecompressedOrder = order + fsrv.Browse.RevealSymlinks = true + } - case "status": - if !h.NextArg() { - return nil, h.ArgErr() + 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) } - fsrv.StatusCode = caddyhttp.WeakString(h.Val()) - - case "disable_canonical_uris": - if h.NextArg() { - return nil, h.ArgErr() + inst := mod.New() + precompress, ok := inst.(encode.Precompressed) + if !ok { + return d.Errf("module %s is not a precompressor; is %T", modID, inst) } - falseBool := false - fsrv.CanonicalURIs = &falseBool - - case "pass_thru": - if h.NextArg() { - return nil, h.ArgErr() + if fsrv.PrecompressedRaw == nil { + fsrv.PrecompressedRaw = make(caddy.ModuleMap) } - fsrv.PassThru = true + fsrv.PrecompressedRaw[d.Val()] = caddyconfig.JSON(precompress, nil) + order = append(order, d.Val()) + } + fsrv.PrecompressedOrder = order - default: - return nil, h.Errf("unknown subdirective '%s'", h.Val()) + 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 2bc816743c6..a76998405d3 100644 --- a/modules/caddyhttp/fileserver/command.go +++ b/modules/caddyhttp/fileserver/command.go @@ -39,7 +39,7 @@ import ( func init() { caddycmd.RegisterCommand(caddycmd.Command{ Name: "file-server", - Usage: "[--domain ] [--root ] [--listen ] [--browse] [--access-log] [--precompressed]", + 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, @@ -62,6 +62,7 @@ respond with a file listing.`, cmd.Flags().StringP("root", "r", "", "The path to the root of the site") 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", "a", false, "Enable the access log") cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs") @@ -91,12 +92,12 @@ 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 { @@ -150,7 +151,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) { } if browse { - handler.Browse = new(Browse) + handler.Browse = &Browse{RevealSymlinks: revealSymlinks} } handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "file_server", nil)) diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go index c8f5b226e90..d4a40b58d43 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 bab34cc63e9..5ffb63b6ade 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 0ed558e8baf..57d1bc85180 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, "/") + } + // preserve the query string if present + if r.URL.RawQuery != "" { + toPath += "?" + r.URL.RawQuery } - http.Redirect(w, r, to, http.StatusPermanentRedirect) + 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 2b069100bc9..821c0d83034 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,55 +147,51 @@ 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, ":") + // 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() - } + var value, replacement string + if h.NextArg() { + value = h.Val() + } + if h.NextArg() { + replacement = h.Val() if h.NextArg() { - replacement = h.Val() - if h.NextArg() { - return nil, h.ArgErr() - } + 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()) - } + 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)...) + configValues = append(configValues, h.NewRoute(matcherSet, hdr)...) - if h.NextArg() { - return nil, h.ArgErr() - } + if h.NextArg() { + return nil, h.ArgErr() } return configValues, nil } diff --git a/modules/caddyhttp/ip_matchers.go b/modules/caddyhttp/ip_matchers.go index 57a229578ce..d5571802d3b 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 728befd2bc2..81b2830fc1e 100644 --- a/modules/caddyhttp/logging.go +++ b/modules/caddyhttp/logging.go @@ -166,7 +166,7 @@ func (e *ExtraLogFields) Set(field zap.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 00000000000..010b48919a1 --- /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 00000000000..3b554367f93 --- /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 9cc7d8c6d32..8f7b5d34e6b 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) - } + // 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) - } + // 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") } - - // cannot have more outputs than destinations - if len(outs) > len(handler.Destinations) { - return nil, h.Err("too many outputs") + handler.Defaults = h.RemainingArgs() + for len(handler.Defaults) < len(handler.Destinations) { + handler.Defaults = append(handler.Defaults, "") } + continue + } - // 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) { + // 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) - } - - // create the mapping - mapping := Mapping{Outputs: outs} - if strings.HasPrefix(in, "~") { - mapping.InputRegexp = in[1:] } else { - mapping.Input = in + outs = append(outs, val) } + } - handler.Mappings = append(handler.Mappings, mapping) + // 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 9a955e3b6f1..8f4472a27ce 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 b3859797083..f01f187fac7 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 041975d80b5..5f76a36b14d 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 9c0f961e58d..11138921811 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()) + return err } - 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 6311935ae66..8f88549d5a2 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/module.go b/modules/caddyhttp/proxyprotocol/module.go index c6dc283ea5d..75a156a2071 100644 --- a/modules/caddyhttp/proxyprotocol/module.go +++ b/modules/caddyhttp/proxyprotocol/module.go @@ -39,40 +39,40 @@ func (ListenerWrapper) CaddyModule() caddy.ModuleInfo { // 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()...) - 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: + 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/push/caddyfile.go b/modules/caddyhttp/push/caddyfile.go index 963117c1c9b..07f6ccea736 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) { - handler := new(Handler) - - for h.Next() { - if h.NextArg() { - handler.Resources = append(handler.Resources, Resource{Target: h.Val()}) - } + h.Next() // consume directive name - // 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 + handler := new(Handler) - // include current token, which we treat as an argument here - args := []string{h.Val()} - args = append(args, h.RemainingArgs()...) + // inline resources + if h.NextArg() { + handler.Resources = append(handler.Resources, Resource{Target: h.Val()}) + } - if handler.Headers == nil { - handler.Headers = new(HeaderConfig) - } + // 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 - 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() - } + // include current token, which we treat as an argument here + args := []string{h.Val()} + args = append(args, h.RemainingArgs()...) - if err != nil { - return nil, h.Err(err.Error()) - } + if handler.Headers == nil { + handler.Headers = new(HeaderConfig) } - case "GET", "HEAD": - method := h.Val() - if !h.NextArg() { + 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() } - target := h.Val() - handler.Resources = append(handler.Resources, Resource{ - Method: method, - Target: target, - }) - default: - handler.Resources = append(handler.Resources, Resource{Target: h.Val()}) + 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 ef5084e3a40..10258da0180 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" @@ -470,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 8a92909fb19..d2956cae087 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 6bac7800be5..cf96b00cda9 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) - } + 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{} - } + 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 - } + // 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{} - } + matcher.Headers = http.Header(headerMatcher) + case "status": + if matcher.StatusCode == nil { + matcher.StatusCode = []int{} + } - args := d.RemainingArgs() - if len(args) == 0 { - return d.ArgErr() - } + 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) + for _, arg := range args { + if len(arg) == 3 && strings.HasSuffix(arg, "xx") { + arg = arg[:1] } - default: - return d.Errf("unrecognized response matcher %s", d.Val()) + 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 } + matchers[definitionName] = matcher return nil } diff --git a/modules/caddyhttp/reverseproxy/admin.go b/modules/caddyhttp/reverseproxy/admin.go index f64d1ecf0aa..7e72a4cdb51 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 d1f0c1da903..de67963bd7c 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 bcbe7441914..42fd6f99d65 100644 --- a/modules/caddyhttp/reverseproxy/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/caddyfile.go @@ -210,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) { @@ -774,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.") } @@ -930,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) - } - 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 "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_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": + 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_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_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_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 "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 "versions": - h.Versions = d.RemainingArgs() - if len(h.Versions) == 0 { - return d.ArgErr() - } + 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 "compression": - if d.NextArg() { - if d.Val() == "off" { - var disable bool - h.Compression = &disable - } - } + case "versions": + h.Versions = d.RemainingArgs() + if len(h.Versions) == 0 { + return d.ArgErr() + } - 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) + case "compression": + if d.NextArg() { + if d.Val() == "off" { + var disable bool + h.Compression = &disable } - 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 @@ -1231,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 + + 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()) + 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 @@ -1271,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 @@ -1305,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() + args := d.RemainingArgs() + if len(args) > 1 { + return d.ArgErr() + } + if len(args) > 0 { + u.Name = args[0] + } - case "proto": - if !d.NextArg() { - return d.ArgErr() - } - if u.Proto != "" { - return d.Errf("srv proto has already been specified") - } - u.Proto = d.Val() + 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 "name": - if !d.NextArg() { - return d.ArgErr() - } - if u.Name != "" { - return d.Errf("srv name has already been specified") - } - u.Name = 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 "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 "name": + if !d.NextArg() { + return d.ArgErr() + } + if u.Name != "" { + return d.Errf("srv name has already been specified") + } + u.Name = d.Val() - 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 "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 "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 "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_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 "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) - default: - return d.Errf("unrecognized srv option '%s'", d.Val()) + 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 } @@ -1403,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 + + 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() + 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 "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 "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 "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_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 "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") - } + 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{} - } + 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) - } + 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()) } + + default: + return d.Errf("unrecognized a option '%s'", d.Val()) } } - return nil } @@ -1511,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 11f935cf96a..59fa67df0b5 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 a24a3ed69d4..68eee32be6a 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() - } + 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 "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 "split": + t.SplitPath = d.RemainingArgs() + if len(t.SplitPath) == 0 { + return d.ArgErr() + } - case "resolve_root_symlink": - if d.NextArg() { - return d.ArgErr() - } - t.ResolveRootSymlink = true + 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 "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 "resolve_root_symlink": + if d.NextArg() { + return d.ArgErr() + } + t.ResolveRootSymlink = true - 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 "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 "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 "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 "capture_stderr": - if d.NextArg() { - return d.ArgErr() - } - t.CaptureStderr = true + 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) - default: - return d.Errf("unrecognized subdirective %s", d.Val()) + 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 04513dd85f3..d944c5778c1 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 29bb5785b18..a2227a65332 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/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index 187bccc660c..0a803a83a0e 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -491,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. @@ -556,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 } @@ -579,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 1a76aef4c5b..1648b779123 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -487,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, noUpstreamsAvailable) + proxyErr = caddyhttp.Error(http.StatusServiceUnavailable, errNoUpstream) } if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r) { return true, proxyErr @@ -589,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 { @@ -779,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 @@ -1037,7 +1041,7 @@ func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int // we have to assume the upstream received the request, and // retries need to be carefully decided, because some requests // are not idempotent - if !isDialError && !(isHandlerError && errors.Is(herr, noUpstreamsAvailable)) { + 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 @@ -1093,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 @@ -1415,38 +1419,13 @@ 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" -var noUpstreamsAvailable = fmt.Errorf("no upstreams available") +// errNoUpstream occurs when there are no upstream available. +var errNoUpstream = fmt.Errorf("no upstreams available") // Interface guards var ( diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies.go b/modules/caddyhttp/reverseproxy/selectionpolicies.go index acb069a74de..b6f807c16d2 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 } @@ -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 dc613a53896..d7e79626c1b 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 155a1df0c4a..2a5b5286a17 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 } diff --git a/modules/caddyhttp/reverseproxy/upstreams.go b/modules/caddyhttp/reverseproxy/upstreams.go index 2d21a5ca4ed..46e45c64652 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)) diff --git a/modules/caddyhttp/rewrite/caddyfile.go b/modules/caddyhttp/rewrite/caddyfile.go index a34c1bb08fd..31f7e9b4859 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() + 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, + }) - 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) - } + 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 } + } - rewr.URISubstring = append(rewr.URISubstring, substrReplacer{ - Find: find, - Replace: replace, - Limit: limInt, - }) - case "path_regexp": - if len(args) != 3 { - return nil, h.ArgErr() + 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 } - 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]) } + + 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 77ef668bfa3..1859f9df2b5 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 bb937ec5bad..aaa142bc23e 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 d060738f1dc..04ae003a7c4 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. @@ -240,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. @@ -288,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)) } } @@ -618,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 { @@ -695,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 } @@ -839,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, @@ -884,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 96a241ba8a4..fd0e1e3497c 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 d7bb2800c91..236e7be1e09 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 7cf1a3e9543..b6e10ff328c 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 4fe5910e40e..3ec76f29f8d 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) - if err != nil { - return caddy.ExitCodeFailedStartup, err + + 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 } - buf := new(bytes.Buffer) - err = tpl.Execute(buf, tplCtx) + } else { + server, err := buildHTTPServer(0, 0, listen, statusCode, hdr, body, accessLog) 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 c3039aa890e..d23493483d9 100644 --- a/modules/caddyhttp/templates/caddyfile.go +++ b/modules/caddyhttp/templates/caddyfile.go @@ -34,47 +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() - } - 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() + 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 } - if t.ExtensionsRaw != nil { - return nil, h.Err("extensions already specified") + cf, ok := unm.(CustomFunctions) + if !ok { + return nil, h.Errf("module %s (%T) does not provide template functions", modID, unm) } - 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 - } - cf, ok := unm.(CustomFunctions) - if !ok { - return nil, h.Errf("module %s (%T) does not provide template functions", modID, unm) - } - if t.ExtensionsRaw == nil { - t.ExtensionsRaw = make(caddy.ModuleMap) - } - t.ExtensionsRaw[extensionModuleName] = caddyconfig.JSON(cf, nil) + 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 418f09e531c..b97093b6f63 100644 --- a/modules/caddyhttp/templates/templates.go +++ b/modules/caddyhttp/templates/templates.go @@ -329,7 +329,7 @@ type Templates struct { 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 diff --git a/modules/caddyhttp/templates/tplcontext_test.go b/modules/caddyhttp/templates/tplcontext_test.go index fdf2c1065b6..67ebbac7031 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 fd117c53757..85fd630020e 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 cb2e5936f9a..5a5df0a23f2 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 d2d7f62c5b1..f5afe264a28 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 8ac1b6c892e..eaf6e930ab2 100644 --- a/modules/caddypki/acmeserver/acmeserver.go +++ b/modules/caddypki/acmeserver/acmeserver.go @@ -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 3b52113b527..7eaaec49a40 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() - } - - dur, err := caddy.ParseDuration(h.Val()) - if err != nil { - return nil, err - } + 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() + } - if d := time.Duration(ca.IntermediateLifetime); d > 0 && dur > d { - return nil, h.Errf("certificate lifetime (%s) exceeds intermediate certificate lifetime (%s)", dur, d) + 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()) + } } - - 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") + } + 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 00000000000..728a7492813 --- /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 00000000000..96137e6e642 --- /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 00000000000..02d7856d970 --- /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 a7dbd26eccc..8a7f8b49910 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -16,12 +16,8 @@ package caddytls import ( "context" - "crypto/tls" "crypto/x509" - "errors" "fmt" - "net" - "net/url" "os" "strconv" "time" @@ -277,263 +273,221 @@ 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() - if d.NextArg() { - return d.ArgErr() - } + 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() - } + 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 "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 "test_dir": + if !d.AllArgs(&iss.TestCA) { + return d.ArgErr() + } - 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 "email": + if !d.AllArgs(&iss.Email) { + return d.ArgErr() + } - 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 "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 "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 "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 "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 "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 "eab": - iss.ExternalAccount = new(acme.EAB) - if !d.AllArgs(&iss.ExternalAccount.KeyID, &iss.ExternalAccount.MACKey) { - return d.ArgErr() - } + 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 "trusted_roots": - iss.TrustedRootsPEMFiles = d.RemainingArgs() + 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 "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 "eab": + iss.ExternalAccount = new(acme.EAB) + if !d.AllArgs(&iss.ExternalAccount.KeyID, &iss.ExternalAccount.MACKey) { + return d.ArgErr() + } - 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 "trusted_roots": + iss.TrustedRootsPEMFiles = d.RemainingArgs() - 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 "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 "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 "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 "dns_ttl": - if !d.NextArg() { - return d.ArgErr() - } - ttlStr := d.Val() - ttl, err := caddy.ParseDuration(ttlStr) + 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 dns_ttl duration %s: %v", ttlStr, err) - } - if iss.Challenges == nil { - iss.Challenges = new(ChallengesConfig) + return d.Errf("invalid propagation_timeout duration %s: %v", timeoutStr, err) } - if iss.Challenges.DNS == nil { - iss.Challenges.DNS = new(DNSChallengeConfig) - } - iss.Challenges.DNS.TTL = caddy.Duration(ttl) + } + 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 "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 "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 "preferred_chains": - chainPref, err := ParseCaddyfilePreferredChainsOptions(d) - if err != nil { - return err - } - iss.PreferredChains = chainPref + 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) - default: - return d.Errf("unrecognized ACME issuer property: %s", d.Val()) + case "dns_challenge_override_domain": + arg := d.RemainingArgs() + if len(arg) != 1 { + return d.ArgErr() } - } - } - return nil -} + if iss.Challenges == nil { + iss.Challenges = new(ChallengesConfig) + } + if iss.Challenges.DNS == nil { + iss.Challenges.DNS = new(DNSChallengeConfig) + } + iss.Challenges.DNS.OverrideDomain = arg[0] -// 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(ctx context.Context, 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() + case "preferred_chains": + chainPref, err := ParseCaddyfilePreferredChainsOptions(d) + if err != nil { + return err + } + iss.PreferredChains = chainPref - 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() - - // logging out the client IP can be useful for servers that want to count - // attempts from clients to detect patterns of abuse - var clientIP string - if hello, ok := ctx.Value(certmagic.ClientHelloInfoCtxKey).(*tls.ClientHelloInfo); ok && hello != nil { - if remote := hello.Conn.RemoteAddr(); remote != nil { - clientIP, _, _ = net.SplitHostPort(remote.String()) + default: + return d.Errf("unrecognized ACME issuer property: %s", d.Val()) } } - - logger.Debug("response from ask endpoint", - zap.String("client_ip", clientIP), - 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 } @@ -604,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 6d085ee3f2d..a90e5ded803 100644 --- a/modules/caddytls/automation.go +++ b/modules/caddytls/automation.go @@ -16,12 +16,12 @@ package caddytls import ( "context" + "crypto/tls" "encoding/json" "errors" "fmt" - "net/http" + "net" "strings" - "time" "github.com/caddyserver/certmagic" "github.com/mholt/acmez" @@ -138,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 @@ -245,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(ctx context.Context, name string) error { if tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil { return nil } - if err := onDemandAskRequest(ctx, 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, @@ -288,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, @@ -454,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 @@ -498,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 00000000000..44a2fa2c2f9 --- /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 00000000000..d04354a1898 --- /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 ad26468a9dd..9bb436a3746 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/connpolicy.go b/modules/caddytls/connpolicy.go index 64fdd513861..49c7add49d3 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 00000000000..573b9c163d1 --- /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/internalissuer.go b/modules/caddytls/internalissuer.go index 1cf2461ab7c..0d7f4157ec0 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 00000000000..1d3f3a3e59f --- /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 00000000000..940ed78bd70 --- /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 00000000000..5c7b06e7681 --- /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 00000000000..35fecba89b3 --- /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 00000000000..28467ccf2c7 --- /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 00000000000..04a9efd2531 --- /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 00000000000..0215c8af2a6 --- /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 00000000000..31f6ef2dceb --- /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/tls.go b/modules/caddytls/tls.go index b66b09c4de9..2ec7bd8fb86 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", diff --git a/modules/caddytls/zerosslissuer.go b/modules/caddytls/zerosslissuer.go index 697bab07d1d..1c091a076e8 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 672c66cbe3c..76aff4e8b7a 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 00000000000..63bd532d02b --- /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 a4409e7e459..7399423bfc4 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 11c051d783f..3b1001b7c2b 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)) - 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)) + 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 4d51e645c36..c46df0788bf 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 233d5d7133e..79d908fca63 100644 --- a/modules/logging/filters.go +++ b/modules/logging/filters.go @@ -128,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 } @@ -169,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 - - 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()) + d.Next() // consume filter name + + 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 + + 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 @@ -328,45 +347,44 @@ 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() + } + + 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() } - m.Actions = append(m.Actions, qfa) + 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 } @@ -460,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() + } + + 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() } - m.Actions = append(m.Actions, cfa) + 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 } @@ -571,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 } @@ -625,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/netwriter.go b/modules/logging/netwriter.go index 1939cb711dc..dc2b0922cba 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 a9e0f0efa54..dc6196a15c8 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 a9d0b396825..813c63d6c44 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 5d33b7f18de..046be867a06 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 41ada7d6da0..c8868394767 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 {