diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 705c3f0f816..cba485386b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,18 +27,18 @@ jobs: - mac - windows go: - - '1.21' - '1.22' + - '1.23' include: # Set the minimum Go patch version for the given Go minor # Usable via ${{ matrix.GO_SEMVER }} - - go: '1.21' - GO_SEMVER: '~1.21.0' - - go: '1.22' GO_SEMVER: '~1.22.3' + - go: '1.23' + GO_SEMVER: '~1.23.0' + # Set some variables per OS, usable via ${{ matrix.VAR }} # OS_LABEL: the VM label from GitHub Actions (see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories) # CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing @@ -103,7 +103,7 @@ jobs: env: CGO_ENABLED: 0 run: | - go build -tags nobdger -trimpath -ldflags="-w -s" -v + go build -tags nobadger -trimpath -ldflags="-w -s" -v - name: Smoke test Caddy working-directory: ./cmd/caddy @@ -178,6 +178,7 @@ jobs: uses: actions/checkout@v4 - name: Run Tests run: | + set +e mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa # short sha is enough? @@ -185,7 +186,7 @@ jobs: # The environment is fresh, so there's no point in keeping accepting and adding the key. rsync -arz -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" --progress --delete --exclude '.git' . "$CI_USER"@ci-s390x.caddyserver.com:/var/tmp/"$short_sha" - ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t "$CI_USER"@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -tags nobadger -v ./..." + ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t "$CI_USER"@ci-s390x.caddyserver.com "cd /var/tmp/$short_sha; go version; go env; printf "\n\n";CGO_ENABLED=0 go test -p 1 -tags nobadger -v ./..." test_result=$? # There's no need leaving the files around @@ -203,7 +204,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - uses: goreleaser/goreleaser-action@v5 + - uses: goreleaser/goreleaser-action@v6 with: version: latest args: check diff --git a/.github/workflows/cross-build.yml b/.github/workflows/cross-build.yml index c54a9a80dbf..e77e4e992d0 100644 --- a/.github/workflows/cross-build.yml +++ b/.github/workflows/cross-build.yml @@ -28,6 +28,7 @@ jobs: - 'netbsd' go: - '1.22' + - '1.23' include: # Set the minimum Go patch version for the given Go minor @@ -35,6 +36,9 @@ jobs: - go: '1.22' GO_SEMVER: '~1.22.3' + - go: '1.23' + GO_SEMVER: '~1.23.0' + runs-on: ubuntu-latest continue-on-error: true steps: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1ebdb0afea1..94250f88f35 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -43,13 +43,13 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '~1.22.3' + go-version: '~1.23' check-latest: true - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: - version: v1.55 + version: v1.60 # Windows times out frequently after about 5m50s if we don't set a longer timeout. args: --timeout 10m @@ -63,5 +63,5 @@ jobs: - name: govulncheck uses: golang/govulncheck-action@v1 with: - go-version-input: '~1.22.3' + go-version-input: '~1.23.0' check-latest: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 514237ffb1a..1eb59e9d02d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,13 +13,13 @@ jobs: os: - ubuntu-latest go: - - '1.22' + - '1.23' include: # Set the minimum Go patch version for the given Go minor # Usable via ${{ matrix.GO_SEMVER }} - - go: '1.22' - GO_SEMVER: '~1.22.3' + - go: '1.23' + GO_SEMVER: '~1.23.0' runs-on: ${{ matrix.os }} # https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233 @@ -106,7 +106,7 @@ jobs: run: syft version # GoReleaser will take care of publishing those artifacts into the release - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v5 + uses: goreleaser/goreleaser-action@v6 with: version: latest args: release --clean --timeout 60m diff --git a/.golangci.yml b/.golangci.yml index d144395dbda..74e3503c484 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,7 +1,9 @@ linters-settings: errcheck: - ignore: fmt:.*,go.uber.org/zap/zapcore:^Add.* - ignoretests: true + exclude-functions: + - fmt.* + - (go.uber.org/zap/zapcore.ObjectEncoder).AddObject + - (go.uber.org/zap/zapcore.ObjectEncoder).AddArray gci: sections: - standard # Standard section: captures all standard packages. @@ -130,13 +132,14 @@ linters: run: # default concurrency is a available CPU number. # concurrency: 4 # explicitly omit this value to fully utilize available resources. - deadline: 5m + timeout: 5m issues-exit-code: 1 tests: false # output configuration options output: - format: 'colored-line-number' + formats: + - format: 'colored-line-number' print-issued-lines: true print-linter-name: true @@ -166,3 +169,6 @@ issues: - path: modules/logging/filters.go linters: - dupl + - path: _test\.go + linters: + - errcheck diff --git a/.goreleaser.yml b/.goreleaser.yml index 0fba8e2c435..c7d01571ee4 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,3 +1,5 @@ +version: 2 + before: hooks: # The build is done in this particular way to build Caddy in a designated directory named in .gitignore. @@ -10,6 +12,10 @@ before: - mkdir -p caddy-build - cp cmd/caddy/main.go caddy-build/main.go - /bin/sh -c 'cd ./caddy-build && go mod init caddy' + # prepare syso files for windows embedding + - go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest + - /bin/sh -c 'for a in amd64 arm arm64; do XCADDY_SKIP_BUILD=1 GOOS=windows GOARCH=$a $GOPATH/bin/xcaddy build {{.Env.TAG}}; done' + - /bin/sh -c 'mv /tmp/buildenv_*/*.syso caddy-build' # GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env # so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate - go mod edit -require=github.com/caddyserver/caddy/v2@{{.Env.TAG}} ./caddy-build/go.mod @@ -29,7 +35,6 @@ builds: - env: - CGO_ENABLED=0 - GO111MODULE=on - main: main.go dir: ./caddy-build binary: caddy goos: diff --git a/README.md b/README.md index 57463180fa3..2185eccd8cf 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ See [our online documentation](https://caddyserver.com/docs/install) for other i Requirements: -- [Go 1.21 or newer](https://golang.org/dl/) +- [Go 1.22.3 or newer](https://golang.org/dl/) ### For development diff --git a/caddy.go b/caddy.go index 27acabb1267..7dd989c9e1e 100644 --- a/caddy.go +++ b/caddy.go @@ -397,6 +397,58 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error { // will want to use Run instead, which also // updates the config's raw state. func run(newCfg *Config, start bool) (Context, error) { + ctx, err := provisionContext(newCfg, start) + if err != nil { + return ctx, err + } + + if !start { + return ctx, nil + } + + // Provision any admin routers which may need to access + // some of the other apps at runtime + err = ctx.cfg.Admin.provisionAdminRouters(ctx) + if err != nil { + return ctx, err + } + + // Start + err = func() error { + started := make([]string, 0, len(ctx.cfg.apps)) + for name, a := range ctx.cfg.apps { + err := a.Start() + if err != nil { + // an app failed to start, so we need to stop + // all other apps that were already started + for _, otherAppName := range started { + err2 := ctx.cfg.apps[otherAppName].Stop() + if err2 != nil { + err = fmt.Errorf("%v; additionally, aborting app %s: %v", + err, otherAppName, err2) + } + } + return fmt.Errorf("%s app module: start: %v", name, err) + } + started = append(started, name) + } + return nil + }() + if err != nil { + return ctx, err + } + + // now that the user's config is running, finish setting up anything else, + // such as remote admin endpoint, config loader, etc. + return ctx, finishSettingUp(ctx, ctx.cfg) +} + +// provisionContext creates a new context from the given configuration and provisions +// storage and apps. +// If `newCfg` is nil a new empty configuration will be created. +// If `replaceAdminServer` is true any currently active admin server will be replaced +// with a new admin server based on the provided configuration. +func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error) { // because we will need to roll back any state // modifications if this function errors, we // keep a single error value and scope all @@ -444,7 +496,7 @@ func run(newCfg *Config, start bool) (Context, error) { } // start the admin endpoint (and stop any prior one) - if start { + if replaceAdminServer { err = replaceLocalAdminServer(newCfg) if err != nil { return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err) @@ -491,49 +543,16 @@ func run(newCfg *Config, start bool) (Context, error) { } return nil }() - if err != nil { - return ctx, err - } - - if !start { - return ctx, nil - } - - // Provision any admin routers which may need to access - // some of the other apps at runtime - err = newCfg.Admin.provisionAdminRouters(ctx) - if err != nil { - return ctx, err - } - - // Start - err = func() error { - started := make([]string, 0, len(newCfg.apps)) - for name, a := range newCfg.apps { - err := a.Start() - if err != nil { - // an app failed to start, so we need to stop - // all other apps that were already started - for _, otherAppName := range started { - err2 := newCfg.apps[otherAppName].Stop() - if err2 != nil { - err = fmt.Errorf("%v; additionally, aborting app %s: %v", - err, otherAppName, err2) - } - } - return fmt.Errorf("%s app module: start: %v", name, err) - } - started = append(started, name) - } - return nil - }() - if err != nil { - return ctx, err - } + return ctx, err +} - // now that the user's config is running, finish setting up anything else, - // such as remote admin endpoint, config loader, etc. - return ctx, finishSettingUp(ctx, newCfg) +// ProvisionContext creates a new context from the configuration and provisions storage +// and app modules. +// The function is intended for testing and advanced use cases only, typically `Run` should be +// use to ensure a fully functional caddy instance. +// EXPERIMENTAL: While this is public the interface and implementation details of this function may change. +func ProvisionContext(newCfg *Config) (Context, error) { + return provisionContext(newCfg, false) } // finishSettingUp should be run after all apps have successfully started. diff --git a/caddyconfig/caddyfile/dispenser.go b/caddyconfig/caddyfile/dispenser.go index e36275a1c1b..325bb54d3f3 100644 --- a/caddyconfig/caddyfile/dispenser.go +++ b/caddyconfig/caddyfile/dispenser.go @@ -415,7 +415,7 @@ func (d *Dispenser) EOFErr() error { // Err generates a custom parse-time error with a message of msg. func (d *Dispenser) Err(msg string) error { - return d.Errf(msg) + return d.WrapErr(errors.New(msg)) } // Errf is like Err, but for formatted error messages diff --git a/caddyconfig/caddyfile/parse.go b/caddyconfig/caddyfile/parse.go index 17b0ca8e28a..17d824efd5d 100644 --- a/caddyconfig/caddyfile/parse.go +++ b/caddyconfig/caddyfile/parse.go @@ -364,9 +364,45 @@ func (p *parser) doImport(nesting int) error { // set up a replacer for non-variadic args replacement repl := makeArgsReplacer(args) + // grab all the tokens (if it exists) from within a block that follows the import + var blockTokens []Token + for currentNesting := p.Nesting(); p.NextBlock(currentNesting); { + blockTokens = append(blockTokens, p.Token()) + } + // initialize with size 1 + blockMapping := make(map[string][]Token, 1) + if len(blockTokens) > 0 { + // use such tokens to create a new dispenser, and then use it to parse each block + bd := NewDispenser(blockTokens) + for bd.Next() { + // see if we can grab a key + var currentMappingKey string + if bd.Val() == "{" { + return p.Err("anonymous blocks are not supported") + } + currentMappingKey = bd.Val() + currentMappingTokens := []Token{} + // read all args until end of line / { + if bd.NextArg() { + currentMappingTokens = append(currentMappingTokens, bd.Token()) + for bd.NextArg() { + currentMappingTokens = append(currentMappingTokens, bd.Token()) + } + // TODO(elee1766): we don't enter another mapping here because it's annoying to extract the { and } properly. + // maybe someone can do that in the future + } else { + // attempt to enter a block and add tokens to the currentMappingTokens + for mappingNesting := bd.Nesting(); bd.NextBlock(mappingNesting); { + currentMappingTokens = append(currentMappingTokens, bd.Token()) + } + } + blockMapping[currentMappingKey] = currentMappingTokens + } + } + // splice out the import directive and its arguments // (2 tokens, plus the length of args) - tokensBefore := p.tokens[:p.cursor-1-len(args)] + tokensBefore := p.tokens[:p.cursor-1-len(args)-len(blockTokens)] tokensAfter := p.tokens[p.cursor+1:] var importedTokens []Token var nodes []string @@ -495,6 +531,33 @@ func (p *parser) doImport(nesting int) error { maybeSnippet = false } } + // if it is {block}, we substitute with all tokens in the block + // if it is {blocks.*}, we substitute with the tokens in the mapping for the * + var skip bool + var tokensToAdd []Token + switch { + case token.Text == "{block}": + tokensToAdd = blockTokens + case strings.HasPrefix(token.Text, "{blocks.") && strings.HasSuffix(token.Text, "}"): + // {blocks.foo.bar} will be extracted to key `foo.bar` + blockKey := strings.TrimPrefix(strings.TrimSuffix(token.Text, "}"), "{blocks.") + val, ok := blockMapping[blockKey] + if ok { + tokensToAdd = val + } + default: + skip = true + } + if !skip { + if len(tokensToAdd) == 0 { + // if there is no content in the snippet block, don't do any replacement + // this allows snippets which contained {block}/{block.*} before this change to continue functioning as normal + tokensCopy = append(tokensCopy, token) + } else { + tokensCopy = append(tokensCopy, tokensToAdd...) + } + continue + } if maybeSnippet { tokensCopy = append(tokensCopy, token) @@ -516,7 +579,7 @@ func (p *parser) doImport(nesting int) error { // splice the imported tokens in the place of the import statement // and rewind cursor so Next() will land on first imported token p.tokens = append(tokensBefore, append(tokensCopy, tokensAfter...)...) - p.cursor -= len(args) + 1 + p.cursor -= len(args) + len(blockTokens) + 1 return nil } diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index 35a08ef2782..e1e95e00a37 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -849,6 +849,7 @@ func parseInvoke(h Helper) (caddyhttp.MiddlewareHandler, error) { // log { // hostnames // output ... +// core ... // format ... // level // } @@ -960,6 +961,22 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue } cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings) + case "core": + if !h.NextArg() { + return nil, h.ArgErr() + } + moduleName := h.Val() + moduleID := "caddy.logging.cores." + moduleName + unm, err := caddyfile.UnmarshalModule(h.Dispenser, moduleID) + if err != nil { + return nil, err + } + core, ok := unm.(zapcore.Core) + if !ok { + return nil, h.Errf("module %s (%T) is not a zapcore.Core", moduleID, unm) + } + cl.CoreRaw = caddyconfig.JSONModuleObject(core, "module", moduleName, h.warnings) + case "format": if !h.NextArg() { return nil, h.ArgErr() diff --git a/caddyconfig/httpcaddyfile/builtins_test.go b/caddyconfig/httpcaddyfile/builtins_test.go index 70f347dd9dc..cf746348487 100644 --- a/caddyconfig/httpcaddyfile/builtins_test.go +++ b/caddyconfig/httpcaddyfile/builtins_test.go @@ -25,11 +25,12 @@ func TestLogDirectiveSyntax(t *testing.T) { { input: `:8080 { log { + core mock output file foo.log } } `, - output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"writer":{"filename":"foo.log","output":"file"},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`, + output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"writer":{"filename":"foo.log","output":"file"},"core":{"module":"mock"},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`, expectError: false, }, { @@ -53,11 +54,12 @@ func TestLogDirectiveSyntax(t *testing.T) { { input: `:8080 { log name-override { + core mock output file foo.log } } `, - output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.name-override"]},"name-override":{"writer":{"filename":"foo.log","output":"file"},"include":["http.log.access.name-override"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"name-override"}}}}}}`, + output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.name-override"]},"name-override":{"writer":{"filename":"foo.log","output":"file"},"core":{"module":"mock"},"include":["http.log.access.name-override"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"name-override"}}}}}}`, expectError: false, }, } { diff --git a/caddytest/caddytest_test.go b/caddytest/caddytest_test.go index 937537faa7c..a9d5da9365b 100644 --- a/caddytest/caddytest_test.go +++ b/caddytest/caddytest_test.go @@ -84,7 +84,6 @@ func TestLoadUnorderedJSON(t *testing.T) { "servers": { "s_server": { "listen": [ - ":9443", ":9080" ], "routes": [ diff --git a/caddytest/integration/caddyfile_adapt/file_server_sort.caddyfiletest b/caddytest/integration/caddyfile_adapt/file_server_sort.caddyfiletest new file mode 100644 index 00000000000..7f07cba8051 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/file_server_sort.caddyfiletest @@ -0,0 +1,39 @@ +:80 + +file_server { + browse { + sort size desc + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":80" + ], + "routes": [ + { + "handle": [ + { + "browse": { + "sort": [ + "size", + "desc" + ] + }, + "handler": "file_server", + "hide": [ + "./Caddyfile" + ] + } + ] + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/caddytest/integration/caddyfile_adapt/import_block_snippet.caddyfiletest b/caddytest/integration/caddyfile_adapt/import_block_snippet.caddyfiletest new file mode 100644 index 00000000000..a60c238ceb6 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/import_block_snippet.caddyfiletest @@ -0,0 +1,58 @@ +(snippet) { + header { + {block} + } +} + +example.com { + import snippet { + foo bar + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "headers", + "response": { + "set": { + "Foo": [ + "bar" + ] + } + } + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/import_block_snippet_args.caddyfiletest b/caddytest/integration/caddyfile_adapt/import_block_snippet_args.caddyfiletest new file mode 100644 index 00000000000..7f2e68b7995 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/import_block_snippet_args.caddyfiletest @@ -0,0 +1,56 @@ +(snippet) { + {block} +} + +example.com { + import snippet { + header foo bar + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "headers", + "response": { + "set": { + "Foo": [ + "bar" + ] + } + } + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/import_blocks_snippet.caddyfiletest b/caddytest/integration/caddyfile_adapt/import_blocks_snippet.caddyfiletest new file mode 100644 index 00000000000..4098f90b2c1 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/import_blocks_snippet.caddyfiletest @@ -0,0 +1,76 @@ +(snippet) { + header { + {blocks.foo} + } + header { + {blocks.bar} + } +} + +example.com { + import snippet { + foo { + foo a + } + bar { + bar b + } + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "headers", + "response": { + "set": { + "Foo": [ + "a" + ] + } + } + }, + { + "handler": "headers", + "response": { + "set": { + "Bar": [ + "b" + ] + } + } + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/import_blocks_snippet_nested.caddyfiletest b/caddytest/integration/caddyfile_adapt/import_blocks_snippet_nested.caddyfiletest new file mode 100644 index 00000000000..ac1c5226c1c --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/import_blocks_snippet_nested.caddyfiletest @@ -0,0 +1,82 @@ +(snippet) { + header { + {blocks.bar} + } + import sub_snippet { + bar { + {blocks.foo} + } + } +} +(sub_snippet) { + header { + {blocks.bar} + } +} +example.com { + import snippet { + foo { + foo a + } + bar { + bar b + } + } +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "headers", + "response": { + "set": { + "Bar": [ + "b" + ] + } + } + }, + { + "handler": "headers", + "response": { + "set": { + "Foo": [ + "a" + ] + } + } + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_health_method.caddyfiletest b/caddytest/integration/caddyfile_adapt/reverse_proxy_health_method.caddyfiletest new file mode 100644 index 00000000000..920702c10b4 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_health_method.caddyfiletest @@ -0,0 +1,40 @@ +:8884 + +reverse_proxy 127.0.0.1:65535 { + health_uri /health + health_method HEAD +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":8884" + ], + "routes": [ + { + "handle": [ + { + "handler": "reverse_proxy", + "health_checks": { + "active": { + "method": "HEAD", + "uri": "/health" + } + }, + "upstreams": [ + { + "dial": "127.0.0.1:65535" + } + ] + } + ] + } + ] + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_health_reqbody.caddyfiletest b/caddytest/integration/caddyfile_adapt/reverse_proxy_health_reqbody.caddyfiletest new file mode 100644 index 00000000000..ae5a6791ef7 --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_health_reqbody.caddyfiletest @@ -0,0 +1,40 @@ +:8884 + +reverse_proxy 127.0.0.1:65535 { + health_uri /health + health_request_body "test body" +} +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":8884" + ], + "routes": [ + { + "handle": [ + { + "handler": "reverse_proxy", + "health_checks": { + "active": { + "body": "test body", + "uri": "/health" + } + }, + "upstreams": [ + { + "dial": "127.0.0.1:65535" + } + ] + } + ] + } + ] + } + } + } + } +} diff --git a/caddytest/integration/caddyfile_adapt/reverse_proxy_localaddr.caddyfiletest b/caddytest/integration/caddyfile_adapt/reverse_proxy_localaddr.caddyfiletest new file mode 100644 index 00000000000..d734c9ce09a --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/reverse_proxy_localaddr.caddyfiletest @@ -0,0 +1,57 @@ +https://example.com { + reverse_proxy http://localhost:54321 { + transport http { + local_address 192.168.0.1 + } + } +} + +---------- +{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":443" + ], + "routes": [ + { + "match": [ + { + "host": [ + "example.com" + ] + } + ], + "handle": [ + { + "handler": "subroute", + "routes": [ + { + "handle": [ + { + "handler": "reverse_proxy", + "transport": { + "local_address": "192.168.0.1", + "protocol": "http" + }, + "upstreams": [ + { + "dial": "localhost:54321" + } + ] + } + ] + } + ] + } + ], + "terminal": true + } + ] + } + } + } + } +} diff --git a/caddytest/integration/intercept_test.go b/caddytest/integration/intercept_test.go index 81db6a7d639..6f8ffc92959 100644 --- a/caddytest/integration/intercept_test.go +++ b/caddytest/integration/intercept_test.go @@ -18,17 +18,23 @@ func TestIntercept(t *testing.T) { localhost:9080 { respond /intercept "I'm a teapot" 408 + header /intercept To-Intercept ok respond /no-intercept "I'm not a teapot" intercept { @teapot status 408 handle_response @teapot { + header /intercept intercepted {resp.header.To-Intercept} respond /intercept "I'm a combined coffee/tea pot that is temporarily out of coffee" 503 } } } `, "caddyfile") - tester.AssertGetResponse("http://localhost:9080/intercept", 503, "I'm a combined coffee/tea pot that is temporarily out of coffee") + r, _ := tester.AssertGetResponse("http://localhost:9080/intercept", 503, "I'm a combined coffee/tea pot that is temporarily out of coffee") + if r.Header.Get("intercepted") != "ok" { + t.Fatalf(`header "intercepted" value is not "ok": %s`, r.Header.Get("intercepted")) + } + tester.AssertGetResponse("http://localhost:9080/no-intercept", 200, "I'm not a teapot") } diff --git a/caddytest/integration/testdata/foo_with_multiple_trailing_newlines.txt b/caddytest/integration/testdata/foo_with_multiple_trailing_newlines.txt new file mode 100644 index 00000000000..75d7bfb873a --- /dev/null +++ b/caddytest/integration/testdata/foo_with_multiple_trailing_newlines.txt @@ -0,0 +1,2 @@ +foo + diff --git a/caddytest/integration/testdata/foo_with_trailing_newline.txt b/caddytest/integration/testdata/foo_with_trailing_newline.txt new file mode 100644 index 00000000000..257cc5642cb --- /dev/null +++ b/caddytest/integration/testdata/foo_with_trailing_newline.txt @@ -0,0 +1 @@ +foo diff --git a/cmd/caddy/main.go b/cmd/caddy/main.go index 48fa149aa08..f1aeda0a420 100644 --- a/cmd/caddy/main.go +++ b/cmd/caddy/main.go @@ -1,3 +1,8 @@ +// The below line is required to enable post-quantum key agreement in Go 1.23 +// by default without insisting on setting a minimum version of 1.23 in go.mod. +// See https://github.com/caddyserver/caddy/issues/6540#issuecomment-2313094905 +//go:debug tlskyber=1 + // Copyright 2015 Matthew Holt and The Caddy Authors // // Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/cmd/cobra.go b/cmd/cobra.go index 1a2509206a8..9ecb389e2aa 100644 --- a/cmd/cobra.go +++ b/cmd/cobra.go @@ -8,9 +8,10 @@ import ( "github.com/caddyserver/caddy/v2" ) -var rootCmd = &cobra.Command{ - Use: "caddy", - Long: `Caddy is an extensible server platform written in Go. +var defaultFactory = newRootCommandFactory(func() *cobra.Command { + return &cobra.Command{ + Use: "caddy", + Long: `Caddy is an extensible server platform written in Go. At its core, Caddy merely manages configuration. Modules are plugged in statically at compile-time to provide useful functionality. Caddy's @@ -91,23 +92,26 @@ package installers: https://caddyserver.com/docs/install Instructions for running Caddy in production are also available: https://caddyserver.com/docs/running `, - Example: ` $ caddy run + Example: ` $ caddy run $ caddy run --config caddy.json $ caddy reload --config caddy.json $ caddy stop`, - // kind of annoying to have all the help text printed out if - // caddy has an error provisioning its modules, for instance... - SilenceUsage: true, - Version: onlyVersionText(), -} + // kind of annoying to have all the help text printed out if + // caddy has an error provisioning its modules, for instance... + SilenceUsage: true, + Version: onlyVersionText(), + } +}) const fullDocsFooter = `Full documentation is available at: https://caddyserver.com/docs/command-line` func init() { - rootCmd.SetVersionTemplate("{{.Version}}\n") - rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n") + defaultFactory.Use(func(rootCmd *cobra.Command) { + rootCmd.SetVersionTemplate("{{.Version}}\n") + rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n") + }) } func onlyVersionText() string { diff --git a/cmd/commandfactory.go b/cmd/commandfactory.go new file mode 100644 index 00000000000..ac571a21ae1 --- /dev/null +++ b/cmd/commandfactory.go @@ -0,0 +1,28 @@ +package caddycmd + +import ( + "github.com/spf13/cobra" +) + +type rootCommandFactory struct { + constructor func() *cobra.Command + options []func(*cobra.Command) +} + +func newRootCommandFactory(fn func() *cobra.Command) *rootCommandFactory { + return &rootCommandFactory{ + constructor: fn, + } +} + +func (f *rootCommandFactory) Use(fn func(cmd *cobra.Command)) { + f.options = append(f.options, fn) +} + +func (f *rootCommandFactory) Build() *cobra.Command { + o := f.constructor() + for _, v := range f.options { + v(o) + } + return o +} diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index 746cf3da6b6..49d0321efae 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -74,6 +74,10 @@ func cmdStart(fl Flags) (int, error) { // sure by giving it some random bytes and having it echo // them back to us) cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String()) + // we should be able to run caddy in relative paths + if errors.Is(cmd.Err, exec.ErrDot) { + cmd.Err = nil + } if configFlag != "" { cmd.Args = append(cmd.Args, "--config", configFlag) } diff --git a/cmd/commands.go b/cmd/commands.go index e5e1265e441..0853ebf8359 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -438,43 +438,44 @@ EXPERIMENTAL: May be changed or removed. }, }) - RegisterCommand(Command{ - Name: "manpage", - Usage: "--directory ", - Short: "Generates the manual pages for Caddy commands", - Long: ` + defaultFactory.Use(func(rootCmd *cobra.Command) { + RegisterCommand(Command{ + Name: "manpage", + Usage: "--directory ", + Short: "Generates the manual pages for Caddy commands", + Long: ` Generates the manual pages for Caddy commands into the designated directory tagged into section 8 (System Administration). The manual page files are generated into the directory specified by the argument of --directory. If the directory does not exist, it will be created. `, - CobraFunc: func(cmd *cobra.Command) { - cmd.Flags().StringP("directory", "o", "", "The output directory where the manpages are generated") - cmd.RunE = WrapCommandFuncForCobra(func(fl Flags) (int, error) { - dir := strings.TrimSpace(fl.String("directory")) - if dir == "" { - return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required") - } - if err := os.MkdirAll(dir, 0o755); err != nil { - return caddy.ExitCodeFailedQuit, err - } - if err := doc.GenManTree(rootCmd, &doc.GenManHeader{ - Title: "Caddy", - Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections - }, dir); err != nil { - return caddy.ExitCodeFailedQuit, err - } - return caddy.ExitCodeSuccess, nil - }) - }, - }) - - // source: https://github.com/spf13/cobra/blob/main/shell_completions.md - rootCmd.AddCommand(&cobra.Command{ - Use: "completion [bash|zsh|fish|powershell]", - Short: "Generate completion script", - Long: fmt.Sprintf(`To load completions: + CobraFunc: func(cmd *cobra.Command) { + cmd.Flags().StringP("directory", "o", "", "The output directory where the manpages are generated") + cmd.RunE = WrapCommandFuncForCobra(func(fl Flags) (int, error) { + dir := strings.TrimSpace(fl.String("directory")) + if dir == "" { + return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required") + } + if err := os.MkdirAll(dir, 0o755); err != nil { + return caddy.ExitCodeFailedQuit, err + } + if err := doc.GenManTree(rootCmd, &doc.GenManHeader{ + Title: "Caddy", + Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections + }, dir); err != nil { + return caddy.ExitCodeFailedQuit, err + } + return caddy.ExitCodeSuccess, nil + }) + }, + }) + + // source: https://github.com/spf13/cobra/blob/main/shell_completions.md + rootCmd.AddCommand(&cobra.Command{ + Use: "completion [bash|zsh|fish|powershell]", + Short: "Generate completion script", + Long: fmt.Sprintf(`To load completions: Bash: @@ -513,23 +514,24 @@ argument of --directory. If the directory does not exist, it will be created. PS> %[1]s completion powershell > %[1]s.ps1 # and source this file from your PowerShell profile. `, rootCmd.Root().Name()), - DisableFlagsInUseLine: true, - ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, - Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), - RunE: func(cmd *cobra.Command, args []string) error { - switch args[0] { - case "bash": - return cmd.Root().GenBashCompletion(os.Stdout) - case "zsh": - return cmd.Root().GenZshCompletion(os.Stdout) - case "fish": - return cmd.Root().GenFishCompletion(os.Stdout, true) - case "powershell": - return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) - default: - return fmt.Errorf("unrecognized shell: %s", args[0]) - } - }, + DisableFlagsInUseLine: true, + ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + RunE: func(cmd *cobra.Command, args []string) error { + switch args[0] { + case "bash": + return cmd.Root().GenBashCompletion(os.Stdout) + case "zsh": + return cmd.Root().GenZshCompletion(os.Stdout) + case "fish": + return cmd.Root().GenFishCompletion(os.Stdout, true) + case "powershell": + return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) + default: + return fmt.Errorf("unrecognized shell: %s", args[0]) + } + }, + }) }) } @@ -563,7 +565,9 @@ func RegisterCommand(cmd Command) { if !commandNameRegex.MatchString(cmd.Name) { panic("invalid command name") } - rootCmd.AddCommand(caddyCmdToCobra(cmd)) + defaultFactory.Use(func(rootCmd *cobra.Command) { + rootCmd.AddCommand(caddyCmdToCobra(cmd)) + }) } var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`) diff --git a/cmd/main.go b/cmd/main.go index 3c3ae627087..655c0084bea 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -72,7 +72,7 @@ func Main() { caddy.Log().Warn("failed to set GOMAXPROCS", zap.Error(err)) } - if err := rootCmd.Execute(); err != nil { + if err := defaultFactory.Build().Execute(); err != nil { var exitError *exitError if errors.As(err, &exitError) { os.Exit(exitError.ExitCode) diff --git a/go.mod b/go.mod index 8729be44d6d..6ae4a4a8ea7 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/caddyserver/caddy/v2 -go 1.21.0 +go 1.22.3 -toolchain go1.22.2 +toolchain go1.23.0 require ( github.com/BurntSushi/toml v1.3.2 @@ -19,14 +19,14 @@ require ( github.com/klauspost/cpuid/v2 v2.2.7 github.com/mholt/acmez/v2 v2.0.1 github.com/prometheus/client_golang v1.19.1 - github.com/quic-go/quic-go v0.44.0 + github.com/quic-go/quic-go v0.46.0 github.com/smallstep/certificates v0.26.1 github.com/smallstep/nosql v0.6.1 github.com/smallstep/truststore v0.13.0 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 - github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 + github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 github.com/yuin/goldmark v1.7.1 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 @@ -37,11 +37,11 @@ require ( go.uber.org/automaxprocs v1.5.3 go.uber.org/zap v1.27.0 go.uber.org/zap/exp v0.2.0 - golang.org/x/crypto v0.23.0 + golang.org/x/crypto v0.26.0 golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 - golang.org/x/net v0.25.0 - golang.org/x/sync v0.7.0 - golang.org/x/term v0.20.0 + golang.org/x/net v0.28.0 + golang.org/x/sync v0.8.0 + golang.org/x/term v0.23.0 golang.org/x/time v0.5.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 @@ -123,7 +123,7 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/pires/go-proxyproto v0.7.0 + github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964 github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.48.0 // indirect @@ -147,9 +147,9 @@ require ( go.step.sm/linkedca v0.20.1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.17.0 // indirect - golang.org/x/sys v0.20.0 - golang.org/x/text v0.15.0 // indirect - golang.org/x/tools v0.21.0 // indirect + golang.org/x/sys v0.23.0 + golang.org/x/text v0.17.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/grpc v1.63.2 // indirect google.golang.org/protobuf v1.34.1 // indirect howett.net/plist v1.0.0 // indirect diff --git a/go.sum b/go.sum index 351e449ca33..5bf39b424bf 100644 --- a/go.sum +++ b/go.sum @@ -320,8 +320,8 @@ github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8P github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= -github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= -github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= +github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964 h1:ct/vxNBgHpASQ4sT8NaBX9LtsEtluZqaUJydLG50U3E= +github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -339,8 +339,8 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/quic-go/quic-go v0.44.0 h1:So5wOr7jyO4vzL2sd8/pD9Kesciv91zSk8BoFngItQ0= -github.com/quic-go/quic-go v0.44.0/go.mod h1:z4cx/9Ny9UtGITIPzmPTXh1ULfOyWh4qGQlpnPcWmek= +github.com/quic-go/quic-go v0.46.0 h1:uuwLClEEyk1DNvchH8uCByQVjo3yKL9opKulExNDs7Y= +github.com/quic-go/quic-go v0.46.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= @@ -415,8 +415,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 h1:pV0H+XIvFoP7pl1MRtyPXh5hqoxB5I7snOtTHgrn6HU= -github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU= +github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ= +github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= @@ -510,8 +510,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw= golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= @@ -532,15 +532,15 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -567,8 +567,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -576,8 +576,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -588,8 +588,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -603,8 +603,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= -golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/ranges.go b/internal/ranges.go new file mode 100644 index 00000000000..e9429e2635f --- /dev/null +++ b/internal/ranges.go @@ -0,0 +1,14 @@ +package internal + +// PrivateRangesCIDR returns a list of private CIDR range +// strings, which can be used as a configuration shortcut. +func PrivateRangesCIDR() []string { + return []string{ + "192.168.0.0/16", + "172.16.0.0/12", + "10.0.0.0/8", + "127.0.0.1/8", + "fd00::/8", + "::1", + } +} diff --git a/listeners.go b/listeners.go index bb0e9b69c6c..fa5ac1f56c0 100644 --- a/listeners.go +++ b/listeners.go @@ -60,8 +60,6 @@ type NetworkAddress struct { // ListenAll calls Listen() for all addresses represented by this struct, i.e. all ports in the range. // (If the address doesn't use ports or has 1 port only, then only 1 listener will be created.) // It returns an error if any listener failed to bind, and closes any listeners opened up to that point. -// -// TODO: Experimental API: subject to change or removal. func (na NetworkAddress) ListenAll(ctx context.Context, config net.ListenConfig) ([]any, error) { var listeners []any var err error @@ -130,8 +128,6 @@ func (na NetworkAddress) ListenAll(ctx context.Context, config net.ListenConfig) // Unix sockets will be unlinked before being created, to ensure we can bind to // it even if the previous program using it exited uncleanly; it will also be // unlinked upon a graceful exit (or when a new config does not use that socket). -// -// TODO: Experimental API: subject to change or removal. func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) { if na.IsUnixNetwork() { unixSocketsMu.Lock() @@ -221,8 +217,6 @@ func (na NetworkAddress) JoinHostPort(offset uint) string { } // Expand returns one NetworkAddress for each port in the port range. -// -// This is EXPERIMENTAL and subject to change or removal. func (na NetworkAddress) Expand() []NetworkAddress { size := na.PortRangeSize() addrs := make([]NetworkAddress, size) diff --git a/logging.go b/logging.go index d3e7bf32b56..ca10beeeddc 100644 --- a/logging.go +++ b/logging.go @@ -292,6 +292,10 @@ type BaseLog struct { // The encoder is how the log entries are formatted or encoded. EncoderRaw json.RawMessage `json:"encoder,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"` + // Tees entries through a zap.Core module which can extract + // log entry metadata and fields for further processing. + CoreRaw json.RawMessage `json:"core,omitempty" caddy:"namespace=caddy.logging.cores inline_key=module"` + // Level is the minimum level to emit, and is inclusive. // Possible levels: DEBUG, INFO, WARN, ERROR, PANIC, and FATAL Level string `json:"level,omitempty"` @@ -366,6 +370,14 @@ func (cl *BaseLog) provisionCommon(ctx Context, logging *Logging) error { cl.encoder = newDefaultProductionLogEncoder(cl.writerOpener) } cl.buildCore() + if cl.CoreRaw != nil { + mod, err := ctx.LoadModule(cl, "CoreRaw") + if err != nil { + return fmt.Errorf("loading log core module: %v", err) + } + core := mod.(zapcore.Core) + cl.core = zapcore.NewTee(cl.core, core) + } return nil } diff --git a/modules/caddyevents/app.go b/modules/caddyevents/app.go index 902c6d8429f..fe76a675756 100644 --- a/modules/caddyevents/app.go +++ b/modules/caddyevents/app.go @@ -355,6 +355,11 @@ type Event struct { origin caddy.Module } +func (e Event) ID() uuid.UUID { return e.id } +func (e Event) Timestamp() time.Time { return e.ts } +func (e Event) Name() string { return e.name } +func (e Event) Origin() caddy.Module { return e.origin } + // CloudEvent exports event e as a structure that, when // serialized as JSON, is compatible with the // CloudEvents spec. diff --git a/modules/caddyhttp/celmatcher.go b/modules/caddyhttp/celmatcher.go index d4016478ec2..a5565eb9840 100644 --- a/modules/caddyhttp/celmatcher.go +++ b/modules/caddyhttp/celmatcher.go @@ -340,7 +340,7 @@ func (celTypeAdapter) NativeToValue(value any) ref.Val { case time.Time: return types.Timestamp{Time: v} case error: - types.NewErr(v.Error()) + return types.WrapErr(v) } return types.DefaultTypeAdapter.NativeToValue(value) } @@ -499,7 +499,7 @@ func CELMatcherRuntimeFunction(funcName string, fac CELMatcherFactory) functions return func(celReq, matcherData ref.Val) ref.Val { matcher, err := fac(matcherData) if err != nil { - return types.NewErr(err.Error()) + return types.WrapErr(err) } httpReq := celReq.Value().(celHTTPRequest) return types.Bool(matcher.Match(httpReq.Request)) diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go index 908e37b359f..00e5072772a 100644 --- a/modules/caddyhttp/encode/encode.go +++ b/modules/caddyhttp/encode/encode.go @@ -112,7 +112,8 @@ func (enc *Encode) Provision(ctx caddy.Context) error { "application/x-ttf*", "application/xhtml+xml*", "application/xml*", - "font/*", + "font/ttf*", + "font/otf*", "image/svg+xml*", "image/vnd.microsoft.icon*", "image/x-icon*", @@ -265,6 +266,14 @@ func (rw *responseWriter) FlushError() error { // to rw.Write (see bug in #4314) return nil } + // also flushes the encoder, if any + // see: https://github.com/jjiang-stripe/caddy-slow-gzip + if rw.w != nil { + err := rw.w.Flush() + if err != nil { + return err + } + } //nolint:bodyclose return http.NewResponseController(rw.ResponseWriter).Flush() } @@ -474,6 +483,7 @@ type encodingPreference struct { type Encoder interface { io.WriteCloser Reset(io.Writer) + Flush() error // encoder by default buffers data to maximize compressing rate } // Encoding is a type which can create encoders of its kind diff --git a/modules/caddyhttp/fileserver/browse.go b/modules/caddyhttp/fileserver/browse.go index 8b2b48e77a1..f5cc16b42ab 100644 --- a/modules/caddyhttp/fileserver/browse.go +++ b/modules/caddyhttp/fileserver/browse.go @@ -52,8 +52,19 @@ var BrowseTemplate string type Browse struct { // Filename of the template to use instead of the embedded browse template. TemplateFile string `json:"template_file,omitempty"` + // Determines whether or not targets of symlinks should be revealed. RevealSymlinks bool `json:"reveal_symlinks,omitempty"` + + // Override the default sort. + // It includes the following options: + // - sort_by: name(default), namedirfirst, size, time + // - order: asc(default), desc + // eg.: + // - `sort time desc` will sort by time in descending order + // - `sort size` will sort by size in ascending order + // The first option must be `sort_by` and the second option must be `order` (if exists). + SortOptions []string `json:"sort,omitempty"` } func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { @@ -206,11 +217,34 @@ func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, fileSystem fs // browseApplyQueryParams applies query parameters to the listing. // It mutates the listing and may set cookies. func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Request, listing *browseTemplateContext) { + var orderParam, sortParam string + + // The configs in Caddyfile have lower priority than Query params, + // so put it at first. + for idx, item := range fsrv.Browse.SortOptions { + // Only `sort` & `order`, 2 params are allowed + if idx >= 2 { + break + } + switch item { + case sortByName, sortByNameDirFirst, sortBySize, sortByTime: + sortParam = item + case sortOrderAsc, sortOrderDesc: + orderParam = item + } + } + layoutParam := r.URL.Query().Get("layout") - sortParam := r.URL.Query().Get("sort") - orderParam := r.URL.Query().Get("order") limitParam := r.URL.Query().Get("limit") offsetParam := r.URL.Query().Get("offset") + sortParamTmp := r.URL.Query().Get("sort") + if sortParamTmp != "" { + sortParam = sortParamTmp + } + orderParamTmp := r.URL.Query().Get("order") + if orderParamTmp != "" { + orderParam = orderParamTmp + } switch layoutParam { case "list", "grid", "": @@ -233,11 +267,11 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re // then figure out the order switch orderParam { case "": - orderParam = "asc" + orderParam = sortOrderAsc if orderCookie, orderErr := r.Cookie("order"); orderErr == nil { orderParam = orderCookie.Value } - case "asc", "desc": + case sortOrderAsc, sortOrderDesc: http.SetCookie(w, &http.Cookie{Name: "order", Value: orderParam, Secure: r.TLS != nil}) } diff --git a/modules/caddyhttp/fileserver/browse.html b/modules/caddyhttp/fileserver/browse.html index 7b0df1e5fb6..d2d698197e6 100644 --- a/modules/caddyhttp/fileserver/browse.html +++ b/modules/caddyhttp/fileserver/browse.html @@ -1,10 +1,17 @@ +{{ $nonce := uuidv4 -}} +{{ $nonceAttribute := print "nonce=" (quote $nonce) -}} +{{ $csp := printf "default-src 'none'; img-src 'self'; object-src 'none'; base-uri 'none'; script-src 'nonce-%s'; style-src 'nonce-%s'; frame-ancestors 'self'; form-action 'self';" $nonce $nonce -}} +{{/* To disable the Content-Security-Policy, set this to false */}}{{ $enableCsp := true -}} +{{ if $enableCsp -}} + {{- .RespHeader.Set "Content-Security-Policy" $csp -}} +{{ end -}} {{- define "icon"}} {{- if .IsDir}} {{- if .IsSymlink}} - + {{- else}} @@ -303,7 +310,7 @@ - {{- if eq .Layout "grid"}} - + {{- end}} - +
@@ -799,7 +810,7 @@

{{- end}}

- + @@ -807,7 +818,7 @@

List - + @@ -886,7 +897,7 @@

- + @@ -980,7 +991,7 @@

- {{.HumanSize}} + {{if .IsSymlink}}↱ {{end}}{{.HumanSize}}
@@ -1000,70 +1011,70 @@

-