mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-27 06:03:48 +03:00
Merge branch 'master' into forward-proxy
This commit is contained in:
commit
32d1c5e1bf
48 changed files with 1279 additions and 213 deletions
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
|
@ -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?
|
||||||
|
@ -157,7 +158,7 @@ jobs:
|
||||||
|
|
||||||
# The environment is fresh, so there's no point in keeping accepting and adding the key.
|
# 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"
|
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=$?
|
test_result=$?
|
||||||
|
|
||||||
# There's no need leaving the files around
|
# There's no need leaving the files around
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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_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 {
|
localhost:9080 {
|
||||||
respond /intercept "I'm a teapot" 408
|
respond /intercept "I'm a teapot" 408
|
||||||
|
header /intercept To-Intercept ok
|
||||||
respond /no-intercept "I'm not a teapot"
|
respond /no-intercept "I'm not a teapot"
|
||||||
|
|
||||||
intercept {
|
intercept {
|
||||||
@teapot status 408
|
@teapot status 408
|
||||||
handle_response @teapot {
|
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
|
respond /intercept "I'm a combined coffee/tea pot that is temporarily out of coffee" 503
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, "caddyfile")
|
`, "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")
|
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
|
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)
|
||||||
|
|
18
go.mod
18
go.mod
|
@ -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",
|
||||||
|
}
|
||||||
|
}
|
|
@ -60,8 +60,6 @@ type NetworkAddress struct {
|
||||||
// ListenAll calls Listen() for all addresses represented by this struct, i.e. all ports in the range.
|
// 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.)
|
// (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.
|
// 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) {
|
func (na NetworkAddress) ListenAll(ctx context.Context, config net.ListenConfig) ([]any, error) {
|
||||||
var listeners []any
|
var listeners []any
|
||||||
var err error
|
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
|
// 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
|
// 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).
|
// 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) {
|
func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
|
||||||
if na.IsUnixNetwork() {
|
if na.IsUnixNetwork() {
|
||||||
unixSocketsMu.Lock()
|
unixSocketsMu.Lock()
|
||||||
|
@ -221,8 +217,6 @@ func (na NetworkAddress) JoinHostPort(offset uint) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expand returns one NetworkAddress for each port in the port range.
|
// 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 {
|
func (na NetworkAddress) Expand() []NetworkAddress {
|
||||||
size := na.PortRangeSize()
|
size := na.PortRangeSize()
|
||||||
addrs := make([]NetworkAddress, size)
|
addrs := make([]NetworkAddress, size)
|
||||||
|
|
|
@ -112,7 +112,8 @@ func (enc *Encode) Provision(ctx caddy.Context) error {
|
||||||
"application/x-ttf*",
|
"application/x-ttf*",
|
||||||
"application/xhtml+xml*",
|
"application/xhtml+xml*",
|
||||||
"application/xml*",
|
"application/xml*",
|
||||||
"font/*",
|
"font/ttf*",
|
||||||
|
"font/otf*",
|
||||||
"image/svg+xml*",
|
"image/svg+xml*",
|
||||||
"image/vnd.microsoft.icon*",
|
"image/vnd.microsoft.icon*",
|
||||||
"image/x-icon*",
|
"image/x-icon*",
|
||||||
|
@ -265,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()
|
||||||
}
|
}
|
||||||
|
@ -474,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})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"}}
|
{{- define "icon"}}
|
||||||
{{- if .IsDir}}
|
{{- if .IsDir}}
|
||||||
{{- if .IsSymlink}}
|
{{- 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">
|
<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 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 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>
|
</svg>
|
||||||
{{- else}}
|
{{- 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">
|
<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 charset="utf-8">
|
||||||
<meta name="color-scheme" content="light dark">
|
<meta name="color-scheme" content="light dark">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<style>
|
<style {{ $nonceAttribute }}>
|
||||||
* { padding: 0; margin: 0; box-sizing: border-box; }
|
* { padding: 0; margin: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
@ -342,6 +349,10 @@ svg,
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#layout-list, #layout-grid {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
@ -768,10 +779,10 @@ footer {
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
{{- if eq .Layout "grid"}}
|
{{- 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}}
|
{{- end}}
|
||||||
</head>
|
</head>
|
||||||
<body onload="initPage()">
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div class="breadcrumbs">Folder Path</div>
|
<div class="breadcrumbs">Folder Path</div>
|
||||||
|
@ -799,7 +810,7 @@ footer {
|
||||||
</span>
|
</span>
|
||||||
{{- end}}
|
{{- end}}
|
||||||
</div>
|
</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">
|
<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 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"/>
|
<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>
|
</svg>
|
||||||
List
|
List
|
||||||
</a>
|
</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">
|
<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 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"/>
|
<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="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0"/>
|
||||||
<path d="M21 21l-6 -6"/>
|
<path d="M21 21l-6 -6"/>
|
||||||
</svg>
|
</svg>
|
||||||
<input type="search" placeholder="Search" id="filter" onkeyup='filter()'>
|
<input type="search" placeholder="Search" id="filter">
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
|
@ -980,7 +991,7 @@ footer {
|
||||||
<div class="sizebar">
|
<div class="sizebar">
|
||||||
<div class="sizebar-bar"></div>
|
<div class="sizebar-bar"></div>
|
||||||
<div class="sizebar-text">
|
<div class="sizebar-text">
|
||||||
{{.HumanSize}}
|
{{if .IsSymlink}}↱ {{end}}{{.HumanSize}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -1000,70 +1011,70 @@ footer {
|
||||||
<footer>
|
<footer>
|
||||||
Served with
|
Served with
|
||||||
<a rel="noopener noreferrer" href="https://caddyserver.com">
|
<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,0,0,1,-1982.99,-530.985)">
|
||||||
<g transform="matrix(1.16548,0,0,1.10195,1823.12,393.466)">
|
<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 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 id="Icon" transform="matrix(0.858013,0,0,0.907485,-3224.99,-1435.83)">
|
||||||
<g>
|
<g>
|
||||||
<g transform="matrix(-0.191794,-0.715786,0.715786,-0.191794,4329.14,4673.64)">
|
<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>
|
||||||
<g transform="matrix(-0.191794,-0.715786,0.715786,-0.191794,4329.14,4673.64)">
|
<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>
|
||||||
<g>
|
<g>
|
||||||
<g transform="matrix(-3.37109,-0.514565,0.514565,-3.37109,4078.07,1806.88)">
|
<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>
|
||||||
<g transform="matrix(-5.33921,-5.26159,-3.12106,-6.96393,4073.87,1861.55)">
|
<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>
|
</g>
|
||||||
<g id="Padlock" transform="matrix(3.11426,0,0,3.11426,3938.31,1737.25)">
|
<g id="Padlock" transform="matrix(3.11426,0,0,3.11426,3938.31,1737.25)">
|
||||||
<g>
|
<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="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" style="fill:none;fill-rule:nonzero;stroke:rgb(34,182,56);stroke-width:1.89px;"/>
|
<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>
|
||||||
<g>
|
<g>
|
||||||
<g transform="matrix(5.30977,0.697415,-0.697415,5.30977,3852.72,1727.97)">
|
<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>
|
||||||
<g transform="matrix(4.93114,2.49604,1.11018,5.44847,3921.41,1726.72)">
|
<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>
|
||||||
</g>
|
</g>
|
||||||
<g id="Text">
|
<g id="Text">
|
||||||
<g id="Wordmark" transform="matrix(1.32271,0,0,2.60848,-899.259,-791.691)">
|
<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)">
|
<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>
|
||||||
<g id="add" transform="matrix(0.525075,0,0,0.281607,801.871,304.987)">
|
<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)">
|
<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>
|
||||||
<g transform="matrix(116.242,0,0,116.242,226.592,267.39)">
|
<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>
|
||||||
<g transform="matrix(116.242,0,0,116.242,290.293,267.39)">
|
<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>
|
</g>
|
||||||
<g id="c" transform="matrix(-0.0716462,0.31304,-0.583685,-0.0384251,1489.76,-444.051)">
|
<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>
|
</g>
|
||||||
<g id="R" transform="matrix(0.426446,0,0,0.451034,-1192.44,-722.167)">
|
<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(1,0,0,1,-0.10786,0.450801)">
|
||||||
<g transform="matrix(12.1247,0,0,12.1247,3862.61,1929.9)">
|
<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>
|
</g>
|
||||||
<g transform="matrix(1,0,0,1,0.278569,0.101881)">
|
<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>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
|
@ -1074,7 +1085,7 @@ footer {
|
||||||
</a>
|
</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script {{ $nonceAttribute }}>
|
||||||
const filterEl = document.getElementById('filter');
|
const filterEl = document.getElementById('filter');
|
||||||
filterEl?.focus({ preventScroll: true });
|
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) {
|
function queryParam(k, v) {
|
||||||
const qs = new URLSearchParams(window.location.search);
|
const qs = new URLSearchParams(window.location.search);
|
||||||
if (!v) {
|
if (!v) {
|
||||||
|
|
|
@ -80,6 +80,13 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS,
|
||||||
}
|
}
|
||||||
|
|
||||||
size := info.Size()
|
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)
|
fileIsSymlink := isSymlink(info)
|
||||||
symlinkPath := ""
|
symlinkPath := ""
|
||||||
if fileIsSymlink {
|
if fileIsSymlink {
|
||||||
|
@ -103,7 +110,8 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS,
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isDir {
|
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
|
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.
|
// The number of files (items that aren't directories) in the listing.
|
||||||
NumFiles int `json:"num_files"`
|
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"`
|
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 column used
|
||||||
Sort string `json:"sort,omitempty"`
|
Sort string `json:"sort,omitempty"`
|
||||||
|
|
||||||
|
@ -288,6 +302,12 @@ func (btc browseTemplateContext) HumanTotalFileSize() string {
|
||||||
return humanize.IBytes(uint64(btc.TotalFileSize))
|
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
|
// HumanModTime returns the modified time of the file
|
||||||
// as a human-readable string given by format.
|
// as a human-readable string given by format.
|
||||||
func (fi fileInfo) HumanModTime(format string) string {
|
func (fi fileInfo) HumanModTime(format string) string {
|
||||||
|
@ -353,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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
package fileserver
|
package fileserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -152,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"`
|
||||||
|
|
||||||
|
@ -235,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -690,6 +717,10 @@ func (fsrv *FileServer) getEtagFromFile(fileSystem fs.FS, filename string) (stri
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("cannot read etag from file %s: %v", etagFilename, err)
|
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 string(etag), nil
|
||||||
}
|
}
|
||||||
return "", nil
|
return "", nil
|
||||||
|
|
|
@ -50,7 +50,6 @@ type Intercept struct {
|
||||||
//
|
//
|
||||||
// Three new placeholders are available in this handler chain:
|
// Three new placeholders are available in this handler chain:
|
||||||
// - `{http.intercept.status_code}` The status code from the response
|
// - `{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
|
// - `{http.intercept.header.*}` The headers from the response
|
||||||
HandleResponse []caddyhttp.ResponseHandler `json:"handle_response,omitempty"`
|
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
|
// set up the replacer so that parts of the original response can be
|
||||||
// used for routing decisions
|
// 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.header."+field, strings.Join(value, ","))
|
||||||
}
|
}
|
||||||
repl.Set("http.intercept.status_code", rec.Status())
|
repl.Set("http.intercept.status_code", rec.Status())
|
||||||
|
|
|
@ -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())
|
||||||
|
@ -143,6 +144,9 @@ func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
|
||||||
|
|
||||||
// Match returns true if r matches m.
|
// Match returns true if r matches m.
|
||||||
func (m MatchRemoteIP) Match(r *http.Request) bool {
|
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
|
address := r.RemoteAddr
|
||||||
clientIP, zoneID, err := parseIPZoneFromString(address)
|
clientIP, zoneID, err := parseIPZoneFromString(address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -170,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())
|
||||||
|
@ -228,6 +232,9 @@ func (m *MatchClientIP) Provision(ctx caddy.Context) error {
|
||||||
|
|
||||||
// Match returns true if r matches m.
|
// Match returns true if r matches m.
|
||||||
func (m MatchClientIP) Match(r *http.Request) bool {
|
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)
|
address := GetVar(r.Context(), ClientIPVarKey).(string)
|
||||||
clientIP, zoneID, err := parseIPZoneFromString(address)
|
clientIP, zoneID, err := parseIPZoneFromString(address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -244,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()
|
||||||
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ import (
|
||||||
"github.com/google/cel-go/cel"
|
"github.com/google/cel-go/cel"
|
||||||
"github.com/google/cel-go/common/types"
|
"github.com/google/cel-go/common/types"
|
||||||
"github.com/google/cel-go/common/types/ref"
|
"github.com/google/cel-go/common/types/ref"
|
||||||
|
"golang.org/x/net/idna"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
|
@ -177,6 +178,22 @@ type (
|
||||||
// "http/2", "http/3", or minimum versions: "http/2+", etc.
|
// "http/2", "http/3", or minimum versions: "http/2+", etc.
|
||||||
MatchProtocol string
|
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
|
// MatchNot matches requests by negating the results of its matcher
|
||||||
// sets. A single "not" matcher takes one or more matcher sets. Each
|
// 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
|
// matcher set is OR'ed; in other words, if any matcher set returns
|
||||||
|
@ -212,6 +229,7 @@ func init() {
|
||||||
caddy.RegisterModule(MatchHeader{})
|
caddy.RegisterModule(MatchHeader{})
|
||||||
caddy.RegisterModule(MatchHeaderRE{})
|
caddy.RegisterModule(MatchHeaderRE{})
|
||||||
caddy.RegisterModule(new(MatchProtocol))
|
caddy.RegisterModule(new(MatchProtocol))
|
||||||
|
caddy.RegisterModule(MatchTLS{})
|
||||||
caddy.RegisterModule(MatchNot{})
|
caddy.RegisterModule(MatchNot{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,13 +257,20 @@ func (m *MatchHost) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
func (m MatchHost) Provision(_ caddy.Context) error {
|
func (m MatchHost) Provision(_ caddy.Context) error {
|
||||||
// check for duplicates; they are nonsensical and reduce efficiency
|
// check for duplicates; they are nonsensical and reduce efficiency
|
||||||
// (we could just remove them, but the user should know their config is erroneous)
|
// (we could just remove them, but the user should know their config is erroneous)
|
||||||
seen := make(map[string]int)
|
seen := make(map[string]int, len(m))
|
||||||
for i, h := range m {
|
for i, host := range m {
|
||||||
h = strings.ToLower(h)
|
asciiHost, err := idna.ToASCII(host)
|
||||||
if firstI, ok := seen[h]; ok {
|
if err != nil {
|
||||||
return fmt.Errorf("host at index %d is repeated at index %d: %s", firstI, i, h)
|
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() {
|
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.
|
// CaddyModule returns the Caddy module information.
|
||||||
func (MatchNot) CaddyModule() caddy.ModuleInfo {
|
func (MatchNot) CaddyModule() caddy.ModuleInfo {
|
||||||
return caddy.ModuleInfo{
|
return caddy.ModuleInfo{
|
||||||
|
|
|
@ -78,6 +78,11 @@ func TestHostMatcher(t *testing.T) {
|
||||||
input: "bar.example.com",
|
input: "bar.example.com",
|
||||||
expect: false,
|
expect: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
match: MatchHost{"éxàmplê.com"},
|
||||||
|
input: "xn--xmpl-0na6cm.com",
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
match: MatchHost{"*.example.com"},
|
match: MatchHost{"*.example.com"},
|
||||||
input: "example.com",
|
input: "example.com",
|
||||||
|
@ -149,6 +154,10 @@ func TestHostMatcher(t *testing.T) {
|
||||||
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
|
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
|
||||||
req = req.WithContext(ctx)
|
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)
|
actual := tc.match.Match(req)
|
||||||
if actual != tc.expect {
|
if actual != tc.expect {
|
||||||
t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
|
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 []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
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,8 +142,16 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
||||||
}
|
}
|
||||||
return port, true
|
return port, true
|
||||||
case "http.request.remote":
|
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
|
return req.RemoteAddr, true
|
||||||
case "http.request.remote.host":
|
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)
|
host, _, err := net.SplitHostPort(req.RemoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// req.RemoteAddr is host:port for tcp and udp sockets and /unix/socket.path
|
// req.RemoteAddr is host:port for tcp and udp sockets and /unix/socket.path
|
||||||
|
|
|
@ -16,6 +16,7 @@ package reverseproxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -27,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"
|
||||||
|
@ -67,14 +69,16 @@ 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...>]
|
||||||
|
@ -353,6 +357,26 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
h.HealthChecks.Active.Path = d.Val()
|
h.HealthChecks.Active.Path = d.Val()
|
||||||
caddy.Log().Named("config.adapter.caddyfile").Warn("the 'health_path' subdirective is deprecated, please use 'health_uri' instead!")
|
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":
|
case "health_port":
|
||||||
if !d.NextArg() {
|
if !d.NextArg() {
|
||||||
return d.ArgErr()
|
return d.ArgErr()
|
||||||
|
@ -363,6 +387,9 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
if h.HealthChecks.Active == nil {
|
if h.HealthChecks.Active == nil {
|
||||||
h.HealthChecks.Active = new(ActiveHealthChecks)
|
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())
|
portNum, err := strconv.Atoi(d.Val())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return d.Errf("bad port number '%s': %v", d.Val(), err)
|
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
|
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":
|
case "health_interval":
|
||||||
if !d.NextArg() {
|
if !d.NextArg() {
|
||||||
return d.ArgErr()
|
return d.ArgErr()
|
||||||
|
@ -651,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())
|
||||||
|
@ -1275,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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -229,11 +229,13 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
|
||||||
|
|
||||||
if changeHost {
|
if changeHost {
|
||||||
if handler.Headers == nil {
|
if handler.Headers == nil {
|
||||||
handler.Headers = &headers.Handler{
|
handler.Headers = new(headers.Handler)
|
||||||
Request: &headers.HeaderOps{
|
}
|
||||||
Set: http.Header{},
|
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}")
|
handler.Headers.Request.Set.Set("Host", "{http.reverse_proxy.upstream.hostport}")
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
@ -75,13 +76,27 @@ type ActiveHealthChecks struct {
|
||||||
// The URI (path and query) to use for health checks
|
// The URI (path and query) to use for health checks
|
||||||
URI string `json:"uri,omitempty"`
|
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
|
// 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"`
|
Port int `json:"port,omitempty"`
|
||||||
|
|
||||||
// HTTP headers to set on health check requests.
|
// HTTP headers to set on health check requests.
|
||||||
Headers http.Header `json:"headers,omitempty"`
|
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).
|
// 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"`
|
||||||
|
|
||||||
|
@ -133,6 +148,11 @@ func (a *ActiveHealthChecks) Provision(ctx caddy.Context, h *Handler) error {
|
||||||
}
|
}
|
||||||
a.Headers = cleaned
|
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")
|
h.HealthChecks.Active.logger = h.logger.Named("health_checker.active")
|
||||||
|
|
||||||
timeout := time.Duration(a.Timeout)
|
timeout := time.Duration(a.Timeout)
|
||||||
|
@ -165,9 +185,14 @@ func (a *ActiveHealthChecks) Provision(ctx caddy.Context, h *Handler) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, upstream := range h.Upstreams {
|
for _, upstream := range h.Upstreams {
|
||||||
// if there's an alternative port for health-check provided in the config,
|
// if there's an alternative upstream for health-check provided in the config,
|
||||||
// then use it, otherwise use the port of upstream.
|
// then use it, otherwise use the upstream's dial address. if upstream is used,
|
||||||
if a.Port != 0 {
|
// then the port is ignored.
|
||||||
|
if a.Upstream != "" {
|
||||||
|
upstream.activeHealthCheckUpstream = a.Upstream
|
||||||
|
} else if a.Port != 0 {
|
||||||
|
// if there's an alternative port for health-check provided in the config,
|
||||||
|
// then use it, otherwise use the port of upstream.
|
||||||
upstream.activeHealthCheckPort = a.Port
|
upstream.activeHealthCheckPort = a.Port
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -312,7 +337,7 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
|
||||||
// so use a fake Host value instead; unix sockets are usually local
|
// so use a fake Host value instead; unix sockets are usually local
|
||||||
hostAddr = "localhost"
|
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 {
|
if err != nil {
|
||||||
h.HealthChecks.Active.logger.Error("active health check failed",
|
h.HealthChecks.Active.logger.Error("active health check failed",
|
||||||
zap.String("address", hostAddr),
|
zap.String("address", hostAddr),
|
||||||
|
@ -330,7 +355,7 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
|
||||||
// according to whether it passes the health check. An error is
|
// according to whether it passes the health check. An error is
|
||||||
// returned only if the health check fails to occur or if marking
|
// returned only if the health check fails to occur or if marking
|
||||||
// the host's health status fails.
|
// 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
|
// create the URL for the request that acts as a health check
|
||||||
u := &url.URL{
|
u := &url.URL{
|
||||||
Scheme: "http",
|
Scheme: "http",
|
||||||
|
@ -342,7 +367,12 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre
|
||||||
if err != nil {
|
if err != nil {
|
||||||
host = hostAddr
|
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)
|
port := strconv.Itoa(h.HealthChecks.Active.Port)
|
||||||
u.Host = net.JoinHostPort(host, 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
|
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
|
||||||
|
@ -377,15 +417,15 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre
|
||||||
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, http.MethodGet, 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)
|
||||||
for key, vals := range h.HealthChecks.Active.Headers {
|
for key, vals := range h.HealthChecks.Active.Headers {
|
||||||
key = repl.ReplaceAll(key, "")
|
key = repl.ReplaceAll(key, "")
|
||||||
if key == "Host" {
|
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.Host.activeHealthPasses() >= h.HealthChecks.Active.Passes {
|
||||||
if upstream.setHealthy(true) {
|
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})
|
h.events.Emit(h.ctx, "healthy", map[string]any{"host": hostAddr})
|
||||||
upstream.Host.resetHealth()
|
upstream.Host.resetHealth()
|
||||||
}
|
}
|
||||||
|
@ -492,7 +533,6 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre
|
||||||
}
|
}
|
||||||
|
|
||||||
// passed health check parameters, so mark as healthy
|
// passed health check parameters, so mark as healthy
|
||||||
h.HealthChecks.Active.logger.Info("host is up", zap.String("host", hostAddr))
|
|
||||||
markHealthy()
|
markHealthy()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -57,10 +57,11 @@ type Upstream struct {
|
||||||
// HeaderAffinity string
|
// HeaderAffinity string
|
||||||
// IPAffinity string
|
// IPAffinity string
|
||||||
|
|
||||||
activeHealthCheckPort int
|
activeHealthCheckPort int
|
||||||
healthCheckPolicy *PassiveHealthChecks
|
activeHealthCheckUpstream string
|
||||||
cb CircuitBreaker
|
healthCheckPolicy *PassiveHealthChecks
|
||||||
unhealthy int32 // accessed atomically; status from active health checker
|
cb CircuitBreaker
|
||||||
|
unhealthy int32 // accessed atomically; status from active health checker
|
||||||
}
|
}
|
||||||
|
|
||||||
// (pointer receiver necessary to avoid a race condition, since
|
// (pointer receiver necessary to avoid a race condition, since
|
||||||
|
|
|
@ -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:"-"`
|
||||||
|
|
||||||
|
@ -188,6 +192,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 {
|
||||||
|
@ -376,6 +405,13 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
|
||||||
// site owners control the backends), so it must be exclusive
|
// site owners control the backends), so it must be exclusive
|
||||||
if len(h.Versions) == 1 && h.Versions[0] == "3" {
|
if len(h.Versions) == 1 && h.Versions[0] == "3" {
|
||||||
h.h3Transport = new(http3.RoundTripper)
|
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") {
|
} 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")
|
return nil, fmt.Errorf("if HTTP/3 is enabled to the upstream, no other HTTP versions are supported")
|
||||||
}
|
}
|
||||||
|
@ -452,6 +488,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -605,6 +605,18 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http.
|
||||||
req.Header.Set("User-Agent", "")
|
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)
|
reqUpType := upgradeType(req.Header)
|
||||||
removeConnectionHeaders(req.Header)
|
removeConnectionHeaders(req.Header)
|
||||||
|
|
||||||
|
@ -967,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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/cespare/xxhash/v2"
|
"github.com/cespare/xxhash/v2"
|
||||||
|
|
||||||
|
@ -613,6 +614,8 @@ type CookieHashSelection struct {
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
// Secret to hash (Hmac256) chosen upstream in cookie
|
// Secret to hash (Hmac256) chosen upstream in cookie
|
||||||
Secret string `json:"secret,omitempty"`
|
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`.
|
// 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"`
|
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.Secure = true
|
||||||
cookie.SameSite = http.SameSiteNoneMode
|
cookie.SameSite = http.SameSiteNoneMode
|
||||||
}
|
}
|
||||||
|
if s.MaxAge > 0 {
|
||||||
|
cookie.MaxAge = int(time.Duration(s.MaxAge).Seconds())
|
||||||
|
}
|
||||||
http.SetCookie(w, cookie)
|
http.SetCookie(w, cookie)
|
||||||
return upstream
|
return upstream
|
||||||
}
|
}
|
||||||
|
@ -699,6 +705,7 @@ func (s CookieHashSelection) Select(pool UpstreamPool, req *http.Request, w http
|
||||||
//
|
//
|
||||||
// lb_policy cookie [<name> [<secret>]] {
|
// lb_policy cookie [<name> [<secret>]] {
|
||||||
// fallback <policy>
|
// fallback <policy>
|
||||||
|
// max_age <duration>
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// By default name is `lb`
|
// By default name is `lb`
|
||||||
|
@ -728,6 +735,24 @@ func (s *CookieHashSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.FallbackRaw = mod
|
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:
|
default:
|
||||||
return d.Errf("unrecognized option '%s'", d.Val())
|
return d.Errf("unrecognized option '%s'", d.Val())
|
||||||
}
|
}
|
||||||
|
|
|
@ -231,6 +231,19 @@ type IPVersions struct {
|
||||||
IPv6 *bool `json:"ipv6,omitempty"`
|
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.
|
// AUpstreams provides upstreams from A/AAAA lookups.
|
||||||
// Results are cached and refreshed at the configured
|
// Results are cached and refreshed at the configured
|
||||||
// refresh interval.
|
// refresh interval.
|
||||||
|
@ -313,9 +326,6 @@ func (au *AUpstreams) Provision(ctx caddy.Context) error {
|
||||||
func (au AUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
|
func (au AUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
|
||||||
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
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.
|
// 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
|
// This should be fairly inexpensive and comes and the upside of
|
||||||
// allowing the same dynamic upstream (name + port combination)
|
// 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
|
// It also forced a cache-miss if a previously cached dynamic
|
||||||
// upstream changes its ip version, e.g. after a config reload,
|
// upstream changes its ip version, e.g. after a config reload,
|
||||||
// while keeping the cache-invalidation as simple as it currently is.
|
// while keeping the cache-invalidation as simple as it currently is.
|
||||||
var ipVersion string
|
ipVersion := resolveIpVersion(au.Versions)
|
||||||
switch {
|
|
||||||
case resolveIpv4 && !resolveIpv6:
|
|
||||||
ipVersion = "ip4"
|
|
||||||
case !resolveIpv4 && resolveIpv6:
|
|
||||||
ipVersion = "ip6"
|
|
||||||
default:
|
|
||||||
ipVersion = "ip"
|
|
||||||
}
|
|
||||||
|
|
||||||
auStr := repl.ReplaceAll(au.String()+ipVersion, "")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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