Merge branch 'master' into hurl-tests

This commit is contained in:
Mohammed Al Sahaf 2024-09-09 10:20:16 +03:00 committed by GitHub
commit 3bdc6c035a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
75 changed files with 2344 additions and 314 deletions

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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

105
caddy.go
View file

@ -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
}
return ctx, err
}
if !start {
return ctx, nil
}
// Provision any admin routers which may need to access
// some of the other apps at runtime
err = newCfg.Admin.provisionAdminRouters(ctx)
if err != nil {
return ctx, err
}
// Start
err = func() error {
started := make([]string, 0, len(newCfg.apps))
for name, a := range newCfg.apps {
err := a.Start()
if err != nil {
// an app failed to start, so we need to stop
// all other apps that were already started
for _, otherAppName := range started {
err2 := newCfg.apps[otherAppName].Stop()
if err2 != nil {
err = fmt.Errorf("%v; additionally, aborting app %s: %v",
err, otherAppName, err2)
}
}
return fmt.Errorf("%s app module: start: %v", name, err)
}
started = append(started, name)
}
return nil
}()
if err != nil {
return ctx, err
}
// now that the user's config is running, finish setting up anything else,
// such as remote admin endpoint, config loader, etc.
return ctx, finishSettingUp(ctx, newCfg)
// ProvisionContext creates a new context from the configuration and provisions storage
// and app modules.
// The function is intended for testing and advanced use cases only, typically `Run` should be
// use to ensure a fully functional caddy instance.
// EXPERIMENTAL: While this is public the interface and implementation details of this function may change.
func ProvisionContext(newCfg *Config) (Context, error) {
return provisionContext(newCfg, false)
}
// finishSettingUp should be run after all apps have successfully started.

View file

@ -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

View file

@ -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
}

View file

@ -849,6 +849,7 @@ func parseInvoke(h Helper) (caddyhttp.MiddlewareHandler, error) {
// log <logger_name> {
// hostnames <hostnames...>
// output <writer_module> ...
// core <core_module> ...
// format <encoder_module> ...
// level <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()

View file

@ -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,
},
} {

View file

@ -84,7 +84,6 @@ func TestLoadUnorderedJSON(t *testing.T) {
"servers": {
"s_server": {
"listen": [
":9443",
":9080"
],
"routes": [

View file

@ -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"
]
}
]
}
]
}
}
}
}
}

View file

@ -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
}
]
}
}
}
}
}

View file

@ -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
}
]
}
}
}
}
}

View file

@ -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
}
]
}
}
}
}
}

View file

@ -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
}
]
}
}
}
}
}

View file

@ -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"
}
]
}
]
}
]
}
}
}
}
}

View file

@ -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"
}
]
}
]
}
]
}
}
}
}
}

View file

@ -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
}
]
}
}
}
}
}

View file

@ -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")
}

View file

@ -0,0 +1,2 @@
foo

View file

@ -0,0 +1 @@
foo

View file

@ -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");

View file

@ -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 {

28
cmd/commandfactory.go Normal file
View file

@ -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
}

View file

@ -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)
}

View file

@ -438,43 +438,44 @@ EXPERIMENTAL: May be changed or removed.
},
})
RegisterCommand(Command{
Name: "manpage",
Usage: "--directory <path>",
Short: "Generates the manual pages for Caddy commands",
Long: `
defaultFactory.Use(func(rootCmd *cobra.Command) {
RegisterCommand(Command{
Name: "manpage",
Usage: "--directory <path>",
Short: "Generates the manual pages for Caddy commands",
Long: `
Generates the manual pages for Caddy commands into the designated directory
tagged into section 8 (System Administration).
The manual page files are generated into the directory specified by the
argument of --directory. If the directory does not exist, it will be created.
`,
CobraFunc: func(cmd *cobra.Command) {
cmd.Flags().StringP("directory", "o", "", "The output directory where the manpages are generated")
cmd.RunE = WrapCommandFuncForCobra(func(fl Flags) (int, error) {
dir := strings.TrimSpace(fl.String("directory"))
if dir == "" {
return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required")
}
if err := os.MkdirAll(dir, 0o755); err != nil {
return caddy.ExitCodeFailedQuit, err
}
if err := doc.GenManTree(rootCmd, &doc.GenManHeader{
Title: "Caddy",
Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections
}, dir); err != nil {
return caddy.ExitCodeFailedQuit, err
}
return caddy.ExitCodeSuccess, nil
})
},
})
CobraFunc: func(cmd *cobra.Command) {
cmd.Flags().StringP("directory", "o", "", "The output directory where the manpages are generated")
cmd.RunE = WrapCommandFuncForCobra(func(fl Flags) (int, error) {
dir := strings.TrimSpace(fl.String("directory"))
if dir == "" {
return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required")
}
if err := os.MkdirAll(dir, 0o755); err != nil {
return caddy.ExitCodeFailedQuit, err
}
if err := doc.GenManTree(rootCmd, &doc.GenManHeader{
Title: "Caddy",
Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections
}, dir); err != nil {
return caddy.ExitCodeFailedQuit, err
}
return caddy.ExitCodeSuccess, nil
})
},
})
// source: https://github.com/spf13/cobra/blob/main/shell_completions.md
rootCmd.AddCommand(&cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Long: fmt.Sprintf(`To load completions:
// source: https://github.com/spf13/cobra/blob/main/shell_completions.md
rootCmd.AddCommand(&cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Long: fmt.Sprintf(`To load completions:
Bash:
@ -513,23 +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]$`)

View file

@ -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)

24
go.mod
View file

@ -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

40
go.sum
View file

@ -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=

14
internal/ranges.go Normal file
View file

@ -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",
}
}

View file

@ -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)

View file

@ -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
}

View file

@ -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.

View file

@ -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))

View file

@ -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

View file

@ -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})
}

View file

