mirror of
https://github.com/caddyserver/caddy.git
synced 2025-01-14 14:56:27 +03:00
Merge branch 'master' into master
This commit is contained in:
commit
7ac050a5b1
41 changed files with 873 additions and 157 deletions
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
|
@ -23,18 +23,18 @@ jobs:
|
||||||
- mac
|
- mac
|
||||||
- windows
|
- windows
|
||||||
go:
|
go:
|
||||||
- '1.21'
|
|
||||||
- '1.22'
|
- '1.22'
|
||||||
|
- '1.23'
|
||||||
|
|
||||||
include:
|
include:
|
||||||
# Set the minimum Go patch version for the given Go minor
|
# Set the minimum Go patch version for the given Go minor
|
||||||
# Usable via ${{ matrix.GO_SEMVER }}
|
# Usable via ${{ matrix.GO_SEMVER }}
|
||||||
- go: '1.21'
|
|
||||||
GO_SEMVER: '~1.21.0'
|
|
||||||
|
|
||||||
- go: '1.22'
|
- go: '1.22'
|
||||||
GO_SEMVER: '~1.22.3'
|
GO_SEMVER: '~1.22.3'
|
||||||
|
|
||||||
|
- go: '1.23'
|
||||||
|
GO_SEMVER: '~1.23.0'
|
||||||
|
|
||||||
# Set some variables per OS, usable via ${{ matrix.VAR }}
|
# 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)
|
# 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
|
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
|
||||||
|
@ -99,7 +99,7 @@ jobs:
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
run: |
|
run: |
|
||||||
go build -tags nobdger -trimpath -ldflags="-w -s" -v
|
go build -tags nobadger -trimpath -ldflags="-w -s" -v
|
||||||
|
|
||||||
- name: Smoke test Caddy
|
- name: Smoke test Caddy
|
||||||
working-directory: ./cmd/caddy
|
working-directory: ./cmd/caddy
|
||||||
|
@ -150,6 +150,7 @@ jobs:
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: |
|
run: |
|
||||||
|
set +e
|
||||||
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
|
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
|
||||||
|
|
||||||
# short sha is enough?
|
# short sha is enough?
|
||||||
|
|
4
.github/workflows/cross-build.yml
vendored
4
.github/workflows/cross-build.yml
vendored
|
@ -28,6 +28,7 @@ jobs:
|
||||||
- 'netbsd'
|
- 'netbsd'
|
||||||
go:
|
go:
|
||||||
- '1.22'
|
- '1.22'
|
||||||
|
- '1.23'
|
||||||
|
|
||||||
include:
|
include:
|
||||||
# Set the minimum Go patch version for the given Go minor
|
# Set the minimum Go patch version for the given Go minor
|
||||||
|
@ -35,6 +36,9 @@ jobs:
|
||||||
- go: '1.22'
|
- go: '1.22'
|
||||||
GO_SEMVER: '~1.22.3'
|
GO_SEMVER: '~1.22.3'
|
||||||
|
|
||||||
|
- go: '1.23'
|
||||||
|
GO_SEMVER: '~1.23.0'
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
|
|
6
.github/workflows/lint.yml
vendored
6
.github/workflows/lint.yml
vendored
|
@ -43,13 +43,13 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '~1.22.3'
|
go-version: '~1.23'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v6
|
uses: golangci/golangci-lint-action@v6
|
||||||
with:
|
with:
|
||||||
version: v1.55
|
version: v1.60
|
||||||
|
|
||||||
# Windows times out frequently after about 5m50s if we don't set a longer timeout.
|
# Windows times out frequently after about 5m50s if we don't set a longer timeout.
|
||||||
args: --timeout 10m
|
args: --timeout 10m
|
||||||
|
@ -63,5 +63,5 @@ jobs:
|
||||||
- name: govulncheck
|
- name: govulncheck
|
||||||
uses: golang/govulncheck-action@v1
|
uses: golang/govulncheck-action@v1
|
||||||
with:
|
with:
|
||||||
go-version-input: '~1.22.3'
|
go-version-input: '~1.23.0'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
|
@ -13,13 +13,13 @@ jobs:
|
||||||
os:
|
os:
|
||||||
- ubuntu-latest
|
- ubuntu-latest
|
||||||
go:
|
go:
|
||||||
- '1.22'
|
- '1.23'
|
||||||
|
|
||||||
include:
|
include:
|
||||||
# Set the minimum Go patch version for the given Go minor
|
# Set the minimum Go patch version for the given Go minor
|
||||||
# Usable via ${{ matrix.GO_SEMVER }}
|
# Usable via ${{ matrix.GO_SEMVER }}
|
||||||
- go: '1.22'
|
- go: '1.23'
|
||||||
GO_SEMVER: '~1.22.3'
|
GO_SEMVER: '~1.23.0'
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
|
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
linters-settings:
|
linters-settings:
|
||||||
errcheck:
|
errcheck:
|
||||||
ignore: fmt:.*,go.uber.org/zap/zapcore:^Add.*
|
exclude-functions:
|
||||||
ignoretests: true
|
- fmt.*
|
||||||
|
- (go.uber.org/zap/zapcore.ObjectEncoder).AddObject
|
||||||
|
- (go.uber.org/zap/zapcore.ObjectEncoder).AddArray
|
||||||
gci:
|
gci:
|
||||||
sections:
|
sections:
|
||||||
- standard # Standard section: captures all standard packages.
|
- standard # Standard section: captures all standard packages.
|
||||||
|
@ -130,13 +132,14 @@ linters:
|
||||||
run:
|
run:
|
||||||
# default concurrency is a available CPU number.
|
# default concurrency is a available CPU number.
|
||||||
# concurrency: 4 # explicitly omit this value to fully utilize available resources.
|
# concurrency: 4 # explicitly omit this value to fully utilize available resources.
|
||||||
deadline: 5m
|
timeout: 5m
|
||||||
issues-exit-code: 1
|
issues-exit-code: 1
|
||||||
tests: false
|
tests: false
|
||||||
|
|
||||||
# output configuration options
|
# output configuration options
|
||||||
output:
|
output:
|
||||||
format: 'colored-line-number'
|
formats:
|
||||||
|
- format: 'colored-line-number'
|
||||||
print-issued-lines: true
|
print-issued-lines: true
|
||||||
print-linter-name: true
|
print-linter-name: true
|
||||||
|
|
||||||
|
@ -166,3 +169,6 @@ issues:
|
||||||
- path: modules/logging/filters.go
|
- path: modules/logging/filters.go
|
||||||
linters:
|
linters:
|
||||||
- dupl
|
- dupl
|
||||||
|
- path: _test\.go
|
||||||
|
linters:
|
||||||
|
- errcheck
|
||||||
|
|
|
@ -87,7 +87,7 @@ See [our online documentation](https://caddyserver.com/docs/install) for other i
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
|
|
||||||
- [Go 1.21 or newer](https://golang.org/dl/)
|
- [Go 1.22.3 or newer](https://golang.org/dl/)
|
||||||
|
|
||||||
### For development
|
### For development
|
||||||
|
|
||||||
|
|
|
@ -415,7 +415,7 @@ func (d *Dispenser) EOFErr() error {
|
||||||
|
|
||||||
// Err generates a custom parse-time error with a message of msg.
|
// Err generates a custom parse-time error with a message of msg.
|
||||||
func (d *Dispenser) Err(msg string) error {
|
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
|
// Errf is like Err, but for formatted error messages
|
||||||
|
|
|
@ -84,7 +84,6 @@ func TestLoadUnorderedJSON(t *testing.T) {
|
||||||
"servers": {
|
"servers": {
|
||||||
"s_server": {
|
"s_server": {
|
||||||
"listen": [
|
"listen": [
|
||||||
":9443",
|
|
||||||
":9080"
|
":9080"
|
||||||
],
|
],
|
||||||
"routes": [
|
"routes": [
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
:80
|
||||||
|
|
||||||
|
file_server browse {
|
||||||
|
sort size desc
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"browse": {},
|
||||||
|
"handler": "file_server",
|
||||||
|
"hide": [
|
||||||
|
"./Caddyfile"
|
||||||
|
],
|
||||||
|
"sort": [
|
||||||
|
"size",
|
||||||
|
"desc"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
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
|
26
cmd/cobra.go
26
cmd/cobra.go
|
@ -8,9 +8,10 @@ import (
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var defaultFactory = newRootCommandFactory(func() *cobra.Command {
|
||||||
Use: "caddy",
|
return &cobra.Command{
|
||||||
Long: `Caddy is an extensible server platform written in Go.
|
Use: "caddy",
|
||||||
|
Long: `Caddy is an extensible server platform written in Go.
|
||||||
|
|
||||||
At its core, Caddy merely manages configuration. Modules are plugged
|
At its core, Caddy merely manages configuration. Modules are plugged
|
||||||
in statically at compile-time to provide useful functionality. Caddy's
|
in statically at compile-time to provide useful functionality. Caddy's
|
||||||
|
@ -91,23 +92,26 @@ package installers: https://caddyserver.com/docs/install
|
||||||
Instructions for running Caddy in production are also available:
|
Instructions for running Caddy in production are also available:
|
||||||
https://caddyserver.com/docs/running
|
https://caddyserver.com/docs/running
|
||||||
`,
|
`,
|
||||||
Example: ` $ caddy run
|
Example: ` $ caddy run
|
||||||
$ caddy run --config caddy.json
|
$ caddy run --config caddy.json
|
||||||
$ caddy reload --config caddy.json
|
$ caddy reload --config caddy.json
|
||||||
$ caddy stop`,
|
$ caddy stop`,
|
||||||
|
|
||||||
// kind of annoying to have all the help text printed out if
|
// kind of annoying to have all the help text printed out if
|
||||||
// caddy has an error provisioning its modules, for instance...
|
// caddy has an error provisioning its modules, for instance...
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
Version: onlyVersionText(),
|
Version: onlyVersionText(),
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const fullDocsFooter = `Full documentation is available at:
|
const fullDocsFooter = `Full documentation is available at:
|
||||||
https://caddyserver.com/docs/command-line`
|
https://caddyserver.com/docs/command-line`
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.SetVersionTemplate("{{.Version}}\n")
|
defaultFactory.Use(func(rootCmd *cobra.Command) {
|
||||||
rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n")
|
rootCmd.SetVersionTemplate("{{.Version}}\n")
|
||||||
|
rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func onlyVersionText() string {
|
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
|
// sure by giving it some random bytes and having it echo
|
||||||
// them back to us)
|
// them back to us)
|
||||||
cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String())
|
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 != "" {
|
if configFlag != "" {
|
||||||
cmd.Args = append(cmd.Args, "--config", configFlag)
|
cmd.Args = append(cmd.Args, "--config", configFlag)
|
||||||
}
|
}
|
||||||
|
|
100
cmd/commands.go
100
cmd/commands.go
|
@ -438,43 +438,44 @@ EXPERIMENTAL: May be changed or removed.
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
RegisterCommand(Command{
|
defaultFactory.Use(func(rootCmd *cobra.Command) {
|
||||||
Name: "manpage",
|
RegisterCommand(Command{
|
||||||
Usage: "--directory <path>",
|
Name: "manpage",
|
||||||
Short: "Generates the manual pages for Caddy commands",
|
Usage: "--directory <path>",
|
||||||
Long: `
|
Short: "Generates the manual pages for Caddy commands",
|
||||||
|
Long: `
|
||||||
Generates the manual pages for Caddy commands into the designated directory
|
Generates the manual pages for Caddy commands into the designated directory
|
||||||
tagged into section 8 (System Administration).
|
tagged into section 8 (System Administration).
|
||||||
|
|
||||||
The manual page files are generated into the directory specified by the
|
The manual page files are generated into the directory specified by the
|
||||||
argument of --directory. If the directory does not exist, it will be created.
|
argument of --directory. If the directory does not exist, it will be created.
|
||||||
`,
|
`,
|
||||||
CobraFunc: func(cmd *cobra.Command) {
|
CobraFunc: func(cmd *cobra.Command) {
|
||||||
cmd.Flags().StringP("directory", "o", "", "The output directory where the manpages are generated")
|
cmd.Flags().StringP("directory", "o", "", "The output directory where the manpages are generated")
|
||||||
cmd.RunE = WrapCommandFuncForCobra(func(fl Flags) (int, error) {
|
cmd.RunE = WrapCommandFuncForCobra(func(fl Flags) (int, error) {
|
||||||
dir := strings.TrimSpace(fl.String("directory"))
|
dir := strings.TrimSpace(fl.String("directory"))
|
||||||
if dir == "" {
|
if dir == "" {
|
||||||
return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required")
|
return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required")
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
return caddy.ExitCodeFailedQuit, err
|
return caddy.ExitCodeFailedQuit, err
|
||||||
}
|
}
|
||||||
if err := doc.GenManTree(rootCmd, &doc.GenManHeader{
|
if err := doc.GenManTree(rootCmd, &doc.GenManHeader{
|
||||||
Title: "Caddy",
|
Title: "Caddy",
|
||||||
Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections
|
Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections
|
||||||
}, dir); err != nil {
|
}, dir); err != nil {
|
||||||
return caddy.ExitCodeFailedQuit, err
|
return caddy.ExitCodeFailedQuit, err
|
||||||
}
|
}
|
||||||
return caddy.ExitCodeSuccess, nil
|
return caddy.ExitCodeSuccess, nil
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// source: https://github.com/spf13/cobra/blob/main/shell_completions.md
|
// source: https://github.com/spf13/cobra/blob/main/shell_completions.md
|
||||||
rootCmd.AddCommand(&cobra.Command{
|
rootCmd.AddCommand(&cobra.Command{
|
||||||
Use: "completion [bash|zsh|fish|powershell]",
|
Use: "completion [bash|zsh|fish|powershell]",
|
||||||
Short: "Generate completion script",
|
Short: "Generate completion script",
|
||||||
Long: fmt.Sprintf(`To load completions:
|
Long: fmt.Sprintf(`To load completions:
|
||||||
|
|
||||||
Bash:
|
Bash:
|
||||||
|
|
||||||
|
@ -513,23 +514,24 @@ argument of --directory. If the directory does not exist, it will be created.
|
||||||
PS> %[1]s completion powershell > %[1]s.ps1
|
PS> %[1]s completion powershell > %[1]s.ps1
|
||||||
# and source this file from your PowerShell profile.
|
# and source this file from your PowerShell profile.
|
||||||
`, rootCmd.Root().Name()),
|
`, rootCmd.Root().Name()),
|
||||||
DisableFlagsInUseLine: true,
|
DisableFlagsInUseLine: true,
|
||||||
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
|
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
|
||||||
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
switch args[0] {
|
switch args[0] {
|
||||||
case "bash":
|
case "bash":
|
||||||
return cmd.Root().GenBashCompletion(os.Stdout)
|
return cmd.Root().GenBashCompletion(os.Stdout)
|
||||||
case "zsh":
|
case "zsh":
|
||||||
return cmd.Root().GenZshCompletion(os.Stdout)
|
return cmd.Root().GenZshCompletion(os.Stdout)
|
||||||
case "fish":
|
case "fish":
|
||||||
return cmd.Root().GenFishCompletion(os.Stdout, true)
|
return cmd.Root().GenFishCompletion(os.Stdout, true)
|
||||||
case "powershell":
|
case "powershell":
|
||||||
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
|
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unrecognized shell: %s", args[0])
|
return fmt.Errorf("unrecognized shell: %s", args[0])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -563,7 +565,9 @@ func RegisterCommand(cmd Command) {
|
||||||
if !commandNameRegex.MatchString(cmd.Name) {
|
if !commandNameRegex.MatchString(cmd.Name) {
|
||||||
panic("invalid command name")
|
panic("invalid command name")
|
||||||
}
|
}
|
||||||
rootCmd.AddCommand(caddyCmdToCobra(cmd))
|
defaultFactory.Use(func(rootCmd *cobra.Command) {
|
||||||
|
rootCmd.AddCommand(caddyCmdToCobra(cmd))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`)
|
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))
|
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
|
var exitError *exitError
|
||||||
if errors.As(err, &exitError) {
|
if errors.As(err, &exitError) {
|
||||||
os.Exit(exitError.ExitCode)
|
os.Exit(exitError.ExitCode)
|
||||||
|
|
22
go.mod
22
go.mod
|
@ -1,8 +1,8 @@
|
||||||
module github.com/caddyserver/caddy/v2
|
module github.com/caddyserver/caddy/v2
|
||||||
|
|
||||||
go 1.21.0
|
go 1.22.3
|
||||||
|
|
||||||
toolchain go1.22.2
|
toolchain go1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.3.2
|
github.com/BurntSushi/toml v1.3.2
|
||||||
|
@ -19,7 +19,7 @@ require (
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7
|
github.com/klauspost/cpuid/v2 v2.2.7
|
||||||
github.com/mholt/acmez/v2 v2.0.1
|
github.com/mholt/acmez/v2 v2.0.1
|
||||||
github.com/prometheus/client_golang v1.19.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/certificates v0.26.1
|
||||||
github.com/smallstep/nosql v0.6.1
|
github.com/smallstep/nosql v0.6.1
|
||||||
github.com/smallstep/truststore v0.13.0
|
github.com/smallstep/truststore v0.13.0
|
||||||
|
@ -37,11 +37,11 @@ require (
|
||||||
go.uber.org/automaxprocs v1.5.3
|
go.uber.org/automaxprocs v1.5.3
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
go.uber.org/zap/exp v0.2.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/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595
|
||||||
golang.org/x/net v0.25.0
|
golang.org/x/net v0.28.0
|
||||||
golang.org/x/sync v0.7.0
|
golang.org/x/sync v0.8.0
|
||||||
golang.org/x/term v0.20.0
|
golang.org/x/term v0.23.0
|
||||||
golang.org/x/time v0.5.0
|
golang.org/x/time v0.5.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
@ -123,7 +123,7 @@ require (
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // 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/pkg/errors v0.9.1 // indirect
|
||||||
github.com/prometheus/client_model v0.5.0 // indirect
|
github.com/prometheus/client_model v0.5.0 // indirect
|
||||||
github.com/prometheus/common v0.48.0 // indirect
|
github.com/prometheus/common v0.48.0 // indirect
|
||||||
|
@ -147,9 +147,9 @@ require (
|
||||||
go.step.sm/linkedca v0.20.1 // indirect
|
go.step.sm/linkedca v0.20.1 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/mod v0.17.0 // indirect
|
golang.org/x/mod v0.17.0 // indirect
|
||||||
golang.org/x/sys v0.20.0
|
golang.org/x/sys v0.23.0
|
||||||
golang.org/x/text v0.15.0 // indirect
|
golang.org/x/text v0.17.0 // indirect
|
||||||
golang.org/x/tools v0.21.0 // indirect
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||||
google.golang.org/grpc v1.63.2 // indirect
|
google.golang.org/grpc v1.63.2 // indirect
|
||||||
google.golang.org/protobuf v1.34.1 // indirect
|
google.golang.org/protobuf v1.34.1 // indirect
|
||||||
howett.net/plist v1.0.0 // indirect
|
howett.net/plist v1.0.0 // indirect
|
||||||
|
|
36
go.sum
36
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/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 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
|
||||||
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
|
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.1-0.20240628150027-b718e7ce4964 h1:ct/vxNBgHpASQ4sT8NaBX9LtsEtluZqaUJydLG50U3E=
|
||||||
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/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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/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 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
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.46.0 h1:uuwLClEEyk1DNvchH8uCByQVjo3yKL9opKulExNDs7Y=
|
||||||
github.com/quic-go/quic-go v0.44.0/go.mod h1:z4cx/9Ny9UtGITIPzmPTXh1ULfOyWh4qGQlpnPcWmek=
|
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.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 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
|
@ -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.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.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.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
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 h1:TgSqweA595vD0Zt86JzLv3Pb/syKg8gd5KMGGbJPYFw=
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
|
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=
|
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.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.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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
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 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
|
||||||
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
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-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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
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-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-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.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.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
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-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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.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.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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
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.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
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.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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.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.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.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.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.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
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 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
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=
|
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.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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||||
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
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-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-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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",
|
||||||
|
}
|
||||||
|
}
|
|
@ -340,7 +340,7 @@ func (celTypeAdapter) NativeToValue(value any) ref.Val {
|
||||||
case time.Time:
|
case time.Time:
|
||||||
return types.Timestamp{Time: v}
|
return types.Timestamp{Time: v}
|
||||||
case error:
|
case error:
|
||||||
types.NewErr(v.Error())
|
return types.WrapErr(v)
|
||||||
}
|
}
|
||||||
return types.DefaultTypeAdapter.NativeToValue(value)
|
return types.DefaultTypeAdapter.NativeToValue(value)
|
||||||
}
|
}
|
||||||
|
@ -499,7 +499,7 @@ func CELMatcherRuntimeFunction(funcName string, fac CELMatcherFactory) functions
|
||||||
return func(celReq, matcherData ref.Val) ref.Val {
|
return func(celReq, matcherData ref.Val) ref.Val {
|
||||||
matcher, err := fac(matcherData)
|
matcher, err := fac(matcherData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.NewErr(err.Error())
|
return types.WrapErr(err)
|
||||||
}
|
}
|
||||||
httpReq := celReq.Value().(celHTTPRequest)
|
httpReq := celReq.Value().(celHTTPRequest)
|
||||||
return types.Bool(matcher.Match(httpReq.Request))
|
return types.Bool(matcher.Match(httpReq.Request))
|
||||||
|
|
|
@ -266,6 +266,14 @@ func (rw *responseWriter) FlushError() error {
|
||||||
// to rw.Write (see bug in #4314)
|
// to rw.Write (see bug in #4314)
|
||||||
return nil
|
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
|
//nolint:bodyclose
|
||||||
return http.NewResponseController(rw.ResponseWriter).Flush()
|
return http.NewResponseController(rw.ResponseWriter).Flush()
|
||||||
}
|
}
|
||||||
|
@ -475,6 +483,7 @@ type encodingPreference struct {
|
||||||
type Encoder interface {
|
type Encoder interface {
|
||||||
io.WriteCloser
|
io.WriteCloser
|
||||||
Reset(io.Writer)
|
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
|
// Encoding is a type which can create encoders of its kind
|
||||||
|
|
|
@ -206,11 +206,34 @@ func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, fileSystem fs
|
||||||
// browseApplyQueryParams applies query parameters to the listing.
|
// browseApplyQueryParams applies query parameters to the listing.
|
||||||
// It mutates the listing and may set cookies.
|
// It mutates the listing and may set cookies.
|
||||||
func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Request, listing *browseTemplateContext) {
|
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.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")
|
layoutParam := r.URL.Query().Get("layout")
|
||||||
sortParam := r.URL.Query().Get("sort")
|
|
||||||
orderParam := r.URL.Query().Get("order")
|
|
||||||
limitParam := r.URL.Query().Get("limit")
|
limitParam := r.URL.Query().Get("limit")
|
||||||
offsetParam := r.URL.Query().Get("offset")
|
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 {
|
switch layoutParam {
|
||||||
case "list", "grid", "":
|
case "list", "grid", "":
|
||||||
|
@ -233,11 +256,11 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re
|
||||||
// then figure out the order
|
// then figure out the order
|
||||||
switch orderParam {
|
switch orderParam {
|
||||||
case "":
|
case "":
|
||||||
orderParam = "asc"
|
orderParam = sortOrderAsc
|
||||||
if orderCookie, orderErr := r.Cookie("order"); orderErr == nil {
|
if orderCookie, orderErr := r.Cookie("order"); orderErr == nil {
|
||||||
orderParam = orderCookie.Value
|
orderParam = orderCookie.Value
|
||||||
}
|
}
|
||||||
case "asc", "desc":
|
case sortOrderAsc, sortOrderDesc:
|
||||||
http.SetCookie(w, &http.Cookie{Name: "order", Value: orderParam, Secure: r.TLS != nil})
|
http.SetCookie(w, &http.Cookie{Name: "order", Value: orderParam, Secure: r.TLS != nil})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -373,4 +373,7 @@ const (
|
||||||
sortByNameDirFirst = "namedirfirst"
|
sortByNameDirFirst = "namedirfirst"
|
||||||
sortBySize = "size"
|
sortBySize = "size"
|
||||||
sortByTime = "time"
|
sortByTime = "time"
|
||||||
|
|
||||||
|
sortOrderAsc = "asc"
|
||||||
|
sortOrderDesc = "desc"
|
||||||
)
|
)
|
||||||
|
|
|
@ -171,6 +171,17 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
}
|
}
|
||||||
fsrv.EtagFileExtensions = etagFileExtensions
|
fsrv.EtagFileExtensions = etagFileExtensions
|
||||||
|
|
||||||
|
case "sort":
|
||||||
|
for d.NextArg() {
|
||||||
|
dVal := d.Val()
|
||||||
|
switch dVal {
|
||||||
|
case sortByName, sortBySize, sortByTime, sortOrderAsc, sortOrderDesc:
|
||||||
|
fsrv.SortOptions = append(fsrv.SortOptions, dVal)
|
||||||
|
default:
|
||||||
|
return d.Errf("unknown sort option '%s'", dVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return d.Errf("unknown subdirective '%s'", d.Val())
|
return d.Errf("unknown subdirective '%s'", d.Val())
|
||||||
}
|
}
|
||||||
|
|
|
@ -153,6 +153,16 @@ type FileServer struct {
|
||||||
// a 404 error. By default, this is false (disabled).
|
// a 404 error. By default, this is false (disabled).
|
||||||
PassThru bool `json:"pass_thru,omitempty"`
|
PassThru bool `json:"pass_thru,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"`
|
||||||
|
|
||||||
// Selection of encoders to use to check for precompressed files.
|
// Selection of encoders to use to check for precompressed files.
|
||||||
PrecompressedRaw caddy.ModuleMap `json:"precompressed,omitempty" caddy:"namespace=http.precompressed"`
|
PrecompressedRaw caddy.ModuleMap `json:"precompressed,omitempty" caddy:"namespace=http.precompressed"`
|
||||||
|
|
||||||
|
@ -236,6 +246,22 @@ func (fsrv *FileServer) Provision(ctx caddy.Context) error {
|
||||||
fsrv.precompressors[ae] = p
|
fsrv.precompressors[ae] = p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check sort options
|
||||||
|
for idx, sortOption := range fsrv.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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ import (
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
|
"github.com/caddyserver/caddy/v2/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MatchRemoteIP matches requests by the remote IP address,
|
// 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")
|
return d.Err("the 'forwarded' option is no longer supported; use the 'client_ip' matcher instead")
|
||||||
}
|
}
|
||||||
if d.Val() == "private_ranges" {
|
if d.Val() == "private_ranges" {
|
||||||
m.Ranges = append(m.Ranges, PrivateRangesCIDR()...)
|
m.Ranges = append(m.Ranges, internal.PrivateRangesCIDR()...)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
m.Ranges = append(m.Ranges, d.Val())
|
m.Ranges = append(m.Ranges, d.Val())
|
||||||
|
@ -173,7 +174,7 @@ func (m *MatchClientIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
for d.Next() {
|
for d.Next() {
|
||||||
for d.NextArg() {
|
for d.NextArg() {
|
||||||
if d.Val() == "private_ranges" {
|
if d.Val() == "private_ranges" {
|
||||||
m.Ranges = append(m.Ranges, PrivateRangesCIDR()...)
|
m.Ranges = append(m.Ranges, internal.PrivateRangesCIDR()...)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
m.Ranges = append(m.Ranges, d.Val())
|
m.Ranges = append(m.Ranges, d.Val())
|
||||||
|
@ -250,7 +251,9 @@ func (m MatchClientIP) Match(r *http.Request) bool {
|
||||||
func provisionCidrsZonesFromRanges(ranges []string) ([]*netip.Prefix, []string, error) {
|
func provisionCidrsZonesFromRanges(ranges []string) ([]*netip.Prefix, []string, error) {
|
||||||
cidrs := []*netip.Prefix{}
|
cidrs := []*netip.Prefix{}
|
||||||
zones := []string{}
|
zones := []string{}
|
||||||
|
repl := caddy.NewReplacer()
|
||||||
for _, str := range ranges {
|
for _, str := range ranges {
|
||||||
|
str = repl.ReplaceAll(str, "")
|
||||||
// Exclude the zone_id from the IP
|
// Exclude the zone_id from the IP
|
||||||
if strings.Contains(str, "%") {
|
if strings.Contains(str, "%") {
|
||||||
split := strings.Split(str, "%")
|
split := strings.Split(str, "%")
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
|
"github.com/caddyserver/caddy/v2/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -92,7 +93,7 @@ func (m *StaticIPRange) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
}
|
}
|
||||||
for d.NextArg() {
|
for d.NextArg() {
|
||||||
if d.Val() == "private_ranges" {
|
if d.Val() == "private_ranges" {
|
||||||
m.Ranges = append(m.Ranges, PrivateRangesCIDR()...)
|
m.Ranges = append(m.Ranges, internal.PrivateRangesCIDR()...)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
m.Ranges = append(m.Ranges, d.Val())
|
m.Ranges = append(m.Ranges, d.Val())
|
||||||
|
@ -121,22 +122,16 @@ func CIDRExpressionToPrefix(expr string) (netip.Prefix, error) {
|
||||||
return prefix, nil
|
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
|
// Interface guards
|
||||||
var (
|
var (
|
||||||
_ caddy.Provisioner = (*StaticIPRange)(nil)
|
_ caddy.Provisioner = (*StaticIPRange)(nil)
|
||||||
_ caddyfile.Unmarshaler = (*StaticIPRange)(nil)
|
_ caddyfile.Unmarshaler = (*StaticIPRange)(nil)
|
||||||
_ IPRangeSource = (*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()
|
||||||
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ type ListenerWrapper struct {
|
||||||
Allow []string `json:"allow,omitempty"`
|
Allow []string `json:"allow,omitempty"`
|
||||||
allow []netip.Prefix
|
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 PROXY headers from.
|
||||||
Deny []string `json:"deny,omitempty"`
|
Deny []string `json:"deny,omitempty"`
|
||||||
deny []netip.Prefix
|
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
|
// Policy definitions are here: https://pkg.go.dev/github.com/pires/go-proxyproto@v0.7.0#Policy
|
||||||
FallbackPolicy Policy `json:"fallback_policy,omitempty"`
|
FallbackPolicy Policy `json:"fallback_policy,omitempty"`
|
||||||
|
|
||||||
policy goproxy.PolicyFunc
|
policy goproxy.ConnPolicyFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provision sets up the listener wrapper.
|
// 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.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
|
// trust unix sockets
|
||||||
if network := upstream.Network(); caddy.IsUnixNetwork(network) {
|
if network := options.Upstream.Network(); caddy.IsUnixNetwork(network) {
|
||||||
return goproxy.USE, nil
|
return goproxy.USE, nil
|
||||||
}
|
}
|
||||||
ret := pp.FallbackPolicy
|
ret := pp.FallbackPolicy
|
||||||
host, _, err := net.SplitHostPort(upstream.String())
|
host, _, err := net.SplitHostPort(options.Upstream.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return goproxy.REJECT, err
|
return goproxy.REJECT, err
|
||||||
}
|
}
|
||||||
|
@ -106,6 +107,6 @@ func (pp *ListenerWrapper) WrapListener(l net.Listener) net.Listener {
|
||||||
Listener: l,
|
Listener: l,
|
||||||
ReadHeaderTimeout: time.Duration(pp.Timeout),
|
ReadHeaderTimeout: time.Duration(pp.Timeout),
|
||||||
}
|
}
|
||||||
pl.Policy = pp.policy
|
pl.ConnPolicy = pp.policy
|
||||||
return pl
|
return pl
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ import (
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
"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"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
|
||||||
|
@ -68,19 +69,20 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
||||||
// lb_retry_match <request-matcher>
|
// lb_retry_match <request-matcher>
|
||||||
//
|
//
|
||||||
// # active health checking
|
// # active health checking
|
||||||
// health_uri <uri>
|
// health_uri <uri>
|
||||||
// health_port <port>
|
// health_port <port>
|
||||||
// health_interval <interval>
|
// health_interval <interval>
|
||||||
// health_passes <num>
|
// health_passes <num>
|
||||||
// health_fails <num>
|
// health_fails <num>
|
||||||
// health_timeout <duration>
|
// health_timeout <duration>
|
||||||
// health_status <status>
|
// health_status <status>
|
||||||
// health_body <regexp>
|
// health_body <regexp>
|
||||||
|
// health_method <value>
|
||||||
|
// health_request_body <value>
|
||||||
// health_follow_redirects
|
// health_follow_redirects
|
||||||
// health_headers {
|
// health_headers {
|
||||||
// <field> [<values...>]
|
// <field> [<values...>]
|
||||||
// }
|
// }
|
||||||
// health_method <value>
|
|
||||||
//
|
//
|
||||||
// # passive health checking
|
// # passive health checking
|
||||||
// fail_duration <duration>
|
// fail_duration <duration>
|
||||||
|
@ -424,6 +426,18 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
}
|
}
|
||||||
h.HealthChecks.Active.Method = d.Val()
|
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":
|
case "health_interval":
|
||||||
if !d.NextArg() {
|
if !d.NextArg() {
|
||||||
return d.ArgErr()
|
return d.ArgErr()
|
||||||
|
@ -688,7 +702,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
case "trusted_proxies":
|
case "trusted_proxies":
|
||||||
for d.NextArg() {
|
for d.NextArg() {
|
||||||
if d.Val() == "private_ranges" {
|
if d.Val() == "private_ranges" {
|
||||||
h.TrustedProxies = append(h.TrustedProxies, caddyhttp.PrivateRangesCIDR()...)
|
h.TrustedProxies = append(h.TrustedProxies, internal.PrivateRangesCIDR()...)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
h.TrustedProxies = append(h.TrustedProxies, d.Val())
|
h.TrustedProxies = append(h.TrustedProxies, d.Val())
|
||||||
|
@ -1312,7 +1326,11 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
return d.Err("cannot specify \"tls_trust_pool\" twice in caddyfile")
|
return d.Err("cannot specify \"tls_trust_pool\" twice in caddyfile")
|
||||||
}
|
}
|
||||||
h.TLS.CARaw = caddyconfig.JSONModuleObject(ca, "provider", modStem, nil)
|
h.TLS.CARaw = caddyconfig.JSONModuleObject(ca, "provider", modStem, nil)
|
||||||
|
case "local_address":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return d.ArgErr()
|
||||||
|
}
|
||||||
|
h.LocalAddress = d.Val()
|
||||||
default:
|
default:
|
||||||
return d.Errf("unrecognized subdirective %s", d.Val())
|
return d.Errf("unrecognized subdirective %s", d.Val())
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
@ -93,6 +94,9 @@ type ActiveHealthChecks struct {
|
||||||
// The HTTP method to use for health checks (default "GET").
|
// The HTTP method to use for health checks (default "GET").
|
||||||
Method string `json:"method,omitempty"`
|
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).
|
// Whether to follow HTTP redirects in response to active health checks (default off).
|
||||||
FollowRedirects bool `json:"follow_redirects,omitempty"`
|
FollowRedirects bool `json:"follow_redirects,omitempty"`
|
||||||
|
|
||||||
|
@ -396,6 +400,16 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ
|
||||||
u.Path = h.HealthChecks.Active.Path
|
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
|
// attach dialing information to this request, as well as context values that
|
||||||
// may be expected by handlers of this request
|
// may be expected by handlers of this request
|
||||||
ctx := h.ctx.Context
|
ctx := h.ctx.Context
|
||||||
|
@ -403,15 +417,14 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ
|
||||||
ctx = context.WithValue(ctx, caddyhttp.VarsCtxKey, map[string]any{
|
ctx = context.WithValue(ctx, caddyhttp.VarsCtxKey, map[string]any{
|
||||||
dialInfoVarKey: dialInfo,
|
dialInfoVarKey: dialInfo,
|
||||||
})
|
})
|
||||||
req, err := http.NewRequestWithContext(ctx, h.HealthChecks.Active.Method, u.String(), nil)
|
req, err := http.NewRequestWithContext(ctx, h.HealthChecks.Active.Method, u.String(), requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("making request: %v", err)
|
return fmt.Errorf("making request: %v", err)
|
||||||
}
|
}
|
||||||
ctx = context.WithValue(ctx, caddyhttp.OriginalRequestCtxKey, *req)
|
ctx = context.WithValue(ctx, caddyhttp.OriginalRequestCtxKey, *req)
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
// set headers, using a replacer with only globals (env vars, system info, etc.)
|
// set headers, using replacer
|
||||||
repl := caddy.NewReplacer()
|
|
||||||
repl.Set("http.reverse_proxy.active.target_upstream", networkAddr)
|
repl.Set("http.reverse_proxy.active.target_upstream", networkAddr)
|
||||||
for key, vals := range h.HealthChecks.Active.Headers {
|
for key, vals := range h.HealthChecks.Active.Headers {
|
||||||
key = repl.ReplaceAll(key, "")
|
key = repl.ReplaceAll(key, "")
|
||||||
|
|
|
@ -132,6 +132,10 @@ type HTTPTransport struct {
|
||||||
// to change or removal while experimental.
|
// to change or removal while experimental.
|
||||||
Versions []string `json:"versions,omitempty"`
|
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.
|
// The pre-configured underlying HTTP transport.
|
||||||
Transport *http.Transport `json:"-"`
|
Transport *http.Transport `json:"-"`
|
||||||
|
|
||||||
|
@ -185,6 +189,31 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
|
||||||
FallbackDelay: time.Duration(h.FallbackDelay),
|
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 {
|
if h.Resolver != nil {
|
||||||
err := h.Resolver.ParseAddresses()
|
err := h.Resolver.ParseAddresses()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -446,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
|
// if H2C ("HTTP/2 over cleartext") is enabled and the upstream request is
|
||||||
// HTTP without TLS, use the alternate H2C-capable transport instead
|
// HTTP without TLS, use the alternate H2C-capable transport instead
|
||||||
if req.URL.Scheme == "http" && h.h2cTransport != nil {
|
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)
|
return h.h2cTransport.RoundTrip(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -979,7 +979,7 @@ func (h *Handler) finalizeResponse(
|
||||||
// we'll just log the error and abort the stream here and panic just as
|
// 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.
|
// the standard lib's proxy to propagate the stream error.
|
||||||
// see issue https://github.com/caddyserver/caddy/issues/5951
|
// 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
|
// no extra logging from stdlib
|
||||||
panic(http.ErrAbortHandler)
|
panic(http.ErrAbortHandler)
|
||||||
}
|
}
|
||||||
|
|
|
@ -602,6 +602,7 @@ func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error
|
||||||
QUICConfig: &quic.Config{
|
QUICConfig: &quic.Config{
|
||||||
Versions: []quic.Version{quic.Version1, quic.Version2},
|
Versions: []quic.Version{quic.Version1, quic.Version2},
|
||||||
},
|
},
|
||||||
|
IdleTimeout: time.Duration(s.IdleTimeout),
|
||||||
ConnContext: func(ctx context.Context, c quic.Connection) context.Context {
|
ConnContext: func(ctx context.Context, c quic.Connection) context.Context {
|
||||||
return context.WithValue(ctx, quicConnCtxKey, c)
|
return context.WithValue(ctx, quicConnCtxKey, c)
|
||||||
},
|
},
|
||||||
|
|
|
@ -105,8 +105,7 @@ func (e StaticError) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler
|
||||||
}
|
}
|
||||||
statusCode = intVal
|
statusCode = intVal
|
||||||
}
|
}
|
||||||
|
return Error(statusCode, fmt.Errorf("%s", repl.ReplaceKnown(e.Error, "")))
|
||||||
return Error(statusCode, fmt.Errorf("%s", e.Error))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guard
|
// Interface guard
|
||||||
|
|
|
@ -22,6 +22,8 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CustomCertSelectionPolicy represents a policy for selecting the certificate
|
// CustomCertSelectionPolicy represents a policy for selecting the certificate
|
||||||
|
@ -122,6 +124,79 @@ nextChoice:
|
||||||
return certmagic.DefaultCertificateSelector(hello, viable)
|
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.
|
// bigInt is a big.Int type that interops with JSON encodings as a string.
|
||||||
type bigInt struct{ big.Int }
|
type bigInt struct{ big.Int }
|
||||||
|
|
||||||
|
@ -144,3 +219,6 @@ func (bi *bigInt) UnmarshalJSON(p []byte) error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Interface guard
|
||||||
|
var _ caddyfile.Unmarshaler = (*CustomCertSelectionPolicy)(nil)
|
||||||
|
|
|
@ -363,6 +363,136 @@ func (p ConnectionPolicy) SettingsEmpty() bool {
|
||||||
p.InsecureSecretsLog == ""
|
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.
|
// ClientAuthentication configures TLS client auth.
|
||||||
type ClientAuthentication struct {
|
type ClientAuthentication struct {
|
||||||
// Certificate authority module which provides the certificate pool of trusted certificates
|
// Certificate authority module which provides the certificate pool of trusted certificates
|
||||||
|
@ -819,4 +949,46 @@ func (d destructableWriter) Destruct() error { return d.Close() }
|
||||||
|
|
||||||
var secretsLogPool = caddy.NewUsagePool()
|
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"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
|
"github.com/caddyserver/caddy/v2/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -48,14 +50,47 @@ func (MatchServerName) CaddyModule() caddy.ModuleInfo {
|
||||||
|
|
||||||
// Match matches hello based on SNI.
|
// Match matches hello based on SNI.
|
||||||
func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool {
|
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 {
|
for _, name := range m {
|
||||||
if certmagic.MatchWildcard(hello.ServerName, name) {
|
rs := repl.ReplaceAll(name, "")
|
||||||
|
if certmagic.MatchWildcard(hello.ServerName, rs) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
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
|
// MatchRemoteIP matches based on the remote IP of the
|
||||||
// connection. Specific IPs or CIDR ranges can be specified.
|
// 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.
|
// Provision parses m's IP ranges, either from IP or CIDR expressions.
|
||||||
func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
|
func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
|
||||||
|
repl := caddy.NewReplacer()
|
||||||
m.logger = ctx.Logger()
|
m.logger = ctx.Logger()
|
||||||
for _, str := range m.Ranges {
|
for _, str := range m.Ranges {
|
||||||
cidrs, err := m.parseIPRange(str)
|
rs := repl.ReplaceAll(str, "")
|
||||||
|
cidrs, err := m.parseIPRange(rs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
m.cidrs = append(m.cidrs, cidrs...)
|
m.cidrs = append(m.cidrs, cidrs...)
|
||||||
}
|
}
|
||||||
for _, str := range m.NotRanges {
|
for _, str := range m.NotRanges {
|
||||||
cidrs, err := m.parseIPRange(str)
|
rs := repl.ReplaceAll(str, "")
|
||||||
|
cidrs, err := m.parseIPRange(rs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -145,6 +183,46 @@ func (MatchRemoteIP) matches(ip netip.Addr, ranges []netip.Prefix) bool {
|
||||||
return false
|
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
|
// MatchLocalIP matches based on the IP address of the interface
|
||||||
// receiving the connection. Specific IPs or CIDR ranges can be specified.
|
// receiving the connection. Specific IPs or CIDR ranges can be specified.
|
||||||
type MatchLocalIP struct {
|
type MatchLocalIP struct {
|
||||||
|
@ -165,9 +243,11 @@ func (MatchLocalIP) CaddyModule() caddy.ModuleInfo {
|
||||||
|
|
||||||
// Provision parses m's IP ranges, either from IP or CIDR expressions.
|
// Provision parses m's IP ranges, either from IP or CIDR expressions.
|
||||||
func (m *MatchLocalIP) Provision(ctx caddy.Context) error {
|
func (m *MatchLocalIP) Provision(ctx caddy.Context) error {
|
||||||
|
repl := caddy.NewReplacer()
|
||||||
m.logger = ctx.Logger()
|
m.logger = ctx.Logger()
|
||||||
for _, str := range m.Ranges {
|
for _, str := range m.Ranges {
|
||||||
cidrs, err := m.parseIPRange(str)
|
rs := repl.ReplaceAll(str, "")
|
||||||
|
cidrs, err := m.parseIPRange(rs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -219,6 +299,36 @@ func (MatchLocalIP) matches(ip netip.Addr, ranges []netip.Prefix) bool {
|
||||||
return false
|
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
|
// Interface guards
|
||||||
var (
|
var (
|
||||||
_ ConnectionMatcher = (*MatchServerName)(nil)
|
_ ConnectionMatcher = (*MatchServerName)(nil)
|
||||||
|
@ -226,4 +336,8 @@ var (
|
||||||
|
|
||||||
_ caddy.Provisioner = (*MatchLocalIP)(nil)
|
_ caddy.Provisioner = (*MatchLocalIP)(nil)
|
||||||
_ ConnectionMatcher = (*MatchLocalIP)(nil)
|
_ ConnectionMatcher = (*MatchLocalIP)(nil)
|
||||||
|
|
||||||
|
_ caddyfile.Unmarshaler = (*MatchLocalIP)(nil)
|
||||||
|
_ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
|
||||||
|
_ caddyfile.Unmarshaler = (*MatchServerName)(nil)
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
package caddy
|
package caddy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -354,6 +355,8 @@ func (f fileReplacementProvider) replace(key string) (any, bool) {
|
||||||
zap.Error(err))
|
zap.Error(err))
|
||||||
return nil, true
|
return nil, true
|
||||||
}
|
}
|
||||||
|
body = bytes.TrimSuffix(body, []byte("\n"))
|
||||||
|
body = bytes.TrimSuffix(body, []byte("\r"))
|
||||||
return string(body), true
|
return string(body), true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -431,6 +431,14 @@ func TestReplacerNew(t *testing.T) {
|
||||||
variable: "file.caddytest/integration/testdata/foo.txt",
|
variable: "file.caddytest/integration/testdata/foo.txt",
|
||||||
value: "foo",
|
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, ok := repl.providers[1].replace(tc.variable); ok {
|
||||||
if val != tc.value {
|
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) {
|
func TestReplacerNewWithoutFile(t *testing.T) {
|
||||||
repl := NewReplacer().WithoutFile()
|
repl := NewReplacer().WithoutFile()
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue