mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-27 06:03:48 +03:00
Merge branch 'master' into hurl-tests
This commit is contained in:
commit
3bdc6c035a
75 changed files with 2344 additions and 314 deletions
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
|
@ -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
|
||||
|
|
4
.github/workflows/cross-build.yml
vendored
4
.github/workflows/cross-build.yml
vendored
|
@ -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:
|
||||
|
|
6
.github/workflows/lint.yml
vendored
6
.github/workflows/lint.yml
vendored
|
@ -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
|
||||
|
|
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
101
caddy.go
101
caddy.go
|
@ -397,6 +397,58 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
|||
// will want to use Run instead, which also
|
||||
// updates the config's raw state.
|
||||
func run(newCfg *Config, start bool) (Context, error) {
|
||||
ctx, err := provisionContext(newCfg, start)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
if !start {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// Provision any admin routers which may need to access
|
||||
// some of the other apps at runtime
|
||||
err = ctx.cfg.Admin.provisionAdminRouters(ctx)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
// Start
|
||||
err = func() error {
|
||||
started := make([]string, 0, len(ctx.cfg.apps))
|
||||
for name, a := range ctx.cfg.apps {
|
||||
err := a.Start()
|
||||
if err != nil {
|
||||
// an app failed to start, so we need to stop
|
||||
// all other apps that were already started
|
||||
for _, otherAppName := range started {
|
||||
err2 := ctx.cfg.apps[otherAppName].Stop()
|
||||
if err2 != nil {
|
||||
err = fmt.Errorf("%v; additionally, aborting app %s: %v",
|
||||
err, otherAppName, err2)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s app module: start: %v", name, err)
|
||||
}
|
||||
started = append(started, name)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
// now that the user's config is running, finish setting up anything else,
|
||||
// such as remote admin endpoint, config loader, etc.
|
||||
return ctx, finishSettingUp(ctx, ctx.cfg)
|
||||
}
|
||||
|
||||
// provisionContext creates a new context from the given configuration and provisions
|
||||
// storage and apps.
|
||||
// If `newCfg` is nil a new empty configuration will be created.
|
||||
// If `replaceAdminServer` is true any currently active admin server will be replaced
|
||||
// with a new admin server based on the provided configuration.
|
||||
func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error) {
|
||||
// because we will need to roll back any state
|
||||
// modifications if this function errors, we
|
||||
// keep a single error value and scope all
|
||||
|
@ -444,7 +496,7 @@ func run(newCfg *Config, start bool) (Context, error) {
|
|||
}
|
||||
|
||||
// start the admin endpoint (and stop any prior one)
|
||||
if start {
|
||||
if replaceAdminServer {
|
||||
err = replaceLocalAdminServer(newCfg)
|
||||
if err != nil {
|
||||
return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err)
|
||||
|
@ -491,49 +543,16 @@ func run(newCfg *Config, start bool) (Context, error) {
|
|||
}
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
if !start {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// Provision any admin routers which may need to access
|
||||
// some of the other apps at runtime
|
||||
err = newCfg.Admin.provisionAdminRouters(ctx)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
// Start
|
||||
err = func() error {
|
||||
started := make([]string, 0, len(newCfg.apps))
|
||||
for name, a := range newCfg.apps {
|
||||
err := a.Start()
|
||||
if err != nil {
|
||||
// an app failed to start, so we need to stop
|
||||
// all other apps that were already started
|
||||
for _, otherAppName := range started {
|
||||
err2 := newCfg.apps[otherAppName].Stop()
|
||||
if err2 != nil {
|
||||
err = fmt.Errorf("%v; additionally, aborting app %s: %v",
|
||||
err, otherAppName, err2)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s app module: start: %v", name, err)
|
||||
}
|
||||
started = append(started, name)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
} {
|
||||
|
|
|
@ -84,7 +84,6 @@ func TestLoadUnorderedJSON(t *testing.T) {
|
|||
"servers": {
|
||||
"s_server": {
|
||||
"listen": [
|
||||
":9443",
|
||||
":9080"
|
||||
],
|
||||
"routes": [
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
|
2
caddytest/integration/testdata/foo_with_multiple_trailing_newlines.txt
vendored
Normal file
2
caddytest/integration/testdata/foo_with_multiple_trailing_newlines.txt
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
foo
|
||||
|
1
caddytest/integration/testdata/foo_with_trailing_newline.txt
vendored
Normal file
1
caddytest/integration/testdata/foo_with_trailing_newline.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
foo
|
|
@ -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");
|
||||
|
|
|
@ -8,7 +8,8 @@ import (
|
|||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
var defaultFactory = newRootCommandFactory(func() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "caddy",
|
||||
Long: `Caddy is an extensible server platform written in Go.
|
||||
|
||||
|
@ -101,13 +102,16 @@ https://caddyserver.com/docs/running
|
|||
SilenceUsage: true,
|
||||
Version: onlyVersionText(),
|
||||
}
|
||||
})
|
||||
|
||||
const fullDocsFooter = `Full documentation is available at:
|
||||
https://caddyserver.com/docs/command-line`
|
||||
|
||||
func init() {
|
||||
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
28
cmd/commandfactory.go
Normal 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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -438,6 +438,7 @@ EXPERIMENTAL: May be changed or removed.
|
|||
},
|
||||
})
|
||||
|
||||
defaultFactory.Use(func(rootCmd *cobra.Command) {
|
||||
RegisterCommand(Command{
|
||||
Name: "manpage",
|
||||
Usage: "--directory <path>",
|
||||
|
@ -531,6 +532,7 @@ argument of --directory. If the directory does not exist, it will be created.
|
|||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterCommand registers the command cmd.
|
||||
|
@ -563,7 +565,9 @@ func RegisterCommand(cmd Command) {
|
|||
if !commandNameRegex.MatchString(cmd.Name) {
|
||||
panic("invalid command name")
|
||||
}
|
||||
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]$`)
|
||||
|
|
|
@ -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
24
go.mod
|
@ -1,8 +1,8 @@
|
|||
module github.com/caddyserver/caddy/v2
|
||||
|
||||
go 1.21.0
|
||||
go 1.22.3
|
||||
|
||||
toolchain go1.22.2
|
||||
toolchain go1.23.0
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.3.2
|
||||
|
@ -19,14 +19,14 @@ require (
|
|||
github.com/klauspost/cpuid/v2 v2.2.7
|
||||
github.com/mholt/acmez/v2 v2.0.1
|
||||
github.com/prometheus/client_golang v1.19.1
|
||||
github.com/quic-go/quic-go v0.44.0
|
||||
github.com/quic-go/quic-go v0.46.0
|
||||
github.com/smallstep/certificates v0.26.1
|
||||
github.com/smallstep/nosql v0.6.1
|
||||
github.com/smallstep/truststore v0.13.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933
|
||||
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53
|
||||
github.com/yuin/goldmark v1.7.1
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0
|
||||
|
@ -37,11 +37,11 @@ require (
|
|||
go.uber.org/automaxprocs v1.5.3
|
||||
go.uber.org/zap v1.27.0
|
||||
go.uber.org/zap/exp v0.2.0
|
||||
golang.org/x/crypto v0.23.0
|
||||
golang.org/x/crypto v0.26.0
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595
|
||||
golang.org/x/net v0.25.0
|
||||
golang.org/x/sync v0.7.0
|
||||
golang.org/x/term v0.20.0
|
||||
golang.org/x/net v0.28.0
|
||||
golang.org/x/sync v0.8.0
|
||||
golang.org/x/term v0.23.0
|
||||
golang.org/x/time v0.5.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
|
@ -123,7 +123,7 @@ require (
|
|||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/pires/go-proxyproto v0.7.0
|
||||
github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
|
@ -147,9 +147,9 @@ require (
|
|||
go.step.sm/linkedca v0.20.1 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/sys v0.20.0
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
golang.org/x/tools v0.21.0 // indirect
|
||||
golang.org/x/sys v0.23.0
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
google.golang.org/grpc v1.63.2 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
|
|
40
go.sum
40
go.sum
|
@ -320,8 +320,8 @@ github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8P
|
|||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
|
||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
||||
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
|
||||
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
||||
github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964 h1:ct/vxNBgHpASQ4sT8NaBX9LtsEtluZqaUJydLG50U3E=
|
||||
github.com/pires/go-proxyproto v0.7.1-0.20240628150027-b718e7ce4964/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
|
@ -339,8 +339,8 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k
|
|||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/quic-go v0.44.0 h1:So5wOr7jyO4vzL2sd8/pD9Kesciv91zSk8BoFngItQ0=
|
||||
github.com/quic-go/quic-go v0.44.0/go.mod h1:z4cx/9Ny9UtGITIPzmPTXh1ULfOyWh4qGQlpnPcWmek=
|
||||
github.com/quic-go/quic-go v0.46.0 h1:uuwLClEEyk1DNvchH8uCByQVjo3yKL9opKulExNDs7Y=
|
||||
github.com/quic-go/quic-go v0.46.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
|
@ -415,8 +415,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
|||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 h1:pV0H+XIvFoP7pl1MRtyPXh5hqoxB5I7snOtTHgrn6HU=
|
||||
github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
|
||||
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ=
|
||||
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk=
|
||||
github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA=
|
||||
|
@ -510,8 +510,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
|
|||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw=
|
||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||
|
@ -532,15 +532,15 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
|||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
|
||||
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -567,8 +567,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
|
||||
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
|
@ -576,8 +576,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
|||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
|
||||
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
@ -588,8 +588,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
@ -603,8 +603,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
|||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
|
||||
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
14
internal/ranges.go
Normal file
14
internal/ranges.go
Normal 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",
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
12
logging.go
12
logging.go
|
@ -292,6 +292,10 @@ type BaseLog struct {
|
|||
// The encoder is how the log entries are formatted or encoded.
|
||||
EncoderRaw json.RawMessage `json:"encoder,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"`
|
||||
|
||||
// Tees entries through a zap.Core module which can extract
|
||||
// log entry metadata and fields for further processing.
|
||||
CoreRaw json.RawMessage `json:"core,omitempty" caddy:"namespace=caddy.logging.cores inline_key=module"`
|
||||
|
||||
// Level is the minimum level to emit, and is inclusive.
|
||||
// Possible levels: DEBUG, INFO, WARN, ERROR, PANIC, and FATAL
|
||||
Level string `json:"level,omitempty"`
|
||||
|
@ -366,6 +370,14 @@ func (cl *BaseLog) provisionCommon(ctx Context, logging *Logging) error {
|
|||
cl.encoder = newDefaultProductionLogEncoder(cl.writerOpener)
|
||||
}
|
||||
cl.buildCore()
|
||||
if cl.CoreRaw != nil {
|
||||
mod, err := ctx.LoadModule(cl, "CoreRaw")
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading log core module: %v", err)
|
||||
}
|
||||
core := mod.(zapcore.Core)
|
||||
cl.core = zapcore.NewTee(cl.core, core)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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})
|
||||
}
|
||||
|
||||
|
|
|
@ -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}}↱ {{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) {
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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, "%")
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
@ -75,6 +77,8 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
|||
// 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())
|
||||
}
|
||||
|
|
|
@ -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}")
|
||||
}
|
||||
|
|
|
@ -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 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.
|
||||
if a.Port != 0 {
|
||||
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
|
||||
|
|
|
@ -58,6 +58,7 @@ type Upstream struct {
|
|||
// IPAffinity string
|
||||
|
||||
activeHealthCheckPort int
|
||||
activeHealthCheckUpstream string
|
||||
healthCheckPolicy *PassiveHealthChecks
|
||||
cb CircuitBreaker
|
||||
unhealthy int32 // accessed atomically; status from active health checker
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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, "")
|
||||
|
||||
|
|
56
modules/caddyhttp/reverseproxy/upstreams_test.go
Normal file
56
modules/caddyhttp/reverseproxy/upstreams_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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...,
|
||||
)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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
36
modules/logging/cores.go
Normal 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)
|
||||
)
|
|
@ -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
|
||||
|
|
386
modules/logging/filewriter_test.go
Normal file
386
modules/logging/filewriter_test.go
Normal 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)
|
||||
}
|
||||
}
|
55
modules/logging/filewriter_test_windows.go
Normal file
55
modules/logging/filewriter_test_windows.go
Normal 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())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
Loading…
Reference in a new issue