@ -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}}
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-folder-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M9 3a1 1 0 0 1 .608 .206l.1 .087l2.706 2.707h6.586a3 3 0 0 1 2.995 2.824l.005 .176v8a3 3 0 0 1 -2.824 2.995l-.176 .005h-14a3 3 0 0 1 -2.995 -2.824l-.005 -.176v-11a3 3 0 0 1 2.824 -2.995l.176 -.005h4z" stroke-width="0" fill="currentColor"/>
<path fill="#000" d="M2.795 17.306c0-2.374 1.792-4.314 4.078-4.538v-1.104a.38.38 0 0 1 .651-.272l2.45 2.492a.132.132 0 0 1 0 .188l-2.45 2.492a.381.381 0 0 1-.651-.272V15.24c-1.889.297-3.436 1.39-3.817 3.26a2.809 2.809 0 0 1-.261-1.193Z" style="stroke-width:.127478"/>
<path fill="#000" d="M2.795 17.306c0-2.374 1.792-4.314 4.078-4.538v-1.104a.38.38 0 0 1 .651-.272l2.45 2.492a.132.132 0 0 1 0 .188l-2.45 2.492a.381.381 0 0 1-.651-.272V15.24c-1.889.297-3.436 1.39-3.817 3.26a2.809 2.809 0 0 1-.261-1.193Z" stroke-width=".127478"/>
</svg>
{{- else}}
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-folder-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
@ -303,7 +310,7 @@
<meta charset="utf-8">
<meta name="color-scheme" content="light dark">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
<style {{ $nonceAttribute }}>
* { padding: 0; margin: 0; box-sizing: border-box; }
body {
@ -342,6 +349,10 @@ svg,
text-decoration: none;
}
#layout-list, #layout-grid {
cursor: pointer;
}
.wrapper {
max-width: 1200px;
margin-left: auto;
@ -768,10 +779,10 @@ footer {
</style>
{{- if eq .Layout "grid"}}
<style>.wrapper { max-width: none; } main { margin-top: 1px; }</style>
<style {{ $nonceAttribute }}>.wrapper { max-width: none; } main { margin-top: 1px; }</style>
{{- end}}
</head>
<body onload="initPage()">
<body>
<header>
<div class="wrapper">
<div class="breadcrumbs">Folder Path</div>
@ -799,7 +810,7 @@ footer {
</span>
{{- end}}
</div>
<a href="javascript:queryParam('layout', '')" id="layout-list" class='layout{{if eq $.Layout "list" ""}}current{{end}}'>
<a id="layout-list" class='layout{{if eq $.Layout "list" ""}}current{{end}}'>
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-layout-list" width="16" height="16" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"/>
@ -807,7 +818,7 @@ footer {
</svg>
List
</a>
<a href="javascript:queryParam('layout', 'grid')" id="layout-grid" class='layout{{if eq $.Layout "grid"}}current{{end}}'>
<a id="layout-grid" class='layout{{if eq $.Layout "grid"}}current{{end}}'>
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-layout-grid" width="16" height="16" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M4 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/>
@ -886,7 +897,7 @@ footer {
<path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0"/>
<path d="M21 21l-6 -6"/>
</svg>
<input type="search" placeholder="Search" id="filter" onkeyup='filter()'>
<input type="search" placeholder="Search" id="filter">
</div>
</th>
<th>
@ -980,7 +991,7 @@ footer {
<div class="sizebar">
<div class="sizebar-bar"></div>
<div class="sizebar-text">
{{.HumanSize}}
{{if .IsSymlink}}↱&nbsp;{{end}}{{.HumanSize}}
</div>
</div>
</td>
@ -1000,70 +1011,70 @@ footer {
<footer>
Served with
<a rel="noopener noreferrer" href="https://caddyserver.com">
<svg class="caddy-logo" viewBox="0 0 379 114" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;">
<svg class="caddy-logo" viewBox="0 0 379 114" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g transform="matrix(1,0,0,1,-1982.99,-530.985)">
<g transform="matrix(1.16548,0,0,1.10195,1823.12,393.466)">
<g transform="matrix(1,0,0,1,0.233052,1.17986)">
<g id="Icon" transform="matrix(0.858013,0,0,0.907485,-3224.99,-1435.83)">
<g>
<g transform="matrix(-0.191794,-0.715786,0.715786,-0.191794,4329.14,4673.64)">
<path d="M3901.56,610.734C3893.53,610.261 3886.06,608.1 3879.2,604.877C3872.24,601.608 3866.04,597.093 3860.8,591.633C3858.71,589.457 3856.76,587.149 3854.97,584.709C3853.2,582.281 3851.57,579.733 3850.13,577.066C3845.89,569.224 3843.21,560.381 3842.89,550.868C3842.57,543.321 3843.64,536.055 3845.94,529.307C3848.37,522.203 3852.08,515.696 3856.83,510.049L3855.79,509.095C3850.39,514.54 3846.02,520.981 3842.9,528.125C3839.84,535.125 3838.03,542.781 3837.68,550.868C3837.34,561.391 3839.51,571.425 3843.79,580.306C3845.27,583.38 3847.03,586.304 3849.01,589.049C3851.01,591.806 3853.24,594.39 3855.69,596.742C3861.75,602.568 3869,607.19 3877.03,610.1C3884.66,612.867 3892.96,614.059 3901.56,613.552L3901.56,610.734Z" style="fill:rgb(0,144,221);"/>
<path d="M3901.56,610.734C3893.53,610.261 3886.06,608.1 3879.2,604.877C3872.24,601.608 3866.04,597.093 3860.8,591.633C3858.71,589.457 3856.76,587.149 3854.97,584.709C3853.2,582.281 3851.57,579.733 3850.13,577.066C3845.89,569.224 3843.21,560.381 3842.89,550.868C3842.57,543.321 3843.64,536.055 3845.94,529.307C3848.37,522.203 3852.08,515.696 3856.83,510.049L3855.79,509.095C3850.39,514.54 3846.02,520.981 3842.9,528.125C3839.84,535.125 3838.03,542.781 3837.68,550.868C3837.34,561.391 3839.51,571.425 3843.79,580.306C3845.27,583.38 3847.03,586.304 3849.01,589.049C3851.01,591.806 3853.24,594.39 3855.69,596.742C3861.75,602.568 3869,607.19 3877.03,610.1C3884.66,612.867 3892.96,614.059 3901.56,613.552L3901.56,610.734Z" fill="rgb(0,144,221)"/>
</g>
<g transform="matrix(-0.191794,-0.715786,0.715786,-0.191794,4329.14,4673.64)">
<path d="M3875.69,496.573C3879.62,494.538 3883.8,492.897 3888.2,491.786C3892.49,490.704 3896.96,490.124 3901.56,490.032C3903.82,490.13 3906.03,490.332 3908.21,490.688C3917.13,492.147 3925.19,495.814 3932.31,500.683C3936.13,503.294 3939.59,506.335 3942.81,509.619C3947.09,513.98 3950.89,518.816 3953.85,524.232C3958.2,532.197 3960.96,541.186 3961.32,550.868C3961.61,558.748 3960.46,566.345 3957.88,573.322C3956.09,578.169 3953.7,582.753 3950.66,586.838C3947.22,591.461 3942.96,595.427 3938.27,598.769C3933.66,602.055 3928.53,604.619 3923.09,606.478C3922.37,606.721 3921.6,606.805 3920.93,607.167C3920.42,607.448 3920.14,607.854 3919.69,608.224L3920.37,610.389C3920.98,610.432 3921.47,610.573 3922.07,610.474C3922.86,610.344 3923.55,609.883 3924.28,609.566C3931.99,606.216 3938.82,601.355 3944.57,595.428C3947.02,592.903 3949.25,590.174 3951.31,587.319C3953.59,584.168 3955.66,580.853 3957.43,577.348C3961.47,569.34 3964.01,560.422 3964.36,550.868C3964.74,540.511 3962.66,530.628 3958.48,521.868C3955.57,515.775 3951.72,510.163 3946.95,505.478C3943.37,501.962 3939.26,498.99 3934.84,496.562C3926.88,492.192 3917.87,489.76 3908.37,489.229C3906.12,489.104 3903.86,489.054 3901.56,489.154C3896.87,489.06 3892.3,489.519 3887.89,490.397C3883.3,491.309 3878.89,492.683 3874.71,494.525L3875.69,496.573Z" style="fill:rgb(0,144,221);"/>
<path d="M3875.69,496.573C3879.62,494.538 3883.8,492.897 3888.2,491.786C3892.49,490.704 3896.96,490.124 3901.56,490.032C3903.82,490.13 3906.03,490.332 3908.21,490.688C3917.13,492.147 3925.19,495.814 3932.31,500.683C3936.13,503.294 3939.59,506.335 3942.81,509.619C3947.09,513.98 3950.89,518.816 3953.85,524.232C3958.2,532.197 3960.96,541.186 3961.32,550.868C3961.61,558.748 3960.46,566.345 3957.88,573.322C3956.09,578.169 3953.7,582.753 3950.66,586.838C3947.22,591.461 3942.96,595.427 3938.27,598.769C3933.66,602.055 3928.53,604.619 3923.09,606.478C3922.37,606.721 3921.6,606.805 3920.93,607.167C3920.42,607.448 3920.14,607.854 3919.69,608.224L3920.37,610.389C3920.98,610.432 3921.47,610.573 3922.07,610.474C3922.86,610.344 3923.55,609.883 3924.28,609.566C3931.99,606.216 3938.82,601.355 3944.57,595.428C3947.02,592.903 3949.25,590.174 3951.31,587.319C3953.59,584.168 3955.66,580.853 3957.43,577.348C3961.47,569.34 3964.01,560.422 3964.36,550.868C3964.74,540.511 3962.66,530.628 3958.48,521.868C3955.57,515.775 3951.72,510.163 3946.95,505.478C3943.37,501.962 3939.26,498.99 3934.84,496.562C3926.88,492.192 3917.87,489.76 3908.37,489.229C3906.12,489.104 3903.86,489.054 3901.56,489.154C3896.87,489.06 3892.3,489.519 3887.89,490.397C3883.3,491.309 3878.89,492.683 3874.71,494.525L3875.69,496.573Z" fill="rgb(0,144,221)"/>
</g>
</g>
<g>
<g transform="matrix(-3.37109,-0.514565,0.514565,-3.37109,4078.07,1806.88)">
<path d="M22,12C22,10.903 21.097,10 20,10C19.421,10 18.897,10.251 18.53,10.649C18.202,11.006 18,11.481 18,12C18,13.097 18.903,14 20,14C21.097,14 22,13.097 22,12Z" style="fill:none;fill-rule:nonzero;stroke:rgb(0,144,221);stroke-width:1.05px;"/>
<path d="M22,12C22,10.903 21.097,10 20,10C19.421,10 18.897,10.251 18.53,10.649C18.202,11.006 18,11.481 18,12C18,13.097 18.903,14 20,14C21.097,14 22,13.097 22,12Z" fill="none" fill-rule="nonzero" stroke="rgb(0,144,221)" stroke-width="1.05px"/>
</g>
<g transform="matrix(-5.33921,-5.26159,-3.12106,-6.96393,4073.87,1861.55)">
<path d="M10.315,5.333C10.315,5.333 9.748,5.921 9.03,6.673C7.768,7.995 6.054,9.805 6.054,9.805L6.237,9.86C6.237,9.86 8.045,8.077 9.36,6.771C10.107,6.028 10.689,5.444 10.689,5.444L10.315,5.333Z" style="fill:rgb(0,144,221);"/>
<path d="M10.315,5.333C10.315,5.333 9.748,5.921 9.03,6.673C7.768,7.995 6.054,9.805 6.054,9.805L6.237,9.86C6.237,9.86 8.045,8.077 9.36,6.771C10.107,6.028 10.689,5.444 10.689,5.444L10.315,5.333Z" fill="rgb(0,144,221)"/>
</g>
</g>
<g id="Padlock" transform="matrix(3.11426,0,0,3.11426,3938.31,1737.25)">
<g>
<path d="M9.876,21L18.162,21C18.625,21 19,20.625 19,20.162L19,11.838C19,11.375 18.625,11 18.162,11L5.838,11C5.375,11 5,11.375 5,11.838L5,16.758" style="fill:none;stroke:rgb(34,182,56);stroke-width:1.89px;stroke-linecap:butt;stroke-linejoin:miter;"/>
<path d="M8,11L8,7C8,4.806 9.806,3 12,3C14.194,3 16,4.806 16,7L16,11" style="fill:none;fill-rule:nonzero;stroke:rgb(34,182,56);stroke-width:1.89px;"/>
<path d="M9.876,21L18.162,21C18.625,21 19,20.625 19,20.162L19,11.838C19,11.375 18.625,11 18.162,11L5.838,11C5.375,11 5,11.375 5,11.838L5,16.758" fill="none" stroke="rgb(34,182,56)" stroke-width="1.89px" stroke-linecap="butt" stroke-linejoin="miter"/>
<path d="M8,11L8,7C8,4.806 9.806,3 12,3C14.194,3 16,4.806 16,7L16,11" fill="none" fill-rule="nonzero" stroke="rgb(34,182,56)" stroke-width="1.89px"/>
</g>
</g>
<g>
<g transform="matrix(5.30977,0.697415,-0.697415,5.30977,3852.72,1727.97)">
<path d="M22,12C22,11.659 21.913,11.337 21.76,11.055C21.421,10.429 20.756,10 20,10C18.903,10 18,10.903 18,12C18,13.097 18.903,14 20,14C21.097,14 22,13.097 22,12Z" style="fill:none;fill-rule:nonzero;stroke:rgb(0,144,221);stroke-width:0.98px;"/>
<path d="M22,12C22,11.659 21.913,11.337 21.76,11.055C21.421,10.429 20.756,10 20,10C18.903,10 18,10.903 18,12C18,13.097 18.903,14 20,14C21.097,14 22,13.097 22,12Z" fill="none" fill-rule="nonzero" stroke="rgb(0,144,221)" stroke-width="0.98px"/>
</g>
<g transform="matrix(4.93114,2.49604,1.11018,5.44847,3921.41,1726.72)">
<path d="M8.902,6.77C8.902,6.77 7.235,8.253 6.027,9.366C5.343,9.996 4.819,10.502 4.819,10.502L5.52,11.164C5.52,11.164 6.021,10.637 6.646,9.951C7.749,8.739 9.219,7.068 9.219,7.068L8.902,6.77Z" style="fill:rgb(0,144,221);"/>
<path d="M8.902,6.77C8.902,6.77 7.235,8.253 6.027,9.366C5.343,9.996 4.819,10.502 4.819,10.502L5.52,11.164C5.52,11.164 6.021,10.637 6.646,9.951C7.749,8.739 9.219,7.068 9.219,7.068L8.902,6.77Z" fill="rgb(0,144,221)"/>
</g>
</g>
</g>
<g id="Text">
<g id="Wordmark" transform="matrix(1.32271,0,0,2.60848,-899.259,-791.691)">
<g id="y" transform="matrix(0.50291,0,0,0.281607,905.533,304.987)">
<path d="M192.152,286.875L202.629,268.64C187.804,270.106 183.397,265.779 180.143,263.391C176.888,261.004 174.362,257.99 172.563,254.347C170.765,250.705 169.866,246.691 169.866,242.305L169.866,208.107L183.21,208.107L183.21,242.213C183.21,245.188 183.896,247.822 185.268,250.116C186.64,252.41 188.465,254.197 190.743,255.475C193.022,256.754 195.501,257.393 198.182,257.393C200.894,257.393 203.393,256.75 205.68,255.463C207.966,254.177 209.799,252.391 211.178,250.105C212.558,247.818 213.248,245.188 213.248,242.213L213.248,208.107L226.545,208.107L226.545,242.305C226.545,246.707 225.378,258.46 218.079,268.64C215.735,271.909 207.835,286.875 207.835,286.875L192.152,286.875Z" style="fill:rgb(47,47,47);fill-rule:nonzero;"/>
<path d="M192.152,286.875L202.629,268.64C187.804,270.106 183.397,265.779 180.143,263.391C176.888,261.004 174.362,257.99 172.563,254.347C170.765,250.705 169.866,246.691 169.866,242.305L169.866,208.107L183.21,208.107L183.21,242.213C183.21,245.188 183.896,247.822 185.268,250.116C186.64,252.41 188.465,254.197 190.743,255.475C193.022,256.754 195.501,257.393 198.182,257.393C200.894,257.393 203.393,256.75 205.68,255.463C207.966,254.177 209.799,252.391 211.178,250.105C212.558,247.818 213.248,245.188 213.248,242.213L213.248,208.107L226.545,208.107L226.545,242.305C226.545,246.707 225.378,258.46 218.079,268.64C215.735,271.909 207.835,286.875 207.835,286.875L192.152,286.875Z" fill="rgb(47,47,47)" fill-rule="nonzero"/>
</g>
<g id="add" transform="matrix(0.525075,0,0,0.281607,801.871,304.987)">
<g transform="matrix(116.242,0,0,116.242,161.846,267.39)">
<path d="M0.276,0.012C0.227,0.012 0.186,0 0.15,-0.024C0.115,-0.048 0.088,-0.08 0.069,-0.12C0.05,-0.161 0.04,-0.205 0.04,-0.254C0.04,-0.305 0.051,-0.35 0.072,-0.39C0.094,-0.431 0.125,-0.463 0.165,-0.487C0.205,-0.51 0.254,-0.522 0.31,-0.522C0.366,-0.522 0.413,-0.51 0.452,-0.486C0.491,-0.463 0.521,-0.431 0.542,-0.39C0.562,-0.35 0.573,-0.305 0.573,-0.256L0.573,-0L0.458,-0L0.458,-0.095L0.456,-0.095C0.446,-0.076 0.433,-0.058 0.417,-0.042C0.401,-0.026 0.381,-0.013 0.358,-0.003C0.335,0.007 0.307,0.012 0.276,0.012ZM0.307,-0.086C0.337,-0.086 0.363,-0.093 0.386,-0.108C0.408,-0.123 0.426,-0.144 0.438,-0.17C0.45,-0.195 0.456,-0.224 0.456,-0.256C0.456,-0.288 0.45,-0.317 0.438,-0.342C0.426,-0.367 0.409,-0.387 0.387,-0.402C0.365,-0.417 0.338,-0.424 0.308,-0.424C0.276,-0.424 0.249,-0.417 0.226,-0.402C0.204,-0.387 0.186,-0.366 0.174,-0.341C0.162,-0.315 0.156,-0.287 0.156,-0.255C0.156,-0.224 0.162,-0.195 0.174,-0.169C0.186,-0.144 0.203,-0.123 0.226,-0.108C0.248,-0.093 0.275,-0.086 0.307,-0.086Z" style="fill:rgb(47,47,47);fill-rule:nonzero;"/>
<path d="M0.276,0.012C0.227,0.012 0.186,0 0.15,-0.024C0.115,-0.048 0.088,-0.08 0.069,-0.12C0.05,-0.161 0.04,-0.205 0.04,-0.254C0.04,-0.305 0.051,-0.35 0.072,-0.39C0.094,-0.431 0.125,-0.463 0.165,-0.487C0.205,-0.51 0.254,-0.522 0.31,-0.522C0.366,-0.522 0.413,-0.51 0.452,-0.486C0.491,-0.463 0.521,-0.431 0.542,-0.39C0.562,-0.35 0.573,-0.305 0.573,-0.256L0.573,-0L0.458,-0L0.458,-0.095L0.456,-0.095C0.446,-0.076 0.433,-0.058 0.417,-0.042C0.401,-0.026 0.381,-0.013 0.358,-0.003C0.335,0.007 0.307,0.012 0.276,0.012ZM0.307,-0.086C0.337,-0.086 0.363,-0.093 0.386,-0.108C0.408,-0.123 0.426,-0.144 0.438,-0.17C0.45,-0.195 0.456,-0.224 0.456,-0.256C0.456,-0.288 0.45,-0.317 0.438,-0.342C0.426,-0.367 0.409,-0.387 0.387,-0.402C0.365,-0.417 0.338,-0.424 0.308,-0.424C0.276,-0.424 0.249,-0.417 0.226,-0.402C0.204,-0.387 0.186,-0.366 0.174,-0.341C0.162,-0.315 0.156,-0.287 0.156,-0.255C0.156,-0.224 0.162,-0.195 0.174,-0.169C0.186,-0.144 0.203,-0.123 0.226,-0.108C0.248,-0.093 0.275,-0.086 0.307,-0.086Z" fill="rgb(47,47,47)" fill-rule="nonzero"/>
</g>
<g transform="matrix(116.242,0,0,116.242,226.592,267.39)">
<path d="M0.306,0.012C0.265,0.012 0.229,0.006 0.196,-0.008C0.163,-0.021 0.135,-0.039 0.112,-0.064C0.089,-0.088 0.071,-0.117 0.059,-0.151C0.046,-0.185 0.04,-0.222 0.04,-0.263C0.04,-0.315 0.051,-0.36 0.072,-0.399C0.093,-0.437 0.122,-0.468 0.159,-0.489C0.196,-0.511 0.239,-0.522 0.287,-0.522C0.311,-0.522 0.333,-0.518 0.355,-0.511C0.377,-0.504 0.396,-0.493 0.413,-0.48C0.431,-0.466 0.445,-0.451 0.455,-0.433L0.456,-0.433L0.456,-0.73L0.571,-0.73L0.571,-0.261C0.571,-0.205 0.56,-0.156 0.537,-0.115C0.515,-0.074 0.484,-0.043 0.444,-0.021C0.405,0.001 0.358,0.012 0.306,0.012ZM0.306,-0.086C0.335,-0.086 0.361,-0.093 0.384,-0.107C0.406,-0.122 0.423,-0.141 0.436,-0.167C0.448,-0.192 0.455,-0.221 0.455,-0.255C0.455,-0.288 0.448,-0.317 0.436,-0.343C0.423,-0.368 0.406,-0.388 0.383,-0.402C0.361,-0.417 0.335,-0.424 0.305,-0.424C0.276,-0.424 0.251,-0.417 0.228,-0.402C0.206,-0.387 0.188,-0.368 0.175,-0.342C0.163,-0.317 0.156,-0.288 0.156,-0.255C0.156,-0.222 0.163,-0.193 0.175,-0.167C0.188,-0.142 0.206,-0.122 0.229,-0.108C0.251,-0.093 0.277,-0.086 0.306,-0.086Z" style="fill:rgb(47,47,47);fill-rule:nonzero;"/>
<path d="M0.306,0.012C0.265,0.012 0.229,0.006 0.196,-0.008C0.163,-0.021 0.135,-0.039 0.112,-0.064C0.089,-0.088 0.071,-0.117 0.059,-0.151C0.046,-0.185 0.04,-0.222 0.04,-0.263C0.04,-0.315 0.051,-0.36 0.072,-0.399C0.093,-0.437 0.122,-0.468 0.159,-0.489C0.196,-0.511 0.239,-0.522 0.287,-0.522C0.311,-0.522 0.333,-0.518 0.355,-0.511C0.377,-0.504 0.396,-0.493 0.413,-0.48C0.431,-0.466 0.445,-0.451 0.455,-0.433L0.456,-0.433L0.456,-0.73L0.571,-0.73L0.571,-0.261C0.571,-0.205 0.56,-0.156 0.537,-0.115C0.515,-0.074 0.484,-0.043 0.444,-0.021C0.405,0.001 0.358,0.012 0.306,0.012ZM0.306,-0.086C0.335,-0.086 0.361,-0.093 0.384,-0.107C0.406,-0.122 0.423,-0.141 0.436,-0.167C0.448,-0.192 0.455,-0.221 0.455,-0.255C0.455,-0.288 0.448,-0.317 0.436,-0.343C0.423,-0.368 0.406,-0.388 0.383,-0.402C0.361,-0.417 0.335,-0.424 0.305,-0.424C0.276,-0.424 0.251,-0.417 0.228,-0.402C0.206,-0.387 0.188,-0.368 0.175,-0.342C0.163,-0.317 0.156,-0.288 0.156,-0.255C0.156,-0.222 0.163,-0.193 0.175,-0.167C0.188,-0.142 0.206,-0.122 0.229,-0.108C0.251,-0.093 0.277,-0.086 0.306,-0.086Z" fill="rgb(47,47,47)" fill-rule="nonzero"/>
</g>
<g transform="matrix(116.242,0,0,116.242,290.293,267.39)">
<path d="M0.306,0.012C0.265,0.012 0.229,0.006 0.196,-0.008C0.163,-0.021 0.135,-0.039 0.112,-0.064C0.089,-0.088 0.071,-0.117 0.059,-0.151C0.046,-0.185 0.04,-0.222 0.04,-0.263C0.04,-0.315 0.051,-0.36 0.072,-0.399C0.093,-0.437 0.122,-0.468 0.159,-0.489C0.196,-0.511 0.239,-0.522 0.287,-0.522C0.311,-0.522 0.333,-0.518 0.355,-0.511C0.377,-0.504 0.396,-0.493 0.413,-0.48C0.431,-0.466 0.445,-0.451 0.455,-0.433L0.456,-0.433L0.456,-0.73L0.571,-0.73L0.571,-0.261C0.571,-0.205 0.56,-0.156 0.537,-0.115C0.515,-0.074 0.484,-0.043 0.444,-0.021C0.405,0.001 0.358,0.012 0.306,0.012ZM0.306,-0.086C0.335,-0.086 0.361,-0.093 0.384,-0.107C0.406,-0.122 0.423,-0.141 0.436,-0.167C0.448,-0.192 0.455,-0.221 0.455,-0.255C0.455,-0.288 0.448,-0.317 0.436,-0.343C0.423,-0.368 0.406,-0.388 0.383,-0.402C0.361,-0.417 0.335,-0.424 0.305,-0.424C0.276,-0.424 0.251,-0.417 0.228,-0.402C0.206,-0.387 0.188,-0.368 0.175,-0.342C0.163,-0.317 0.156,-0.288 0.156,-0.255C0.156,-0.222 0.163,-0.193 0.175,-0.167C0.188,-0.142 0.206,-0.122 0.229,-0.108C0.251,-0.093 0.277,-0.086 0.306,-0.086Z" style="fill:rgb(47,47,47);fill-rule:nonzero;"/>
<path d="M0.306,0.012C0.265,0.012 0.229,0.006 0.196,-0.008C0.163,-0.021 0.135,-0.039 0.112,-0.064C0.089,-0.088 0.071,-0.117 0.059,-0.151C0.046,-0.185 0.04,-0.222 0.04,-0.263C0.04,-0.315 0.051,-0.36 0.072,-0.399C0.093,-0.437 0.122,-0.468 0.159,-0.489C0.196,-0.511 0.239,-0.522 0.287,-0.522C0.311,-0.522 0.333,-0.518 0.355,-0.511C0.377,-0.504 0.396,-0.493 0.413,-0.48C0.431,-0.466 0.445,-0.451 0.455,-0.433L0.456,-0.433L0.456,-0.73L0.571,-0.73L0.571,-0.261C0.571,-0.205 0.56,-0.156 0.537,-0.115C0.515,-0.074 0.484,-0.043 0.444,-0.021C0.405,0.001 0.358,0.012 0.306,0.012ZM0.306,-0.086C0.335,-0.086 0.361,-0.093 0.384,-0.107C0.406,-0.122 0.423,-0.141 0.436,-0.167C0.448,-0.192 0.455,-0.221 0.455,-0.255C0.455,-0.288 0.448,-0.317 0.436,-0.343C0.423,-0.368 0.406,-0.388 0.383,-0.402C0.361,-0.417 0.335,-0.424 0.305,-0.424C0.276,-0.424 0.251,-0.417 0.228,-0.402C0.206,-0.387 0.188,-0.368 0.175,-0.342C0.163,-0.317 0.156,-0.288 0.156,-0.255C0.156,-0.222 0.163,-0.193 0.175,-0.167C0.188,-0.142 0.206,-0.122 0.229,-0.108C0.251,-0.093 0.277,-0.086 0.306,-0.086Z" fill="rgb(47,47,47)" fill-rule="nonzero"/>
</g>
</g>
<g id="c" transform="matrix(-0.0716462,0.31304,-0.583685,-0.0384251,1489.76,-444.051)">
<path d="M2668.11,700.4C2666.79,703.699 2666.12,707.216 2666.12,710.766C2666.12,726.268 2678.71,738.854 2694.21,738.854C2709.71,738.854 2722.3,726.268 2722.3,710.766C2722.3,704.111 2719.93,697.672 2715.63,692.597L2707.63,699.378C2710.33,702.559 2711.57,706.602 2711.81,710.766C2712.2,717.38 2706.61,724.52 2697.27,726.637C2683.9,728.581 2676.61,720.482 2676.61,710.766C2676.61,708.541 2677.03,706.336 2677.85,704.269L2668.11,700.4Z" style="fill:rgb(46,46,46);"/>
<path d="M2668.11,700.4C2666.79,703.699 2666.12,707.216 2666.12,710.766C2666.12,726.268 2678.71,738.854 2694.21,738.854C2709.71,738.854 2722.3,726.268 2722.3,710.766C2722.3,704.111 2719.93,697.672 2715.63,692.597L2707.63,699.378C2710.33,702.559 2711.57,706.602 2711.81,710.766C2712.2,717.38 2706.61,724.52 2697.27,726.637C2683.9,728.581 2676.61,720.482 2676.61,710.766C2676.61,708.541 2677.03,706.336 2677.85,704.269L2668.11,700.4Z" fill="rgb(46,46,46)"/>
</g>
</g>
<g id="R" transform="matrix(0.426446,0,0,0.451034,-1192.44,-722.167)">
<g transform="matrix(1,0,0,1,-0.10786,0.450801)">
<g transform="matrix(12.1247,0,0,12.1247,3862.61,1929.9)">
<path d="M0.073,-0L0.073,-0.7L0.383,-0.7C0.428,-0.7 0.469,-0.69 0.506,-0.67C0.543,-0.651 0.572,-0.623 0.594,-0.588C0.616,-0.553 0.627,-0.512 0.627,-0.465C0.627,-0.418 0.615,-0.377 0.592,-0.342C0.569,-0.306 0.539,-0.279 0.501,-0.259L0.57,-0.128C0.574,-0.12 0.579,-0.115 0.584,-0.111C0.59,-0.107 0.596,-0.106 0.605,-0.106L0.664,-0.106L0.664,-0L0.587,-0C0.56,-0 0.535,-0.007 0.514,-0.02C0.493,-0.034 0.476,-0.052 0.463,-0.075L0.381,-0.232C0.375,-0.231 0.368,-0.231 0.361,-0.231C0.354,-0.231 0.347,-0.231 0.34,-0.231L0.192,-0.231L0.192,-0L0.073,-0ZM0.192,-0.336L0.368,-0.336C0.394,-0.336 0.417,-0.341 0.438,-0.351C0.459,-0.361 0.476,-0.376 0.489,-0.396C0.501,-0.415 0.507,-0.438 0.507,-0.465C0.507,-0.492 0.501,-0.516 0.488,-0.535C0.475,-0.554 0.459,-0.569 0.438,-0.579C0.417,-0.59 0.394,-0.595 0.369,-0.595L0.192,-0.595L0.192,-0.336Z" style="fill:rgb(46,46,46);fill-rule:nonzero;"/>
<path d="M0.073,-0L0.073,-0.7L0.383,-0.7C0.428,-0.7 0.469,-0.69 0.506,-0.67C0.543,-0.651 0.572,-0.623 0.594,-0.588C0.616,-0.553 0.627,-0.512 0.627,-0.465C0.627,-0.418 0.615,-0.377 0.592,-0.342C0.569,-0.306 0.539,-0.279 0.501,-0.259L0.57,-0.128C0.574,-0.12 0.579,-0.115 0.584,-0.111C0.59,-0.107 0.596,-0.106 0.605,-0.106L0.664,-0.106L0.664,-0L0.587,-0C0.56,-0 0.535,-0.007 0.514,-0.02C0.493,-0.034 0.476,-0.052 0.463,-0.075L0.381,-0.232C0.375,-0.231 0.368,-0.231 0.361,-0.231C0.354,-0.231 0.347,-0.231 0.34,-0.231L0.192,-0.231L0.192,-0L0.073,-0ZM0.192,-0.336L0.368,-0.336C0.394,-0.336 0.417,-0.341 0.438,-0.351C0.459,-0.361 0.476,-0.376 0.489,-0.396C0.501,-0.415 0.507,-0.438 0.507,-0.465C0.507,-0.492 0.501,-0.516 0.488,-0.535C0.475,-0.554 0.459,-0.569 0.438,-0.579C0.417,-0.59 0.394,-0.595 0.369,-0.595L0.192,-0.595L0.192,-0.336Z" fill="rgb(46,46,46)" fill-rule="nonzero"/>
</g>
</g>
<g transform="matrix(1,0,0,1,0.278569,0.101881)">
<circle cx="3866.43" cy="1926.14" r="8.923" style="fill:none;stroke:rgb(46,46,46);stroke-width:2px;stroke-linecap:butt;stroke-linejoin:miter;"/>
<circle cx="3866.43" cy="1926.14" r="8.923" fill="none" stroke="rgb(46,46,46)" stroke-width="2px" stroke-linecap="butt" stroke-linejoin="miter"/>
</g>
</g>
</g>
@ -1074,7 +1085,7 @@ footer {
</a>
</footer>
<script>
<script {{ $nonceAttribute }}>
const filterEl = document.getElementById('filter');
filterEl?.focus({ preventScroll: true });
@ -1120,6 +1131,20 @@ footer {
});
}
const filterElem = document.getElementById("filter");
if (filterElem) {
filterElem.addEventListener("keyup", filter);
}
document.getElementById("layout-list").addEventListener("click", function() {
queryParam('layout', '');
});
document.getElementById("layout-grid").addEventListener("click", function() {
queryParam('layout', 'grid');
});
window.addEventListener("load", initPage);
function queryParam(k, v) {
const qs = new URLSearchParams(window.location.search);
if (!v) {

View file

@ -80,6 +80,13 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS,
}
size := info.Size()
if !isDir {
// increase the total by the symlink's size, not the target's size,
// by incrementing before we follow the symlink
tplCtx.TotalFileSize += size
}
fileIsSymlink := isSymlink(info)
symlinkPath := ""
if fileIsSymlink {
@ -103,7 +110,8 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS,
}
if !isDir {
tplCtx.TotalFileSize += size
// increase the total including the symlink target's size
tplCtx.TotalFileSizeFollowingSymlinks += size
}
u := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name
@ -150,9 +158,15 @@ 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.
// The total size of all files in the listing. Only includes the
// size of the files themselves, not the size of symlink targets
// (i.e. the calculation of this value does not follow symlinks).
TotalFileSize int64 `json:"total_file_size"`
// The total size of all files in the listing, including the
// size of the files targeted by symlinks.
TotalFileSizeFollowingSymlinks int64 `json:"total_file_size_following_symlinks"`
// Sort column used
Sort string `json:"sort,omitempty"`
@ -288,6 +302,12 @@ func (btc browseTemplateContext) HumanTotalFileSize() string {
return humanize.IBytes(uint64(btc.TotalFileSize))
}
// HumanTotalFileSizeFollowingSymlinks is the same as HumanTotalFileSize
// except the returned value reflects the size of symlink targets.
func (btc browseTemplateContext) HumanTotalFileSizeFollowingSymlinks() string {
return humanize.IBytes(uint64(btc.TotalFileSizeFollowingSymlinks))
}
// HumanModTime returns the modified time of the file
// as a human-readable string given by format.
func (fi fileInfo) HumanModTime(format string) string {
@ -353,4 +373,7 @@ const (
sortByNameDirFirst = "namedirfirst"
sortBySize = "size"
sortByTime = "time"
sortOrderAsc = "asc"
sortOrderDesc = "desc"
)

View file

@ -119,6 +119,16 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return d.Err("Symlinks path reveal is already enabled")
}
fsrv.Browse.RevealSymlinks = true
case "sort":
for d.NextArg() {
dVal := d.Val()
switch dVal {
case sortByName, sortByNameDirFirst, sortBySize, sortByTime, sortOrderAsc, sortOrderDesc:
fsrv.Browse.SortOptions = append(fsrv.Browse.SortOptions, dVal)
default:
return d.Errf("unknown sort option '%s'", dVal)
}
}
default:
return d.Errf("unknown subdirective '%s'", d.Val())
}

View file

@ -15,6 +15,7 @@
package fileserver
import (
"bytes"
"errors"
"fmt"
"io"
@ -235,6 +236,24 @@ func (fsrv *FileServer) Provision(ctx caddy.Context) error {
fsrv.precompressors[ae] = p
}
if fsrv.Browse != nil {
// check sort options
for idx, sortOption := range fsrv.Browse.SortOptions {
switch idx {
case 0:
if sortOption != sortByName && sortOption != sortByNameDirFirst && sortOption != sortBySize && sortOption != sortByTime {
return fmt.Errorf("the first option must be one of the following: %s, %s, %s, %s, but got %s", sortByName, sortByNameDirFirst, sortBySize, sortByTime, sortOption)
}
case 1:
if sortOption != sortOrderAsc && sortOption != sortOrderDesc {
return fmt.Errorf("the second option must be one of the following: %s, %s, but got %s", sortOrderAsc, sortOrderDesc, sortOption)
}
default:
return fmt.Errorf("only max 2 sort options are allowed, but got %d", idx+1)
}
}
}
return nil
}
@ -690,6 +709,10 @@ func (fsrv *FileServer) getEtagFromFile(fileSystem fs.FS, filename string) (stri
if err != nil {
return "", fmt.Errorf("cannot read etag from file %s: %v", etagFilename, err)
}
// Etags should not contain newline characters
etag = bytes.ReplaceAll(etag, []byte("\n"), []byte{})
return string(etag), nil
}
return "", nil

View file

@ -50,7 +50,6 @@ type Intercept struct {
//
// Three new placeholders are available in this handler chain:
// - `{http.intercept.status_code}` The status code from the response
// - `{http.intercept.status_text}` The status text from the response
// - `{http.intercept.header.*}` The headers from the response
HandleResponse []caddyhttp.ResponseHandler `json:"handle_response,omitempty"`
@ -161,7 +160,7 @@ func (ir Intercept) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
// set up the replacer so that parts of the original response can be
// used for routing decisions
for field, value := range r.Header {
for field, value := range rec.Header() {
repl.Set("http.intercept.header."+field, strings.Join(value, ","))
}
repl.Set("http.intercept.status_code", rec.Status())

View file

@ -29,6 +29,7 @@ import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/internal"
)
// MatchRemoteIP matches requests by the remote IP address,
@ -79,7 +80,7 @@ func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return d.Err("the 'forwarded' option is no longer supported; use the 'client_ip' matcher instead")
}
if d.Val() == "private_ranges" {
m.Ranges = append(m.Ranges, PrivateRangesCIDR()...)
m.Ranges = append(m.Ranges, internal.PrivateRangesCIDR()...)
continue
}
m.Ranges = append(m.Ranges, d.Val())
@ -143,6 +144,9 @@ func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
// Match returns true if r matches m.
func (m MatchRemoteIP) Match(r *http.Request) bool {
if r.TLS != nil && !r.TLS.HandshakeComplete {
return false // if handshake is not finished, we infer 0-RTT that has not verified remote IP; could be spoofed
}
address := r.RemoteAddr
clientIP, zoneID, err := parseIPZoneFromString(address)
if err != nil {
@ -170,7 +174,7 @@ 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()...)
m.Ranges = append(m.Ranges, internal.PrivateRangesCIDR()...)
continue
}
m.Ranges = append(m.Ranges, d.Val())
@ -228,6 +232,9 @@ func (m *MatchClientIP) Provision(ctx caddy.Context) error {
// Match returns true if r matches m.
func (m MatchClientIP) Match(r *http.Request) bool {
if r.TLS != nil && !r.TLS.HandshakeComplete {
return false // if handshake is not finished, we infer 0-RTT that has not verified remote IP; could be spoofed
}
address := GetVar(r.Context(), ClientIPVarKey).(string)
clientIP, zoneID, err := parseIPZoneFromString(address)
if err != nil {
@ -244,7 +251,9 @@ func (m MatchClientIP) Match(r *http.Request) bool {
func provisionCidrsZonesFromRanges(ranges []string) ([]*netip.Prefix, []string, error) {
cidrs := []*netip.Prefix{}
zones := []string{}
repl := caddy.NewReplacer()
for _, str := range ranges {
str = repl.ReplaceAll(str, "")
// Exclude the zone_id from the IP
if strings.Contains(str, "%") {
split := strings.Split(str, "%")

View file

@ -22,6 +22,7 @@ import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/internal"
)
func init() {
@ -92,7 +93,7 @@ func (m *StaticIPRange) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
for d.NextArg() {
if d.Val() == "private_ranges" {
m.Ranges = append(m.Ranges, PrivateRangesCIDR()...)
m.Ranges = append(m.Ranges, internal.PrivateRangesCIDR()...)
continue
}
m.Ranges = append(m.Ranges, d.Val())
@ -121,22 +122,16 @@ func CIDRExpressionToPrefix(expr string) (netip.Prefix, error) {
return prefix, nil
}
// 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",
}
}
// Interface guards
var (
_ caddy.Provisioner = (*StaticIPRange)(nil)
_ caddyfile.Unmarshaler = (*StaticIPRange)(nil)
_ IPRangeSource = (*StaticIPRange)(nil)
)
// PrivateRangesCIDR returns a list of private CIDR range
// strings, which can be used as a configuration shortcut.
// Note: this function is used at least by mholt/caddy-l4.
func PrivateRangesCIDR() []string {
return internal.PrivateRangesCIDR()
}

View file

@ -34,6 +34,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/net/idna"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@ -177,6 +178,22 @@ type (
// "http/2", "http/3", or minimum versions: "http/2+", etc.
MatchProtocol string
// MatchTLS matches HTTP requests based on the underlying
// TLS connection state. If this matcher is specified but
// the request did not come over TLS, it will never match.
// If this matcher is specified but is empty and the request
// did come in over TLS, it will always match.
MatchTLS struct {
// Matches if the TLS handshake has completed. QUIC 0-RTT early
// data may arrive before the handshake completes. Generally, it
// is unsafe to replay these requests if they are not idempotent;
// additionally, the remote IP of early data packets can more
// easily be spoofed. It is conventional to respond with HTTP 425
// Too Early if the request cannot risk being processed in this
// state.
HandshakeComplete *bool `json:"handshake_complete,omitempty"`
}
// MatchNot matches requests by negating the results of its matcher
// sets. A single "not" matcher takes one or more matcher sets. Each
// matcher set is OR'ed; in other words, if any matcher set returns
@ -212,6 +229,7 @@ func init() {
caddy.RegisterModule(MatchHeader{})
caddy.RegisterModule(MatchHeaderRE{})
caddy.RegisterModule(new(MatchProtocol))
caddy.RegisterModule(MatchTLS{})
caddy.RegisterModule(MatchNot{})
}
@ -239,13 +257,20 @@ func (m *MatchHost) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
func (m MatchHost) Provision(_ caddy.Context) error {
// check for duplicates; they are nonsensical and reduce efficiency
// (we could just remove them, but the user should know their config is erroneous)
seen := make(map[string]int)
for i, h := range m {
h = strings.ToLower(h)
if firstI, ok := seen[h]; ok {
return fmt.Errorf("host at index %d is repeated at index %d: %s", firstI, i, h)
seen := make(map[string]int, len(m))
for i, host := range m {
asciiHost, err := idna.ToASCII(host)
if err != nil {
return fmt.Errorf("converting hostname '%s' to ASCII: %v", host, err)
}
seen[h] = i
if asciiHost != host {
m[i] = asciiHost
}
normalizedHost := strings.ToLower(asciiHost)
if firstI, ok := seen[normalizedHost]; ok {
return fmt.Errorf("host at index %d is repeated at index %d: %s", firstI, i, host)
}
seen[normalizedHost] = i
}
if m.large() {
@ -1228,6 +1253,53 @@ func (MatchProtocol) CELLibrary(_ caddy.Context) (cel.Library, error) {
)
}
// CaddyModule returns the Caddy module information.
func (MatchTLS) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.matchers.tls",
New: func() caddy.Module { return new(MatchTLS) },
}
}
// Match returns true if r matches m.
func (m MatchTLS) Match(r *http.Request) bool {
if r.TLS == nil {
return false
}
if m.HandshakeComplete != nil {
if (!*m.HandshakeComplete && r.TLS.HandshakeComplete) ||
(*m.HandshakeComplete && !r.TLS.HandshakeComplete) {
return false
}
}
return true
}
// UnmarshalCaddyfile parses Caddyfile tokens for this matcher. Syntax:
//
// ... tls [early_data]
//
// EXPERIMENTAL SYNTAX: Subject to change.
func (m *MatchTLS) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// iterate to merge multiple matchers into one
for d.Next() {
if d.NextArg() {
switch d.Val() {
case "early_data":
var false bool
m.HandshakeComplete = &false
}
}
if d.NextArg() {
return d.ArgErr()
}
if d.NextBlock(0) {
return d.Err("malformed tls matcher: blocks are not supported yet")
}
}
return nil
}
// CaddyModule returns the Caddy module information.
func (MatchNot) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{

View file

@ -78,6 +78,11 @@ func TestHostMatcher(t *testing.T) {
input: "bar.example.com",
expect: false,
},
{
match: MatchHost{"éxàmplê.com"},
input: "xn--xmpl-0na6cm.com",
expect: true,
},
{
match: MatchHost{"*.example.com"},
input: "example.com",
@ -149,6 +154,10 @@ func TestHostMatcher(t *testing.T) {
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
if err := tc.match.Provision(caddy.Context{}); err != nil {
t.Errorf("Test %d %v: provisioning failed: %v", i, tc.match, err)
}
actual := tc.match.Match(req)
if actual != tc.expect {
t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)

View file

@ -40,7 +40,7 @@ type ListenerWrapper struct {
Allow []string `json:"allow,omitempty"`
allow []netip.Prefix
// Denby is an optional list of CIDR ranges to
// Deny is an optional list of CIDR ranges to
// deny PROXY headers from.
Deny []string `json:"deny,omitempty"`
deny []netip.Prefix
@ -50,7 +50,7 @@ type ListenerWrapper struct {
// Policy definitions are here: https://pkg.go.dev/github.com/pires/go-proxyproto@v0.7.0#Policy
FallbackPolicy Policy `json:"fallback_policy,omitempty"`
policy goproxy.PolicyFunc
policy goproxy.ConnPolicyFunc
}
// Provision sets up the listener wrapper.
@ -69,13 +69,14 @@ func (pp *ListenerWrapper) Provision(ctx caddy.Context) error {
}
pp.deny = append(pp.deny, ipnet)
}
pp.policy = func(upstream net.Addr) (goproxy.Policy, error) {
pp.policy = func(options goproxy.ConnPolicyOptions) (goproxy.Policy, error) {
// trust unix sockets
if network := upstream.Network(); caddy.IsUnixNetwork(network) {
if network := options.Upstream.Network(); caddy.IsUnixNetwork(network) {
return goproxy.USE, nil
}
ret := pp.FallbackPolicy
host, _, err := net.SplitHostPort(upstream.String())
host, _, err := net.SplitHostPort(options.Upstream.String())
if err != nil {
return goproxy.REJECT, err
}
@ -106,6 +107,6 @@ func (pp *ListenerWrapper) WrapListener(l net.Listener) net.Listener {
Listener: l,
ReadHeaderTimeout: time.Duration(pp.Timeout),
}
pl.Policy = pp.policy
pl.ConnPolicy = pp.policy
return pl
}

View file

@ -142,8 +142,16 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
}
return port, true
case "http.request.remote":
if req.TLS != nil && !req.TLS.HandshakeComplete {
// without a complete handshake (QUIC "early data") we can't trust the remote IP address to not be spoofed
return nil, true
}
return req.RemoteAddr, true
case "http.request.remote.host":
if req.TLS != nil && !req.TLS.HandshakeComplete {
// without a complete handshake (QUIC "early data") we can't trust the remote IP address to not be spoofed
return nil, true
}
host, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
// req.RemoteAddr is host:port for tcp and udp sockets and /unix/socket.path

View file

@ -219,13 +219,13 @@ func (rr *responseRecorder) Buffered() bool {
}
func (rr *responseRecorder) WriteResponse() error {
if rr.stream {
return nil
}
if rr.statusCode == 0 {
// could happen if no handlers actually wrote anything,
// and this prevents a panic; status must be > 0
rr.statusCode = http.StatusOK
rr.WriteHeader(http.StatusOK)
}
if rr.stream {
return nil
}
rr.ResponseWriterWrapper.WriteHeader(rr.statusCode)
_, err := io.Copy(rr.ResponseWriterWrapper, rr.buf)

View file

@ -16,6 +16,7 @@ package reverseproxy
import (
"fmt"
"net"
"net/http"
"reflect"
"strconv"
@ -27,6 +28,7 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/internal"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
@ -67,14 +69,16 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
// lb_retry_match <request-matcher>
//
// # active health checking
// health_uri <uri>
// health_port <port>
// health_interval <interval>
// health_passes <num>
// health_fails <num>
// health_timeout <duration>
// health_status <status>
// health_body <regexp>
// health_uri <uri>
// health_port <port>
// health_interval <interval>
// health_passes <num>
// health_fails <num>
// health_timeout <duration>
// health_status <status>
// health_body <regexp>
// health_method <value>
// health_request_body <value>
// health_follow_redirects
// health_headers {
// <field> [<values...>]
@ -353,6 +357,26 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
h.HealthChecks.Active.Path = d.Val()
caddy.Log().Named("config.adapter.caddyfile").Warn("the 'health_path' subdirective is deprecated, please use 'health_uri' instead!")
case "health_upstream":
if !d.NextArg() {
return d.ArgErr()
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Active == nil {
h.HealthChecks.Active = new(ActiveHealthChecks)
}
_, port, err := net.SplitHostPort(d.Val())
if err != nil {
return d.Errf("health_upstream is malformed '%s': %v", d.Val(), err)
}
_, err = strconv.Atoi(port)
if err != nil {
return d.Errf("bad port number '%s': %v", d.Val(), err)
}
h.HealthChecks.Active.Upstream = d.Val()
case "health_port":
if !d.NextArg() {
return d.ArgErr()
@ -363,6 +387,9 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if h.HealthChecks.Active == nil {
h.HealthChecks.Active = new(ActiveHealthChecks)
}
if h.HealthChecks.Active.Upstream != "" {
return d.Errf("the 'health_port' subdirective is ignored if 'health_upstream' is used!")
}
portNum, err := strconv.Atoi(d.Val())
if err != nil {
return d.Errf("bad port number '%s': %v", d.Val(), err)
@ -387,6 +414,30 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
h.HealthChecks.Active.Headers = healthHeaders
case "health_method":
if !d.NextArg() {
return d.ArgErr()
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Active == nil {
h.HealthChecks.Active = new(ActiveHealthChecks)
}
h.HealthChecks.Active.Method = d.Val()
case "health_request_body":
if !d.NextArg() {
return d.ArgErr()
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Active == nil {
h.HealthChecks.Active = new(ActiveHealthChecks)
}
h.HealthChecks.Active.Body = d.Val()
case "health_interval":
if !d.NextArg() {
return d.ArgErr()
@ -651,7 +702,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
case "trusted_proxies":
for d.NextArg() {
if d.Val() == "private_ranges" {
h.TrustedProxies = append(h.TrustedProxies, caddyhttp.PrivateRangesCIDR()...)
h.TrustedProxies = append(h.TrustedProxies, internal.PrivateRangesCIDR()...)
continue
}
h.TrustedProxies = append(h.TrustedProxies, d.Val())
@ -1275,7 +1326,11 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return d.Err("cannot specify \"tls_trust_pool\" twice in caddyfile")
}
h.TLS.CARaw = caddyconfig.JSONModuleObject(ca, "provider", modStem, nil)
case "local_address":
if !d.NextArg() {
return d.ArgErr()
}
h.LocalAddress = d.Val()
default:
return d.Errf("unrecognized subdirective %s", d.Val())
}

View file

@ -229,11 +229,13 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
if changeHost {
if handler.Headers == nil {
handler.Headers = &headers.Handler{
Request: &headers.HeaderOps{
Set: http.Header{},
},
}
handler.Headers = new(headers.Handler)
}
if handler.Headers.Request == nil {
handler.Headers.Request = new(headers.HeaderOps)
}
if handler.Headers.Request.Set == nil {
handler.Headers.Request.Set = http.Header{}
}
handler.Headers.Request.Set.Set("Host", "{http.reverse_proxy.upstream.hostport}")
}

View file

@ -24,6 +24,7 @@ import (
"regexp"
"runtime/debug"
"strconv"
"strings"
"time"
"go.uber.org/zap"
@ -75,13 +76,27 @@ type ActiveHealthChecks struct {
// The URI (path and query) to use for health checks
URI string `json:"uri,omitempty"`
// The host:port to use (if different from the upstream's dial address)
// for health checks. This should be used in tandem with `health_header` and
// `{http.reverse_proxy.active.target_upstream}`. This can be helpful when
// creating an intermediate service to do a more thorough health check.
// If upstream is set, the active health check port is ignored.
Upstream string `json:"upstream,omitempty"`
// The port to use (if different from the upstream's dial
// address) for health checks.
// address) for health checks. If active upstream is set,
// this value is ignored.
Port int `json:"port,omitempty"`
// HTTP headers to set on health check requests.
Headers http.Header `json:"headers,omitempty"`
// The HTTP method to use for health checks (default "GET").
Method string `json:"method,omitempty"`
// The body to send with the health check request.
Body string `json:"body,omitempty"`
// Whether to follow HTTP redirects in response to active health checks (default off).
FollowRedirects bool `json:"follow_redirects,omitempty"`
@ -133,6 +148,11 @@ func (a *ActiveHealthChecks) Provision(ctx caddy.Context, h *Handler) error {
}
a.Headers = cleaned
// If Method is not set, default to GET
if a.Method == "" {
a.Method = http.MethodGet
}
h.HealthChecks.Active.logger = h.logger.Named("health_checker.active")
timeout := time.Duration(a.Timeout)
@ -165,9 +185,14 @@ func (a *ActiveHealthChecks) Provision(ctx caddy.Context, h *Handler) error {
}
for _, upstream := range h.Upstreams {
// if there's an alternative port for health-check provided in the config,
// then use it, otherwise use the port of upstream.
if a.Port != 0 {
// if there's an alternative upstream for health-check provided in the config,
// then use it, otherwise use the upstream's dial address. if upstream is used,
// then the port is ignored.
if a.Upstream != "" {
upstream.activeHealthCheckUpstream = a.Upstream
} else if a.Port != 0 {
// if there's an alternative port for health-check provided in the config,
// then use it, otherwise use the port of upstream.
upstream.activeHealthCheckPort = a.Port
}
}
@ -312,7 +337,7 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
// so use a fake Host value instead; unix sockets are usually local
hostAddr = "localhost"
}
err = h.doActiveHealthCheck(DialInfo{Network: addr.Network, Address: dialAddr}, hostAddr, upstream)
err = h.doActiveHealthCheck(DialInfo{Network: addr.Network, Address: dialAddr}, hostAddr, networkAddr, upstream)
if err != nil {
h.HealthChecks.Active.logger.Error("active health check failed",
zap.String("address", hostAddr),
@ -330,7 +355,7 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
// according to whether it passes the health check. An error is
// returned only if the health check fails to occur or if marking
// the host's health status fails.
func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstream *Upstream) error {
func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networkAddr string, upstream *Upstream) error {
// create the URL for the request that acts as a health check
u := &url.URL{
Scheme: "http",
@ -342,7 +367,12 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre
if err != nil {
host = hostAddr
}
if h.HealthChecks.Active.Port != 0 {
// ignore active health check port if active upstream is provided as the
// active upstream already contains the replacement port
if h.HealthChecks.Active.Upstream != "" {
u.Host = h.HealthChecks.Active.Upstream
} else if h.HealthChecks.Active.Port != 0 {
port := strconv.Itoa(h.HealthChecks.Active.Port)
u.Host = net.JoinHostPort(host, port)
}
@ -370,6 +400,16 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre
u.Path = h.HealthChecks.Active.Path
}
// replacer used for both body and headers. Only globals (env vars, system info, etc.) are available
repl := caddy.NewReplacer()
// if body is provided, create a reader for it, otherwise nil
var requestBody io.Reader
if h.HealthChecks.Active.Body != "" {
// set body, using replacer
requestBody = strings.NewReader(repl.ReplaceAll(h.HealthChecks.Active.Body, ""))
}
// attach dialing information to this request, as well as context values that
// may be expected by handlers of this request
ctx := h.ctx.Context
@ -377,15 +417,15 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre
ctx = context.WithValue(ctx, caddyhttp.VarsCtxKey, map[string]any{
dialInfoVarKey: dialInfo,
})
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
req, err := http.NewRequestWithContext(ctx, h.HealthChecks.Active.Method, u.String(), requestBody)
if err != nil {
return fmt.Errorf("making request: %v", err)
}
ctx = context.WithValue(ctx, caddyhttp.OriginalRequestCtxKey, *req)
req = req.WithContext(ctx)
// set headers, using a replacer with only globals (env vars, system info, etc.)
repl := caddy.NewReplacer()
// set headers, using replacer
repl.Set("http.reverse_proxy.active.target_upstream", networkAddr)
for key, vals := range h.HealthChecks.Active.Headers {
key = repl.ReplaceAll(key, "")
if key == "Host" {
@ -426,6 +466,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre
}
if upstream.Host.activeHealthPasses() >= h.HealthChecks.Active.Passes {
if upstream.setHealthy(true) {
h.HealthChecks.Active.logger.Info("host is up", zap.String("host", hostAddr))
h.events.Emit(h.ctx, "healthy", map[string]any{"host": hostAddr})
upstream.Host.resetHealth()
}
@ -492,7 +533,6 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre
}
// passed health check parameters, so mark as healthy
h.HealthChecks.Active.logger.Info("host is up", zap.String("host", hostAddr))
markHealthy()
return nil

View file

@ -57,10 +57,11 @@ type Upstream struct {
// HeaderAffinity string
// IPAffinity string
activeHealthCheckPort int
healthCheckPolicy *PassiveHealthChecks
cb CircuitBreaker
unhealthy int32 // accessed atomically; status from active health checker
activeHealthCheckPort int
activeHealthCheckUpstream string
healthCheckPolicy *PassiveHealthChecks
cb CircuitBreaker
unhealthy int32 // accessed atomically; status from active health checker
}
// (pointer receiver necessary to avoid a race condition, since

View file

@ -132,6 +132,10 @@ type HTTPTransport struct {
// to change or removal while experimental.
Versions []string `json:"versions,omitempty"`
// Specify the address to bind to when connecting to an upstream. In other words,
// it is the address the upstream sees as the remote address.
LocalAddress string `json:"local_address,omitempty"`
// The pre-configured underlying HTTP transport.
Transport *http.Transport `json:"-"`
@ -185,6 +189,31 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
FallbackDelay: time.Duration(h.FallbackDelay),
}
if h.LocalAddress != "" {
netaddr, err := caddy.ParseNetworkAddressWithDefaults(h.LocalAddress, "tcp", 0)
if err != nil {
return nil, err
}
if netaddr.PortRangeSize() > 1 {
return nil, fmt.Errorf("local_address must be a single address, not a port range")
}
switch netaddr.Network {
case "tcp", "tcp4", "tcp6":
dialer.LocalAddr, err = net.ResolveTCPAddr(netaddr.Network, netaddr.JoinHostPort(0))
if err != nil {
return nil, err
}
case "unix", "unixgram", "unixpacket":
dialer.LocalAddr, err = net.ResolveUnixAddr(netaddr.Network, netaddr.JoinHostPort(0))
if err != nil {
return nil, err
}
case "udp", "udp4", "udp6":
return nil, fmt.Errorf("local_address must be a TCP address, not a UDP address")
default:
return nil, fmt.Errorf("unsupported network")
}
}
if h.Resolver != nil {
err := h.Resolver.ParseAddresses()
if err != nil {
@ -363,6 +392,13 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
// site owners control the backends), so it must be exclusive
if len(h.Versions) == 1 && h.Versions[0] == "3" {
h.h3Transport = new(http3.RoundTripper)
if h.TLS != nil {
var err error
h.h3Transport.TLSClientConfig, err = h.TLS.MakeTLSClientConfig(caddyCtx)
if err != nil {
return nil, fmt.Errorf("making TLS client config for HTTP/3 transport: %v", err)
}
}
} else if len(h.Versions) > 1 && sliceContains(h.Versions, "3") {
return nil, fmt.Errorf("if HTTP/3 is enabled to the upstream, no other HTTP versions are supported")
}
@ -439,6 +475,9 @@ func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// if H2C ("HTTP/2 over cleartext") is enabled and the upstream request is
// HTTP without TLS, use the alternate H2C-capable transport instead
if req.URL.Scheme == "http" && h.h2cTransport != nil {
// There is no dedicated DisableKeepAlives field in *http2.Transport.
// This is an alternative way to disable keep-alive.
req.Close = h.Transport.DisableKeepAlives
return h.h2cTransport.RoundTrip(req)
}

View file

@ -67,6 +67,7 @@ func init() {
// `{http.reverse_proxy.upstream.duration_ms}` | Same as 'upstream.duration', but in milliseconds.
// `{http.reverse_proxy.duration}` | Total time spent proxying, including selecting an upstream, retries, and writing response.
// `{http.reverse_proxy.duration_ms}` | Same as 'duration', but in milliseconds.
// `{http.reverse_proxy.retries}` | The number of retries actually performed to communicate with an upstream.
type Handler struct {
// Configures the method of transport for the proxy. A transport
// is what performs the actual "round trip" to the backend.
@ -443,6 +444,9 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
retries++
}
// number of retries actually performed
repl.Set("http.reverse_proxy.retries", retries)
if proxyErr != nil {
return statusError(proxyErr)
}
@ -605,6 +609,18 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http.
req.Header.Set("User-Agent", "")
}
// Indicate if request has been conveyed in early data.
// RFC 8470: "An intermediary that forwards a request prior to the
// completion of the TLS handshake with its client MUST send it with
// the Early-Data header field set to “1” (i.e., it adds it if not
// present in the request). An intermediary MUST use the Early-Data
// header field if the request might have been subject to a replay and
// might already have been forwarded by it or another instance
// (see Section 6.2)."
if req.TLS != nil && !req.TLS.HandshakeComplete {
req.Header.Set("Early-Data", "1")
}
reqUpType := upgradeType(req.Header)
removeConnectionHeaders(req.Header)
@ -967,7 +983,7 @@ func (h *Handler) finalizeResponse(
// we'll just log the error and abort the stream here and panic just as
// the standard lib's proxy to propagate the stream error.
// see issue https://github.com/caddyserver/caddy/issues/5951
logger.Error("aborting with incomplete response", zap.Error(err))
logger.Warn("aborting with incomplete response", zap.Error(err))
// no extra logging from stdlib
panic(http.ErrAbortHandler)
}

View file

@ -26,6 +26,7 @@ import (
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/cespare/xxhash/v2"
@ -613,6 +614,8 @@ type CookieHashSelection struct {
Name string `json:"name,omitempty"`
// Secret to hash (Hmac256) chosen upstream in cookie
Secret string `json:"secret,omitempty"`
// The cookie's Max-Age before it expires. Default is no expiry.
MaxAge caddy.Duration `json:"max_age,omitempty"`
// The fallback policy to use if the cookie is not present. Defaults to `random`.
FallbackRaw json.RawMessage `json:"fallback,omitempty" caddy:"namespace=http.reverse_proxy.selection_policies inline_key=policy"`
@ -671,6 +674,9 @@ func (s CookieHashSelection) Select(pool UpstreamPool, req *http.Request, w http
cookie.Secure = true
cookie.SameSite = http.SameSiteNoneMode
}
if s.MaxAge > 0 {
cookie.MaxAge = int(time.Duration(s.MaxAge).Seconds())
}
http.SetCookie(w, cookie)
return upstream
}
@ -699,6 +705,7 @@ func (s CookieHashSelection) Select(pool UpstreamPool, req *http.Request, w http
//
// lb_policy cookie [<name> [<secret>]] {
// fallback <policy>
// max_age <duration>
// }
//
// By default name is `lb`
@ -728,6 +735,24 @@ func (s *CookieHashSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return err
}
s.FallbackRaw = mod
case "max_age":
if !d.NextArg() {
return d.ArgErr()
}
if s.MaxAge != 0 {
return d.Err("cookie max_age already specified")
}
maxAge, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("invalid duration: %s", d.Val())
}
if maxAge <= 0 {
return d.Errf("invalid duration: %s, max_age should be non-zero and positive", d.Val())
}
if d.NextArg() {
return d.ArgErr()
}
s.MaxAge = caddy.Duration(maxAge)
default:
return d.Errf("unrecognized option '%s'", d.Val())
}

View file

@ -231,6 +231,19 @@ type IPVersions struct {
IPv6 *bool `json:"ipv6,omitempty"`
}
func resolveIpVersion(versions *IPVersions) string {
resolveIpv4 := versions == nil || (versions.IPv4 == nil && versions.IPv6 == nil) || (versions.IPv4 != nil && *versions.IPv4)
resolveIpv6 := versions == nil || (versions.IPv6 == nil && versions.IPv4 == nil) || (versions.IPv6 != nil && *versions.IPv6)
switch {
case resolveIpv4 && !resolveIpv6:
return "ip4"
case !resolveIpv4 && resolveIpv6:
return "ip6"
default:
return "ip"
}
}
// AUpstreams provides upstreams from A/AAAA lookups.
// Results are cached and refreshed at the configured
// refresh interval.
@ -313,9 +326,6 @@ func (au *AUpstreams) Provision(ctx caddy.Context) error {
func (au AUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
resolveIpv4 := au.Versions == nil || au.Versions.IPv4 == nil || *au.Versions.IPv4
resolveIpv6 := au.Versions == nil || au.Versions.IPv6 == nil || *au.Versions.IPv6
// Map ipVersion early, so we can use it as part of the cache-key.
// This should be fairly inexpensive and comes and the upside of
// allowing the same dynamic upstream (name + port combination)
@ -324,15 +334,7 @@ func (au AUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
// It also forced a cache-miss if a previously cached dynamic
// upstream changes its ip version, e.g. after a config reload,
// while keeping the cache-invalidation as simple as it currently is.
var ipVersion string
switch {
case resolveIpv4 && !resolveIpv6:
ipVersion = "ip4"
case !resolveIpv4 && resolveIpv6:
ipVersion = "ip6"
default:
ipVersion = "ip"
}
ipVersion := resolveIpVersion(au.Versions)
auStr := repl.ReplaceAll(au.String()+ipVersion, "")

View file

@ -0,0 +1,56 @@
package reverseproxy
import "testing"
func TestResolveIpVersion(t *testing.T) {
falseBool := false
trueBool := true
tests := []struct {
Versions *IPVersions
expectedIpVersion string
}{
{
Versions: &IPVersions{IPv4: &trueBool},
expectedIpVersion: "ip4",
},
{
Versions: &IPVersions{IPv4: &falseBool},
expectedIpVersion: "ip",
},
{
Versions: &IPVersions{IPv4: &trueBool, IPv6: &falseBool},
expectedIpVersion: "ip4",
},
{
Versions: &IPVersions{IPv6: &trueBool},
expectedIpVersion: "ip6",
},
{
Versions: &IPVersions{IPv6: &falseBool},
expectedIpVersion: "ip",
},
{
Versions: &IPVersions{IPv6: &trueBool, IPv4: &falseBool},
expectedIpVersion: "ip6",
},
{
Versions: &IPVersions{},
expectedIpVersion: "ip",
},
{
Versions: &IPVersions{IPv4: &trueBool, IPv6: &trueBool},
expectedIpVersion: "ip",
},
{
Versions: &IPVersions{IPv4: &falseBool, IPv6: &falseBool},
expectedIpVersion: "ip",
},
}
for _, test := range tests {
ipVersion := resolveIpVersion(test.Versions)
if ipVersion != test.expectedIpVersion {
t.Errorf("resolveIpVersion(): Expected %s got %s", test.expectedIpVersion, ipVersion)
}
}
}

View file

@ -131,6 +131,12 @@ func (rewr *Rewrite) Provision(ctx caddy.Context) error {
func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
const message = "rewrote request"
if rewr.logger.Check(zap.DebugLevel, message) == nil {
rewr.Rewrite(r, repl)
return next.ServeHTTP(w, r)
}
logger := rewr.logger.With(
zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: r}),
@ -139,7 +145,7 @@ func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
changed := rewr.Rewrite(r, repl)
if changed {
logger.Debug("rewrote request",
logger.Debug(message,
zap.String("method", r.Method),
zap.String("uri", r.RequestURI),
)

View file

@ -159,6 +159,9 @@ func (r *Route) ProvisionHandlers(ctx caddy.Context, metrics *Metrics) error {
r.Handlers = append(r.Handlers, handler.(MiddlewareHandler))
}
// Make ProvisionHandlers idempotent by clearing the middleware field
r.middleware = []Middleware{}
// pre-compile the middleware handler chain
for _, midhandler := range r.Handlers {
r.middleware = append(r.middleware, wrapMiddleware(ctx, midhandler, metrics))

View file

@ -602,6 +602,7 @@ func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error
QUICConfig: &quic.Config{
Versions: []quic.Version{quic.Version1, quic.Version2},
},
IdleTimeout: time.Duration(s.IdleTimeout),
ConnContext: func(ctx context.Context, c quic.Connection) context.Context {
return context.WithValue(ctx, quicConnCtxKey, c)
},

View file

@ -105,8 +105,7 @@ func (e StaticError) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler
}
statusCode = intVal
}
return Error(statusCode, fmt.Errorf("%s", e.Error))
return Error(statusCode, fmt.Errorf("%s", repl.ReplaceKnown(e.Error, "")))
}
// Interface guard

View file

@ -22,6 +22,8 @@ import (
"math/big"
"github.com/caddyserver/certmagic"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
// CustomCertSelectionPolicy represents a policy for selecting the certificate
@ -122,6 +124,79 @@ nextChoice:
return certmagic.DefaultCertificateSelector(hello, viable)
}
// UnmarshalCaddyfile sets up the CustomCertSelectionPolicy from Caddyfile tokens. Syntax:
//
// cert_selection {
// all_tags <values...>
// any_tag <values...>
// public_key_algorithm <dsa|ecdsa|rsa>
// serial_number <big_integers...>
// subject_organization <values...>
// }
func (p *CustomCertSelectionPolicy) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
_, wrapper := d.Next(), d.Val() // consume wrapper name
// No same-line options are supported
if d.CountRemainingArgs() > 0 {
return d.ArgErr()
}
var hasPublicKeyAlgorithm bool
for nesting := d.Nesting(); d.NextBlock(nesting); {
optionName := d.Val()
switch optionName {
case "all_tags":
if d.CountRemainingArgs() == 0 {
return d.ArgErr()
}
p.AllTags = append(p.AllTags, d.RemainingArgs()...)
case "any_tag":
if d.CountRemainingArgs() == 0 {
return d.ArgErr()
}
p.AnyTag = append(p.AnyTag, d.RemainingArgs()...)
case "public_key_algorithm":
if hasPublicKeyAlgorithm {
return d.Errf("duplicate %s option '%s'", wrapper, optionName)
}
if d.CountRemainingArgs() != 1 {
return d.ArgErr()
}
d.NextArg()
if err := p.PublicKeyAlgorithm.UnmarshalJSON([]byte(d.Val())); err != nil {
return d.Errf("parsing %s option '%s': %v", wrapper, optionName, err)
}
hasPublicKeyAlgorithm = true
case "serial_number":
if d.CountRemainingArgs() == 0 {
return d.ArgErr()
}
for d.NextArg() {
val, bi := d.Val(), bigInt{}
_, ok := bi.SetString(val, 10)
if !ok {
return d.Errf("parsing %s option '%s': invalid big.int value %s", wrapper, optionName, val)
}
p.SerialNumber = append(p.SerialNumber, bi)
}
case "subject_organization":
if d.CountRemainingArgs() == 0 {
return d.ArgErr()
}
p.SubjectOrganization = append(p.SubjectOrganization, d.RemainingArgs()...)
default:
return d.ArgErr()
}
// No nested blocks are supported
if d.NextBlock(nesting + 1) {
return d.Errf("malformed %s option '%s': blocks are not supported", wrapper, optionName)
}
}
return nil
}
// bigInt is a big.Int type that interops with JSON encodings as a string.
type bigInt struct{ big.Int }
@ -144,3 +219,6 @@ func (bi *bigInt) UnmarshalJSON(p []byte) error {
}
return nil
}
// Interface guard
var _ caddyfile.Unmarshaler = (*CustomCertSelectionPolicy)(nil)

View file

@ -1,24 +0,0 @@
//go:build cfgo
package caddytls
// This file adds support for X25519Kyber768Draft00, a post-quantum
// key agreement that is currently being rolled out by Chrome [1]
// and Cloudflare [2,3]. For more context, see the PR [4].
//
// [1] https://blog.chromium.org/2023/08/protecting-chrome-traffic-with-hybrid.html
// [2] https://blog.cloudflare.com/post-quantum-for-all/
// [3] https://blog.cloudflare.com/post-quantum-to-origins/
// [4] https://github.com/caddyserver/caddy/pull/5852
import (
"crypto/tls"
)
func init() {
SupportedCurves["X25519Kyber768Draft00"] = tls.X25519Kyber768Draft00
defaultCurves = append(
[]tls.CurveID{tls.X25519Kyber768Draft00},
defaultCurves...,
)
}

View file

@ -363,6 +363,136 @@ func (p ConnectionPolicy) SettingsEmpty() bool {
p.InsecureSecretsLog == ""
}
// UnmarshalCaddyfile sets up the ConnectionPolicy from Caddyfile tokens. Syntax:
//
// connection_policy {
// alpn <values...>
// cert_selection {
// ...
// }
// ciphers <cipher_suites...>
// client_auth {
// ...
// }
// curves <curves...>
// default_sni <server_name>
// match {
// ...
// }
// protocols <min> [<max>]
// # EXPERIMENTAL:
// drop
// fallback_sni <server_name>
// insecure_secrets_log <log_file>
// }
func (cp *ConnectionPolicy) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
_, wrapper := d.Next(), d.Val()
// No same-line options are supported
if d.CountRemainingArgs() > 0 {
return d.ArgErr()
}
var hasCertSelection, hasClientAuth, hasDefaultSNI, hasDrop,
hasFallbackSNI, hasInsecureSecretsLog, hasMatch, hasProtocols bool
for nesting := d.Nesting(); d.NextBlock(nesting); {
optionName := d.Val()
switch optionName {
case "alpn":
if d.CountRemainingArgs() == 0 {
return d.ArgErr()
}
cp.ALPN = append(cp.ALPN, d.RemainingArgs()...)
case "cert_selection":
if hasCertSelection {
return d.Errf("duplicate %s option '%s'", wrapper, optionName)
}
p := &CustomCertSelectionPolicy{}
if err := p.UnmarshalCaddyfile(d.NewFromNextSegment()); err != nil {
return err
}
cp.CertSelection, hasCertSelection = p, true
case "client_auth":
if hasClientAuth {
return d.Errf("duplicate %s option '%s'", wrapper, optionName)
}
ca := &ClientAuthentication{}
if err := ca.UnmarshalCaddyfile(d.NewFromNextSegment()); err != nil {
return err
}
cp.ClientAuthentication, hasClientAuth = ca, true
case "ciphers":
if d.CountRemainingArgs() == 0 {
return d.ArgErr()
}
cp.CipherSuites = append(cp.CipherSuites, d.RemainingArgs()...)
case "curves":
if d.CountRemainingArgs() == 0 {
return d.ArgErr()
}
cp.Curves = append(cp.Curves, d.RemainingArgs()...)
case "default_sni":
if hasDefaultSNI {
return d.Errf("duplicate %s option '%s'", wrapper, optionName)
}
if d.CountRemainingArgs() != 1 {
return d.ArgErr()
}
_, cp.DefaultSNI, hasDefaultSNI = d.NextArg(), d.Val(), true
case "drop": // EXPERIMENTAL
if hasDrop {
return d.Errf("duplicate %s option '%s'", wrapper, optionName)
}
cp.Drop, hasDrop = true, true
case "fallback_sni": // EXPERIMENTAL
if hasFallbackSNI {
return d.Errf("duplicate %s option '%s'", wrapper, optionName)
}
if d.CountRemainingArgs() != 1 {
return d.ArgErr()
}
_, cp.FallbackSNI, hasFallbackSNI = d.NextArg(), d.Val(), true
case "insecure_secrets_log": // EXPERIMENTAL
if hasInsecureSecretsLog {
return d.Errf("duplicate %s option '%s'", wrapper, optionName)
}
if d.CountRemainingArgs() != 1 {
return d.ArgErr()
}
_, cp.InsecureSecretsLog, hasInsecureSecretsLog = d.NextArg(), d.Val(), true
case "match":
if hasMatch {
return d.Errf("duplicate %s option '%s'", wrapper, optionName)
}
matcherSet, err := ParseCaddyfileNestedMatcherSet(d)
if err != nil {
return err
}
cp.MatchersRaw, hasMatch = matcherSet, true
case "protocols":
if hasProtocols {
return d.Errf("duplicate %s option '%s'", wrapper, optionName)
}
if d.CountRemainingArgs() == 0 || d.CountRemainingArgs() > 2 {
return d.ArgErr()
}
_, cp.ProtocolMin, hasProtocols = d.NextArg(), d.Val(), true
if d.NextArg() {
cp.ProtocolMax = d.Val()
}
default:
return d.ArgErr()
}
// No nested blocks are supported
if d.NextBlock(nesting + 1) {
return d.Errf("malformed %s option '%s': blocks are not supported", wrapper, optionName)
}
}
return nil
}
// ClientAuthentication configures TLS client auth.
type ClientAuthentication struct {
// Certificate authority module which provides the certificate pool of trusted certificates
@ -711,7 +841,15 @@ func setDefaultTLSParams(cfg *tls.Config) {
cfg.CipherSuites = append([]uint16{tls.TLS_FALLBACK_SCSV}, cfg.CipherSuites...)
if len(cfg.CurvePreferences) == 0 {
cfg.CurvePreferences = defaultCurves
// We would want to write
//
// cfg.CurvePreferences = defaultCurves
//
// but that would disable the post-quantum key agreement X25519Kyber768
// supported in Go 1.23, for which the CurveID is not exported.
// Instead, we'll set CurvePreferences to nil, which will enable PQC.
// See https://github.com/caddyserver/caddy/issues/6540
cfg.CurvePreferences = nil
}
if cfg.MinVersion == 0 {
@ -819,4 +957,46 @@ func (d destructableWriter) Destruct() error { return d.Close() }
var secretsLogPool = caddy.NewUsagePool()
var _ caddyfile.Unmarshaler = (*ClientAuthentication)(nil)
// Interface guards
var (
_ caddyfile.Unmarshaler = (*ClientAuthentication)(nil)
_ caddyfile.Unmarshaler = (*ConnectionPolicy)(nil)
)
// ParseCaddyfileNestedMatcherSet parses the Caddyfile tokens for a nested
// matcher set, and returns its raw module map value.
func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, error) {
matcherMap := make(map[string]ConnectionMatcher)
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 {
dd := caddyfile.NewDispenser(tokens)
dd.Next() // consume wrapper name
unm, err := caddyfile.UnmarshalModule(dd, "tls.handshake_match."+matcherName)
if err != nil {
return nil, err
}
cm, ok := unm.(ConnectionMatcher)
if !ok {
return nil, fmt.Errorf("matcher module '%s' is not a connection matcher", matcherName)
}
matcherMap[matcherName] = cm
}
matcherSet := make(caddy.ModuleMap)
for name, matcher := range matcherMap {
jsonBytes, err := json.Marshal(matcher)
if err != nil {
return nil, fmt.Errorf("marshaling %T matcher: %v", matcher, err)
}
matcherSet[name] = jsonBytes
}
return matcherSet, nil
}

View file

@ -25,6 +25,8 @@ import (
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/internal"
)
func init() {
@ -48,14 +50,47 @@ func (MatchServerName) CaddyModule() caddy.ModuleInfo {
// Match matches hello based on SNI.
func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool {
repl := caddy.NewReplacer()
// caddytls.TestServerNameMatcher calls this function without any context
if ctx := hello.Context(); ctx != nil {
// In some situations the existing context may have no replacer
if replAny := ctx.Value(caddy.ReplacerCtxKey); replAny != nil {
repl = replAny.(*caddy.Replacer)
}
}
for _, name := range m {
if certmagic.MatchWildcard(hello.ServerName, name) {
rs := repl.ReplaceAll(name, "")
if certmagic.MatchWildcard(hello.ServerName, rs) {
return true
}
}
return false
}
// UnmarshalCaddyfile sets up the MatchServerName from Caddyfile tokens. Syntax:
//
// sni <domains...>
func (m *MatchServerName) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
wrapper := d.Val()
// At least one same-line option must be provided
if d.CountRemainingArgs() == 0 {
return d.ArgErr()
}
*m = append(*m, d.RemainingArgs()...)
// No blocks are supported
if d.NextBlock(d.Nesting()) {
return d.Errf("malformed TLS handshake matcher '%s': blocks are not supported", wrapper)
}
}
return nil
}
// MatchRemoteIP matches based on the remote IP of the
// connection. Specific IPs or CIDR ranges can be specified.
//
@ -83,16 +118,19 @@ func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo {
// Provision parses m's IP ranges, either from IP or CIDR expressions.
func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
repl := caddy.NewReplacer()
m.logger = ctx.Logger()
for _, str := range m.Ranges {
cidrs, err := m.parseIPRange(str)
rs := repl.ReplaceAll(str, "")
cidrs, err := m.parseIPRange(rs)
if err != nil {
return err
}
m.cidrs = append(m.cidrs, cidrs...)
}
for _, str := range m.NotRanges {
cidrs, err := m.parseIPRange(str)
rs := repl.ReplaceAll(str, "")
cidrs, err := m.parseIPRange(rs)
if err != nil {
return err
}
@ -145,6 +183,46 @@ func (MatchRemoteIP) matches(ip netip.Addr, ranges []netip.Prefix) bool {
return false
}
// UnmarshalCaddyfile sets up the MatchRemoteIP from Caddyfile tokens. Syntax:
//
// remote_ip <ranges...>
//
// Note: IPs and CIDRs prefixed with ! symbol are treated as not_ranges
func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
wrapper := d.Val()
// At least one same-line option must be provided
if d.CountRemainingArgs() == 0 {
return d.ArgErr()
}
for d.NextArg() {
val := d.Val()
var exclamation bool
if len(val) > 1 && val[0] == '!' {
exclamation, val = true, val[1:]
}
ranges := []string{val}
if val == "private_ranges" {
ranges = internal.PrivateRangesCIDR()
}
if exclamation {
m.NotRanges = append(m.NotRanges, ranges...)
} else {
m.Ranges = append(m.Ranges, ranges...)
}
}
// No blocks are supported
if d.NextBlock(d.Nesting()) {
return d.Errf("malformed TLS handshake matcher '%s': blocks are not supported", wrapper)
}
}
return nil
}
// MatchLocalIP matches based on the IP address of the interface
// receiving the connection. Specific IPs or CIDR ranges can be specified.
type MatchLocalIP struct {
@ -165,9 +243,11 @@ func (MatchLocalIP) CaddyModule() caddy.ModuleInfo {
// Provision parses m's IP ranges, either from IP or CIDR expressions.
func (m *MatchLocalIP) Provision(ctx caddy.Context) error {
repl := caddy.NewReplacer()
m.logger = ctx.Logger()
for _, str := range m.Ranges {
cidrs, err := m.parseIPRange(str)
rs := repl.ReplaceAll(str, "")
cidrs, err := m.parseIPRange(rs)
if err != nil {
return err
}
@ -219,6 +299,36 @@ func (MatchLocalIP) matches(ip netip.Addr, ranges []netip.Prefix) bool {
return false
}
// UnmarshalCaddyfile sets up the MatchLocalIP from Caddyfile tokens. Syntax:
//
// local_ip <ranges...>
func (m *MatchLocalIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.Next() {
wrapper := d.Val()
// At least one same-line option must be provided
if d.CountRemainingArgs() == 0 {
return d.ArgErr()
}
for d.NextArg() {
val := d.Val()
if val == "private_ranges" {
m.Ranges = append(m.Ranges, internal.PrivateRangesCIDR()...)
continue
}
m.Ranges = append(m.Ranges, val)
}
// No blocks are supported
if d.NextBlock(d.Nesting()) {
return d.Errf("malformed TLS handshake matcher '%s': blocks are not supported", wrapper)
}
}
return nil
}
// Interface guards
var (
_ ConnectionMatcher = (*MatchServerName)(nil)
@ -226,4 +336,8 @@ var (
_ caddy.Provisioner = (*MatchLocalIP)(nil)
_ ConnectionMatcher = (*MatchLocalIP)(nil)
_ caddyfile.Unmarshaler = (*MatchLocalIP)(nil)
_ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
_ caddyfile.Unmarshaler = (*MatchServerName)(nil)
)

View file

@ -108,6 +108,11 @@ var supportedCertKeyTypes = map[string]certmagic.KeyType{
// implementation exists (e.g. P256). The latter ones can be
// found here:
// https://github.com/golang/go/tree/master/src/crypto/elliptic
//
// Temporily we ignore these default, to take advantage of X25519Kyber768
// in Go's defaults (X25519Kyber768, X25519, P-256, P-384, P-521), which
// isn't exported. See https://github.com/caddyserver/caddy/issues/6540
// nolint:unused
var defaultCurves = []tls.CurveID{
tls.X25519,
tls.CurveP256,

36
modules/logging/cores.go Normal file
View file

@ -0,0 +1,36 @@
package logging
import (
"go.uber.org/zap/zapcore"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
func init() {
caddy.RegisterModule(MockCore{})
}
// MockCore is a no-op module, purely for testing
type MockCore struct {
zapcore.Core `json:"-"`
}
// CaddyModule returns the Caddy module information.
func (MockCore) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "caddy.logging.cores.mock",
New: func() caddy.Module { return new(MockCore) },
}
}
func (lec *MockCore) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return nil
}
// Interface guards
var (
_ zapcore.Core = (*MockCore)(nil)
_ caddy.Module = (*MockCore)(nil)
_ caddyfile.Unmarshaler = (*MockCore)(nil)
)

View file

@ -15,6 +15,7 @@
package logging
import (
"encoding/json"
"fmt"
"io"
"math"
@ -33,6 +34,48 @@ func init() {
caddy.RegisterModule(FileWriter{})
}
// fileMode is a string made of 1 to 4 octal digits representing
// a numeric mode as specified with the `chmod` unix command.
// `"0777"` and `"777"` are thus equivalent values.
type fileMode os.FileMode
// UnmarshalJSON satisfies json.Unmarshaler.
func (m *fileMode) UnmarshalJSON(b []byte) error {
if len(b) == 0 {
return io.EOF
}
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
mode, err := parseFileMode(s)
if err != nil {
return err
}
*m = fileMode(mode)
return err
}
// MarshalJSON satisfies json.Marshaler.
func (m *fileMode) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("\"%04o\"", *m)), nil
}
// parseFileMode parses a file mode string,
// adding support for `chmod` unix command like
// 1 to 4 digital octal values.
func parseFileMode(s string) (os.FileMode, error) {
modeStr := fmt.Sprintf("%04s", s)
mode, err := strconv.ParseUint(modeStr, 8, 32)
if err != nil {
return 0, err
}
return os.FileMode(mode), nil
}
// FileWriter can write logs to files. By default, log files
// are rotated ("rolled") when they get large, and old log
// files get deleted, to ensure that the process does not
@ -41,6 +84,10 @@ type FileWriter struct {
// Filename is the name of the file to write.
Filename string `json:"filename,omitempty"`
// The file permissions mode.
// 0600 by default.
Mode fileMode `json:"mode,omitempty"`
// Roll toggles log rolling or rotation, which is
// enabled by default.
Roll *bool `json:"roll,omitempty"`
@ -100,6 +147,10 @@ func (fw FileWriter) WriterKey() string {
// OpenWriter opens a new file writer.
func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
if fw.Mode == 0 {
fw.Mode = 0o600
}
// roll log files by default
if fw.Roll == nil || *fw.Roll {
if fw.RollSizeMB == 0 {
@ -116,6 +167,19 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
fw.RollKeepDays = 90
}
// create the file if it does not exist with the right mode.
// lumberjack will reuse the file mode across log rotation.
f_tmp, err := os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(fw.Mode))
if err != nil {
return nil, err
}
f_tmp.Close()
// ensure already existing files have the right mode,
// since OpenFile will not set the mode in such case.
if err = os.Chmod(fw.Filename, os.FileMode(fw.Mode)); err != nil {
return nil, err
}
return &lumberjack.Logger{
Filename: fw.Filename,
MaxSize: fw.RollSizeMB,
@ -127,12 +191,13 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
}
// otherwise just open a regular file
return os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o666)
return os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(fw.Mode))
}
// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
//
// file <filename> {
// mode <mode>
// roll_disabled
// roll_size <size>
// roll_uncompressed
@ -150,7 +215,7 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
// The roll_keep_for duration has day resolution.
// Fractional values are rounded up to the next whole number of days.
//
// If any of the roll_size, roll_keep, or roll_keep_for subdirectives are
// If any of the mode, roll_size, roll_keep, or roll_keep_for subdirectives are
// 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 {
@ -165,6 +230,17 @@ func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for d.NextBlock(0) {
switch d.Val() {
case "mode":
var modeStr string
if !d.AllArgs(&modeStr) {
return d.ArgErr()
}
mode, err := parseFileMode(modeStr)
if err != nil {
return d.Errf("parsing mode: %v", err)
}
fw.Mode = fileMode(mode)
case "roll_disabled":
var f bool
fw.Roll = &f

View file

@ -0,0 +1,386 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !windows
package logging
import (
"encoding/json"
"os"
"path"
"syscall"
"testing"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
func TestFileCreationMode(t *testing.T) {
on := true
off := false
tests := []struct {
name string
fw FileWriter
wantMode os.FileMode
}{
{
name: "default mode no roll",
fw: FileWriter{
Roll: &off,
},
wantMode: 0o600,
},
{
name: "default mode roll",
fw: FileWriter{
Roll: &on,
},
wantMode: 0o600,
},
{
name: "custom mode no roll",
fw: FileWriter{
Roll: &off,
Mode: 0o666,
},
wantMode: 0o666,
},
{
name: "custom mode roll",
fw: FileWriter{
Roll: &on,
Mode: 0o666,
},
wantMode: 0o666,
},
}
m := syscall.Umask(0o000)
defer syscall.Umask(m)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dir, err := os.MkdirTemp("", "caddytest")
if err != nil {
t.Fatalf("failed to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
fpath := path.Join(dir, "test.log")
tt.fw.Filename = fpath
logger, err := tt.fw.OpenWriter()
if err != nil {
t.Fatalf("failed to create file: %v", err)
}
defer logger.Close()
st, err := os.Stat(fpath)
if err != nil {
t.Fatalf("failed to check file permissions: %v", err)
}
if st.Mode() != tt.wantMode {
t.Errorf("file mode is %v, want %v", st.Mode(), tt.wantMode)
}
})
}
}
func TestFileRotationPreserveMode(t *testing.T) {
m := syscall.Umask(0o000)
defer syscall.Umask(m)
dir, err := os.MkdirTemp("", "caddytest")
if err != nil {
t.Fatalf("failed to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
fpath := path.Join(dir, "test.log")
roll := true
mode := fileMode(0o640)
fw := FileWriter{
Filename: fpath,
Mode: mode,
Roll: &roll,
RollSizeMB: 1,
}
logger, err := fw.OpenWriter()
if err != nil {
t.Fatalf("failed to create file: %v", err)
}
defer logger.Close()
b := make([]byte, 1024*1024-1000)
logger.Write(b)
logger.Write(b[0:2000])
files, err := os.ReadDir(dir)
if err != nil {
t.Fatalf("failed to read temporary log dir: %v", err)
}
// We might get 2 or 3 files depending
// on the race between compressed log file generation,
// removal of the non compressed file and reading the directory.
// Ordering of the files are [ test-*.log test-*.log.gz test.log ]
if len(files) < 2 || len(files) > 3 {
t.Log("got files: ", files)
t.Fatalf("got %v files want 2", len(files))
}
wantPattern := "test-*-*-*-*-*.*.log"
test_date_log := files[0]
if m, _ := path.Match(wantPattern, test_date_log.Name()); m != true {
t.Fatalf("got %v filename want %v", test_date_log.Name(), wantPattern)
}
st, err := os.Stat(path.Join(dir, test_date_log.Name()))
if err != nil {
t.Fatalf("failed to check file permissions: %v", err)
}
if st.Mode() != os.FileMode(mode) {
t.Errorf("file mode is %v, want %v", st.Mode(), mode)
}
test_dot_log := files[len(files)-1]
if test_dot_log.Name() != "test.log" {
t.Fatalf("got %v filename want test.log", test_dot_log.Name())
}
st, err = os.Stat(path.Join(dir, test_dot_log.Name()))
if err != nil {
t.Fatalf("failed to check file permissions: %v", err)
}
if st.Mode() != os.FileMode(mode) {
t.Errorf("file mode is %v, want %v", st.Mode(), mode)
}
}
func TestFileModeConfig(t *testing.T) {
tests := []struct {
name string
d *caddyfile.Dispenser
fw FileWriter
wantErr bool
}{
{
name: "set mode",
d: caddyfile.NewTestDispenser(`
file test.log {
mode 0666
}
`),
fw: FileWriter{
Mode: 0o666,
},
wantErr: false,
},
{
name: "set mode 3 digits",
d: caddyfile.NewTestDispenser(`
file test.log {
mode 666
}
`),
fw: FileWriter{
Mode: 0o666,
},
wantErr: false,
},
{
name: "set mode 2 digits",
d: caddyfile.NewTestDispenser(`
file test.log {
mode 66
}
`),
fw: FileWriter{
Mode: 0o066,
},
wantErr: false,
},
{
name: "set mode 1 digits",
d: caddyfile.NewTestDispenser(`
file test.log {
mode 6
}
`),
fw: FileWriter{
Mode: 0o006,
},
wantErr: false,
},
{
name: "invalid mode",
d: caddyfile.NewTestDispenser(`
file test.log {
mode foobar
}
`),
fw: FileWriter{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fw := &FileWriter{}
if err := fw.UnmarshalCaddyfile(tt.d); (err != nil) != tt.wantErr {
t.Fatalf("UnmarshalCaddyfile() error = %v, want %v", err, tt.wantErr)
}
if fw.Mode != tt.fw.Mode {
t.Errorf("got mode %v, want %v", fw.Mode, tt.fw.Mode)
}
})
}
}
func TestFileModeJSON(t *testing.T) {
tests := []struct {
name string
config string
fw FileWriter
wantErr bool
}{
{
name: "set mode",
config: `
{
"mode": "0666"
}
`,
fw: FileWriter{
Mode: 0o666,
},
wantErr: false,
},
{
name: "set mode invalid value",
config: `
{
"mode": "0x666"
}
`,
fw: FileWriter{},
wantErr: true,
},
{
name: "set mode invalid string",
config: `
{
"mode": 777
}
`,
fw: FileWriter{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fw := &FileWriter{}
if err := json.Unmarshal([]byte(tt.config), fw); (err != nil) != tt.wantErr {
t.Fatalf("UnmarshalJSON() error = %v, want %v", err, tt.wantErr)
}
if fw.Mode != tt.fw.Mode {
t.Errorf("got mode %v, want %v", fw.Mode, tt.fw.Mode)
}
})
}
}
func TestFileModeToJSON(t *testing.T) {
tests := []struct {
name string
mode fileMode
want string
wantErr bool
}{
{
name: "none zero",
mode: 0644,
want: `"0644"`,
wantErr: false,
},
{
name: "zero mode",
mode: 0,
want: `"0000"`,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var b []byte
var err error
if b, err = json.Marshal(&tt.mode); (err != nil) != tt.wantErr {
t.Fatalf("MarshalJSON() error = %v, want %v", err, tt.wantErr)
}
got := string(b[:])
if got != tt.want {
t.Errorf("got mode %v, want %v", got, tt.want)
}
})
}
}
func TestFileModeModification(t *testing.T) {
m := syscall.Umask(0o000)
defer syscall.Umask(m)
dir, err := os.MkdirTemp("", "caddytest")
if err != nil {
t.Fatalf("failed to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
fpath := path.Join(dir, "test.log")
f_tmp, err := os.OpenFile(fpath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(0600))
if err != nil {
t.Fatalf("failed to create test file: %v", err)
}
f_tmp.Close()
fw := FileWriter{
Mode: 0o666,
Filename: fpath,
}
logger, err := fw.OpenWriter()
if err != nil {
t.Fatalf("failed to create file: %v", err)
}
defer logger.Close()
st, err := os.Stat(fpath)
if err != nil {
t.Fatalf("failed to check file permissions: %v", err)
}
want := os.FileMode(fw.Mode)
if st.Mode() != want {
t.Errorf("file mode is %v, want %v", st.Mode(), want)
}
}

View file

@ -0,0 +1,55 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package logging
import (
"os"
"path"
"testing"
)
// Windows relies on ACLs instead of unix permissions model.
// Go allows to open files with a particular mode put it is limited to read or write.
// See https://cs.opensource.google/go/go/+/refs/tags/go1.22.3:src/syscall/syscall_windows.go;l=708.
// This is pretty restrictive and has few interest for log files and thus we just test that log files are
// opened with R/W permissions by default on Windows too.
func TestFileCreationMode(t *testing.T) {
dir, err := os.MkdirTemp("", "caddytest")
if err != nil {
t.Fatalf("failed to create tempdir: %v", err)
}
defer os.RemoveAll(dir)
fw := &FileWriter{
Filename: path.Join(dir, "test.log"),
}
logger, err := fw.OpenWriter()
if err != nil {
t.Fatalf("failed to create file: %v", err)
}
defer logger.Close()
st, err := os.Stat(fw.Filename)
if err != nil {
t.Fatalf("failed to check file permissions: %v", err)
}
if st.Mode().Perm()&0o600 != 0o600 {
t.Fatalf("file mode is %v, want rw for user", st.Mode().Perm())
}
}

View file

@ -15,6 +15,7 @@
package caddy
import (
"bytes"
"fmt"
"io"
"net/http"
@ -354,6 +355,8 @@ func (f fileReplacementProvider) replace(key string) (any, bool) {
zap.Error(err))
return nil, true
}
body = bytes.TrimSuffix(body, []byte("\n"))
body = bytes.TrimSuffix(body, []byte("\r"))
return string(body), true
}

View file

@ -431,6 +431,14 @@ func TestReplacerNew(t *testing.T) {
variable: "file.caddytest/integration/testdata/foo.txt",
value: "foo",
},
{
variable: "file.caddytest/integration/testdata/foo_with_trailing_newline.txt",
value: "foo",
},
{
variable: "file.caddytest/integration/testdata/foo_with_multiple_trailing_newlines.txt",
value: "foo" + getEOL(),
},
} {
if val, ok := repl.providers[1].replace(tc.variable); ok {
if val != tc.value {
@ -442,6 +450,13 @@ func TestReplacerNew(t *testing.T) {
}
}
func getEOL() string {
if os.PathSeparator == '\\' {
return "\r\n" // Windows EOL
}
return "\n" // Unix and modern macOS EOL
}
func TestReplacerNewWithoutFile(t *testing.T) {
repl := NewReplacer().WithoutFile()