mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-26 00:32:31 -04:00
Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 18b346f6f9 | |||
| 52441e3037 | |||
| b825a10927 | |||
| 52f43d2f4c | |||
| 5e24e84288 | |||
| b16aba5c27 | |||
| 362f33daae | |||
| 3d7d60f7cf | |||
| dc12bd9743 | |||
| 56c6b3f673 | |||
| cbbd1df904 | |||
| 7d919af01b | |||
| 4a09cf0dc0 | |||
| b24ae63ea6 | |||
| 4173e2c77a | |||
| 18f34290d2 | |||
| 22eecdb90c | |||
| 4de2c1c65e | |||
| 878d491834 | |||
| 96f638eaad | |||
| 7e52db8280 | |||
| 3b3d678714 | |||
| ee358550e4 | |||
| 3f55efcfde | |||
| f71d779009 | |||
| d949caf459 | |||
| ac0ad4da84 | |||
| 4c10a05431 | |||
| fe2a02bf7a | |||
| 9fc55a9792 | |||
| 4e8245df0b | |||
| ac1f20b9e4 | |||
| 174c19a953 | |||
| c8559c4485 | |||
| 24b0ecc310 | |||
| 7c82e265da | |||
| 0900844c81 | |||
| 7984e6f6fd | |||
| d70608b656 | |||
| 1f60328e17 | |||
| 0e204b730a | |||
| fae195ac7e | |||
| 130f6d1f83 | |||
| 289934f3d1 | |||
| 3a3182fba3 | |||
| e8b8d4a8cd | |||
| a8586b05aa | |||
| 05dbe1c171 | |||
| 33d8d2c6b5 | |||
| 9c419f1e1a | |||
| b245ecd325 | |||
| 2a6859a5e4 | |||
| df99502977 | |||
| e0aaefab80 | |||
| fa5a579b60 | |||
| 88b4fbf244 | |||
| 5653c36bc2 | |||
| 4feac4d83c | |||
| 82c356f254 | |||
| 1405683c2b | |||
| 89c407aa34 | |||
| 58ab3a01a0 | |||
| a306c5f769 | |||
| 1e0dea59ef | |||
| 2cac3c5491 | |||
| f2ab7099db | |||
| 50cea4e263 | |||
| 1b73e3862d | |||
| c46ec3b500 | |||
| ed8bb13c5d | |||
| b7e472d548 | |||
| 7103ea096f | |||
| 888c6d7e93 | |||
| b377208ede | |||
| 4776f62caa | |||
| 38a7b6b3d0 | |||
| 84d5e1c5d6 | |||
| 288216e1fb | |||
| 10053f7570 | |||
| 0a6d3333b2 | |||
| 568fd2b286 |
@@ -54,7 +54,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
@@ -73,6 +73,7 @@ jobs:
|
||||
|
||||
- name: Print Go version and environment
|
||||
id: vars
|
||||
shell: bash
|
||||
run: |
|
||||
printf "Using go at: $(which go)\n"
|
||||
printf "Go version: $(go version)\n"
|
||||
@@ -135,7 +136,7 @@ jobs:
|
||||
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Run Tests
|
||||
run: |
|
||||
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
|
||||
@@ -161,9 +162,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: goreleaser/goreleaser-action@v4
|
||||
- uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
version: latest
|
||||
args: check
|
||||
|
||||
@@ -16,6 +16,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
goos:
|
||||
- 'aix'
|
||||
- 'android'
|
||||
- 'linux'
|
||||
- 'solaris'
|
||||
@@ -40,7 +41,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
@@ -62,11 +63,12 @@ jobs:
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goos == 'aix' && 'ppc64' || 'amd64' }}
|
||||
shell: bash
|
||||
continue-on-error: true
|
||||
working-directory: ./cmd/caddy
|
||||
run: |
|
||||
GOOS=$GOOS go build -trimpath -o caddy-"$GOOS"-amd64 2> /dev/null
|
||||
GOOS=$GOOS GOARCH=$GOARCH go build -trimpath -o caddy-"$GOOS"-$GOARCH 2> /dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "::warning ::$GOOS Build Failed"
|
||||
exit 0
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
- windows-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '~1.21.0'
|
||||
@@ -50,3 +50,12 @@ jobs:
|
||||
|
||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||
# only-new-issues: true
|
||||
|
||||
govulncheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: govulncheck
|
||||
uses: golang/govulncheck-action@v1
|
||||
with:
|
||||
go-version-input: '~1.21.0'
|
||||
check-latest: true
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
check-latest: true
|
||||
|
||||
# Force fetch upstream tags -- because 65 minutes
|
||||
# tl;dr: actions/checkout@v3 runs this line:
|
||||
# tl;dr: actions/checkout@v4 runs this line:
|
||||
# git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/
|
||||
# which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran:
|
||||
# git fetch --prune --unshallow
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
run: syft version
|
||||
# GoReleaser will take care of publishing those artifacts into the release
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
version: latest
|
||||
args: release --clean --timeout 60m
|
||||
|
||||
@@ -12,6 +12,7 @@ Caddyfile.*
|
||||
cmd/caddy/caddy
|
||||
cmd/caddy/caddy.exe
|
||||
cmd/caddy/tmp/*.exe
|
||||
cmd/caddy/.env
|
||||
|
||||
# mac specific
|
||||
.DS_Store
|
||||
|
||||
+1
-1
@@ -113,7 +113,7 @@ archives:
|
||||
{{- with .Mips }}_{{ . }}{{ end }}
|
||||
{{- if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}
|
||||
|
||||
# packge the 'caddy-build' directory into a tarball,
|
||||
# package the 'caddy-build' directory into a tarball,
|
||||
# allowing users to build the exact same set of files as ours.
|
||||
- id: source
|
||||
meta: true
|
||||
|
||||
@@ -55,6 +55,7 @@ func init() {
|
||||
if env, exists := os.LookupEnv("CADDY_ADMIN"); exists {
|
||||
DefaultAdminListen = env
|
||||
}
|
||||
RegisterNamespace("caddy.config_loaders", []interface{}{(*ConfigLoader)(nil)})
|
||||
}
|
||||
|
||||
// AdminConfig configures Caddy's API endpoint, which is used
|
||||
@@ -1196,15 +1197,27 @@ traverseLoop:
|
||||
}
|
||||
case http.MethodPut:
|
||||
if _, ok := v[part]; ok {
|
||||
return fmt.Errorf("[%s] key already exists: %s", path, part)
|
||||
return APIError{
|
||||
HTTPStatus: http.StatusConflict,
|
||||
Err: fmt.Errorf("[%s] key already exists: %s", path, part),
|
||||
}
|
||||
}
|
||||
v[part] = val
|
||||
case http.MethodPatch:
|
||||
if _, ok := v[part]; !ok {
|
||||
return fmt.Errorf("[%s] key does not exist: %s", path, part)
|
||||
return APIError{
|
||||
HTTPStatus: http.StatusNotFound,
|
||||
Err: fmt.Errorf("[%s] key does not exist: %s", path, part),
|
||||
}
|
||||
}
|
||||
v[part] = val
|
||||
case http.MethodDelete:
|
||||
if _, ok := v[part]; !ok {
|
||||
return APIError{
|
||||
HTTPStatus: http.StatusNotFound,
|
||||
Err: fmt.Errorf("[%s] key does not exist: %s", path, part),
|
||||
}
|
||||
}
|
||||
delete(v, part)
|
||||
default:
|
||||
return fmt.Errorf("unrecognized method %s", method)
|
||||
|
||||
@@ -75,6 +75,12 @@ func TestUnsyncedConfigAccess(t *testing.T) {
|
||||
path: "/bar/qq",
|
||||
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
|
||||
},
|
||||
{
|
||||
method: "DELETE",
|
||||
path: "/bar/qq",
|
||||
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/list",
|
||||
|
||||
@@ -41,6 +41,15 @@ import (
|
||||
"github.com/caddyserver/caddy/v2/notify"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterNamespace("", []interface{}{
|
||||
(*App)(nil),
|
||||
})
|
||||
RegisterNamespace("caddy.storage", []interface{}{
|
||||
(*StorageConverter)(nil),
|
||||
})
|
||||
}
|
||||
|
||||
// Config is the top (or beginning) of the Caddy configuration structure.
|
||||
// Caddy config is expressed natively as a JSON document. If you prefer
|
||||
// not to work with JSON directly, there are [many config adapters](/docs/config-adapters)
|
||||
@@ -72,11 +81,15 @@ type Config struct {
|
||||
// module is `caddy.storage.file_system` (the local file system),
|
||||
// and the default path
|
||||
// [depends on the OS and environment](/docs/conventions#data-directory).
|
||||
// A storage `module` should implement the following interfaces:
|
||||
// - [StorageConverter](https://pkg.go.dev/github.com/caddyserver/caddy/v2#StorageConverter)
|
||||
StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`
|
||||
|
||||
// AppsRaw are the apps that Caddy will load and run. The
|
||||
// app module name is the key, and the app's config is the
|
||||
// associated value.
|
||||
// An `app` should implement the following interfaces:
|
||||
// - [caddy.App](https://pkg.go.dev/github.com/caddyserver/caddy/v2?tab=doc#App)
|
||||
AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="`
|
||||
|
||||
apps map[string]App
|
||||
@@ -825,13 +838,18 @@ func ParseDuration(s string) (time.Duration, error) {
|
||||
// regardless of storage configuration, since each instance is intended to
|
||||
// have its own unique ID.
|
||||
func InstanceID() (uuid.UUID, error) {
|
||||
uuidFilePath := filepath.Join(AppDataDir(), "instance.uuid")
|
||||
appDataDir := AppDataDir()
|
||||
uuidFilePath := filepath.Join(appDataDir, "instance.uuid")
|
||||
uuidFileBytes, err := os.ReadFile(uuidFilePath)
|
||||
if os.IsNotExist(err) {
|
||||
uuid, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return uuid, err
|
||||
}
|
||||
err = os.MkdirAll(appDataDir, 0o600)
|
||||
if err != nil {
|
||||
return uuid, err
|
||||
}
|
||||
err = os.WriteFile(uuidFilePath, []byte(uuid.String()), 0o600)
|
||||
return uuid, err
|
||||
} else if err != nil {
|
||||
|
||||
@@ -391,22 +391,22 @@ func (d *Dispenser) Reset() {
|
||||
// an argument.
|
||||
func (d *Dispenser) ArgErr() error {
|
||||
if d.Val() == "{" {
|
||||
return d.Err("Unexpected token '{', expecting argument")
|
||||
return d.Err("unexpected token '{', expecting argument")
|
||||
}
|
||||
return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val())
|
||||
return d.Errf("wrong argument count or unexpected line ending after '%s'", d.Val())
|
||||
}
|
||||
|
||||
// SyntaxErr creates a generic syntax error which explains what was
|
||||
// found and what was expected.
|
||||
func (d *Dispenser) SyntaxErr(expected string) error {
|
||||
msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s', import chain: ['%s']", d.File(), d.Line(), d.Val(), expected, strings.Join(d.Token().imports, "','"))
|
||||
msg := fmt.Sprintf("syntax error: unexpected token '%s', expecting '%s', at %s:%d import chain: ['%s']", d.Val(), expected, d.File(), d.Line(), strings.Join(d.Token().imports, "','"))
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
// EOFErr returns an error indicating that the dispenser reached
|
||||
// the end of the input when searching for the next token.
|
||||
func (d *Dispenser) EOFErr() error {
|
||||
return d.Errf("Unexpected EOF")
|
||||
return d.Errf("unexpected EOF")
|
||||
}
|
||||
|
||||
// Err generates a custom parse-time error with a message of msg.
|
||||
@@ -421,7 +421,10 @@ func (d *Dispenser) Errf(format string, args ...any) error {
|
||||
|
||||
// WrapErr takes an existing error and adds the Caddyfile file and line number.
|
||||
func (d *Dispenser) WrapErr(err error) error {
|
||||
return fmt.Errorf("%s:%d - Error during parsing: %w, import chain: ['%s']", d.File(), d.Line(), err, strings.Join(d.Token().imports, "','"))
|
||||
if len(d.Token().imports) > 0 {
|
||||
return fmt.Errorf("%w, at %s:%d import chain ['%s']", err, d.File(), d.Line(), strings.Join(d.Token().imports, "','"))
|
||||
}
|
||||
return fmt.Errorf("%w, at %s:%d", err, d.File(), d.Line())
|
||||
}
|
||||
|
||||
// Delete deletes the current token and returns the updated slice
|
||||
|
||||
@@ -52,6 +52,13 @@ func parseVariadic(token Token, argCount int) (bool, int, int) {
|
||||
return false, 0, 0
|
||||
}
|
||||
|
||||
// A valid token may contain several placeholders, and
|
||||
// they may be separated by ":". It's not variadic.
|
||||
// https://github.com/caddyserver/caddy/issues/5716
|
||||
if strings.Contains(start, "}") || strings.Contains(end, "{") {
|
||||
return false, 0, 0
|
||||
}
|
||||
|
||||
var (
|
||||
startIndex = 0
|
||||
endIndex = argCount
|
||||
|
||||
@@ -137,18 +137,32 @@ func (l *lexer) next() (bool, error) {
|
||||
}
|
||||
|
||||
// detect whether we have the start of a heredoc
|
||||
if !inHeredoc && !heredocEscaped && len(val) > 1 && string(val[:2]) == "<<" {
|
||||
if ch == '<' {
|
||||
return false, fmt.Errorf("too many '<' for heredoc on line #%d; only use two, for example <<END", l.line)
|
||||
if !(quoted || btQuoted) && !(inHeredoc || heredocEscaped) &&
|
||||
len(val) > 1 && string(val[:2]) == "<<" {
|
||||
// a space means it's just a regular token and not a heredoc
|
||||
if ch == ' ' {
|
||||
return makeToken(0), nil
|
||||
}
|
||||
|
||||
// skip CR, we only care about LF
|
||||
if ch == '\r' {
|
||||
continue
|
||||
}
|
||||
|
||||
// after hitting a newline, we know that the heredoc marker
|
||||
// is the characters after the two << and the newline.
|
||||
// we reset the val because the heredoc is syntax we don't
|
||||
// want to keep.
|
||||
if ch == '\n' {
|
||||
if len(val) == 2 {
|
||||
return false, fmt.Errorf("missing opening heredoc marker on line #%d; must contain only alpha-numeric characters, dashes and underscores; got empty string", l.line)
|
||||
}
|
||||
|
||||
// check if there's too many <
|
||||
if string(val[:3]) == "<<<" {
|
||||
return false, fmt.Errorf("too many '<' for heredoc on line #%d; only use two, for example <<END", l.line)
|
||||
}
|
||||
|
||||
heredocMarker = string(val[2:])
|
||||
if !heredocMarkerRegexp.Match([]byte(heredocMarker)) {
|
||||
return false, fmt.Errorf("heredoc marker on line #%d must contain only alpha-numeric characters, dashes and underscores; got '%s'", l.line, heredocMarker)
|
||||
|
||||
@@ -322,15 +322,59 @@ EOF same-line-arg
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte(`heredoc <EOF
|
||||
input: []byte(`escaped-heredoc \<< >>`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `escaped-heredoc`},
|
||||
{Line: 1, Text: `<<`},
|
||||
{Line: 1, Text: `>>`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte(`not-a-heredoc <EOF
|
||||
content
|
||||
EOF same-line-arg
|
||||
`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `heredoc`},
|
||||
{Line: 1, Text: `not-a-heredoc`},
|
||||
{Line: 1, Text: `<EOF`},
|
||||
{Line: 2, Text: `content`},
|
||||
{Line: 3, Text: `EOF`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte(`not-a-heredoc <<<EOF content`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `not-a-heredoc`},
|
||||
{Line: 1, Text: `<<<EOF`},
|
||||
{Line: 1, Text: `content`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte(`not-a-heredoc "<<" ">>"`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `not-a-heredoc`},
|
||||
{Line: 1, Text: `<<`},
|
||||
{Line: 1, Text: `>>`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte(`not-a-heredoc << >>`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `not-a-heredoc`},
|
||||
{Line: 1, Text: `<<`},
|
||||
{Line: 1, Text: `>>`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte(`not-a-heredoc <<HERE SAME LINE
|
||||
content
|
||||
HERE same-line-arg
|
||||
`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `not-a-heredoc`},
|
||||
{Line: 1, Text: `<<HERE`},
|
||||
{Line: 1, Text: `SAME`},
|
||||
{Line: 1, Text: `LINE`},
|
||||
{Line: 2, Text: `content`},
|
||||
{Line: 3, Text: `HERE`},
|
||||
{Line: 3, Text: `same-line-arg`},
|
||||
},
|
||||
},
|
||||
@@ -366,12 +410,9 @@ EOF same-line-arg
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte(`heredoc <<HERE SAME LINE
|
||||
content
|
||||
HERE same-line-arg
|
||||
`),
|
||||
input: []byte("not-a-heredoc <<\n"),
|
||||
expectErr: true,
|
||||
errorMessage: "heredoc marker on line #1 must contain only alpha-numeric characters, dashes and underscores; got 'HERE SAME LINE'",
|
||||
errorMessage: "missing opening heredoc marker on line #1; must contain only alpha-numeric characters, dashes and underscores; got empty string",
|
||||
},
|
||||
{
|
||||
input: []byte(`heredoc <<<EOF
|
||||
|
||||
@@ -91,6 +91,10 @@ func TestParseVariadic(t *testing.T) {
|
||||
input: "{args[0:10]}",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
input: "{args[0]}:{args[1]}:{args[2]}",
|
||||
result: false,
|
||||
},
|
||||
} {
|
||||
token := Token{
|
||||
File: "test",
|
||||
|
||||
@@ -197,6 +197,17 @@ func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key str
|
||||
}
|
||||
addr = addr.Normalize()
|
||||
|
||||
switch addr.Scheme {
|
||||
case "wss":
|
||||
return nil, fmt.Errorf("the scheme wss:// is only supported in browsers; use https:// instead")
|
||||
case "ws":
|
||||
return nil, fmt.Errorf("the scheme ws:// is only supported in browsers; use http:// instead")
|
||||
case "https", "http", "":
|
||||
// Do nothing or handle the valid schemes
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported URL scheme %s://", addr.Scheme)
|
||||
}
|
||||
|
||||
// figure out the HTTP and HTTPS ports; either
|
||||
// use defaults, or override with user config
|
||||
httpPort, httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPPort), strconv.Itoa(caddyhttp.DefaultHTTPSPort)
|
||||
|
||||
@@ -181,17 +181,17 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||
case "protocols":
|
||||
args := h.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return nil, h.SyntaxErr("one or two protocols")
|
||||
return nil, h.Errf("protocols requires one or two arguments")
|
||||
}
|
||||
if len(args) > 0 {
|
||||
if _, ok := caddytls.SupportedProtocols[args[0]]; !ok {
|
||||
return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[0])
|
||||
return nil, h.Errf("wrong protocol name or protocol not supported: '%s'", args[0])
|
||||
}
|
||||
cp.ProtocolMin = args[0]
|
||||
}
|
||||
if len(args) > 1 {
|
||||
if _, ok := caddytls.SupportedProtocols[args[1]]; !ok {
|
||||
return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[1])
|
||||
return nil, h.Errf("wrong protocol name or protocol not supported: '%s'", args[1])
|
||||
}
|
||||
cp.ProtocolMax = args[1]
|
||||
}
|
||||
@@ -199,7 +199,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||
case "ciphers":
|
||||
for h.NextArg() {
|
||||
if !caddytls.CipherSuiteNameSupported(h.Val()) {
|
||||
return nil, h.Errf("Wrong cipher suite name or cipher suite not supported: '%s'", h.Val())
|
||||
return nil, h.Errf("wrong cipher suite name or cipher suite not supported: '%s'", h.Val())
|
||||
}
|
||||
cp.CipherSuites = append(cp.CipherSuites, h.Val())
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ package httpcaddyfile
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -82,46 +82,18 @@ func (st ServerType) Setup(
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
originalServerBlocks, err = st.extractNamedRoutes(originalServerBlocks, options, &warnings)
|
||||
// this will replace both static and user-defined placeholder shorthands
|
||||
// with actual identifiers used by Caddy
|
||||
replacer := NewShorthandReplacer()
|
||||
|
||||
originalServerBlocks, err = st.extractNamedRoutes(originalServerBlocks, options, &warnings, replacer)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
// replace shorthand placeholders (which are convenient
|
||||
// when writing a Caddyfile) with their actual placeholder
|
||||
// identifiers or variable names
|
||||
replacer := strings.NewReplacer(placeholderShorthands()...)
|
||||
|
||||
// these are placeholders that allow a user-defined final
|
||||
// parameters, but we still want to provide a shorthand
|
||||
// for those, so we use a regexp to replace
|
||||
regexpReplacements := []struct {
|
||||
search *regexp.Regexp
|
||||
replace string
|
||||
}{
|
||||
{regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"},
|
||||
{regexp.MustCompile(`{cookie\.([\w-]*)}`), "{http.request.cookie.$1}"},
|
||||
{regexp.MustCompile(`{labels\.([\w-]*)}`), "{http.request.host.labels.$1}"},
|
||||
{regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"},
|
||||
{regexp.MustCompile(`{file\.([\w-]*)}`), "{http.request.uri.path.file.$1}"},
|
||||
{regexp.MustCompile(`{query\.([\w-]*)}`), "{http.request.uri.query.$1}"},
|
||||
{regexp.MustCompile(`{re\.([\w-]*)\.([\w-]*)}`), "{http.regexp.$1.$2}"},
|
||||
{regexp.MustCompile(`{vars\.([\w-]*)}`), "{http.vars.$1}"},
|
||||
{regexp.MustCompile(`{rp\.([\w-\.]*)}`), "{http.reverse_proxy.$1}"},
|
||||
{regexp.MustCompile(`{err\.([\w-\.]*)}`), "{http.error.$1}"},
|
||||
{regexp.MustCompile(`{file_match\.([\w-]*)}`), "{http.matchers.file.$1}"},
|
||||
}
|
||||
|
||||
for _, sb := range originalServerBlocks {
|
||||
for _, segment := range sb.block.Segments {
|
||||
for i := 0; i < len(segment); i++ {
|
||||
// simple string replacements
|
||||
segment[i].Text = replacer.Replace(segment[i].Text)
|
||||
// complex regexp replacements
|
||||
for _, r := range regexpReplacements {
|
||||
segment[i].Text = r.search.ReplaceAllString(segment[i].Text, r.replace)
|
||||
}
|
||||
}
|
||||
for i := range sb.block.Segments {
|
||||
replacer.ApplyToSegment(&sb.block.Segments[i])
|
||||
}
|
||||
|
||||
if len(sb.block.Keys) == 0 {
|
||||
@@ -452,6 +424,7 @@ func (ServerType) extractNamedRoutes(
|
||||
serverBlocks []serverBlock,
|
||||
options map[string]any,
|
||||
warnings *[]caddyconfig.Warning,
|
||||
replacer ShorthandReplacer,
|
||||
) ([]serverBlock, error) {
|
||||
namedRoutes := map[string]*caddyhttp.Route{}
|
||||
|
||||
@@ -477,11 +450,14 @@ func (ServerType) extractNamedRoutes(
|
||||
continue
|
||||
}
|
||||
|
||||
// zip up all the segments since ParseSegmentAsSubroute
|
||||
// was designed to take a directive+
|
||||
wholeSegment := caddyfile.Segment{}
|
||||
for _, segment := range sb.block.Segments {
|
||||
wholeSegment = append(wholeSegment, segment...)
|
||||
for i := range sb.block.Segments {
|
||||
// replace user-defined placeholder shorthands in extracted named routes
|
||||
replacer.ApplyToSegment(&sb.block.Segments[i])
|
||||
|
||||
// zip up all the segments since ParseSegmentAsSubroute
|
||||
// was designed to take a directive+
|
||||
wholeSegment = append(wholeSegment, sb.block.Segments[i]...)
|
||||
}
|
||||
|
||||
h := Helper{
|
||||
@@ -710,6 +686,7 @@ func (st *ServerType) serversFromPairings(
|
||||
}
|
||||
|
||||
if len(hosts) > 0 {
|
||||
slices.Sort(hosts) // for deterministic JSON output
|
||||
cp.MatchersRaw = caddy.ModuleMap{
|
||||
"sni": caddyconfig.JSON(hosts, warnings), // make sure to match all hosts, not just auto-HTTPS-qualified ones
|
||||
}
|
||||
@@ -741,10 +718,20 @@ func (st *ServerType) serversFromPairings(
|
||||
}
|
||||
}
|
||||
|
||||
// If TLS is specified as directive, it will also result in 1 or more connection policy being created
|
||||
// Thus, catch-all address with non-standard port, e.g. :8443, can have TLS enabled without
|
||||
// specifying prefix "https://"
|
||||
// Second part of the condition is to allow creating TLS conn policy even though `auto_https` has been disabled
|
||||
// ensuring compatibility with behavior described in below link
|
||||
// https://caddy.community/t/making-sense-of-auto-https-and-why-disabling-it-still-serves-https-instead-of-http/9761
|
||||
createdTLSConnPolicies, ok := sblock.pile["tls.connection_policy"]
|
||||
hasTLSEnabled := (ok && len(createdTLSConnPolicies) > 0) ||
|
||||
(addr.Host != "" && srv.AutoHTTPS != nil && !sliceContains(srv.AutoHTTPS.Skip, addr.Host))
|
||||
|
||||
// we'll need to remember if the address qualifies for auto-HTTPS, so we
|
||||
// can add a TLS conn policy if necessary
|
||||
if addr.Scheme == "https" ||
|
||||
(addr.Scheme != "http" && addr.Host != "" && addr.Port != httpPort) {
|
||||
(addr.Scheme != "http" && addr.Port != httpPort && hasTLSEnabled) {
|
||||
addressQualifiesForTLS = true
|
||||
}
|
||||
// predict whether auto-HTTPS will add the conn policy for us; if so, we
|
||||
@@ -811,7 +798,12 @@ func (st *ServerType) serversFromPairings(
|
||||
if srv.Logs.LoggerNames == nil {
|
||||
srv.Logs.LoggerNames = make(map[string]string)
|
||||
}
|
||||
srv.Logs.LoggerNames[h] = ncl.name
|
||||
// strip the port from the host, if any
|
||||
host, _, err := net.SplitHostPort(h)
|
||||
if err != nil {
|
||||
host = h
|
||||
}
|
||||
srv.Logs.LoggerNames[host] = ncl.name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1449,37 +1441,6 @@ func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (caddy.Modul
|
||||
return msEncoded, nil
|
||||
}
|
||||
|
||||
// placeholderShorthands returns a slice of old-new string pairs,
|
||||
// where the left of the pair is a placeholder shorthand that may
|
||||
// be used in the Caddyfile, and the right is the replacement.
|
||||
func placeholderShorthands() []string {
|
||||
return []string{
|
||||
"{dir}", "{http.request.uri.path.dir}",
|
||||
"{file}", "{http.request.uri.path.file}",
|
||||
"{host}", "{http.request.host}",
|
||||
"{hostport}", "{http.request.hostport}",
|
||||
"{port}", "{http.request.port}",
|
||||
"{method}", "{http.request.method}",
|
||||
"{path}", "{http.request.uri.path}",
|
||||
"{query}", "{http.request.uri.query}",
|
||||
"{remote}", "{http.request.remote}",
|
||||
"{remote_host}", "{http.request.remote.host}",
|
||||
"{remote_port}", "{http.request.remote.port}",
|
||||
"{scheme}", "{http.request.scheme}",
|
||||
"{uri}", "{http.request.uri}",
|
||||
"{tls_cipher}", "{http.request.tls.cipher_suite}",
|
||||
"{tls_version}", "{http.request.tls.version}",
|
||||
"{tls_client_fingerprint}", "{http.request.tls.client.fingerprint}",
|
||||
"{tls_client_issuer}", "{http.request.tls.client.issuer}",
|
||||
"{tls_client_serial}", "{http.request.tls.client.serial}",
|
||||
"{tls_client_subject}", "{http.request.tls.client.subject}",
|
||||
"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
|
||||
"{tls_client_certificate_der_base64}", "{http.request.tls.client.certificate_der_base64}",
|
||||
"{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
|
||||
"{client_ip}", "{http.vars.client_ip}",
|
||||
}
|
||||
}
|
||||
|
||||
// WasReplacedPlaceholderShorthand checks if a token string was
|
||||
// likely a replaced shorthand of the known Caddyfile placeholder
|
||||
// replacement outputs. Useful to prevent some user-defined map
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package httpcaddyfile
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
)
|
||||
|
||||
type ComplexShorthandReplacer struct {
|
||||
search *regexp.Regexp
|
||||
replace string
|
||||
}
|
||||
|
||||
type ShorthandReplacer struct {
|
||||
complex []ComplexShorthandReplacer
|
||||
simple *strings.Replacer
|
||||
}
|
||||
|
||||
func NewShorthandReplacer() ShorthandReplacer {
|
||||
// replace shorthand placeholders (which are convenient
|
||||
// when writing a Caddyfile) with their actual placeholder
|
||||
// identifiers or variable names
|
||||
replacer := strings.NewReplacer(placeholderShorthands()...)
|
||||
|
||||
// these are placeholders that allow a user-defined final
|
||||
// parameters, but we still want to provide a shorthand
|
||||
// for those, so we use a regexp to replace
|
||||
regexpReplacements := []ComplexShorthandReplacer{
|
||||
{regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"},
|
||||
{regexp.MustCompile(`{cookie\.([\w-]*)}`), "{http.request.cookie.$1}"},
|
||||
{regexp.MustCompile(`{labels\.([\w-]*)}`), "{http.request.host.labels.$1}"},
|
||||
{regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"},
|
||||
{regexp.MustCompile(`{file\.([\w-]*)}`), "{http.request.uri.path.file.$1}"},
|
||||
{regexp.MustCompile(`{query\.([\w-]*)}`), "{http.request.uri.query.$1}"},
|
||||
{regexp.MustCompile(`{re\.([\w-]*)\.([\w-]*)}`), "{http.regexp.$1.$2}"},
|
||||
{regexp.MustCompile(`{vars\.([\w-]*)}`), "{http.vars.$1}"},
|
||||
{regexp.MustCompile(`{rp\.([\w-\.]*)}`), "{http.reverse_proxy.$1}"},
|
||||
{regexp.MustCompile(`{err\.([\w-\.]*)}`), "{http.error.$1}"},
|
||||
{regexp.MustCompile(`{file_match\.([\w-]*)}`), "{http.matchers.file.$1}"},
|
||||
}
|
||||
|
||||
return ShorthandReplacer{
|
||||
complex: regexpReplacements,
|
||||
simple: replacer,
|
||||
}
|
||||
}
|
||||
|
||||
// placeholderShorthands returns a slice of old-new string pairs,
|
||||
// where the left of the pair is a placeholder shorthand that may
|
||||
// be used in the Caddyfile, and the right is the replacement.
|
||||
func placeholderShorthands() []string {
|
||||
return []string{
|
||||
"{dir}", "{http.request.uri.path.dir}",
|
||||
"{file}", "{http.request.uri.path.file}",
|
||||
"{host}", "{http.request.host}",
|
||||
"{hostport}", "{http.request.hostport}",
|
||||
"{port}", "{http.request.port}",
|
||||
"{method}", "{http.request.method}",
|
||||
"{path}", "{http.request.uri.path}",
|
||||
"{query}", "{http.request.uri.query}",
|
||||
"{remote}", "{http.request.remote}",
|
||||
"{remote_host}", "{http.request.remote.host}",
|
||||
"{remote_port}", "{http.request.remote.port}",
|
||||
"{scheme}", "{http.request.scheme}",
|
||||
"{uri}", "{http.request.uri}",
|
||||
"{uuid}", "{http.request.uuid}",
|
||||
"{tls_cipher}", "{http.request.tls.cipher_suite}",
|
||||
"{tls_version}", "{http.request.tls.version}",
|
||||
"{tls_client_fingerprint}", "{http.request.tls.client.fingerprint}",
|
||||
"{tls_client_issuer}", "{http.request.tls.client.issuer}",
|
||||
"{tls_client_serial}", "{http.request.tls.client.serial}",
|
||||
"{tls_client_subject}", "{http.request.tls.client.subject}",
|
||||
"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
|
||||
"{tls_client_certificate_der_base64}", "{http.request.tls.client.certificate_der_base64}",
|
||||
"{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
|
||||
"{client_ip}", "{http.vars.client_ip}",
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyToSegment replaces shorthand placeholder to its full placeholder, understandable by Caddy.
|
||||
func (s ShorthandReplacer) ApplyToSegment(segment *caddyfile.Segment) {
|
||||
if segment != nil {
|
||||
for i := 0; i < len(*segment); i++ {
|
||||
// simple string replacements
|
||||
(*segment)[i].Text = s.simple.Replace((*segment)[i].Text)
|
||||
// complex regexp replacements
|
||||
for _, r := range s.complex {
|
||||
(*segment)[i].Text = r.search.ReplaceAllString((*segment)[i].Text, r.replace)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -582,6 +582,7 @@ outer:
|
||||
// eaten up by the one with subjects; and if both have subjects, we
|
||||
// need to combine their lists
|
||||
if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) &&
|
||||
reflect.DeepEqual(aps[i].ManagersRaw, aps[j].ManagersRaw) &&
|
||||
bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
|
||||
aps[i].MustStaple == aps[j].MustStaple &&
|
||||
aps[i].KeyType == aps[j].KeyType &&
|
||||
|
||||
@@ -467,7 +467,7 @@ func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int)
|
||||
}
|
||||
|
||||
if expectedStatusCode != resp.StatusCode {
|
||||
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.RequestURI, expectedStatusCode, resp.StatusCode)
|
||||
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.URL.RequestURI(), expectedStatusCode, resp.StatusCode)
|
||||
}
|
||||
|
||||
return resp
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddytest"
|
||||
)
|
||||
|
||||
func TestACMEServerDirectory(t *testing.T) {
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(`
|
||||
{
|
||||
skip_install_trust
|
||||
local_certs
|
||||
admin localhost:2999
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
pki {
|
||||
ca local {
|
||||
name "Caddy Local Authority"
|
||||
}
|
||||
}
|
||||
}
|
||||
acme.localhost:9443 {
|
||||
acme_server
|
||||
}
|
||||
`, "caddyfile")
|
||||
tester.AssertGetResponse(
|
||||
"https://acme.localhost:9443/acme/local/directory",
|
||||
200,
|
||||
`{"newNonce":"https://acme.localhost:9443/acme/local/new-nonce","newAccount":"https://acme.localhost:9443/acme/local/new-account","newOrder":"https://acme.localhost:9443/acme/local/new-order","revokeCert":"https://acme.localhost:9443/acme/local/revoke-cert","keyChange":"https://acme.localhost:9443/acme/local/key-change"}
|
||||
`)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
:8443 {
|
||||
tls internal {
|
||||
on_demand
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":8443"
|
||||
],
|
||||
"tls_connection_policies": [
|
||||
{}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"issuers": [
|
||||
{
|
||||
"module": "internal"
|
||||
}
|
||||
],
|
||||
"on_demand": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ encode gzip zstd {
|
||||
header Content-Type application/xhtml+xml*
|
||||
header Content-Type application/atom+xml*
|
||||
header Content-Type application/rss+xml*
|
||||
header Content-Type application/wasm*
|
||||
header Content-Type image/svg+xml*
|
||||
}
|
||||
}
|
||||
@@ -47,6 +48,7 @@ encode {
|
||||
"application/xhtml+xml*",
|
||||
"application/atom+xml*",
|
||||
"application/rss+xml*",
|
||||
"application/wasm*",
|
||||
"image/svg+xml*"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -99,7 +99,7 @@ http://localhost:2020 {
|
||||
},
|
||||
"logs": {
|
||||
"logger_names": {
|
||||
"localhost:2020": ""
|
||||
"localhost": ""
|
||||
},
|
||||
"skip_unmapped_hosts": true
|
||||
}
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
output file /baz.txt
|
||||
}
|
||||
}
|
||||
|
||||
example.com:8443 {
|
||||
log {
|
||||
output file /port.txt
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"logging": {
|
||||
@@ -15,7 +21,8 @@
|
||||
"default": {
|
||||
"exclude": [
|
||||
"http.log.access.log0",
|
||||
"http.log.access.log1"
|
||||
"http.log.access.log1",
|
||||
"http.log.access.log2"
|
||||
]
|
||||
},
|
||||
"log0": {
|
||||
@@ -35,6 +42,15 @@
|
||||
"include": [
|
||||
"http.log.access.log1"
|
||||
]
|
||||
},
|
||||
"log2": {
|
||||
"writer": {
|
||||
"filename": "/port.txt",
|
||||
"output": "file"
|
||||
},
|
||||
"include": [
|
||||
"http.log.access.log2"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -64,6 +80,28 @@
|
||||
"foo.example.com": "log0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"srv1": {
|
||||
"listen": [
|
||||
":8443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
],
|
||||
"logs": {
|
||||
"logger_names": {
|
||||
"example.com": "log2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ http://localhost:8881 {
|
||||
},
|
||||
"logs": {
|
||||
"logger_names": {
|
||||
"localhost:8881": "foo"
|
||||
"localhost": "foo"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ http://localhost:8881 {
|
||||
},
|
||||
"logs": {
|
||||
"logger_names": {
|
||||
"localhost:8881": "foo"
|
||||
"localhost": "foo"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
https://example.com {
|
||||
reverse_proxy https://localhost:54321 {
|
||||
request_buffers unlimited
|
||||
response_buffers unlimited
|
||||
}
|
||||
}
|
||||
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"request_buffers": -1,
|
||||
"response_buffers": -1,
|
||||
"transport": {
|
||||
"protocol": "http",
|
||||
"tls": {}
|
||||
},
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "localhost:54321"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,9 @@ reverse_proxy 127.0.0.1:65535 {
|
||||
X-Header-Key 95ca39e3cbe7
|
||||
X-Header-Keys VbG4NZwWnipo 335Q9/MhqcNU3s2TO
|
||||
X-Empty-Value
|
||||
Same-Key 1
|
||||
Same-Key 2
|
||||
X-System-Hostname {system.hostname}
|
||||
}
|
||||
health_uri /health
|
||||
}
|
||||
@@ -29,6 +32,10 @@ reverse_proxy 127.0.0.1:65535 {
|
||||
"Host": [
|
||||
"example.com"
|
||||
],
|
||||
"Same-Key": [
|
||||
"1",
|
||||
"2"
|
||||
],
|
||||
"X-Empty-Value": [
|
||||
""
|
||||
],
|
||||
@@ -38,6 +45,9 @@ reverse_proxy 127.0.0.1:65535 {
|
||||
"X-Header-Keys": [
|
||||
"VbG4NZwWnipo",
|
||||
"335Q9/MhqcNU3s2TO"
|
||||
],
|
||||
"X-System-Hostname": [
|
||||
"{system.hostname}"
|
||||
]
|
||||
},
|
||||
"uri": "/health"
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# example from https://caddy.community/t/21415
|
||||
a.com {
|
||||
tls {
|
||||
get_certificate http http://foo.com/get
|
||||
}
|
||||
}
|
||||
|
||||
b.com {
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"a.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
},
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"b.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"a.com"
|
||||
],
|
||||
"get_certificate": [
|
||||
{
|
||||
"url": "http://foo.com/get",
|
||||
"via": "http"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"subjects": [
|
||||
"b.com"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,3 +135,352 @@ func TestReplIndex(t *testing.T) {
|
||||
// act and assert
|
||||
tester.AssertGetResponse("http://localhost:9080/", 200, "")
|
||||
}
|
||||
|
||||
func TestInvalidPrefix(t *testing.T) {
|
||||
type testCase struct {
|
||||
config, expectedError string
|
||||
}
|
||||
|
||||
failureCases := []testCase{
|
||||
{
|
||||
config: `wss://localhost`,
|
||||
expectedError: `the scheme wss:// is only supported in browsers; use https:// instead`,
|
||||
},
|
||||
{
|
||||
config: `ws://localhost`,
|
||||
expectedError: `the scheme ws:// is only supported in browsers; use http:// instead`,
|
||||
},
|
||||
{
|
||||
config: `someInvalidPrefix://localhost`,
|
||||
expectedError: "unsupported URL scheme someinvalidprefix://",
|
||||
},
|
||||
{
|
||||
config: `h2c://localhost`,
|
||||
expectedError: `unsupported URL scheme h2c://`,
|
||||
},
|
||||
{
|
||||
config: `localhost, wss://localhost`,
|
||||
expectedError: `the scheme wss:// is only supported in browsers; use https:// instead`,
|
||||
},
|
||||
{
|
||||
config: `localhost {
|
||||
reverse_proxy ws://localhost"
|
||||
}`,
|
||||
expectedError: `the scheme ws:// is only supported in browsers; use http:// instead`,
|
||||
},
|
||||
{
|
||||
config: `localhost {
|
||||
reverse_proxy someInvalidPrefix://localhost"
|
||||
}`,
|
||||
expectedError: `unsupported URL scheme someinvalidprefix://`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, failureCase := range failureCases {
|
||||
caddytest.AssertLoadError(t, failureCase.config, "caddyfile", failureCase.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidPrefix(t *testing.T) {
|
||||
type testCase struct {
|
||||
rawConfig, expectedResponse string
|
||||
}
|
||||
|
||||
successCases := []testCase{
|
||||
{
|
||||
"localhost",
|
||||
`{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
"https://localhost",
|
||||
`{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
"http://localhost",
|
||||
`{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":80"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
`localhost {
|
||||
reverse_proxy http://localhost:3000
|
||||
}`,
|
||||
`{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "localhost:3000"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
`localhost {
|
||||
reverse_proxy https://localhost:3000
|
||||
}`,
|
||||
`{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"transport": {
|
||||
"protocol": "http",
|
||||
"tls": {}
|
||||
},
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "localhost:3000"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
`localhost {
|
||||
reverse_proxy h2c://localhost:3000
|
||||
}`,
|
||||
`{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"transport": {
|
||||
"protocol": "http",
|
||||
"versions": [
|
||||
"h2c",
|
||||
"2"
|
||||
]
|
||||
},
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "localhost:3000"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
`localhost {
|
||||
reverse_proxy localhost:3000
|
||||
}`,
|
||||
`{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "localhost:3000"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, successCase := range successCases {
|
||||
caddytest.AssertAdapt(t, successCase.rawConfig, "caddyfile", successCase.expectedResponse)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddytest"
|
||||
)
|
||||
|
||||
func setupListenerWrapperTest(t *testing.T, handlerFunc http.HandlerFunc) *caddytest.Tester {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to listen: %s", err)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", handlerFunc)
|
||||
srv := &http.Server{
|
||||
Handler: mux,
|
||||
}
|
||||
go srv.Serve(l)
|
||||
t.Cleanup(func() {
|
||||
_ = srv.Close()
|
||||
_ = l.Close()
|
||||
})
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(fmt.Sprintf(`
|
||||
{
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
servers :9443 {
|
||||
listener_wrappers {
|
||||
http_redirect
|
||||
tls
|
||||
}
|
||||
}
|
||||
}
|
||||
localhost {
|
||||
reverse_proxy %s
|
||||
}
|
||||
`, l.Addr().String()), "caddyfile")
|
||||
return tester
|
||||
}
|
||||
|
||||
func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) {
|
||||
const uploadSize = (1024 * 1024) + 1 // 1 MB + 1 byte
|
||||
// 1 more than an MB
|
||||
body := make([]byte, uploadSize)
|
||||
rand.New(rand.NewSource(0)).Read(body)
|
||||
|
||||
tester := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) {
|
||||
buf := new(bytes.Buffer)
|
||||
_, err := buf.ReadFrom(request.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read body: %s", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(buf.Bytes(), body) {
|
||||
t.Fatalf("body not the same")
|
||||
}
|
||||
|
||||
writer.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
resp, err := tester.Client.Post("https://localhost:9443", "application/octet-stream", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to post: %s", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
t.Fatalf("unexpected status: %d != %d", resp.StatusCode, http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLargeHttpRequest(t *testing.T) {
|
||||
tester := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) {
|
||||
t.Fatal("not supposed to handle a request")
|
||||
})
|
||||
|
||||
// We never read the body in any way, set an extra long header instead.
|
||||
req, _ := http.NewRequest("POST", "http://localhost:9443", nil)
|
||||
req.Header.Set("Long-Header", strings.Repeat("X", 1024*1024))
|
||||
_, err := tester.Client.Do(req)
|
||||
if err == nil {
|
||||
t.Fatal("not supposed to succeed")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
_ "github.com/caddyserver/caddy/v2/modules/standard"
|
||||
)
|
||||
|
||||
// Validates that Caddy's registered internal modules implement the necessary interfaces of their
|
||||
// respective namespaces
|
||||
func TestTypes(t *testing.T) {
|
||||
var i int
|
||||
for _, v := range caddy.Modules() {
|
||||
mod, _ := caddy.GetModule(v)
|
||||
if ok, err := caddy.ConformsToNamespace(mod.New(), mod.ID.Namespace()); !ok {
|
||||
t.Errorf("%s", err)
|
||||
}
|
||||
i++
|
||||
}
|
||||
t.Logf("Passed through %d modules", i)
|
||||
}
|
||||
+29
-1
@@ -1,7 +1,11 @@
|
||||
package caddycmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
@@ -95,15 +99,22 @@ https://caddyserver.com/docs/running
|
||||
// kind of annoying to have all the help text printed out if
|
||||
// caddy has an error provisioning its modules, for instance...
|
||||
SilenceUsage: true,
|
||||
Version: onlyVersionText(),
|
||||
}
|
||||
|
||||
const fullDocsFooter = `Full documentation is available at:
|
||||
https://caddyserver.com/docs/command-line`
|
||||
|
||||
func init() {
|
||||
rootCmd.SetVersionTemplate("{{.Version}}\n")
|
||||
rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n")
|
||||
}
|
||||
|
||||
func onlyVersionText() string {
|
||||
_, f := caddy.Version()
|
||||
return f
|
||||
}
|
||||
|
||||
func caddyCmdToCobra(caddyCmd Command) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: caddyCmd.Name,
|
||||
@@ -123,7 +134,24 @@ func caddyCmdToCobra(caddyCmd Command) *cobra.Command {
|
||||
// in a cobra command's RunE field.
|
||||
func WrapCommandFuncForCobra(f CommandFunc) func(cmd *cobra.Command, _ []string) error {
|
||||
return func(cmd *cobra.Command, _ []string) error {
|
||||
_, err := f(Flags{cmd.Flags()})
|
||||
status, err := f(Flags{cmd.Flags()})
|
||||
if status > 1 {
|
||||
cmd.SilenceErrors = true
|
||||
return &exitError{ExitCode: status, Err: err}
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// exitError carries the exit code from CommandFunc to Main()
|
||||
type exitError struct {
|
||||
ExitCode int
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *exitError) Error() string {
|
||||
if e.Err == nil {
|
||||
return fmt.Sprintf("exiting with status %d", e.ExitCode)
|
||||
}
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
+120
-87
@@ -42,11 +42,18 @@ import (
|
||||
)
|
||||
|
||||
func cmdStart(fl Flags) (int, error) {
|
||||
startCmdConfigFlag := fl.String("config")
|
||||
startCmdConfigAdapterFlag := fl.String("adapter")
|
||||
startCmdPidfileFlag := fl.String("pidfile")
|
||||
startCmdWatchFlag := fl.Bool("watch")
|
||||
startCmdEnvfileFlag := fl.String("envfile")
|
||||
configFlag := fl.String("config")
|
||||
configAdapterFlag := fl.String("adapter")
|
||||
pidfileFlag := fl.String("pidfile")
|
||||
watchFlag := fl.Bool("watch")
|
||||
|
||||
var err error
|
||||
var envfileFlag []string
|
||||
envfileFlag, err = fl.GetStringSlice("envfile")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("reading envfile flag: %v", err)
|
||||
}
|
||||
|
||||
// open a listener to which the child process will connect when
|
||||
// it is ready to confirm that it has successfully started
|
||||
@@ -67,22 +74,23 @@ func cmdStart(fl Flags) (int, error) {
|
||||
// sure by giving it some random bytes and having it echo
|
||||
// them back to us)
|
||||
cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String())
|
||||
if startCmdConfigFlag != "" {
|
||||
cmd.Args = append(cmd.Args, "--config", startCmdConfigFlag)
|
||||
if configFlag != "" {
|
||||
cmd.Args = append(cmd.Args, "--config", configFlag)
|
||||
}
|
||||
if startCmdEnvfileFlag != "" {
|
||||
cmd.Args = append(cmd.Args, "--envfile", startCmdEnvfileFlag)
|
||||
|
||||
for _, envfile := range envfileFlag {
|
||||
cmd.Args = append(cmd.Args, "--envfile", envfile)
|
||||
}
|
||||
if startCmdConfigAdapterFlag != "" {
|
||||
cmd.Args = append(cmd.Args, "--adapter", startCmdConfigAdapterFlag)
|
||||
if configAdapterFlag != "" {
|
||||
cmd.Args = append(cmd.Args, "--adapter", configAdapterFlag)
|
||||
}
|
||||
if startCmdWatchFlag {
|
||||
if watchFlag {
|
||||
cmd.Args = append(cmd.Args, "--watch")
|
||||
}
|
||||
if startCmdPidfileFlag != "" {
|
||||
cmd.Args = append(cmd.Args, "--pidfile", startCmdPidfileFlag)
|
||||
if pidfileFlag != "" {
|
||||
cmd.Args = append(cmd.Args, "--pidfile", pidfileFlag)
|
||||
}
|
||||
stdinpipe, err := cmd.StdinPipe()
|
||||
stdinPipe, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("creating stdin pipe: %v", err)
|
||||
@@ -94,7 +102,8 @@ func cmdStart(fl Flags) (int, error) {
|
||||
expect := make([]byte, 32)
|
||||
_, err = rand.Read(expect)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("generating random confirmation bytes: %v", err)
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("generating random confirmation bytes: %v", err)
|
||||
}
|
||||
|
||||
// begin writing the confirmation bytes to the child's
|
||||
@@ -102,14 +111,15 @@ func cmdStart(fl Flags) (int, error) {
|
||||
// started yet, and writing synchronously would result
|
||||
// in a deadlock
|
||||
go func() {
|
||||
_, _ = stdinpipe.Write(expect)
|
||||
stdinpipe.Close()
|
||||
_, _ = stdinPipe.Write(expect)
|
||||
stdinPipe.Close()
|
||||
}()
|
||||
|
||||
// start the process
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("starting caddy process: %v", err)
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("starting caddy process: %v", err)
|
||||
}
|
||||
|
||||
// there are two ways we know we're done: either
|
||||
@@ -157,41 +167,37 @@ func cmdStart(fl Flags) (int, error) {
|
||||
func cmdRun(fl Flags) (int, error) {
|
||||
caddy.TrapSignals()
|
||||
|
||||
runCmdConfigFlag := fl.String("config")
|
||||
runCmdConfigAdapterFlag := fl.String("adapter")
|
||||
runCmdResumeFlag := fl.Bool("resume")
|
||||
runCmdLoadEnvfileFlag := fl.String("envfile")
|
||||
runCmdPrintEnvFlag := fl.Bool("environ")
|
||||
runCmdWatchFlag := fl.Bool("watch")
|
||||
runCmdPidfileFlag := fl.String("pidfile")
|
||||
runCmdPingbackFlag := fl.String("pingback")
|
||||
configFlag := fl.String("config")
|
||||
configAdapterFlag := fl.String("adapter")
|
||||
resumeFlag := fl.Bool("resume")
|
||||
printEnvFlag := fl.Bool("environ")
|
||||
watchFlag := fl.Bool("watch")
|
||||
pidfileFlag := fl.String("pidfile")
|
||||
pingbackFlag := fl.String("pingback")
|
||||
|
||||
// load all additional envs as soon as possible
|
||||
if runCmdLoadEnvfileFlag != "" {
|
||||
if err := loadEnvFromFile(runCmdLoadEnvfileFlag); err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("loading additional environment variables: %v", err)
|
||||
}
|
||||
err := handleEnvFileFlag(fl)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
// if we are supposed to print the environment, do that first
|
||||
if runCmdPrintEnvFlag {
|
||||
if printEnvFlag {
|
||||
printEnvironment()
|
||||
}
|
||||
|
||||
// load the config, depending on flags
|
||||
var config []byte
|
||||
var err error
|
||||
if runCmdResumeFlag {
|
||||
if resumeFlag {
|
||||
config, err = os.ReadFile(caddy.ConfigAutosavePath)
|
||||
if os.IsNotExist(err) {
|
||||
// not a bad error; just can't resume if autosave file doesn't exist
|
||||
caddy.Log().Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath))
|
||||
runCmdResumeFlag = false
|
||||
resumeFlag = false
|
||||
} else if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
} else {
|
||||
if runCmdConfigFlag == "" {
|
||||
if configFlag == "" {
|
||||
caddy.Log().Info("resuming from last configuration",
|
||||
zap.String("autosave_file", caddy.ConfigAutosavePath))
|
||||
} else {
|
||||
@@ -204,19 +210,19 @@ func cmdRun(fl Flags) (int, error) {
|
||||
}
|
||||
// we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
|
||||
var configFile string
|
||||
if !runCmdResumeFlag {
|
||||
config, configFile, err = LoadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
|
||||
if !resumeFlag {
|
||||
config, configFile, err = LoadConfig(configFlag, configAdapterFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
}
|
||||
|
||||
// create pidfile now, in case loading config takes a while (issue #5477)
|
||||
if runCmdPidfileFlag != "" {
|
||||
err := caddy.PIDFile(runCmdPidfileFlag)
|
||||
if pidfileFlag != "" {
|
||||
err := caddy.PIDFile(pidfileFlag)
|
||||
if err != nil {
|
||||
caddy.Log().Error("unable to write PID file",
|
||||
zap.String("pidfile", runCmdPidfileFlag),
|
||||
zap.String("pidfile", pidfileFlag),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
@@ -230,13 +236,13 @@ func cmdRun(fl Flags) (int, error) {
|
||||
|
||||
// if we are to report to another process the successful start
|
||||
// of the server, do so now by echoing back contents of stdin
|
||||
if runCmdPingbackFlag != "" {
|
||||
if pingbackFlag != "" {
|
||||
confirmationBytes, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("reading confirmation bytes from stdin: %v", err)
|
||||
}
|
||||
conn, err := net.Dial("tcp", runCmdPingbackFlag)
|
||||
conn, err := net.Dial("tcp", pingbackFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("dialing confirmation address: %v", err)
|
||||
@@ -245,14 +251,14 @@ func cmdRun(fl Flags) (int, error) {
|
||||
_, err = conn.Write(confirmationBytes)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("writing confirmation bytes to %s: %v", runCmdPingbackFlag, err)
|
||||
fmt.Errorf("writing confirmation bytes to %s: %v", pingbackFlag, err)
|
||||
}
|
||||
}
|
||||
|
||||
// if enabled, reload config file automatically on changes
|
||||
// (this better only be used in dev!)
|
||||
if runCmdWatchFlag {
|
||||
go watchConfigFile(configFile, runCmdConfigAdapterFlag)
|
||||
if watchFlag {
|
||||
go watchConfigFile(configFile, configAdapterFlag)
|
||||
}
|
||||
|
||||
// warn if the environment does not provide enough information about the disk
|
||||
@@ -278,11 +284,11 @@ func cmdRun(fl Flags) (int, error) {
|
||||
}
|
||||
|
||||
func cmdStop(fl Flags) (int, error) {
|
||||
addrFlag := fl.String("address")
|
||||
addressFlag := fl.String("address")
|
||||
configFlag := fl.String("config")
|
||||
configAdapterFlag := fl.String("adapter")
|
||||
|
||||
adminAddr, err := DetermineAdminAPIAddress(addrFlag, nil, configFlag, configAdapterFlag)
|
||||
adminAddr, err := DetermineAdminAPIAddress(addressFlag, nil, configFlag, configAdapterFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
|
||||
}
|
||||
@@ -300,7 +306,7 @@ func cmdStop(fl Flags) (int, error) {
|
||||
func cmdReload(fl Flags) (int, error) {
|
||||
configFlag := fl.String("config")
|
||||
configAdapterFlag := fl.String("adapter")
|
||||
addrFlag := fl.String("address")
|
||||
addressFlag := fl.String("address")
|
||||
forceFlag := fl.Bool("force")
|
||||
|
||||
// get the config in caddy's native format
|
||||
@@ -312,7 +318,7 @@ func cmdReload(fl Flags) (int, error) {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
|
||||
}
|
||||
|
||||
adminAddr, err := DetermineAdminAPIAddress(addrFlag, config, configFlag, configAdapterFlag)
|
||||
adminAddr, err := DetermineAdminAPIAddress(addressFlag, config, configFlag, configAdapterFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
|
||||
}
|
||||
@@ -414,48 +420,60 @@ func cmdListModules(fl Flags) (int, error) {
|
||||
return caddy.ExitCodeSuccess, nil
|
||||
}
|
||||
|
||||
func cmdEnviron(_ Flags) (int, error) {
|
||||
func cmdEnviron(fl Flags) (int, error) {
|
||||
// load all additional envs as soon as possible
|
||||
err := handleEnvFileFlag(fl)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
printEnvironment()
|
||||
return caddy.ExitCodeSuccess, nil
|
||||
}
|
||||
|
||||
func cmdAdaptConfig(fl Flags) (int, error) {
|
||||
adaptCmdInputFlag := fl.String("config")
|
||||
adaptCmdAdapterFlag := fl.String("adapter")
|
||||
adaptCmdPrettyFlag := fl.Bool("pretty")
|
||||
adaptCmdValidateFlag := fl.Bool("validate")
|
||||
inputFlag := fl.String("config")
|
||||
adapterFlag := fl.String("adapter")
|
||||
prettyFlag := fl.Bool("pretty")
|
||||
validateFlag := fl.Bool("validate")
|
||||
|
||||
var err error
|
||||
adaptCmdInputFlag, err = configFileWithRespectToDefault(caddy.Log(), adaptCmdInputFlag)
|
||||
inputFlag, err = configFileWithRespectToDefault(caddy.Log(), inputFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
if adaptCmdAdapterFlag == "" {
|
||||
// load all additional envs as soon as possible
|
||||
err = handleEnvFileFlag(fl)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
if adapterFlag == "" {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("adapter name is required (use --adapt flag or leave unspecified for default)")
|
||||
}
|
||||
|
||||
cfgAdapter := caddyconfig.GetAdapter(adaptCmdAdapterFlag)
|
||||
cfgAdapter := caddyconfig.GetAdapter(adapterFlag)
|
||||
if cfgAdapter == nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("unrecognized config adapter: %s", adaptCmdAdapterFlag)
|
||||
fmt.Errorf("unrecognized config adapter: %s", adapterFlag)
|
||||
}
|
||||
|
||||
input, err := os.ReadFile(adaptCmdInputFlag)
|
||||
input, err := os.ReadFile(inputFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("reading input file: %v", err)
|
||||
}
|
||||
|
||||
opts := map[string]any{"filename": adaptCmdInputFlag}
|
||||
opts := map[string]any{"filename": inputFlag}
|
||||
|
||||
adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
if adaptCmdPrettyFlag {
|
||||
if prettyFlag {
|
||||
var prettyBuf bytes.Buffer
|
||||
err = json.Indent(&prettyBuf, adaptedConfig, "", "\t")
|
||||
if err != nil {
|
||||
@@ -473,13 +491,13 @@ func cmdAdaptConfig(fl Flags) (int, error) {
|
||||
if warn.Directive != "" {
|
||||
msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
|
||||
}
|
||||
caddy.Log().Named(adaptCmdAdapterFlag).Warn(msg,
|
||||
caddy.Log().Named(adapterFlag).Warn(msg,
|
||||
zap.String("file", warn.File),
|
||||
zap.Int("line", warn.Line))
|
||||
}
|
||||
|
||||
// validate output if requested
|
||||
if adaptCmdValidateFlag {
|
||||
if validateFlag {
|
||||
var cfg *caddy.Config
|
||||
err = caddy.StrictUnmarshalJSON(adaptedConfig, &cfg)
|
||||
if err != nil {
|
||||
@@ -495,30 +513,26 @@ func cmdAdaptConfig(fl Flags) (int, error) {
|
||||
}
|
||||
|
||||
func cmdValidateConfig(fl Flags) (int, error) {
|
||||
validateCmdConfigFlag := fl.String("config")
|
||||
validateCmdAdapterFlag := fl.String("adapter")
|
||||
runCmdLoadEnvfileFlag := fl.String("envfile")
|
||||
configFlag := fl.String("config")
|
||||
adapterFlag := fl.String("adapter")
|
||||
|
||||
// load all additional envs as soon as possible
|
||||
if runCmdLoadEnvfileFlag != "" {
|
||||
if err := loadEnvFromFile(runCmdLoadEnvfileFlag); err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("loading additional environment variables: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// use default config and ensure a config file is specified
|
||||
var err error
|
||||
validateCmdConfigFlag, err = configFileWithRespectToDefault(caddy.Log(), validateCmdConfigFlag)
|
||||
err := handleEnvFileFlag(fl)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
if validateCmdConfigFlag == "" {
|
||||
|
||||
// use default config and ensure a config file is specified
|
||||
configFlag, err = configFileWithRespectToDefault(caddy.Log(), configFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
if configFlag == "" {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
|
||||
}
|
||||
|
||||
input, _, err := LoadConfig(validateCmdConfigFlag, validateCmdAdapterFlag)
|
||||
input, _, err := LoadConfig(configFlag, adapterFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
@@ -541,13 +555,13 @@ func cmdValidateConfig(fl Flags) (int, error) {
|
||||
}
|
||||
|
||||
func cmdFmt(fl Flags) (int, error) {
|
||||
formatCmdConfigFile := fl.Arg(0)
|
||||
if formatCmdConfigFile == "" {
|
||||
formatCmdConfigFile = "Caddyfile"
|
||||
configFile := fl.Arg(0)
|
||||
if configFile == "" {
|
||||
configFile = "Caddyfile"
|
||||
}
|
||||
|
||||
// as a special case, read from stdin if the file name is "-"
|
||||
if formatCmdConfigFile == "-" {
|
||||
if configFile == "-" {
|
||||
input, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
@@ -557,7 +571,7 @@ func cmdFmt(fl Flags) (int, error) {
|
||||
return caddy.ExitCodeSuccess, nil
|
||||
}
|
||||
|
||||
input, err := os.ReadFile(formatCmdConfigFile)
|
||||
input, err := os.ReadFile(configFile)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("reading input file: %v", err)
|
||||
@@ -566,7 +580,7 @@ func cmdFmt(fl Flags) (int, error) {
|
||||
output := caddyfile.Format(input)
|
||||
|
||||
if fl.Bool("overwrite") {
|
||||
if err := os.WriteFile(formatCmdConfigFile, output, 0o600); err != nil {
|
||||
if err := os.WriteFile(configFile, output, 0o600); err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err)
|
||||
}
|
||||
return caddy.ExitCodeSuccess, nil
|
||||
@@ -590,7 +604,7 @@ func cmdFmt(fl Flags) (int, error) {
|
||||
fmt.Print(string(output))
|
||||
}
|
||||
|
||||
if warning, diff := caddyfile.FormattingDifference(formatCmdConfigFile, input); diff {
|
||||
if warning, diff := caddyfile.FormattingDifference(configFile, input); diff {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf(`%s:%d: Caddyfile input is not formatted; Tip: use '--overwrite' to update your Caddyfile in-place instead of previewing it. Consult '--help' for more options`,
|
||||
warning.File,
|
||||
warning.Line,
|
||||
@@ -600,6 +614,25 @@ func cmdFmt(fl Flags) (int, error) {
|
||||
return caddy.ExitCodeSuccess, nil
|
||||
}
|
||||
|
||||
// handleEnvFileFlag loads the environment variables from the given --envfile
|
||||
// flag if specified. This should be called as early in the command function.
|
||||
func handleEnvFileFlag(fl Flags) error {
|
||||
var err error
|
||||
var envfileFlag []string
|
||||
envfileFlag, err = fl.GetStringSlice("envfile")
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading envfile flag: %v", err)
|
||||
}
|
||||
|
||||
for _, envfile := range envfileFlag {
|
||||
if err := loadEnvFromFile(envfile); err != nil {
|
||||
return fmt.Errorf("loading additional environment variables: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AdminAPIRequest makes an API request according to the CLI flags given,
|
||||
// with the given HTTP method and request URI. If body is non-nil, it will
|
||||
// be assumed to be Content-Type application/json. The caller should close
|
||||
|
||||
+39
-28
@@ -94,8 +94,8 @@ func init() {
|
||||
Starts the Caddy process, optionally bootstrapped with an initial config file.
|
||||
This command unblocks after the server starts running or fails to run.
|
||||
|
||||
If --envfile is specified, an environment file with environment variables in
|
||||
the KEY=VALUE format will be loaded into the Caddy process.
|
||||
If --envfile is specified, an environment file with environment variables
|
||||
in the KEY=VALUE format will be loaded into the Caddy process.
|
||||
|
||||
On Windows, the spawned child process will remain attached to the terminal, so
|
||||
closing the window will forcefully stop Caddy; to avoid forgetting this, try
|
||||
@@ -104,7 +104,7 @@ using 'caddy run' instead to keep it in the foreground.
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringP("config", "c", "", "Configuration file")
|
||||
cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply")
|
||||
cmd.Flags().StringP("envfile", "", "", "Environment file to load")
|
||||
cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
|
||||
cmd.Flags().BoolP("watch", "w", false, "Reload changed config file automatically")
|
||||
cmd.Flags().StringP("pidfile", "", "", "Path of file to which to write process ID")
|
||||
cmd.RunE = WrapCommandFuncForCobra(cmdStart)
|
||||
@@ -133,8 +133,8 @@ As a special case, if the current working directory has a file called
|
||||
that file will be loaded and used to configure Caddy, even without any command
|
||||
line flags.
|
||||
|
||||
If --envfile is specified, an environment file with environment variables in
|
||||
the KEY=VALUE format will be loaded into the Caddy process.
|
||||
If --envfile is specified, an environment file with environment variables
|
||||
in the KEY=VALUE format will be loaded into the Caddy process.
|
||||
|
||||
If --environ is specified, the environment as seen by the Caddy process will
|
||||
be printed before starting. This is the same as the environ command but does
|
||||
@@ -150,7 +150,7 @@ option in a local development environment.
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringP("config", "c", "", "Configuration file")
|
||||
cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply")
|
||||
cmd.Flags().StringP("envfile", "", "", "Environment file to load")
|
||||
cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
|
||||
cmd.Flags().BoolP("environ", "e", false, "Print environment")
|
||||
cmd.Flags().BoolP("resume", "r", false, "Use saved config, if any (and prefer over --config file)")
|
||||
cmd.Flags().BoolP("watch", "w", false, "Watch config file for changes and reload it automatically")
|
||||
@@ -240,6 +240,7 @@ documentation: https://go.dev/doc/modules/version-numbers
|
||||
|
||||
RegisterCommand(Command{
|
||||
Name: "environ",
|
||||
Usage: "[--envfile <path>]",
|
||||
Short: "Prints the environment",
|
||||
Long: `
|
||||
Prints the environment as seen by this Caddy process.
|
||||
@@ -249,6 +250,9 @@ configuration uses environment variables (e.g. "{env.VARIABLE}") then
|
||||
this command can be useful for verifying that the variables will have
|
||||
the values you expect in your config.
|
||||
|
||||
If --envfile is specified, an environment file with environment variables
|
||||
in the KEY=VALUE format will be loaded into the Caddy process.
|
||||
|
||||
Note that environments may be different depending on how you run Caddy.
|
||||
Environments for Caddy instances started by service managers such as
|
||||
systemd are often different than the environment inherited from your
|
||||
@@ -259,12 +263,15 @@ by adding the "--environ" flag.
|
||||
|
||||
Environments may contain sensitive data.
|
||||
`,
|
||||
Func: cmdEnviron,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
|
||||
cmd.RunE = WrapCommandFuncForCobra(cmdEnviron)
|
||||
},
|
||||
})
|
||||
|
||||
RegisterCommand(Command{
|
||||
Name: "adapt",
|
||||
Usage: "--config <path> [--adapter <name>] [--pretty] [--validate]",
|
||||
Usage: "--config <path> [--adapter <name>] [--pretty] [--validate] [--envfile <path>]",
|
||||
Short: "Adapts a configuration to Caddy's native JSON",
|
||||
Long: `
|
||||
Adapts a configuration to Caddy's native JSON format and writes the
|
||||
@@ -276,12 +283,16 @@ for human readability.
|
||||
If --validate is used, the adapted config will be checked for validity.
|
||||
If the config is invalid, an error will be printed to stderr and a non-
|
||||
zero exit status will be returned.
|
||||
|
||||
If --envfile is specified, an environment file with environment variables
|
||||
in the KEY=VALUE format will be loaded into the Caddy process.
|
||||
`,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringP("config", "c", "", "Configuration file to adapt (required)")
|
||||
cmd.Flags().StringP("adapter", "a", "caddyfile", "Name of config adapter")
|
||||
cmd.Flags().BoolP("pretty", "p", false, "Format the output for human readability")
|
||||
cmd.Flags().BoolP("validate", "", false, "Validate the output")
|
||||
cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
|
||||
cmd.RunE = WrapCommandFuncForCobra(cmdAdaptConfig)
|
||||
},
|
||||
})
|
||||
@@ -295,13 +306,13 @@ Loads and provisions the provided config, but does not start running it.
|
||||
This reveals any errors with the configuration through the loading and
|
||||
provisioning stages.
|
||||
|
||||
If --envfile is specified, an environment file with environment variables in
|
||||
the KEY=VALUE format will be loaded into the Caddy process.
|
||||
If --envfile is specified, an environment file with environment variables
|
||||
in the KEY=VALUE format will be loaded into the Caddy process.
|
||||
`,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringP("config", "c", "", "Input configuration file")
|
||||
cmd.Flags().StringP("adapter", "a", "", "Name of config adapter")
|
||||
cmd.Flags().StringP("envfile", "", "", "Environment file to load")
|
||||
cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
|
||||
cmd.RunE = WrapCommandFuncForCobra(cmdValidateConfig)
|
||||
},
|
||||
})
|
||||
@@ -402,7 +413,7 @@ latest versions. EXPERIMENTAL: May be changed or removed.
|
||||
Short: "Adds Caddy packages (EXPERIMENTAL)",
|
||||
Long: `
|
||||
Downloads an updated Caddy binary with the specified packages (module/plugin)
|
||||
added. Retains existing packages. Returns an error if the any of packages are
|
||||
added. Retains existing packages. Returns an error if the any of packages are
|
||||
already included. EXPERIMENTAL: May be changed or removed.
|
||||
`,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
@@ -417,8 +428,8 @@ already included. EXPERIMENTAL: May be changed or removed.
|
||||
Usage: "<packages...>",
|
||||
Short: "Removes Caddy packages (EXPERIMENTAL)",
|
||||
Long: `
|
||||
Downloads an updated Caddy binaries without the specified packages (module/plugin).
|
||||
Returns an error if any of the packages are not included.
|
||||
Downloads an updated Caddy binaries without the specified packages (module/plugin).
|
||||
Returns an error if any of the packages are not included.
|
||||
EXPERIMENTAL: May be changed or removed.
|
||||
`,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
@@ -464,40 +475,40 @@ argument of --directory. If the directory does not exist, it will be created.
|
||||
Use: "completion [bash|zsh|fish|powershell]",
|
||||
Short: "Generate completion script",
|
||||
Long: fmt.Sprintf(`To load completions:
|
||||
|
||||
|
||||
Bash:
|
||||
|
||||
|
||||
$ source <(%[1]s completion bash)
|
||||
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
# Linux:
|
||||
$ %[1]s completion bash > /etc/bash_completion.d/%[1]s
|
||||
# macOS:
|
||||
$ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
|
||||
|
||||
|
||||
Zsh:
|
||||
|
||||
|
||||
# If shell completion is not already enabled in your environment,
|
||||
# you will need to enable it. You can execute the following once:
|
||||
|
||||
|
||||
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
|
||||
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
$ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
|
||||
|
||||
|
||||
# You will need to start a new shell for this setup to take effect.
|
||||
|
||||
|
||||
fish:
|
||||
|
||||
|
||||
$ %[1]s completion fish | source
|
||||
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
|
||||
|
||||
|
||||
PowerShell:
|
||||
|
||||
|
||||
PS> %[1]s completion powershell | Out-String | Invoke-Expression
|
||||
|
||||
|
||||
# To load completions for every new session, run:
|
||||
PS> %[1]s completion powershell > %[1]s.ps1
|
||||
# and source this file from your PowerShell profile.
|
||||
|
||||
+11
-2
@@ -17,6 +17,7 @@ package caddycmd
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -63,6 +64,10 @@ func Main() {
|
||||
}
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
var exitError *exitError
|
||||
if errors.As(err, &exitError) {
|
||||
os.Exit(exitError.ExitCode)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -300,8 +305,12 @@ func loadEnvFromFile(envFile string) error {
|
||||
}
|
||||
|
||||
for k, v := range envMap {
|
||||
if err := os.Setenv(k, v); err != nil {
|
||||
return fmt.Errorf("setting environment variables: %v", err)
|
||||
// do not overwrite existing environment variables
|
||||
_, exists := os.LookupEnv(k)
|
||||
if !exists {
|
||||
if err := os.Setenv(k, v); err != nil {
|
||||
return fmt.Errorf("setting environment variables: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
@@ -103,6 +104,15 @@ func upgradeBuild(pluginPkgs map[string]struct{}, fl Flags) (int, error) {
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("retrieving current executable permission bits: %v", err)
|
||||
}
|
||||
if thisExecStat.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||
symSource := thisExecPath
|
||||
// we are a symlink; resolve it
|
||||
thisExecPath, err = filepath.EvalSymlinks(thisExecPath)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("resolving current executable symlink: %v", err)
|
||||
}
|
||||
l.Info("this executable is a symlink", zap.String("source", symSource), zap.String("target", thisExecPath))
|
||||
}
|
||||
l.Info("this executable will be replaced", zap.String("path", thisExecPath))
|
||||
|
||||
// build the request URL to download this custom build
|
||||
|
||||
+4
-3
@@ -200,9 +200,10 @@ func cmdExportStorage(fl Flags) (int, error) {
|
||||
}
|
||||
|
||||
hdr := &tar.Header{
|
||||
Name: k,
|
||||
Mode: 0o600,
|
||||
Size: int64(len(v)),
|
||||
Name: k,
|
||||
Mode: 0o600,
|
||||
Size: int64(len(v)),
|
||||
ModTime: info.Modified,
|
||||
}
|
||||
|
||||
if err = tw.WriteHeader(hdr); err != nil {
|
||||
|
||||
@@ -5,69 +5,68 @@ go 1.20
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.3.2
|
||||
github.com/Masterminds/sprig/v3 v3.2.3
|
||||
github.com/alecthomas/chroma/v2 v2.7.0
|
||||
github.com/alecthomas/chroma/v2 v2.9.1
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
|
||||
github.com/caddyserver/certmagic v0.19.2
|
||||
github.com/caddyserver/certmagic v0.20.0
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/go-chi/chi v4.1.2+incompatible
|
||||
github.com/go-chi/chi/v5 v5.0.10
|
||||
github.com/google/cel-go v0.15.1
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/klauspost/compress v1.16.7
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/klauspost/compress v1.17.0
|
||||
github.com/klauspost/cpuid/v2 v2.2.5
|
||||
github.com/mastercactapus/proxyprotocol v0.0.4
|
||||
github.com/mholt/acmez v1.2.0
|
||||
github.com/prometheus/client_golang v1.14.0
|
||||
github.com/quic-go/quic-go v0.37.5
|
||||
github.com/smallstep/certificates v0.24.3-rc.5
|
||||
github.com/prometheus/client_golang v1.15.1
|
||||
github.com/quic-go/quic-go v0.40.0
|
||||
github.com/smallstep/certificates v0.25.0
|
||||
github.com/smallstep/nosql v0.6.0
|
||||
github.com/smallstep/truststore v0.12.1
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/tailscale/tscert v0.0.0-20230509043813-4e9cb4f2b4ad
|
||||
github.com/yuin/goldmark v1.5.5
|
||||
github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046
|
||||
github.com/yuin/goldmark v1.5.6
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.42.0
|
||||
go.opentelemetry.io/otel v1.16.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0
|
||||
go.opentelemetry.io/otel/sdk v1.16.0
|
||||
go.opentelemetry.io/otel v1.21.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0
|
||||
go.opentelemetry.io/otel/sdk v1.21.0
|
||||
go.uber.org/zap v1.25.0
|
||||
golang.org/x/crypto v0.12.0
|
||||
golang.org/x/crypto v0.14.0
|
||||
golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0
|
||||
golang.org/x/net v0.14.0
|
||||
golang.org/x/sync v0.3.0
|
||||
golang.org/x/term v0.11.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130
|
||||
golang.org/x/net v0.17.0
|
||||
golang.org/x/sync v0.4.0
|
||||
golang.org/x/term v0.13.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/iam v1.1.2 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
||||
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
|
||||
github.com/golang/glog v1.1.0 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/google/certificate-transparency-go v1.1.4 // indirect
|
||||
github.com/google/go-tpm v0.3.3 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.5.0 // indirect
|
||||
github.com/golang/glog v1.1.2 // indirect
|
||||
github.com/google/certificate-transparency-go v1.1.6 // indirect
|
||||
github.com/google/go-tpm v0.9.0 // indirect
|
||||
github.com/google/go-tspi v0.3.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.3.1 // indirect
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20230509120429-e17291421738 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20230627102604-cf579e53cbd2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/zeebo/blake3 v0.2.3 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.17.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.17.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
|
||||
go.uber.org/mock v0.3.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -85,13 +84,13 @@ require (
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/go-kit/kit v0.10.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.5.1 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-logr/logr v1.3.0 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
@@ -110,17 +109,18 @@ require (
|
||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/micromdm/scep/v2 v2.1.0 // indirect
|
||||
github.com/miekg/dns v1.1.55 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/pires/go-proxyproto v0.7.0
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/prometheus/client_model v0.4.0 // indirect
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.9.0 // indirect
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
@@ -132,20 +132,19 @@ require (
|
||||
github.com/urfave/cli v1.22.14 // indirect
|
||||
go.etcd.io/bbolt v1.3.7 // indirect
|
||||
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.16.0
|
||||
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.21.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.21.0
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
||||
go.step.sm/cli-utils v0.8.0 // indirect
|
||||
go.step.sm/crypto v0.33.0
|
||||
go.step.sm/linkedca v0.20.0 // indirect
|
||||
go.step.sm/crypto v0.35.1
|
||||
go.step.sm/linkedca v0.20.1 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/mod v0.11.0 // indirect
|
||||
golang.org/x/sys v0.11.0
|
||||
golang.org/x/text v0.12.0 // indirect
|
||||
golang.org/x/sys v0.14.0
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/tools v0.10.0 // indirect
|
||||
google.golang.org/grpc v1.56.2 // indirect
|
||||
google.golang.org/grpc v1.59.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
|
||||
@@ -30,18 +30,34 @@ func reuseUnixSocket(network, addr string) (any, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (net.Listener, error) {
|
||||
sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
|
||||
ln, err := config.Listen(ctx, network, address)
|
||||
func listenReusable(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (any, error) {
|
||||
switch network {
|
||||
case "udp", "udp4", "udp6", "unixgram":
|
||||
sharedPc, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
|
||||
pc, err := config.ListenPacket(ctx, network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sharedPacketConn{PacketConn: pc, key: lnKey}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sharedListener{Listener: ln, key: lnKey}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return &fakeClosePacketConn{sharedPacketConn: sharedPc.(*sharedPacketConn)}, nil
|
||||
|
||||
default:
|
||||
sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
|
||||
ln, err := config.Listen(ctx, network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sharedListener{Listener: ln, key: lnKey}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: config.KeepAlive}, nil
|
||||
}
|
||||
return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: config.KeepAlive}, nil
|
||||
}
|
||||
|
||||
// fakeCloseListener is a private wrapper over a listener that
|
||||
@@ -98,7 +114,7 @@ func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
|
||||
// so that it's clear in the code that side-effects are shared with other
|
||||
// users of this listener, not just our own reference to it; we also don't
|
||||
// do anything with the error because all we could do is log it, but we
|
||||
// expliclty assign it to nothing so we don't forget it's there if needed
|
||||
// explicitly assign it to nothing so we don't forget it's there if needed
|
||||
_ = fcl.sharedListener.clearDeadline()
|
||||
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
@@ -172,3 +188,75 @@ func (sl *sharedListener) setDeadline() error {
|
||||
func (sl *sharedListener) Destruct() error {
|
||||
return sl.Listener.Close()
|
||||
}
|
||||
|
||||
// fakeClosePacketConn is like fakeCloseListener, but for PacketConns,
|
||||
// or more specifically, *net.UDPConn
|
||||
type fakeClosePacketConn struct {
|
||||
closed int32 // accessed atomically; belongs to this struct only
|
||||
*sharedPacketConn // embedded, so we also become a net.PacketConn; its key is used in Close
|
||||
}
|
||||
|
||||
func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
// if the listener is already "closed", return error
|
||||
if atomic.LoadInt32(&fcpc.closed) == 1 {
|
||||
return 0, nil, &net.OpError{
|
||||
Op: "readfrom",
|
||||
Net: fcpc.LocalAddr().Network(),
|
||||
Addr: fcpc.LocalAddr(),
|
||||
Err: errFakeClosed,
|
||||
}
|
||||
}
|
||||
|
||||
// call underlying readfrom
|
||||
n, addr, err = fcpc.sharedPacketConn.ReadFrom(p)
|
||||
if err != nil {
|
||||
// this server was stopped, so clear the deadline and let
|
||||
// any new server continue reading; but we will exit
|
||||
if atomic.LoadInt32(&fcpc.closed) == 1 {
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
if err = fcpc.SetReadDeadline(time.Time{}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Close won't close the underlying socket unless there is no more reference, then listenerPool will close it.
|
||||
func (fcpc *fakeClosePacketConn) Close() error {
|
||||
if atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) {
|
||||
_ = fcpc.SetReadDeadline(time.Now()) // unblock ReadFrom() calls to kick old servers out of their loops
|
||||
_, _ = listenerPool.Delete(fcpc.sharedPacketConn.key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fcpc *fakeClosePacketConn) Unwrap() net.PacketConn {
|
||||
return fcpc.sharedPacketConn.PacketConn
|
||||
}
|
||||
|
||||
// sharedPacketConn is like sharedListener, but for net.PacketConns.
|
||||
type sharedPacketConn struct {
|
||||
net.PacketConn
|
||||
key string
|
||||
}
|
||||
|
||||
// Destruct closes the underlying socket.
|
||||
func (spc *sharedPacketConn) Destruct() error {
|
||||
return spc.PacketConn.Close()
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying socket
|
||||
func (spc *sharedPacketConn) Unwrap() net.PacketConn {
|
||||
return spc.PacketConn
|
||||
}
|
||||
|
||||
// Interface guards (see https://github.com/caddyserver/caddy/issues/3998)
|
||||
var (
|
||||
_ (interface {
|
||||
Unwrap() net.PacketConn
|
||||
}) = (*fakeClosePacketConn)(nil)
|
||||
)
|
||||
|
||||
+72
-3
@@ -22,8 +22,10 @@ package caddy
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
@@ -87,7 +89,7 @@ func reuseUnixSocket(network, addr string) (any, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (net.Listener, error) {
|
||||
func listenReusable(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (any, error) {
|
||||
// wrap any Control function set by the user so we can also add our reusePort control without clobbering theirs
|
||||
oldControl := config.Control
|
||||
config.Control = func(network, address string, c syscall.RawConn) error {
|
||||
@@ -103,7 +105,14 @@ func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string,
|
||||
// we still put it in the listenerPool so we can count how many
|
||||
// configs are using this socket; necessary to ensure we can know
|
||||
// whether to enforce shutdown delays, for example (see #5393).
|
||||
ln, err := config.Listen(ctx, network, address)
|
||||
var ln io.Closer
|
||||
var err error
|
||||
switch network {
|
||||
case "udp", "udp4", "udp6", "unixgram":
|
||||
ln, err = config.ListenPacket(ctx, network, address)
|
||||
default:
|
||||
ln, err = config.Listen(ctx, network, address)
|
||||
}
|
||||
if err == nil {
|
||||
listenerPool.LoadOrStore(lnKey, nil)
|
||||
}
|
||||
@@ -117,9 +126,23 @@ func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string,
|
||||
unixSockets[lnKey] = ln.(*unixListener)
|
||||
}
|
||||
|
||||
// TODO: Not 100% sure this is necessary, but we do this for net.UnixListener in listen_unix.go, so...
|
||||
if unix, ok := ln.(*net.UnixConn); ok {
|
||||
ln = &unixConn{unix, address, lnKey, &one}
|
||||
unixSockets[lnKey] = ln.(*unixConn)
|
||||
}
|
||||
|
||||
// lightly wrap the listener so that when it is closed,
|
||||
// we can decrement the usage pool counter
|
||||
return deleteListener{ln, lnKey}, err
|
||||
switch specificLn := ln.(type) {
|
||||
case net.Listener:
|
||||
return deleteListener{specificLn, lnKey}, err
|
||||
case net.PacketConn:
|
||||
return deletePacketConn{specificLn, lnKey}, err
|
||||
}
|
||||
|
||||
// other types, I guess we just return them directly
|
||||
return ln, err
|
||||
}
|
||||
|
||||
// reusePort sets SO_REUSEPORT. Ineffective for unix sockets.
|
||||
@@ -158,6 +181,36 @@ func (uln *unixListener) Close() error {
|
||||
return uln.UnixListener.Close()
|
||||
}
|
||||
|
||||
type unixConn struct {
|
||||
*net.UnixConn
|
||||
filename string
|
||||
mapKey string
|
||||
count *int32 // accessed atomically
|
||||
}
|
||||
|
||||
func (uc *unixConn) Close() error {
|
||||
newCount := atomic.AddInt32(uc.count, -1)
|
||||
if newCount == 0 {
|
||||
defer func() {
|
||||
unixSocketsMu.Lock()
|
||||
delete(unixSockets, uc.mapKey)
|
||||
unixSocketsMu.Unlock()
|
||||
_ = syscall.Unlink(uc.filename)
|
||||
}()
|
||||
}
|
||||
return uc.UnixConn.Close()
|
||||
}
|
||||
|
||||
func (uc *unixConn) Unwrap() net.PacketConn {
|
||||
return uc.UnixConn
|
||||
}
|
||||
|
||||
// unixSockets keeps track of the currently-active unix sockets
|
||||
// so we can transfer their FDs gracefully during reloads.
|
||||
var unixSockets = make(map[string]interface {
|
||||
File() (*os.File, error)
|
||||
})
|
||||
|
||||
// deleteListener is a type that simply deletes itself
|
||||
// from the listenerPool when it closes. It is used
|
||||
// solely for the purpose of reference counting (i.e.
|
||||
@@ -171,3 +224,19 @@ func (dl deleteListener) Close() error {
|
||||
_, _ = listenerPool.Delete(dl.lnKey)
|
||||
return dl.Listener.Close()
|
||||
}
|
||||
|
||||
// deletePacketConn is like deleteListener, but
|
||||
// for net.PacketConns.
|
||||
type deletePacketConn struct {
|
||||
net.PacketConn
|
||||
lnKey string
|
||||
}
|
||||
|
||||
func (dl deletePacketConn) Close() error {
|
||||
_, _ = listenerPool.Delete(dl.lnKey)
|
||||
return dl.PacketConn.Close()
|
||||
}
|
||||
|
||||
func (dl deletePacketConn) Unwrap() net.PacketConn {
|
||||
return dl.PacketConn
|
||||
}
|
||||
|
||||
+139
-173
@@ -28,7 +28,6 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
@@ -38,6 +37,12 @@ import (
|
||||
"github.com/caddyserver/caddy/v2/internal"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterNamespace("caddy.listeners", []interface{}{
|
||||
(*ListenerWrapper)(nil),
|
||||
})
|
||||
}
|
||||
|
||||
// NetworkAddress represents one or more network addresses.
|
||||
// It contains the individual components for a parsed network
|
||||
// address of the form accepted by ParseNetworkAddress().
|
||||
@@ -149,11 +154,13 @@ func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net
|
||||
}
|
||||
|
||||
func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
|
||||
var ln any
|
||||
var err error
|
||||
var address string
|
||||
var unixFileMode fs.FileMode
|
||||
var isAbtractUnixSocket bool
|
||||
var (
|
||||
ln any
|
||||
err error
|
||||
address string
|
||||
unixFileMode fs.FileMode
|
||||
isAbtractUnixSocket bool
|
||||
)
|
||||
|
||||
// split unix socket addr early so lnKey
|
||||
// is independent of permissions bits
|
||||
@@ -181,27 +188,10 @@ func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net
|
||||
|
||||
lnKey := listenerKey(na.Network, address)
|
||||
|
||||
switch na.Network {
|
||||
case "tcp", "tcp4", "tcp6", "unix", "unixpacket":
|
||||
ln, err = listenTCPOrUnix(ctx, lnKey, na.Network, address, config)
|
||||
case "unixgram":
|
||||
ln, err = config.ListenPacket(ctx, na.Network, address)
|
||||
case "udp", "udp4", "udp6":
|
||||
sharedPc, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
|
||||
pc, err := config.ListenPacket(ctx, na.Network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sharedPacketConn{PacketConn: pc, key: lnKey}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
spc := sharedPc.(*sharedPacketConn)
|
||||
ln = &fakeClosePacketConn{spc: spc, UDPConn: spc.PacketConn.(*net.UDPConn)}
|
||||
}
|
||||
if strings.HasPrefix(na.Network, "ip") {
|
||||
ln, err = config.ListenPacket(ctx, na.Network, address)
|
||||
} else {
|
||||
ln, err = listenReusable(ctx, lnKey, na.Network, address, config)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -210,13 +200,6 @@ func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net
|
||||
return nil, fmt.Errorf("unsupported network type: %s", na.Network)
|
||||
}
|
||||
|
||||
// TODO: Not 100% sure this is necessary, but we do this for net.UnixListener in listen_unix.go, so...
|
||||
if unix, ok := ln.(*net.UnixConn); ok {
|
||||
one := int32(1)
|
||||
ln = &unixConn{unix, address, lnKey, &one}
|
||||
unixSockets[lnKey] = unix
|
||||
}
|
||||
|
||||
if IsUnixNetwork(na.Network) {
|
||||
if !isAbtractUnixSocket {
|
||||
if err := os.Chmod(address, unixFileMode); err != nil {
|
||||
@@ -470,53 +453,93 @@ func ListenPacket(network, addr string) (net.PacketConn, error) {
|
||||
// unixgram will be used; otherwise, udp will be used).
|
||||
//
|
||||
// NOTE: This API is EXPERIMENTAL and may be changed or removed.
|
||||
//
|
||||
// TODO: See if we can find a more elegant solution closer to the new NetworkAddress.Listen API.
|
||||
func ListenQUIC(ln net.PacketConn, tlsConf *tls.Config, activeRequests *int64) (http3.QUICEarlyListener, error) {
|
||||
lnKey := listenerKey("quic+"+ln.LocalAddr().Network(), ln.LocalAddr().String())
|
||||
func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config net.ListenConfig, tlsConf *tls.Config, activeRequests *int64) (http3.QUICEarlyListener, error) {
|
||||
lnKey := listenerKey("quic"+na.Network, na.JoinHostPort(portOffset))
|
||||
|
||||
sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
|
||||
sqtc := newSharedQUICTLSConfig(tlsConf)
|
||||
lnAny, err := na.Listen(ctx, portOffset, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ln := lnAny.(net.PacketConn)
|
||||
|
||||
h3ln := ln
|
||||
for {
|
||||
// retrieve the underlying socket, so quic-go can optimize.
|
||||
if unwrapper, ok := h3ln.(interface{ Unwrap() net.PacketConn }); ok {
|
||||
h3ln = unwrapper.Unwrap()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
sqs := newSharedQUICState(tlsConf, activeRequests)
|
||||
// http3.ConfigureTLSConfig only uses this field and tls App sets this field as well
|
||||
//nolint:gosec
|
||||
quicTlsConfig := &tls.Config{GetConfigForClient: sqtc.getConfigForClient}
|
||||
earlyLn, err := quic.ListenEarly(ln, http3.ConfigureTLSConfig(quicTlsConfig), &quic.Config{
|
||||
quicTlsConfig := &tls.Config{GetConfigForClient: sqs.getConfigForClient}
|
||||
earlyLn, err := quic.ListenEarly(h3ln, http3.ConfigureTLSConfig(quicTlsConfig), &quic.Config{
|
||||
Allow0RTT: true,
|
||||
RequireAddressValidation: func(clientAddr net.Addr) bool {
|
||||
var highLoad bool
|
||||
if activeRequests != nil {
|
||||
highLoad = atomic.LoadInt64(activeRequests) > 1000 // TODO: make tunable?
|
||||
}
|
||||
return highLoad
|
||||
// TODO: make tunable?
|
||||
return sqs.getActiveRequests() > 1000
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sharedQuicListener{EarlyListener: earlyLn, sqtc: sqtc, key: lnKey}, nil
|
||||
// using the original net.PacketConn to close them properly
|
||||
return &sharedQuicListener{EarlyListener: earlyLn, packetConn: ln, sqs: sqs, key: lnKey}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sql := sharedEarlyListener.(*sharedQuicListener)
|
||||
// add current tls.Config to sqtc, so GetConfigForClient will always return the latest tls.Config in case of context cancellation
|
||||
ctx, cancel := sql.sqtc.addTLSConfig(tlsConf)
|
||||
|
||||
// TODO: to serve QUIC over a unix socket, currently we need to hold onto
|
||||
// the underlying net.PacketConn (which we wrap as unixConn to keep count
|
||||
// of closes) because closing the quic.EarlyListener doesn't actually close
|
||||
// the underlying PacketConn, but we need to for unix sockets since we dup
|
||||
// the file descriptor and thus need to close the original; track issue:
|
||||
// https://github.com/quic-go/quic-go/issues/3560#issuecomment-1258959608
|
||||
var unix *unixConn
|
||||
if uc, ok := ln.(*unixConn); ok {
|
||||
unix = uc
|
||||
}
|
||||
// add current tls.Config to sqs, so GetConfigForClient will always return the latest tls.Config in case of context cancellation,
|
||||
// and the request counter will reflect current http server
|
||||
ctx, cancel := sql.sqs.addState(tlsConf, activeRequests)
|
||||
|
||||
return &fakeCloseQuicListener{
|
||||
sharedQuicListener: sql,
|
||||
context: ctx,
|
||||
contextCancel: cancel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DEPRECATED: Use NetworkAddress.ListenQUIC instead. This function will likely be changed or removed in the future.
|
||||
// TODO: See if we can find a more elegant solution closer to the new NetworkAddress.Listen API.
|
||||
func ListenQUIC(ln net.PacketConn, tlsConf *tls.Config, activeRequests *int64) (http3.QUICEarlyListener, error) {
|
||||
lnKey := listenerKey("quic+"+ln.LocalAddr().Network(), ln.LocalAddr().String())
|
||||
|
||||
sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
|
||||
sqs := newSharedQUICState(tlsConf, activeRequests)
|
||||
// http3.ConfigureTLSConfig only uses this field and tls App sets this field as well
|
||||
//nolint:gosec
|
||||
quicTlsConfig := &tls.Config{GetConfigForClient: sqs.getConfigForClient}
|
||||
earlyLn, err := quic.ListenEarly(ln, http3.ConfigureTLSConfig(quicTlsConfig), &quic.Config{
|
||||
Allow0RTT: true,
|
||||
RequireAddressValidation: func(clientAddr net.Addr) bool {
|
||||
// TODO: make tunable?
|
||||
return sqs.getActiveRequests() > 1000
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sharedQuicListener{EarlyListener: earlyLn, sqs: sqs, key: lnKey}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sql := sharedEarlyListener.(*sharedQuicListener)
|
||||
// add current tls.Config and request counter to sqs, so GetConfigForClient will always return the latest tls.Config in case of context cancellation,
|
||||
// and the request counter will reflect current http server
|
||||
ctx, cancel := sql.sqs.addState(tlsConf, activeRequests)
|
||||
|
||||
return &fakeCloseQuicListener{
|
||||
sharedQuicListener: sql,
|
||||
uc: unix,
|
||||
context: ctx,
|
||||
contextCancel: cancel,
|
||||
}, nil
|
||||
@@ -534,38 +557,50 @@ type contextAndCancelFunc struct {
|
||||
context.CancelFunc
|
||||
}
|
||||
|
||||
// sharedQUICTLSConfig manages GetConfigForClient
|
||||
// sharedQUICState manages GetConfigForClient and current number of active requests
|
||||
// see issue: https://github.com/caddyserver/caddy/pull/4849
|
||||
type sharedQUICTLSConfig struct {
|
||||
rmu sync.RWMutex
|
||||
tlsConfs map[*tls.Config]contextAndCancelFunc
|
||||
activeTlsConf *tls.Config
|
||||
type sharedQUICState struct {
|
||||
rmu sync.RWMutex
|
||||
tlsConfs map[*tls.Config]contextAndCancelFunc
|
||||
requestCounters map[*tls.Config]*int64
|
||||
activeTlsConf *tls.Config
|
||||
activeRequestsCounter *int64
|
||||
}
|
||||
|
||||
// newSharedQUICTLSConfig creates a new sharedQUICTLSConfig
|
||||
func newSharedQUICTLSConfig(tlsConfig *tls.Config) *sharedQUICTLSConfig {
|
||||
sqtc := &sharedQUICTLSConfig{
|
||||
tlsConfs: make(map[*tls.Config]contextAndCancelFunc),
|
||||
activeTlsConf: tlsConfig,
|
||||
// newSharedQUICState creates a new sharedQUICState
|
||||
func newSharedQUICState(tlsConfig *tls.Config, activeRequests *int64) *sharedQUICState {
|
||||
sqtc := &sharedQUICState{
|
||||
tlsConfs: make(map[*tls.Config]contextAndCancelFunc),
|
||||
requestCounters: make(map[*tls.Config]*int64),
|
||||
activeTlsConf: tlsConfig,
|
||||
activeRequestsCounter: activeRequests,
|
||||
}
|
||||
sqtc.addTLSConfig(tlsConfig)
|
||||
sqtc.addState(tlsConfig, activeRequests)
|
||||
return sqtc
|
||||
}
|
||||
|
||||
// getConfigForClient is used as tls.Config's GetConfigForClient field
|
||||
func (sqtc *sharedQUICTLSConfig) getConfigForClient(ch *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
sqtc.rmu.RLock()
|
||||
defer sqtc.rmu.RUnlock()
|
||||
return sqtc.activeTlsConf.GetConfigForClient(ch)
|
||||
func (sqs *sharedQUICState) getConfigForClient(ch *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
sqs.rmu.RLock()
|
||||
defer sqs.rmu.RUnlock()
|
||||
return sqs.activeTlsConf.GetConfigForClient(ch)
|
||||
}
|
||||
|
||||
// addTLSConfig adds tls.Config to the map if not present and returns the corresponding context and its cancelFunc
|
||||
// so that when cancelled, the active tls.Config will change
|
||||
func (sqtc *sharedQUICTLSConfig) addTLSConfig(tlsConfig *tls.Config) (context.Context, context.CancelFunc) {
|
||||
sqtc.rmu.Lock()
|
||||
defer sqtc.rmu.Unlock()
|
||||
// getActiveRequests returns the number of active requests
|
||||
func (sqs *sharedQUICState) getActiveRequests() int64 {
|
||||
// Prevent a race when a context is cancelled and active request counter is being changed
|
||||
sqs.rmu.RLock()
|
||||
defer sqs.rmu.RUnlock()
|
||||
return atomic.LoadInt64(sqs.activeRequestsCounter)
|
||||
}
|
||||
|
||||
if cacc, ok := sqtc.tlsConfs[tlsConfig]; ok {
|
||||
// addState adds tls.Config and activeRequests to the map if not present and returns the corresponding context and its cancelFunc
|
||||
// so that when cancelled, the active tls.Config and request counter will change
|
||||
func (sqs *sharedQUICState) addState(tlsConfig *tls.Config, activeRequests *int64) (context.Context, context.CancelFunc) {
|
||||
sqs.rmu.Lock()
|
||||
defer sqs.rmu.Unlock()
|
||||
|
||||
if cacc, ok := sqs.tlsConfs[tlsConfig]; ok {
|
||||
return cacc.Context, cacc.CancelFunc
|
||||
}
|
||||
|
||||
@@ -573,23 +608,26 @@ func (sqtc *sharedQUICTLSConfig) addTLSConfig(tlsConfig *tls.Config) (context.Co
|
||||
wrappedCancel := func() {
|
||||
cancel()
|
||||
|
||||
sqtc.rmu.Lock()
|
||||
defer sqtc.rmu.Unlock()
|
||||
sqs.rmu.Lock()
|
||||
defer sqs.rmu.Unlock()
|
||||
|
||||
delete(sqtc.tlsConfs, tlsConfig)
|
||||
if sqtc.activeTlsConf == tlsConfig {
|
||||
// select another tls.Config, if there is none,
|
||||
delete(sqs.tlsConfs, tlsConfig)
|
||||
delete(sqs.requestCounters, tlsConfig)
|
||||
if sqs.activeTlsConf == tlsConfig {
|
||||
// select another tls.Config and request counter, if there is none,
|
||||
// related sharedQuicListener will be destroyed anyway
|
||||
for tc := range sqtc.tlsConfs {
|
||||
sqtc.activeTlsConf = tc
|
||||
for tc, counter := range sqs.requestCounters {
|
||||
sqs.activeTlsConf = tc
|
||||
sqs.activeRequestsCounter = counter
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
sqtc.tlsConfs[tlsConfig] = contextAndCancelFunc{ctx, wrappedCancel}
|
||||
sqs.tlsConfs[tlsConfig] = contextAndCancelFunc{ctx, wrappedCancel}
|
||||
sqs.requestCounters[tlsConfig] = activeRequests
|
||||
// there should be at most 2 tls.Configs
|
||||
if len(sqtc.tlsConfs) > 2 {
|
||||
Log().Warn("quic listener tls configs are more than 2", zap.Int("number of configs", len(sqtc.tlsConfs)))
|
||||
if len(sqs.tlsConfs) > 2 {
|
||||
Log().Warn("quic listener tls configs are more than 2", zap.Int("number of configs", len(sqs.tlsConfs)))
|
||||
}
|
||||
return ctx, wrappedCancel
|
||||
}
|
||||
@@ -597,24 +635,17 @@ func (sqtc *sharedQUICTLSConfig) addTLSConfig(tlsConfig *tls.Config) (context.Co
|
||||
// sharedQuicListener is like sharedListener, but for quic.EarlyListeners.
|
||||
type sharedQuicListener struct {
|
||||
*quic.EarlyListener
|
||||
sqtc *sharedQUICTLSConfig
|
||||
key string
|
||||
packetConn net.PacketConn // we have to hold these because quic-go won't close listeners it didn't create
|
||||
sqs *sharedQUICState
|
||||
key string
|
||||
}
|
||||
|
||||
// Destruct closes the underlying QUIC listener.
|
||||
// Destruct closes the underlying QUIC listener and its associated net.PacketConn.
|
||||
func (sql *sharedQuicListener) Destruct() error {
|
||||
return sql.EarlyListener.Close()
|
||||
}
|
||||
|
||||
// sharedPacketConn is like sharedListener, but for net.PacketConns.
|
||||
type sharedPacketConn struct {
|
||||
net.PacketConn
|
||||
key string
|
||||
}
|
||||
|
||||
// Destruct closes the underlying socket.
|
||||
func (spc *sharedPacketConn) Destruct() error {
|
||||
return spc.PacketConn.Close()
|
||||
// close EarlyListener first to stop any operations being done to the net.PacketConn
|
||||
_ = sql.EarlyListener.Close()
|
||||
// then close the net.PacketConn
|
||||
return sql.packetConn.Close()
|
||||
}
|
||||
|
||||
// fakeClosedErr returns an error value that is not temporary
|
||||
@@ -636,34 +667,9 @@ func fakeClosedErr(l interface{ Addr() net.Addr }) error {
|
||||
// socket is actually left open.
|
||||
var errFakeClosed = fmt.Errorf("listener 'closed' 😉")
|
||||
|
||||
// fakeClosePacketConn is like fakeCloseListener, but for PacketConns,
|
||||
// or more specifically, *net.UDPConn
|
||||
type fakeClosePacketConn struct {
|
||||
closed int32 // accessed atomically; belongs to this struct only
|
||||
spc *sharedPacketConn // its key is used in Close
|
||||
*net.UDPConn // embedded, so we also become a net.PacketConn and enable several other optimizations done by quic-go
|
||||
}
|
||||
|
||||
// interface guard for extra optimizations
|
||||
// needed by QUIC implementation: https://github.com/caddyserver/caddy/issues/3998, https://github.com/caddyserver/caddy/issues/5605
|
||||
var _ quic.OOBCapablePacketConn = (*fakeClosePacketConn)(nil)
|
||||
|
||||
// https://pkg.go.dev/golang.org/x/net/ipv4#NewPacketConn is used by quic-go and requires a net.PacketConn type assertable to a net.Conn,
|
||||
// but doesn't actually use these methods, the only methods needed are `ReadMsgUDP` and `SyscallConn`.
|
||||
var _ net.Conn = (*fakeClosePacketConn)(nil)
|
||||
|
||||
// Close won't close the underlying socket unless there is no more reference, then listenerPool will close it.
|
||||
func (fcpc *fakeClosePacketConn) Close() error {
|
||||
if atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) {
|
||||
_, _ = listenerPool.Delete(fcpc.spc.key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeCloseQuicListener struct {
|
||||
closed int32 // accessed atomically; belongs to this struct only
|
||||
*sharedQuicListener // embedded, so we also become a quic.EarlyListener
|
||||
uc *unixConn // underlying unix socket, if UDS
|
||||
closed int32 // accessed atomically; belongs to this struct only
|
||||
*sharedQuicListener // embedded, so we also become a quic.EarlyListener
|
||||
context context.Context
|
||||
contextCancel context.CancelFunc
|
||||
}
|
||||
@@ -690,11 +696,6 @@ func (fcql *fakeCloseQuicListener) Close() error {
|
||||
if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) {
|
||||
fcql.contextCancel()
|
||||
_, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
|
||||
if fcql.uc != nil {
|
||||
// unix sockets need to be closed ourselves because we dup() the file
|
||||
// descriptor when we reuse them, so this avoids a resource leak
|
||||
fcql.uc.Close()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -720,34 +721,7 @@ func RegisterNetwork(network string, getListener ListenerFunc) {
|
||||
networkTypes[network] = getListener
|
||||
}
|
||||
|
||||
type unixConn struct {
|
||||
*net.UnixConn
|
||||
filename string
|
||||
mapKey string
|
||||
count *int32 // accessed atomically
|
||||
}
|
||||
|
||||
func (uc *unixConn) Close() error {
|
||||
newCount := atomic.AddInt32(uc.count, -1)
|
||||
if newCount == 0 {
|
||||
defer func() {
|
||||
unixSocketsMu.Lock()
|
||||
delete(unixSockets, uc.mapKey)
|
||||
unixSocketsMu.Unlock()
|
||||
_ = syscall.Unlink(uc.filename)
|
||||
}()
|
||||
}
|
||||
return uc.UnixConn.Close()
|
||||
}
|
||||
|
||||
// unixSockets keeps track of the currently-active unix sockets
|
||||
// so we can transfer their FDs gracefully during reloads.
|
||||
var (
|
||||
unixSockets = make(map[string]interface {
|
||||
File() (*os.File, error)
|
||||
})
|
||||
unixSocketsMu sync.Mutex
|
||||
)
|
||||
var unixSocketsMu sync.Mutex
|
||||
|
||||
// getListenerFromPlugin returns a listener on the given network and address
|
||||
// if a plugin has registered the network name. It may return (nil, nil) if
|
||||
@@ -791,11 +765,3 @@ type ListenerWrapper interface {
|
||||
var listenerPool = NewUsagePool()
|
||||
|
||||
const maxPortSpan = 65535
|
||||
|
||||
// Interface guards (see https://github.com/caddyserver/caddy/issues/3998)
|
||||
var (
|
||||
_ (interface{ SetReadBuffer(int) error }) = (*fakeClosePacketConn)(nil)
|
||||
_ (interface {
|
||||
SyscallConn() (syscall.RawConn, error)
|
||||
}) = (*fakeClosePacketConn)(nil)
|
||||
)
|
||||
|
||||
@@ -33,6 +33,12 @@ func init() {
|
||||
RegisterModule(StdoutWriter{})
|
||||
RegisterModule(StderrWriter{})
|
||||
RegisterModule(DiscardWriter{})
|
||||
RegisterNamespace("caddy.logging.encoders", []interface{}{
|
||||
(*zapcore.Encoder)(nil),
|
||||
})
|
||||
RegisterNamespace("caddy.logging.writers", []interface{}{
|
||||
(*WriterOpener)(nil),
|
||||
})
|
||||
}
|
||||
|
||||
// Logging facilitates logging within Caddy. The default log is
|
||||
@@ -265,6 +271,8 @@ type BaseLog struct {
|
||||
WriterRaw json.RawMessage `json:"writer,omitempty" caddy:"namespace=caddy.logging.writers inline_key=output"`
|
||||
|
||||
// The encoder is how the log entries are formatted or encoded.
|
||||
// An `encoder` should implement the following interfaces:
|
||||
// - [zapcore.Encoder](https://pkg.go.dev/go.uber.org/zap/zapcore#Encoder)
|
||||
EncoderRaw json.RawMessage `json:"encoder,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"`
|
||||
|
||||
// Level is the minimum level to emit, and is inclusive.
|
||||
|
||||
@@ -73,7 +73,7 @@ func init() {
|
||||
// `{http.request.remote.host}` | The host (IP) part of the remote client's address
|
||||
// `{http.request.remote.port}` | The port part of the remote client's address
|
||||
// `{http.request.remote}` | The address of the remote client
|
||||
// `{http.request.scheme}` | The request scheme
|
||||
// `{http.request.scheme}` | The request scheme, typically `http` or `https`
|
||||
// `{http.request.tls.version}` | The TLS version name
|
||||
// `{http.request.tls.cipher_suite}` | The TLS cipher suite
|
||||
// `{http.request.tls.resumed}` | The TLS connection resumed a previous connection
|
||||
@@ -378,11 +378,7 @@ func (app *App) Start() error {
|
||||
return context.WithValue(ctx, ConnCtxKey, c)
|
||||
},
|
||||
}
|
||||
h2server := &http2.Server{
|
||||
NewWriteScheduler: func() http2.WriteScheduler {
|
||||
return http2.NewPriorityWriteScheduler(nil)
|
||||
},
|
||||
}
|
||||
h2server := new(http2.Server)
|
||||
|
||||
// disable HTTP/2, which we enabled by default during provisioning
|
||||
if !srv.protocol("h2") {
|
||||
@@ -617,17 +613,6 @@ func (app *App) Stop() error {
|
||||
zap.Error(err),
|
||||
zap.Strings("addresses", server.Listen))
|
||||
}
|
||||
|
||||
// TODO: we have to manually close our listeners because quic-go won't
|
||||
// close listeners it didn't create along with the server itself...
|
||||
// see https://github.com/quic-go/quic-go/issues/3560
|
||||
for _, el := range server.h3listeners {
|
||||
if err := el.Close(); err != nil {
|
||||
app.logger.Error("HTTP/3 listener close",
|
||||
zap.Error(err),
|
||||
zap.String("address", el.LocalAddr().String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
stopH2Listener := func(server *Server) {
|
||||
defer finishedShutdown.Done()
|
||||
|
||||
@@ -31,6 +31,13 @@ import (
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(HTTPBasicAuth{})
|
||||
|
||||
caddy.RegisterNamespace("http.authentication.hashes", []interface{}{
|
||||
(*Comparer)(nil),
|
||||
})
|
||||
caddy.RegisterNamespace("http.authentication.providers", []interface{}{
|
||||
(*Authenticator)(nil),
|
||||
})
|
||||
}
|
||||
|
||||
// HTTPBasicAuth facilitates HTTP basic authentication.
|
||||
|
||||
@@ -37,6 +37,9 @@ import (
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(Encode{})
|
||||
caddy.RegisterNamespace("http.encoders", []interface{}{
|
||||
(*Encoding)(nil),
|
||||
})
|
||||
}
|
||||
|
||||
// Encode is a middleware which can encode responses.
|
||||
@@ -93,6 +96,7 @@ func (enc *Encode) Provision(ctx caddy.Context) error {
|
||||
"application/xhtml+xml*",
|
||||
"application/atom+xml*",
|
||||
"application/rss+xml*",
|
||||
"application/wasm*",
|
||||
"image/svg+xml*",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -36,12 +36,18 @@ import (
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
|
||||
)
|
||||
|
||||
// BrowseTemplate is the default template document to use for
|
||||
// file listings. By default, its default value is an embedded
|
||||
// document. You can override this value at program start, or
|
||||
// if you are running Caddy via config, you can specify a
|
||||
// custom template_file in the browse configuration.
|
||||
//
|
||||
//go:embed browse.html
|
||||
var defaultBrowseTemplate string
|
||||
var BrowseTemplate string
|
||||
|
||||
// Browse configures directory browsing.
|
||||
type Browse struct {
|
||||
// Use this template file instead of the default browse template.
|
||||
// Filename of the template to use instead of the embedded browse template.
|
||||
TemplateFile string `json:"template_file,omitempty"`
|
||||
}
|
||||
|
||||
@@ -205,7 +211,7 @@ func (fsrv *FileServer) makeBrowseTemplate(tplCtx *templateContext) (*template.T
|
||||
}
|
||||
} else {
|
||||
tpl = tplCtx.NewTemplate("default_listing")
|
||||
tpl, err = tpl.Parse(defaultBrowseTemplate)
|
||||
tpl, err = tpl.Parse(BrowseTemplate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing default browse template: %v", err)
|
||||
}
|
||||
|
||||
@@ -1,288 +1,296 @@
|
||||
{{- define "icon"}}
|
||||
{{- if .IsDir}}
|
||||
<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>
|
||||
<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>
|
||||
</svg>
|
||||
{{- if .IsSymlink}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-folder-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M9 3a1 1 0 0 1 .608 .206l.1 .087l2.706 2.707h6.586a3 3 0 0 1 2.995 2.824l.005 .176v8a3 3 0 0 1 -2.824 2.995l-.176 .005h-14a3 3 0 0 1 -2.995 -2.824l-.005 -.176v-11a3 3 0 0 1 2.824 -2.995l.176 -.005h4z" stroke-width="0" fill="currentColor"/>
|
||||
<path fill="#000" d="M2.795 17.306c0-2.374 1.792-4.314 4.078-4.538v-1.104a.38.38 0 0 1 .651-.272l2.45 2.492a.132.132 0 0 1 0 .188l-2.45 2.492a.381.381 0 0 1-.651-.272V15.24c-1.889.297-3.436 1.39-3.817 3.26a2.809 2.809 0 0 1-.261-1.193Z" style="stroke-width:.127478"/>
|
||||
</svg>
|
||||
{{- else}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-folder-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<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"/>
|
||||
</svg>
|
||||
{{- end}}
|
||||
{{- else if or (eq .Name "LICENSE") (eq .Name "README")}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-license" 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>
|
||||
<path d="M15 21h-9a3 3 0 0 1 -3 -3v-1h10v2a2 2 0 0 0 4 0v-14a2 2 0 1 1 2 2h-2m2 -4h-11a3 3 0 0 0 -3 3v11"></path>
|
||||
<path d="M9 7l4 0"></path>
|
||||
<path d="M9 11l4 0"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M15 21h-9a3 3 0 0 1 -3 -3v-1h10v2a2 2 0 0 0 4 0v-14a2 2 0 1 1 2 2h-2m2 -4h-11a3 3 0 0 0 -3 3v11"/>
|
||||
<path d="M9 7l4 0"/>
|
||||
<path d="M9 11l4 0"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp" ".tiff" ".bmp" ".heif" ".heic" ".svg"}}
|
||||
{{- if eq .Tpl.Layout "grid"}}
|
||||
<img loading="lazy" src="{{html .Name}}">
|
||||
{{- else}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-photo" 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>
|
||||
<path d="M15 8h.01"></path>
|
||||
<path d="M3 6a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v12a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3v-12z"></path>
|
||||
<path d="M3 16l5 -5c.928 -.893 2.072 -.893 3 0l5 5"></path>
|
||||
<path d="M14 14l1 -1c.928 -.893 2.072 -.893 3 0l3 3"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M15 8h.01"/>
|
||||
<path d="M3 6a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v12a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3v-12z"/>
|
||||
<path d="M3 16l5 -5c.928 -.893 2.072 -.893 3 0l5 5"/>
|
||||
<path d="M14 14l1 -1c.928 -.893 2.072 -.893 3 0l3 3"/>
|
||||
</svg>
|
||||
{{- end}}
|
||||
{{- else if .HasExt ".mp4" ".mov" ".mpeg" ".mpg" ".avi" ".ogg" ".webm" ".mkv" ".vob" ".gifv" ".3gp"}}
|
||||
{{- else if .HasExt ".mp4" ".mov" ".m4v" ".mpeg" ".mpg" ".avi" ".ogg" ".webm" ".mkv" ".vob" ".gifv" ".3gp"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-movie" 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>
|
||||
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
|
||||
<path d="M8 4l0 16"></path>
|
||||
<path d="M16 4l0 16"></path>
|
||||
<path d="M4 8l4 0"></path>
|
||||
<path d="M4 16l4 0"></path>
|
||||
<path d="M4 12l16 0"></path>
|
||||
<path d="M16 8l4 0"></path>
|
||||
<path d="M16 16l4 0"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"/>
|
||||
<path d="M8 4l0 16"/>
|
||||
<path d="M16 4l0 16"/>
|
||||
<path d="M4 8l4 0"/>
|
||||
<path d="M4 16l4 0"/>
|
||||
<path d="M4 12l16 0"/>
|
||||
<path d="M16 8l4 0"/>
|
||||
<path d="M16 16l4 0"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".mp3" ".m4a" ".aac" ".ogg" ".flac" ".wav" ".wma" ".midi" ".cda"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-music" 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>
|
||||
<path d="M6 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
|
||||
<path d="M16 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
|
||||
<path d="M9 17l0 -13l10 0l0 13"></path>
|
||||
<path d="M9 8l10 0"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"/>
|
||||
<path d="M16 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"/>
|
||||
<path d="M9 17l0 -13l10 0l0 13"/>
|
||||
<path d="M9 8l10 0"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".pdf"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-type-pdf" 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>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path>
|
||||
<path d="M5 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6"></path>
|
||||
<path d="M17 18h2"></path>
|
||||
<path d="M20 15h-3v6"></path>
|
||||
<path d="M11 15v6h1a2 2 0 0 0 2 -2v-2a2 2 0 0 0 -2 -2h-1z"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"/>
|
||||
<path d="M5 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6"/>
|
||||
<path d="M17 18h2"/>
|
||||
<path d="M20 15h-3v6"/>
|
||||
<path d="M11 15v6h1a2 2 0 0 0 2 -2v-2a2 2 0 0 0 -2 -2h-1z"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".csv" ".tsv"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-type-csv" 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>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path>
|
||||
<path d="M7 16.5a1.5 1.5 0 0 0 -3 0v3a1.5 1.5 0 0 0 3 0"></path>
|
||||
<path d="M10 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path>
|
||||
<path d="M16 15l2 6l2 -6"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"/>
|
||||
<path d="M7 16.5a1.5 1.5 0 0 0 -3 0v3a1.5 1.5 0 0 0 3 0"/>
|
||||
<path d="M10 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"/>
|
||||
<path d="M16 15l2 6l2 -6"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".txt" ".doc" ".docx" ".odt" ".fodt" ".rtf"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-text" 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>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"></path>
|
||||
<path d="M9 9l1 0"></path>
|
||||
<path d="M9 13l6 0"></path>
|
||||
<path d="M9 17l6 0"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"/>
|
||||
<path d="M9 9l1 0"/>
|
||||
<path d="M9 13l6 0"/>
|
||||
<path d="M9 17l6 0"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".xls" ".xlsx" ".ods" ".fods"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-spreadsheet" 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>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"></path>
|
||||
<path d="M8 11h8v7h-8z"></path>
|
||||
<path d="M8 15h8"></path>
|
||||
<path d="M11 11v7"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"/>
|
||||
<path d="M8 11h8v7h-8z"/>
|
||||
<path d="M8 15h8"/>
|
||||
<path d="M11 11v7"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".ppt" ".pptx" ".odp" ".fodp"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-presentation-analytics" 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>
|
||||
<path d="M9 12v-4"></path>
|
||||
<path d="M15 12v-2"></path>
|
||||
<path d="M12 12v-1"></path>
|
||||
<path d="M3 4h18"></path>
|
||||
<path d="M4 4v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-10"></path>
|
||||
<path d="M12 16v4"></path>
|
||||
<path d="M9 20h6"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M9 12v-4"/>
|
||||
<path d="M15 12v-2"/>
|
||||
<path d="M12 12v-1"/>
|
||||
<path d="M3 4h18"/>
|
||||
<path d="M4 4v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-10"/>
|
||||
<path d="M12 16v4"/>
|
||||
<path d="M9 20h6"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".zip" ".gz" ".xz" ".tar" ".7z" ".rar" ".xz" ".zst"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-zip" 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>
|
||||
<path d="M6 20.735a2 2 0 0 1 -1 -1.735v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-1"></path>
|
||||
<path d="M11 17a2 2 0 0 1 2 2v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a2 2 0 0 1 2 -2z"></path>
|
||||
<path d="M11 5l-1 0"></path>
|
||||
<path d="M13 7l-1 0"></path>
|
||||
<path d="M11 9l-1 0"></path>
|
||||
<path d="M13 11l-1 0"></path>
|
||||
<path d="M11 13l-1 0"></path>
|
||||
<path d="M13 15l-1 0"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 20.735a2 2 0 0 1 -1 -1.735v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-1"/>
|
||||
<path d="M11 17a2 2 0 0 1 2 2v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a2 2 0 0 1 2 -2z"/>
|
||||
<path d="M11 5l-1 0"/>
|
||||
<path d="M13 7l-1 0"/>
|
||||
<path d="M11 9l-1 0"/>
|
||||
<path d="M13 11l-1 0"/>
|
||||
<path d="M11 13l-1 0"/>
|
||||
<path d="M13 15l-1 0"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".deb" ".dpkg"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-debian" 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>
|
||||
<path d="M12 17c-2.397 -.943 -4 -3.153 -4 -5.635c0 -2.19 1.039 -3.14 1.604 -3.595c2.646 -2.133 6.396 -.27 6.396 3.23c0 2.5 -2.905 2.121 -3.5 1.5c-.595 -.621 -1 -1.5 -.5 -2.5"></path>
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 17c-2.397 -.943 -4 -3.153 -4 -5.635c0 -2.19 1.039 -3.14 1.604 -3.595c2.646 -2.133 6.396 -.27 6.396 3.23c0 2.5 -2.905 2.121 -3.5 1.5c-.595 -.621 -1 -1.5 -.5 -2.5"/>
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".rpm" ".exe" ".flatpak" ".appimage" ".jar" ".msi" ".apk"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-package" 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>
|
||||
<path d="M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5"></path>
|
||||
<path d="M12 12l8 -4.5"></path>
|
||||
<path d="M12 12l0 9"></path>
|
||||
<path d="M12 12l-8 -4.5"></path>
|
||||
<path d="M16 5.25l-8 4.5"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5"/>
|
||||
<path d="M12 12l8 -4.5"/>
|
||||
<path d="M12 12l0 9"/>
|
||||
<path d="M12 12l-8 -4.5"/>
|
||||
<path d="M16 5.25l-8 4.5"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".ps1"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-powershell" 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>
|
||||
<path d="M4.887 20h11.868c.893 0 1.664 -.665 1.847 -1.592l2.358 -12c.212 -1.081 -.442 -2.14 -1.462 -2.366a1.784 1.784 0 0 0 -.385 -.042h-11.868c-.893 0 -1.664 .665 -1.847 1.592l-2.358 12c-.212 1.081 .442 2.14 1.462 2.366c.127 .028 .256 .042 .385 .042z"></path>
|
||||
<path d="M9 8l4 4l-6 4"></path>
|
||||
<path d="M12 16h3"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M4.887 20h11.868c.893 0 1.664 -.665 1.847 -1.592l2.358 -12c.212 -1.081 -.442 -2.14 -1.462 -2.366a1.784 1.784 0 0 0 -.385 -.042h-11.868c-.893 0 -1.664 .665 -1.847 1.592l-2.358 12c-.212 1.081 .442 2.14 1.462 2.366c.127 .028 .256 .042 .385 .042z"/>
|
||||
<path d="M9 8l4 4l-6 4"/>
|
||||
<path d="M12 16h3"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".py" ".pyc" ".pyo"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-python" 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>
|
||||
<path d="M12 9h-7a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h3"></path>
|
||||
<path d="M12 15h7a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2h-3"></path>
|
||||
<path d="M8 9v-4a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v5a2 2 0 0 1 -2 2h-4a2 2 0 0 0 -2 2v5a2 2 0 0 0 2 2h4a2 2 0 0 0 2 -2v-4"></path>
|
||||
<path d="M11 6l0 .01"></path>
|
||||
<path d="M13 18l0 .01"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 9h-7a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h3"/>
|
||||
<path d="M12 15h7a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2h-3"/>
|
||||
<path d="M8 9v-4a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v5a2 2 0 0 1 -2 2h-4a2 2 0 0 0 -2 2v5a2 2 0 0 0 2 2h4a2 2 0 0 0 2 -2v-4"/>
|
||||
<path d="M11 6l0 .01"/>
|
||||
<path d="M13 18l0 .01"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".bash" ".sh" ".com" ".bat" ".dll" ".so"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-script" 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>
|
||||
<path d="M17 20h-11a3 3 0 0 1 0 -6h11a3 3 0 0 0 0 6h1a3 3 0 0 0 3 -3v-11a2 2 0 0 0 -2 -2h-10a2 2 0 0 0 -2 2v8"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M17 20h-11a3 3 0 0 1 0 -6h11a3 3 0 0 0 0 6h1a3 3 0 0 0 3 -3v-11a2 2 0 0 0 -2 -2h-10a2 2 0 0 0 -2 2v8"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".dmg"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-finder" 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>
|
||||
<path d="M3 4m0 1a1 1 0 0 1 1 -1h16a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1z"></path>
|
||||
<path d="M7 8v1"></path>
|
||||
<path d="M17 8v1"></path>
|
||||
<path d="M12.5 4c-.654 1.486 -1.26 3.443 -1.5 9h2.5c-.19 2.867 .094 5.024 .5 7"></path>
|
||||
<path d="M7 15.5c3.667 2 6.333 2 10 0"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M3 4m0 1a1 1 0 0 1 1 -1h16a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1z"/>
|
||||
<path d="M7 8v1"/>
|
||||
<path d="M17 8v1"/>
|
||||
<path d="M12.5 4c-.654 1.486 -1.26 3.443 -1.5 9h2.5c-.19 2.867 .094 5.024 .5 7"/>
|
||||
<path d="M7 15.5c3.667 2 6.333 2 10 0"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".iso" ".img"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-disc" 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>
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"></path>
|
||||
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
|
||||
<path d="M7 12a5 5 0 0 1 5 -5"></path>
|
||||
<path d="M12 17a5 5 0 0 0 5 -5"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/>
|
||||
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"/>
|
||||
<path d="M7 12a5 5 0 0 1 5 -5"/>
|
||||
<path d="M12 17a5 5 0 0 0 5 -5"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".md" ".mdown" ".markdown"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-markdown" 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>
|
||||
<path d="M3 5m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z"></path>
|
||||
<path d="M7 15v-6l2 2l2 -2v6"></path>
|
||||
<path d="M14 13l2 2l2 -2m-2 2v-6"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M3 5m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z"/>
|
||||
<path d="M7 15v-6l2 2l2 -2v6"/>
|
||||
<path d="M14 13l2 2l2 -2m-2 2v-6"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".ttf" ".otf" ".woff" ".woff2" ".eof"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-typography" 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>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"></path>
|
||||
<path d="M11 18h2"></path>
|
||||
<path d="M12 18v-7"></path>
|
||||
<path d="M9 12v-1h6v1"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"/>
|
||||
<path d="M11 18h2"/>
|
||||
<path d="M12 18v-7"/>
|
||||
<path d="M9 12v-1h6v1"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".go"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-golang" 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>
|
||||
<path d="M15.695 14.305c1.061 1.06 2.953 .888 4.226 -.384c1.272 -1.273 1.444 -3.165 .384 -4.226c-1.061 -1.06 -2.953 -.888 -4.226 .384c-1.272 1.273 -1.444 3.165 -.384 4.226z"></path>
|
||||
<path d="M12.68 9.233c-1.084 -.497 -2.545 -.191 -3.591 .846c-1.284 1.273 -1.457 3.165 -.388 4.226c1.07 1.06 2.978 .888 4.261 -.384a3.669 3.669 0 0 0 1.038 -1.921h-2.427"></path>
|
||||
<path d="M5.5 15h-1.5"></path>
|
||||
<path d="M6 9h-2"></path>
|
||||
<path d="M5 12h-3"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M15.695 14.305c1.061 1.06 2.953 .888 4.226 -.384c1.272 -1.273 1.444 -3.165 .384 -4.226c-1.061 -1.06 -2.953 -.888 -4.226 .384c-1.272 1.273 -1.444 3.165 -.384 4.226z"/>
|
||||
<path d="M12.68 9.233c-1.084 -.497 -2.545 -.191 -3.591 .846c-1.284 1.273 -1.457 3.165 -.388 4.226c1.07 1.06 2.978 .888 4.261 -.384a3.669 3.669 0 0 0 1.038 -1.921h-2.427"/>
|
||||
<path d="M5.5 15h-1.5"/>
|
||||
<path d="M6 9h-2"/>
|
||||
<path d="M5 12h-3"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".html" ".htm"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-type-html" 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>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path>
|
||||
<path d="M2 21v-6"></path>
|
||||
<path d="M5 15v6"></path>
|
||||
<path d="M2 18h3"></path>
|
||||
<path d="M20 15v6h2"></path>
|
||||
<path d="M13 21v-6l2 3l2 -3v6"></path>
|
||||
<path d="M7.5 15h3"></path>
|
||||
<path d="M9 15v6"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"/>
|
||||
<path d="M2 21v-6"/>
|
||||
<path d="M5 15v6"/>
|
||||
<path d="M2 18h3"/>
|
||||
<path d="M20 15v6h2"/>
|
||||
<path d="M13 21v-6l2 3l2 -3v6"/>
|
||||
<path d="M7.5 15h3"/>
|
||||
<path d="M9 15v6"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".js"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-type-js" 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>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M3 15h3v4.5a1.5 1.5 0 0 1 -3 0"></path>
|
||||
<path d="M9 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path>
|
||||
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-1"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M3 15h3v4.5a1.5 1.5 0 0 1 -3 0"/>
|
||||
<path d="M9 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"/>
|
||||
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-1"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".css"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-type-css" 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>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path>
|
||||
<path d="M8 16.5a1.5 1.5 0 0 0 -3 0v3a1.5 1.5 0 0 0 3 0"></path>
|
||||
<path d="M11 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path>
|
||||
<path d="M17 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"/>
|
||||
<path d="M8 16.5a1.5 1.5 0 0 0 -3 0v3a1.5 1.5 0 0 0 3 0"/>
|
||||
<path d="M11 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"/>
|
||||
<path d="M17 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".json" ".json5" ".jsonc"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-json" 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>
|
||||
<path d="M20 16v-8l3 8v-8"></path>
|
||||
<path d="M15 8a2 2 0 0 1 2 2v4a2 2 0 1 1 -4 0v-4a2 2 0 0 1 2 -2z"></path>
|
||||
<path d="M1 8h3v6.5a1.5 1.5 0 0 1 -3 0v-.5"></path>
|
||||
<path d="M7 15a1 1 0 0 0 1 1h1a1 1 0 0 0 1 -1v-2a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1h1a1 1 0 0 1 1 1"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M20 16v-8l3 8v-8"/>
|
||||
<path d="M15 8a2 2 0 0 1 2 2v4a2 2 0 1 1 -4 0v-4a2 2 0 0 1 2 -2z"/>
|
||||
<path d="M1 8h3v6.5a1.5 1.5 0 0 1 -3 0v-.5"/>
|
||||
<path d="M7 15a1 1 0 0 0 1 1h1a1 1 0 0 0 1 -1v-2a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1h1a1 1 0 0 1 1 1"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".ts"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-type-ts" 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>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-1"></path>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M9 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path>
|
||||
<path d="M3.5 15h3"></path>
|
||||
<path d="M5 15v6"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-1"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M9 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"/>
|
||||
<path d="M3.5 15h3"/>
|
||||
<path d="M5 15v6"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".sql"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-type-sql" 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>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M5 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"></path>
|
||||
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"></path>
|
||||
<path d="M18 15v6h2"></path>
|
||||
<path d="M13 15a2 2 0 0 1 2 2v2a2 2 0 1 1 -4 0v-2a2 2 0 0 1 2 -2z"></path>
|
||||
<path d="M14 20l1.5 1.5"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M5 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"/>
|
||||
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"/>
|
||||
<path d="M18 15v6h2"/>
|
||||
<path d="M13 15a2 2 0 0 1 2 2v2a2 2 0 1 1 -4 0v-2a2 2 0 0 1 2 -2z"/>
|
||||
<path d="M14 20l1.5 1.5"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".db" ".sqlite" ".bak" ".mdb"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-database" 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>
|
||||
<path d="M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0"></path>
|
||||
<path d="M4 6v6a8 3 0 0 0 16 0v-6"></path>
|
||||
<path d="M4 12v6a8 3 0 0 0 16 0v-6"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0"/>
|
||||
<path d="M4 6v6a8 3 0 0 0 16 0v-6"/>
|
||||
<path d="M4 12v6a8 3 0 0 0 16 0v-6"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".eml" ".email" ".mailbox" ".mbox" ".msg"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-mail" 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>
|
||||
<path d="M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10z"></path>
|
||||
<path d="M3 7l9 6l9 -6"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10z"/>
|
||||
<path d="M3 7l9 6l9 -6"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".crt" ".pem" ".x509" ".cer" ".ca-bundle"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-certificate" 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>
|
||||
<path d="M15 15m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
|
||||
<path d="M13 17.5v4.5l2 -1.5l2 1.5v-4.5"></path>
|
||||
<path d="M10 19h-5a2 2 0 0 1 -2 -2v-10c0 -1.1 .9 -2 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -1 1.73"></path>
|
||||
<path d="M6 9l12 0"></path>
|
||||
<path d="M6 12l3 0"></path>
|
||||
<path d="M6 15l2 0"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M15 15m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"/>
|
||||
<path d="M13 17.5v4.5l2 -1.5l2 1.5v-4.5"/>
|
||||
<path d="M10 19h-5a2 2 0 0 1 -2 -2v-10c0 -1.1 .9 -2 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -1 1.73"/>
|
||||
<path d="M6 9l12 0"/>
|
||||
<path d="M6 12l3 0"/>
|
||||
<path d="M6 15l2 0"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".key" ".keystore" ".jks" ".p12" ".pfx" ".pub"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-key" 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>
|
||||
<path d="M16.555 3.843l3.602 3.602a2.877 2.877 0 0 1 0 4.069l-2.643 2.643a2.877 2.877 0 0 1 -4.069 0l-.301 -.301l-6.558 6.558a2 2 0 0 1 -1.239 .578l-.175 .008h-1.172a1 1 0 0 1 -.993 -.883l-.007 -.117v-1.172a2 2 0 0 1 .467 -1.284l.119 -.13l.414 -.414h2v-2h2v-2l2.144 -2.144l-.301 -.301a2.877 2.877 0 0 1 0 -4.069l2.643 -2.643a2.877 2.877 0 0 1 4.069 0z"></path>
|
||||
<path d="M15 9h.01"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M16.555 3.843l3.602 3.602a2.877 2.877 0 0 1 0 4.069l-2.643 2.643a2.877 2.877 0 0 1 -4.069 0l-.301 -.301l-6.558 6.558a2 2 0 0 1 -1.239 .578l-.175 .008h-1.172a1 1 0 0 1 -.993 -.883l-.007 -.117v-1.172a2 2 0 0 1 .467 -1.284l.119 -.13l.414 -.414h2v-2h2v-2l2.144 -2.144l-.301 -.301a2.877 2.877 0 0 1 0 -4.069l2.643 -2.643a2.877 2.877 0 0 1 4.069 0z"/>
|
||||
<path d="M15 9h.01"/>
|
||||
</svg>
|
||||
{{- else}}
|
||||
{{- if .IsSymlink}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-symlink" 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>
|
||||
<path d="M4 21v-4a3 3 0 0 1 3 -3h5"></path>
|
||||
<path d="M9 17l3 -3l-3 -3"></path>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M5 11v-6a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-9.5"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M4 21v-4a3 3 0 0 1 3 -3h5"/>
|
||||
<path d="M9 17l3 -3l-3 -3"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M5 11v-6a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-9.5"/>
|
||||
</svg>
|
||||
{{- else}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file" 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>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"/>
|
||||
</svg>
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
@@ -291,6 +299,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>{{html .Name}}</title>
|
||||
<link rel="canonical" href="{{.Path}}/" />
|
||||
<meta charset="utf-8">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -789,19 +798,19 @@ footer {
|
||||
</div>
|
||||
<a href="javascript:queryParam('layout', '')" id="layout-list" class='layout{{if eq $.Layout "list" ""}}current{{end}}'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-layout-list" width="16" height="16" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<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>
|
||||
<path d="M4 14m0 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>
|
||||
<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 14m0 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"/>
|
||||
</svg>
|
||||
List
|
||||
</a>
|
||||
<a href="javascript:queryParam('layout', 'grid')" id="layout-grid" class='layout{{if eq $.Layout "grid"}}current{{end}}'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-layout-grid" width="16" height="16" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<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>
|
||||
<path d="M14 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>
|
||||
<path d="M4 14m0 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>
|
||||
<path d="M14 14m0 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>
|
||||
<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="M14 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 14m0 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="M14 14m0 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"/>
|
||||
</svg>
|
||||
Grid
|
||||
</a>
|
||||
@@ -826,22 +835,22 @@ footer {
|
||||
{{- if and (eq .Sort "namedirfirst") (ne .Order "desc")}}
|
||||
<a href="?sort=namedirfirst&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}" class="icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-up" 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>
|
||||
<path d="M18 14l-6 -6l-6 6h12"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M18 14l-6 -6l-6 6h12"/>
|
||||
</svg>
|
||||
</a>
|
||||
{{- else if and (eq .Sort "namedirfirst") (ne .Order "asc")}}
|
||||
<a href="?sort=namedirfirst&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}" class="icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-down" 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>
|
||||
<path d="M6 10l6 6l6 -6h-12"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 10l6 6l6 -6h-12"/>
|
||||
</svg>
|
||||
</a>
|
||||
{{- else}}
|
||||
<a href="?sort=namedirfirst&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}" class="icon sort">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-up" 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>
|
||||
<path d="M18 14l-6 -6l-6 6h12"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M18 14l-6 -6l-6 6h12"/>
|
||||
</svg>
|
||||
</a>
|
||||
{{- end}}
|
||||
@@ -850,16 +859,16 @@ footer {
|
||||
<a href="?sort=name&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
|
||||
Name
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-up" 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>
|
||||
<path d="M18 14l-6 -6l-6 6h12"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M18 14l-6 -6l-6 6h12"/>
|
||||
</svg>
|
||||
</a>
|
||||
{{- else if and (eq .Sort "name") (ne .Order "asc")}}
|
||||
<a href="?sort=name&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
|
||||
Name
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-down" 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>
|
||||
<path d="M6 10l6 6l6 -6h-12"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 10l6 6l6 -6h-12"/>
|
||||
</svg>
|
||||
</a>
|
||||
{{- else}}
|
||||
@@ -870,11 +879,11 @@ footer {
|
||||
|
||||
<div class="filter-container">
|
||||
<svg id="search-icon" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-search" 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>
|
||||
<path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0"></path>
|
||||
<path d="M21 21l-6 -6"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0"/>
|
||||
<path d="M21 21l-6 -6"/>
|
||||
</svg>
|
||||
<input type="text" placeholder="Search" id="filter" onkeyup='filter()'>
|
||||
<input type="search" placeholder="Search" id="filter" onkeyup='filter()'>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
@@ -882,16 +891,16 @@ footer {
|
||||
<a href="?sort=size&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
|
||||
Size
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-up" 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>
|
||||
<path d="M18 14l-6 -6l-6 6h12"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M18 14l-6 -6l-6 6h12"/>
|
||||
</svg>
|
||||
</a>
|
||||
{{- else if and (eq .Sort "size") (ne .Order "asc")}}
|
||||
<a href="?sort=size&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
|
||||
Size
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-down" 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>
|
||||
<path d="M6 10l6 6l6 -6h-12"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 10l6 6l6 -6h-12"/>
|
||||
</svg>
|
||||
</a>
|
||||
{{- else}}
|
||||
@@ -905,16 +914,16 @@ footer {
|
||||
<a href="?sort=time&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
|
||||
Modified
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-up" 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>
|
||||
<path d="M18 14l-6 -6l-6 6h12"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M18 14l-6 -6l-6 6h12"/>
|
||||
</svg>
|
||||
</a>
|
||||
{{- else if and (eq .Sort "time") (ne .Order "asc")}}
|
||||
<a href="?sort=time&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
|
||||
Modified
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-down" 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>
|
||||
<path d="M6 10l6 6l6 -6h-12"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 10l6 6l6 -6h-12"/>
|
||||
</svg>
|
||||
</a>
|
||||
{{- else}}
|
||||
@@ -933,8 +942,8 @@ footer {
|
||||
<td>
|
||||
<a href="..">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-corner-left-up" 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>
|
||||
<path d="M18 18h-6a3 3 0 0 1 -3 -3v-10l-4 4m8 0l-4 -4"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M18 18h-6a3 3 0 0 1 -3 -3v-10l-4 4m8 0l-4 -4"/>
|
||||
</svg>
|
||||
<span class="go-up">Up</span>
|
||||
</a>
|
||||
@@ -1075,7 +1084,10 @@ footer {
|
||||
});
|
||||
document.querySelectorAll('.size').forEach(el => {
|
||||
const size = Number(el.dataset.size);
|
||||
el.querySelector('.sizebar-bar').style.width = `${size/largest * 100}%`;
|
||||
const sizebar = el.querySelector('.sizebar-bar');
|
||||
if (sizebar) {
|
||||
sizebar.style.width = `${size/largest * 100}%`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ package fileserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
@@ -31,13 +32,14 @@ import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
||||
caddytpl "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddycmd.RegisterCommand(caddycmd.Command{
|
||||
Name: "file-server",
|
||||
Usage: "[--domain <example.com>] [--root <path>] [--listen <addr>] [--browse] [--access-log]",
|
||||
Usage: "[--domain <example.com>] [--root <path>] [--listen <addr>] [--browse] [--access-log] [--precompressed]",
|
||||
Short: "Spins up a production-ready file server",
|
||||
Long: `
|
||||
A simple but production-ready file server. Useful for quick deployments,
|
||||
@@ -50,23 +52,28 @@ will be changed to the HTTPS port and the server will use HTTPS. If using
|
||||
a public domain, ensure A/AAAA records are properly configured before
|
||||
using this option.
|
||||
|
||||
By default, Zstandard and Gzip compression are enabled. Use --no-compress
|
||||
to disable compression.
|
||||
|
||||
If --browse is enabled, requests for folders without an index file will
|
||||
respond with a file listing.`,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringP("domain", "d", "", "Domain name at which to serve the files")
|
||||
cmd.Flags().StringP("root", "r", "", "The path to the root of the site")
|
||||
cmd.Flags().StringP("listen", "", "", "The address to which to bind the listener")
|
||||
cmd.Flags().StringP("listen", "l", "", "The address to which to bind the listener")
|
||||
cmd.Flags().BoolP("browse", "b", false, "Enable directory browsing")
|
||||
cmd.Flags().BoolP("templates", "t", false, "Enable template rendering")
|
||||
cmd.Flags().BoolP("access-log", "", false, "Enable the access log")
|
||||
cmd.Flags().BoolP("access-log", "a", false, "Enable the access log")
|
||||
cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
|
||||
cmd.Flags().BoolP("no-compress", "", false, "Disable Zstandard and Gzip compression")
|
||||
cmd.Flags().StringSliceP("precompressed", "p", []string{}, "Specify precompression file extensions. Compression preference implied from flag order.")
|
||||
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdFileServer)
|
||||
cmd.AddCommand(&cobra.Command{
|
||||
Use: "export-template",
|
||||
Short: "Exports the default file browser template",
|
||||
Example: "caddy file-server export-template > browse.html",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
_, err := io.WriteString(os.Stdout, defaultBrowseTemplate)
|
||||
_, err := io.WriteString(os.Stdout, BrowseTemplate)
|
||||
return err
|
||||
},
|
||||
})
|
||||
@@ -84,15 +91,64 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
|
||||
templates := fs.Bool("templates")
|
||||
accessLog := fs.Bool("access-log")
|
||||
debug := fs.Bool("debug")
|
||||
compress := !fs.Bool("no-compress")
|
||||
precompressed, err := fs.GetStringSlice("precompressed")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid precompressed flag: %v", err)
|
||||
}
|
||||
|
||||
var handlers []json.RawMessage
|
||||
|
||||
if compress {
|
||||
zstd, err := caddy.GetModule("http.encoders.zstd")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
gzip, err := caddy.GetModule("http.encoders.gzip")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
handlers = append(handlers, caddyconfig.JSONModuleObject(encode.Encode{
|
||||
EncodingsRaw: caddy.ModuleMap{
|
||||
"zstd": caddyconfig.JSON(zstd.New(), nil),
|
||||
"gzip": caddyconfig.JSON(gzip.New(), nil),
|
||||
},
|
||||
Prefer: []string{"zstd", "gzip"},
|
||||
}, "handler", "encode", nil))
|
||||
}
|
||||
|
||||
if templates {
|
||||
handler := caddytpl.Templates{FileRoot: root}
|
||||
handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "templates", nil))
|
||||
}
|
||||
|
||||
handler := FileServer{Root: root}
|
||||
|
||||
if len(precompressed) != 0 {
|
||||
// logic mirrors modules/caddyhttp/fileserver/caddyfile.go case "precompressed"
|
||||
var order []string
|
||||
for _, compression := range precompressed {
|
||||
modID := "http.precompressed." + compression
|
||||
mod, err := caddy.GetModule(modID)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("getting module named '%s': %v", modID, err)
|
||||
}
|
||||
inst := mod.New()
|
||||
precompress, ok := inst.(encode.Precompressed)
|
||||
if !ok {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("module %s is not a precompressor; is %T", modID, inst)
|
||||
}
|
||||
if handler.PrecompressedRaw == nil {
|
||||
handler.PrecompressedRaw = make(caddy.ModuleMap)
|
||||
}
|
||||
handler.PrecompressedRaw[compression] = caddyconfig.JSON(precompress, nil)
|
||||
order = append(order, compression)
|
||||
}
|
||||
handler.PrecompressedOrder = order
|
||||
}
|
||||
|
||||
if browse {
|
||||
handler.Browse = new(Browse)
|
||||
}
|
||||
@@ -154,7 +210,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
err := caddy.Run(cfg)
|
||||
err = caddy.Run(cfg)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterNamespace("http.precompressed", []interface{}{
|
||||
(*encode.Precompressed)(nil),
|
||||
})
|
||||
|
||||
caddy.RegisterModule(FileServer{})
|
||||
}
|
||||
|
||||
@@ -60,7 +64,23 @@ func init() {
|
||||
// requested directory does not have an index file, Caddy writes a
|
||||
// 404 response. Alternatively, file browsing can be enabled with
|
||||
// the "browse" parameter which shows a list of files when directories
|
||||
// are requested if no index file is present.
|
||||
// are requested if no index file is present. If "browse" is enabled,
|
||||
// Caddy may serve a JSON array of the dirctory listing when the `Accept`
|
||||
// header mentions `application/json` with the following structure:
|
||||
//
|
||||
// [{
|
||||
// "name": "",
|
||||
// "size": 0,
|
||||
// "url": "",
|
||||
// "mod_time": "",
|
||||
// "mode": 0,
|
||||
// "is_dir": false,
|
||||
// "is_symlink": false
|
||||
// }]
|
||||
//
|
||||
// with the `url` being relative to the request path and `mod_time` in the RFC 3339 format
|
||||
// with sub-second precision. For any other value for the `Accept` header, the
|
||||
// respective browse template is executed with `Content-Type: text/html`.
|
||||
//
|
||||
// By default, this handler will canonicalize URIs so that requests to
|
||||
// directories end with a slash, but requests to regular files do not.
|
||||
|
||||
@@ -16,10 +16,11 @@ package caddyhttp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
@@ -42,7 +43,11 @@ func init() {
|
||||
//
|
||||
// This listener wrapper must be placed BEFORE the "tls" listener
|
||||
// wrapper, for it to work properly.
|
||||
type HTTPRedirectListenerWrapper struct{}
|
||||
type HTTPRedirectListenerWrapper struct {
|
||||
// MaxHeaderBytes is the maximum size to parse from a client's
|
||||
// HTTP request headers. Default: 1 MB
|
||||
MaxHeaderBytes int64 `json:"max_header_bytes,omitempty"`
|
||||
}
|
||||
|
||||
func (HTTPRedirectListenerWrapper) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
@@ -56,7 +61,7 @@ func (h *HTTPRedirectListenerWrapper) UnmarshalCaddyfile(d *caddyfile.Dispenser)
|
||||
}
|
||||
|
||||
func (h *HTTPRedirectListenerWrapper) WrapListener(l net.Listener) net.Listener {
|
||||
return &httpRedirectListener{l}
|
||||
return &httpRedirectListener{l, h.MaxHeaderBytes}
|
||||
}
|
||||
|
||||
// httpRedirectListener is listener that checks the first few bytes
|
||||
@@ -64,6 +69,7 @@ func (h *HTTPRedirectListenerWrapper) WrapListener(l net.Listener) net.Listener
|
||||
// to respond to an HTTP request with a redirect.
|
||||
type httpRedirectListener struct {
|
||||
net.Listener
|
||||
maxHeaderBytes int64
|
||||
}
|
||||
|
||||
// Accept waits for and returns the next connection to the listener,
|
||||
@@ -74,16 +80,23 @@ func (l *httpRedirectListener) Accept() (net.Conn, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maxHeaderBytes := l.maxHeaderBytes
|
||||
if maxHeaderBytes == 0 {
|
||||
maxHeaderBytes = 1024 * 1024
|
||||
}
|
||||
|
||||
return &httpRedirectConn{
|
||||
Conn: c,
|
||||
r: bufio.NewReader(c),
|
||||
Conn: c,
|
||||
limit: maxHeaderBytes,
|
||||
r: bufio.NewReader(c),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type httpRedirectConn struct {
|
||||
net.Conn
|
||||
once sync.Once
|
||||
r *bufio.Reader
|
||||
once bool
|
||||
limit int64
|
||||
r *bufio.Reader
|
||||
}
|
||||
|
||||
// Read tries to peek at the first few bytes of the request, and if we get
|
||||
@@ -91,53 +104,58 @@ type httpRedirectConn struct {
|
||||
// like an HTTP request, then we perform a HTTP->HTTPS redirect on the same
|
||||
// port as the original connection.
|
||||
func (c *httpRedirectConn) Read(p []byte) (int, error) {
|
||||
var errReturn error
|
||||
c.once.Do(func() {
|
||||
firstBytes, err := c.r.Peek(5)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if c.once {
|
||||
return c.r.Read(p)
|
||||
}
|
||||
// no need to use sync.Once - net.Conn is not read from concurrently.
|
||||
c.once = true
|
||||
|
||||
// If the request doesn't look like HTTP, then it's probably
|
||||
// TLS bytes and we don't need to do anything.
|
||||
if !firstBytesLookLikeHTTP(firstBytes) {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the HTTP request, so we can get the Host and URL to redirect to.
|
||||
req, err := http.ReadRequest(c.r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Build the redirect response, using the same Host and URL,
|
||||
// but replacing the scheme with https.
|
||||
headers := make(http.Header)
|
||||
headers.Add("Location", "https://"+req.Host+req.URL.String())
|
||||
resp := &http.Response{
|
||||
Proto: "HTTP/1.0",
|
||||
Status: "308 Permanent Redirect",
|
||||
StatusCode: 308,
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 0,
|
||||
Header: headers,
|
||||
}
|
||||
|
||||
err = resp.Write(c.Conn)
|
||||
if err != nil {
|
||||
errReturn = fmt.Errorf("couldn't write HTTP->HTTPS redirect")
|
||||
return
|
||||
}
|
||||
|
||||
errReturn = fmt.Errorf("redirected HTTP request on HTTPS port")
|
||||
c.Conn.Close()
|
||||
})
|
||||
|
||||
if errReturn != nil {
|
||||
return 0, errReturn
|
||||
firstBytes, err := c.r.Peek(5)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return c.r.Read(p)
|
||||
// If the request doesn't look like HTTP, then it's probably
|
||||
// TLS bytes, and we don't need to do anything.
|
||||
if !firstBytesLookLikeHTTP(firstBytes) {
|
||||
return c.r.Read(p)
|
||||
}
|
||||
|
||||
// From now on, we can be almost certain the request is HTTP.
|
||||
// The returned error will be non nil and caller are expected to
|
||||
// close the connection.
|
||||
|
||||
// Set the read limit, io.MultiReader is needed because
|
||||
// when resetting, *bufio.Reader discards buffered data.
|
||||
buffered, _ := c.r.Peek(c.r.Buffered())
|
||||
mr := io.MultiReader(bytes.NewReader(buffered), c.Conn)
|
||||
c.r.Reset(io.LimitReader(mr, c.limit))
|
||||
|
||||
// Parse the HTTP request, so we can get the Host and URL to redirect to.
|
||||
req, err := http.ReadRequest(c.r)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("couldn't read HTTP request")
|
||||
}
|
||||
|
||||
// Build the redirect response, using the same Host and URL,
|
||||
// but replacing the scheme with https.
|
||||
headers := make(http.Header)
|
||||
headers.Add("Location", "https://"+req.Host+req.URL.String())
|
||||
resp := &http.Response{
|
||||
Proto: "HTTP/1.0",
|
||||
Status: "308 Permanent Redirect",
|
||||
StatusCode: 308,
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 0,
|
||||
Header: headers,
|
||||
}
|
||||
|
||||
err = resp.Write(c.Conn)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("couldn't write HTTP->HTTPS redirect")
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("redirected HTTP request on HTTPS port")
|
||||
}
|
||||
|
||||
// firstBytesLookLikeHTTP reports whether a TLS record header
|
||||
|
||||
@@ -151,6 +151,18 @@ func (e *ExtraLogFields) Add(field zap.Field) {
|
||||
e.fields = append(e.fields, field)
|
||||
}
|
||||
|
||||
// Set sets a field in the list of extra fields to log.
|
||||
// If the field already exists, it is replaced.
|
||||
func (e *ExtraLogFields) Set(field zap.Field) {
|
||||
for i := range e.fields {
|
||||
if e.fields[i].Key == field.Key {
|
||||
e.fields[i] = field
|
||||
return
|
||||
}
|
||||
}
|
||||
e.fields = append(e.fields, field)
|
||||
}
|
||||
|
||||
const (
|
||||
// Variable name used to indicate that this request
|
||||
// should be omitted from the access logs
|
||||
|
||||
@@ -197,6 +197,8 @@ type (
|
||||
// where each of the array elements is a matcher set, i.e. an
|
||||
// object keyed by matcher name.
|
||||
MatchNot struct {
|
||||
// A `matcher` should implement the following interfaces:
|
||||
// - [caddyhttp.RequestMatcher](https://pkg.go.dev/github.com/caddyserver/caddy/v2/modules/caddyhttp?tab=doc#RequestMatcher)
|
||||
MatcherSetsRaw []caddy.ModuleMap `json:"-" caddy:"namespace=http.matchers"`
|
||||
MatcherSets []MatcherSet `json:"-"`
|
||||
}
|
||||
@@ -212,6 +214,9 @@ func init() {
|
||||
caddy.RegisterModule(MatchHeaderRE{})
|
||||
caddy.RegisterModule(new(MatchProtocol))
|
||||
caddy.RegisterModule(MatchNot{})
|
||||
caddy.RegisterNamespace("http.matchers", []interface{}{
|
||||
(*RequestMatcher)(nil),
|
||||
})
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
package proxyprotocol
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/mastercactapus/proxyprotocol"
|
||||
goproxy "github.com/pires/go-proxyproto"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
@@ -38,32 +38,74 @@ type ListenerWrapper struct {
|
||||
// Allow is an optional list of CIDR ranges to
|
||||
// allow/require PROXY headers from.
|
||||
Allow []string `json:"allow,omitempty"`
|
||||
allow []netip.Prefix
|
||||
|
||||
rules []proxyprotocol.Rule
|
||||
// Denby is an optional list of CIDR ranges to
|
||||
// deny PROXY headers from.
|
||||
Deny []string `json:"deny,omitempty"`
|
||||
deny []netip.Prefix
|
||||
|
||||
// Accepted values are: ignore, use, reject, require, skip
|
||||
// default: ignore
|
||||
// Policy definitions are here: https://pkg.go.dev/github.com/pires/go-proxyproto@v0.7.0#Policy
|
||||
FallbackPolicy Policy `json:"fallback_policy,omitempty"`
|
||||
|
||||
policy goproxy.PolicyFunc
|
||||
}
|
||||
|
||||
// Provision sets up the listener wrapper.
|
||||
func (pp *ListenerWrapper) Provision(ctx caddy.Context) error {
|
||||
rules := make([]proxyprotocol.Rule, 0, len(pp.Allow))
|
||||
for _, s := range pp.Allow {
|
||||
_, n, err := net.ParseCIDR(s)
|
||||
for _, cidr := range pp.Allow {
|
||||
ipnet, err := netip.ParsePrefix(cidr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid subnet '%s': %w", s, err)
|
||||
return err
|
||||
}
|
||||
rules = append(rules, proxyprotocol.Rule{
|
||||
Timeout: time.Duration(pp.Timeout),
|
||||
Subnet: n,
|
||||
})
|
||||
pp.allow = append(pp.allow, ipnet)
|
||||
}
|
||||
for _, cidr := range pp.Deny {
|
||||
ipnet, err := netip.ParsePrefix(cidr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pp.deny = append(pp.deny, ipnet)
|
||||
}
|
||||
pp.policy = func(upstream net.Addr) (goproxy.Policy, error) {
|
||||
// trust unix sockets
|
||||
if network := upstream.Network(); caddy.IsUnixNetwork(network) {
|
||||
return goproxy.USE, nil
|
||||
}
|
||||
ret := pp.FallbackPolicy
|
||||
host, _, err := net.SplitHostPort(upstream.String())
|
||||
if err != nil {
|
||||
return goproxy.REJECT, err
|
||||
}
|
||||
|
||||
pp.rules = rules
|
||||
|
||||
ip, err := netip.ParseAddr(host)
|
||||
if err != nil {
|
||||
return goproxy.REJECT, err
|
||||
}
|
||||
for _, ipnet := range pp.deny {
|
||||
if ipnet.Contains(ip) {
|
||||
return goproxy.REJECT, nil
|
||||
}
|
||||
}
|
||||
for _, ipnet := range pp.allow {
|
||||
if ipnet.Contains(ip) {
|
||||
ret = PolicyUSE
|
||||
break
|
||||
}
|
||||
}
|
||||
return policyToGoProxyPolicy[ret], nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WrapListener adds PROXY protocol support to the listener.
|
||||
func (pp *ListenerWrapper) WrapListener(l net.Listener) net.Listener {
|
||||
pl := proxyprotocol.NewListener(l, time.Duration(pp.Timeout))
|
||||
pl.SetFilter(pp.rules)
|
||||
pl := &goproxy.Listener{
|
||||
Listener: l,
|
||||
ReadHeaderTimeout: time.Duration(pp.Timeout),
|
||||
}
|
||||
pl.Policy = pp.policy
|
||||
return pl
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ func (ListenerWrapper) CaddyModule() caddy.ModuleInfo {
|
||||
// proxy_protocol {
|
||||
// timeout <duration>
|
||||
// allow <IPs...>
|
||||
// deny <IPs...>
|
||||
// fallback_policy <policy>
|
||||
// }
|
||||
func (w *ListenerWrapper) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
for d.Next() {
|
||||
@@ -57,7 +59,17 @@ func (w *ListenerWrapper) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
|
||||
case "allow":
|
||||
w.Allow = append(w.Allow, d.RemainingArgs()...)
|
||||
|
||||
case "deny":
|
||||
w.Deny = append(w.Deny, d.RemainingArgs()...)
|
||||
case "fallback_policy":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
p, err := parsePolicy(d.Val())
|
||||
if err != nil {
|
||||
return d.WrapErr(err)
|
||||
}
|
||||
w.FallbackPolicy = p
|
||||
default:
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package proxyprotocol
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
goproxy "github.com/pires/go-proxyproto"
|
||||
)
|
||||
|
||||
type Policy int
|
||||
|
||||
// as defined in: https://pkg.go.dev/github.com/pires/go-proxyproto@v0.7.0#Policy
|
||||
const (
|
||||
// IGNORE address from PROXY header, but accept connection
|
||||
PolicyIGNORE Policy = iota
|
||||
// USE address from PROXY header
|
||||
PolicyUSE
|
||||
// REJECT connection when PROXY header is sent
|
||||
// Note: even though the first read on the connection returns an error if
|
||||
// a PROXY header is present, subsequent reads do not. It is the task of
|
||||
// the code using the connection to handle that case properly.
|
||||
PolicyREJECT
|
||||
// REQUIRE connection to send PROXY header, reject if not present
|
||||
// Note: even though the first read on the connection returns an error if
|
||||
// a PROXY header is not present, subsequent reads do not. It is the task
|
||||
// of the code using the connection to handle that case properly.
|
||||
PolicyREQUIRE
|
||||
// SKIP accepts a connection without requiring the PROXY header
|
||||
// Note: an example usage can be found in the SkipProxyHeaderForCIDR
|
||||
// function.
|
||||
PolicySKIP
|
||||
)
|
||||
|
||||
var policyToGoProxyPolicy = map[Policy]goproxy.Policy{
|
||||
PolicyUSE: goproxy.USE,
|
||||
PolicyIGNORE: goproxy.IGNORE,
|
||||
PolicyREJECT: goproxy.REJECT,
|
||||
PolicyREQUIRE: goproxy.REQUIRE,
|
||||
PolicySKIP: goproxy.SKIP,
|
||||
}
|
||||
|
||||
var policyMap = map[Policy]string{
|
||||
PolicyUSE: "USE",
|
||||
PolicyIGNORE: "IGNORE",
|
||||
PolicyREJECT: "REJECT",
|
||||
PolicyREQUIRE: "REQUIRE",
|
||||
PolicySKIP: "SKIP",
|
||||
}
|
||||
|
||||
var policyMapRev = map[string]Policy{
|
||||
"USE": PolicyUSE,
|
||||
"IGNORE": PolicyIGNORE,
|
||||
"REJECT": PolicyREJECT,
|
||||
"REQUIRE": PolicyREQUIRE,
|
||||
"SKIP": PolicySKIP,
|
||||
}
|
||||
|
||||
// MarshalText implements the text marshaller method.
|
||||
func (x Policy) MarshalText() ([]byte, error) {
|
||||
return []byte(policyMap[x]), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the text unmarshaller method.
|
||||
func (x *Policy) UnmarshalText(text []byte) error {
|
||||
name := string(text)
|
||||
tmp, err := parsePolicy(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*x = tmp
|
||||
return nil
|
||||
}
|
||||
|
||||
func parsePolicy(name string) (Policy, error) {
|
||||
if x, ok := policyMapRev[strings.ToUpper(name)]; ok {
|
||||
return x, nil
|
||||
}
|
||||
return Policy(0), fmt.Errorf("%s is %w", name, errInvalidPolicy)
|
||||
}
|
||||
|
||||
var errInvalidPolicy = errors.New("invalid policy")
|
||||
@@ -40,6 +40,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
@@ -157,9 +158,17 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
||||
case "http.request.duration_ms":
|
||||
start := GetVar(req.Context(), "start_time").(time.Time)
|
||||
return time.Since(start).Seconds() * 1e3, true // multiply seconds to preserve decimal (see #4666)
|
||||
|
||||
case "http.request.uuid":
|
||||
// fetch the UUID for this request
|
||||
id := GetVar(req.Context(), "uuid").(*requestID)
|
||||
|
||||
// set it to this request's access log
|
||||
extra := req.Context().Value(ExtraLogFieldsCtxKey).(*ExtraLogFields)
|
||||
extra.Set(zap.String("uuid", id.String()))
|
||||
|
||||
return id.String(), true
|
||||
|
||||
case "http.request.body":
|
||||
if req.Body == nil {
|
||||
return "", true
|
||||
|
||||
@@ -90,6 +90,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
||||
// max_buffer_size <size>
|
||||
// stream_timeout <duration>
|
||||
// stream_close_delay <duration>
|
||||
// trace_logs
|
||||
//
|
||||
// # request manipulation
|
||||
// trusted_proxies [private_ranges] <ranges...>
|
||||
@@ -155,6 +156,18 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
// the underlying JSON does not yet support different
|
||||
// transports (protocols or schemes) to each backend,
|
||||
// so we remember the last one we see and compare them
|
||||
|
||||
switch pa.scheme {
|
||||
case "wss":
|
||||
return d.Errf("the scheme wss:// is only supported in browsers; use https:// instead")
|
||||
case "ws":
|
||||
return d.Errf("the scheme ws:// is only supported in browsers; use http:// instead")
|
||||
case "https", "http", "h2c", "":
|
||||
// Do nothing or handle the valid schemes
|
||||
default:
|
||||
return d.Errf("unsupported URL scheme %s://", pa.scheme)
|
||||
}
|
||||
|
||||
if commonScheme != "" && pa.scheme != commonScheme {
|
||||
return d.Errf("for now, all proxy upstreams must use the same scheme (transport protocol); expecting '%s://' but got '%s://'",
|
||||
commonScheme, pa.scheme)
|
||||
@@ -193,7 +206,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
for _, up := range d.RemainingArgs() {
|
||||
err := appendUpstream(up)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("parsing upstream '%s': %w", up, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +230,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
for _, up := range args {
|
||||
err := appendUpstream(up)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("parsing upstream '%s': %w", up, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,7 +373,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
if len(values) == 0 {
|
||||
values = append(values, "")
|
||||
}
|
||||
healthHeaders[key] = values
|
||||
healthHeaders[key] = append(healthHeaders[key], values...)
|
||||
}
|
||||
if h.HealthChecks == nil {
|
||||
h.HealthChecks = new(HealthChecks)
|
||||
@@ -539,17 +552,24 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
size, err := humanize.ParseBytes(d.Val())
|
||||
if err != nil {
|
||||
return d.Errf("invalid byte size '%s': %v", d.Val(), err)
|
||||
val := d.Val()
|
||||
var size int64
|
||||
if val == "unlimited" {
|
||||
size = -1
|
||||
} else {
|
||||
usize, err := humanize.ParseBytes(val)
|
||||
if err != nil {
|
||||
return d.Errf("invalid byte size '%s': %v", val, err)
|
||||
}
|
||||
size = int64(usize)
|
||||
}
|
||||
if d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
if subdir == "request_buffers" {
|
||||
h.RequestBuffers = int64(size)
|
||||
h.RequestBuffers = size
|
||||
} else if subdir == "response_buffers" {
|
||||
h.ResponseBuffers = int64(size)
|
||||
h.ResponseBuffers = size
|
||||
}
|
||||
|
||||
// TODO: These three properties are deprecated; remove them sometime after v2.6.4
|
||||
@@ -763,6 +783,12 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
responseHandler,
|
||||
)
|
||||
|
||||
case "verbose_logs":
|
||||
if h.VerboseLogs {
|
||||
return d.Err("verbose_logs already specified")
|
||||
}
|
||||
h.VerboseLogs = true
|
||||
|
||||
default:
|
||||
return d.Errf("unrecognized subdirective %s", d.Val())
|
||||
}
|
||||
|
||||
@@ -358,11 +358,17 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre
|
||||
}
|
||||
ctx = context.WithValue(ctx, caddyhttp.OriginalRequestCtxKey, *req)
|
||||
req = req.WithContext(ctx)
|
||||
for key, hdrs := range h.HealthChecks.Active.Headers {
|
||||
|
||||
// set headers, using a replacer with only globals (env vars, system info, etc.)
|
||||
repl := caddy.NewReplacer()
|
||||
for key, vals := range h.HealthChecks.Active.Headers {
|
||||
key = repl.ReplaceAll(key, "")
|
||||
if key == "Host" {
|
||||
req.Host = h.HealthChecks.Active.Headers.Get(key)
|
||||
} else {
|
||||
req.Header[key] = hdrs
|
||||
req.Host = repl.ReplaceAll(h.HealthChecks.Active.Headers.Get(key), "")
|
||||
continue
|
||||
}
|
||||
for _, val := range vals {
|
||||
req.Header.Add(key, repl.ReplaceKnown(val, ""))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mastercactapus/proxyprotocol"
|
||||
"github.com/pires/go-proxyproto"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/net/http2"
|
||||
|
||||
@@ -207,44 +207,42 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to get proxy protocol info from context")
|
||||
}
|
||||
|
||||
// The src and dst have to be of the some address family. As we don't know the original
|
||||
// dst address (it's kind of impossible to know) and this address is generelly of very
|
||||
header := proxyproto.Header{
|
||||
SourceAddr: &net.TCPAddr{
|
||||
IP: proxyProtocolInfo.AddrPort.Addr().AsSlice(),
|
||||
Port: int(proxyProtocolInfo.AddrPort.Port()),
|
||||
Zone: proxyProtocolInfo.AddrPort.Addr().Zone(),
|
||||
},
|
||||
}
|
||||
// The src and dst have to be of the same address family. As we don't know the original
|
||||
// dst address (it's kind of impossible to know) and this address is generally of very
|
||||
// little interest, we just set it to all zeros.
|
||||
var destIP net.IP
|
||||
switch {
|
||||
case proxyProtocolInfo.AddrPort.Addr().Is4():
|
||||
destIP = net.IPv4zero
|
||||
header.TransportProtocol = proxyproto.TCPv4
|
||||
header.DestinationAddr = &net.TCPAddr{
|
||||
IP: net.IPv4zero,
|
||||
}
|
||||
case proxyProtocolInfo.AddrPort.Addr().Is6():
|
||||
destIP = net.IPv6zero
|
||||
header.TransportProtocol = proxyproto.TCPv6
|
||||
header.DestinationAddr = &net.TCPAddr{
|
||||
IP: net.IPv6zero,
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected remote addr type in proxy protocol info")
|
||||
}
|
||||
|
||||
// TODO: We should probably migrate away from net.IP to use netip.Addr,
|
||||
// but due to the upstream dependency, we can't do that yet.
|
||||
switch h.ProxyProtocol {
|
||||
case "v1":
|
||||
header := proxyprotocol.HeaderV1{
|
||||
SrcIP: net.IP(proxyProtocolInfo.AddrPort.Addr().AsSlice()),
|
||||
SrcPort: int(proxyProtocolInfo.AddrPort.Port()),
|
||||
DestIP: destIP,
|
||||
DestPort: 0,
|
||||
}
|
||||
header.Version = 1
|
||||
caddyCtx.Logger().Debug("sending proxy protocol header v1", zap.Any("header", header))
|
||||
_, err = header.WriteTo(conn)
|
||||
case "v2":
|
||||
header := proxyprotocol.HeaderV2{
|
||||
Command: proxyprotocol.CmdProxy,
|
||||
Src: &net.TCPAddr{IP: net.IP(proxyProtocolInfo.AddrPort.Addr().AsSlice()), Port: int(proxyProtocolInfo.AddrPort.Port())},
|
||||
Dest: &net.TCPAddr{IP: destIP, Port: 0},
|
||||
}
|
||||
header.Version = 2
|
||||
caddyCtx.Logger().Debug("sending proxy protocol header v2", zap.Any("header", header))
|
||||
_, err = header.WriteTo(conn)
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected proxy protocol version")
|
||||
}
|
||||
|
||||
_, err = header.WriteTo(conn)
|
||||
if err != nil {
|
||||
// identify this error as one that occurred during
|
||||
// dialing, which can be important when trying to
|
||||
@@ -529,7 +527,8 @@ func (t TLSConfig) MakeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) {
|
||||
certs := caddytls.AllMatchingCertificates(t.ClientCertificateAutomate)
|
||||
var err error
|
||||
for _, cert := range certs {
|
||||
err = cri.SupportsCertificate(&cert.Certificate)
|
||||
certCertificate := cert.Certificate // avoid taking address of iteration variable (gosec warning)
|
||||
err = cri.SupportsCertificate(&certCertificate)
|
||||
if err == nil {
|
||||
return &cert.Certificate, nil
|
||||
}
|
||||
|
||||
@@ -45,6 +45,19 @@ import (
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(Handler{})
|
||||
|
||||
caddy.RegisterNamespace("http.reverse_proxy.circuit_breakers", []interface{}{
|
||||
(*CircuitBreaker)(nil),
|
||||
})
|
||||
caddy.RegisterNamespace("http.reverse_proxy.selection_policies", []interface{}{
|
||||
(*Selector)(nil),
|
||||
})
|
||||
caddy.RegisterNamespace("http.reverse_proxy.transport", []interface{}{
|
||||
(*http.RoundTripper)(nil),
|
||||
})
|
||||
caddy.RegisterNamespace("http.reverse_proxy.upstreams", []interface{}{
|
||||
(*UpstreamSource)(nil),
|
||||
})
|
||||
}
|
||||
|
||||
// Handler implements a highly configurable and production-ready reverse proxy.
|
||||
@@ -191,6 +204,13 @@ type Handler struct {
|
||||
// - `{http.reverse_proxy.header.*}` The headers from the response
|
||||
HandleResponse []caddyhttp.ResponseHandler `json:"handle_response,omitempty"`
|
||||
|
||||
// If set, the proxy will write very detailed logs about its
|
||||
// inner workings. Enable this only when debugging, as it
|
||||
// will produce a lot of output.
|
||||
//
|
||||
// EXPERIMENTAL: This feature is subject to change or removal.
|
||||
VerboseLogs bool `json:"verbose_logs,omitempty"`
|
||||
|
||||
Transport http.RoundTripper `json:"-"`
|
||||
CB CircuitBreaker `json:"-"`
|
||||
DynamicUpstreams UpstreamSource `json:"-"`
|
||||
@@ -357,7 +377,7 @@ func (h *Handler) Provision(ctx caddy.Context) error {
|
||||
// set defaults on passive health checks, if necessary
|
||||
if h.HealthChecks.Passive != nil {
|
||||
h.HealthChecks.Passive.logger = h.logger.Named("health_checker.passive")
|
||||
if h.HealthChecks.Passive.FailDuration > 0 && h.HealthChecks.Passive.MaxFails == 0 {
|
||||
if h.HealthChecks.Passive.MaxFails == 0 {
|
||||
h.HealthChecks.Passive.MaxFails = 1
|
||||
}
|
||||
}
|
||||
@@ -480,7 +500,7 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
|
||||
upstream := h.LoadBalancing.SelectionPolicy.Select(upstreams, r, w)
|
||||
if upstream == nil {
|
||||
if proxyErr == nil {
|
||||
proxyErr = caddyhttp.Error(http.StatusServiceUnavailable, fmt.Errorf("no upstreams available"))
|
||||
proxyErr = caddyhttp.Error(http.StatusServiceUnavailable, noUpstreamsAvailable)
|
||||
}
|
||||
if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r) {
|
||||
return true, proxyErr
|
||||
@@ -848,12 +868,6 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
|
||||
break
|
||||
}
|
||||
|
||||
// otherwise, if there are any routes configured, execute those as the
|
||||
// actual response instead of what we got from the proxy backend
|
||||
if len(rh.Routes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// set up the replacer so that parts of the original response can be
|
||||
// used for routing decisions
|
||||
for field, value := range res.Header {
|
||||
@@ -949,16 +963,24 @@ func (h *Handler) finalizeResponse(
|
||||
}
|
||||
|
||||
rw.WriteHeader(res.StatusCode)
|
||||
if h.VerboseLogs {
|
||||
logger.Debug("wrote header")
|
||||
}
|
||||
|
||||
err := h.copyResponse(rw, res.Body, h.flushInterval(req, res))
|
||||
res.Body.Close() // close now, instead of defer, to populate res.Trailer
|
||||
err := h.copyResponse(rw, res.Body, h.flushInterval(req, res), logger)
|
||||
errClose := res.Body.Close() // close now, instead of defer, to populate res.Trailer
|
||||
if h.VerboseLogs || errClose != nil {
|
||||
logger.Debug("closed response body from upstream", zap.Error(errClose))
|
||||
}
|
||||
if err != nil {
|
||||
// we're streaming the response and we've already written headers, so
|
||||
// there's nothing an error handler can do to recover at this point;
|
||||
// the standard lib's proxy panics at this point, but we'll just log
|
||||
// the error and abort the stream here
|
||||
// we'll just log the error and abort the stream here and panic just as
|
||||
// the standard lib's proxy to propagate the stream error.
|
||||
// see issue https://github.com/caddyserver/caddy/issues/5951
|
||||
logger.Error("aborting with incomplete response", zap.Error(err))
|
||||
return nil
|
||||
// no extra logging from stdlib
|
||||
panic(http.ErrAbortHandler)
|
||||
}
|
||||
|
||||
if len(res.Trailer) > 0 {
|
||||
@@ -985,6 +1007,10 @@ func (h *Handler) finalizeResponse(
|
||||
}
|
||||
}
|
||||
|
||||
if h.VerboseLogs {
|
||||
logger.Debug("response finalized")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1016,17 +1042,23 @@ func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int
|
||||
// should be safe to retry, since without a connection, no
|
||||
// HTTP request can be transmitted; but if the error is not
|
||||
// specifically a dialer error, we need to be careful
|
||||
if _, ok := proxyErr.(DialError); proxyErr != nil && !ok {
|
||||
if proxyErr != nil {
|
||||
_, isDialError := proxyErr.(DialError)
|
||||
herr, isHandlerError := proxyErr.(caddyhttp.HandlerError)
|
||||
|
||||
// if the error occurred after a connection was established,
|
||||
// we have to assume the upstream received the request, and
|
||||
// retries need to be carefully decided, because some requests
|
||||
// are not idempotent
|
||||
if lb.RetryMatch == nil && req.Method != "GET" {
|
||||
// by default, don't retry requests if they aren't GET
|
||||
return false
|
||||
}
|
||||
if !lb.RetryMatch.AnyMatch(req) {
|
||||
return false
|
||||
if !isDialError && !(isHandlerError && errors.Is(herr, noUpstreamsAvailable)) {
|
||||
if lb.RetryMatch == nil && req.Method != "GET" {
|
||||
// by default, don't retry requests if they aren't GET
|
||||
return false
|
||||
}
|
||||
|
||||
if !lb.RetryMatch.AnyMatch(req) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1427,6 +1459,8 @@ func (c ignoreClientGoneContext) Err() error {
|
||||
// from the proxy handler.
|
||||
const proxyHandleResponseContextCtxKey caddy.CtxKey = "reverse_proxy_handle_response_context"
|
||||
|
||||
var noUpstreamsAvailable = fmt.Errorf("no upstreams available")
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ caddy.Provisioner = (*Handler)(nil)
|
||||
|
||||
@@ -269,7 +269,7 @@ func (LeastConnSelection) Select(pool UpstreamPool, _ *http.Request, _ http.Resp
|
||||
// sample: https://en.wikipedia.org/wiki/Reservoir_sampling
|
||||
if numReqs == leastReqs {
|
||||
count++
|
||||
if count > 1 || (weakrand.Int()%count) == 0 { //nolint:gosec
|
||||
if count == 1 || (weakrand.Int()%count) == 0 { //nolint:gosec
|
||||
bestHost = host
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,15 +184,22 @@ func (h Handler) isBidirectionalStream(req *http.Request, res *http.Response) bo
|
||||
(ae == "identity" || ae == "")
|
||||
}
|
||||
|
||||
func (h Handler) copyResponse(dst http.ResponseWriter, src io.Reader, flushInterval time.Duration) error {
|
||||
func (h Handler) copyResponse(dst http.ResponseWriter, src io.Reader, flushInterval time.Duration, logger *zap.Logger) error {
|
||||
var w io.Writer = dst
|
||||
|
||||
if flushInterval != 0 {
|
||||
var mlwLogger *zap.Logger
|
||||
if h.VerboseLogs {
|
||||
mlwLogger = logger.Named("max_latency_writer")
|
||||
} else {
|
||||
mlwLogger = zap.NewNop()
|
||||
}
|
||||
mlw := &maxLatencyWriter{
|
||||
dst: dst,
|
||||
//nolint:bodyclose
|
||||
flush: http.NewResponseController(dst).Flush,
|
||||
latency: flushInterval,
|
||||
logger: mlwLogger,
|
||||
}
|
||||
defer mlw.stop()
|
||||
|
||||
@@ -205,19 +212,30 @@ func (h Handler) copyResponse(dst http.ResponseWriter, src io.Reader, flushInter
|
||||
|
||||
buf := streamingBufPool.Get().(*[]byte)
|
||||
defer streamingBufPool.Put(buf)
|
||||
_, err := h.copyBuffer(w, src, *buf)
|
||||
|
||||
var copyLogger *zap.Logger
|
||||
if h.VerboseLogs {
|
||||
copyLogger = logger
|
||||
} else {
|
||||
copyLogger = zap.NewNop()
|
||||
}
|
||||
|
||||
_, err := h.copyBuffer(w, src, *buf, copyLogger)
|
||||
return err
|
||||
}
|
||||
|
||||
// copyBuffer returns any write errors or non-EOF read errors, and the amount
|
||||
// of bytes written.
|
||||
func (h Handler) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int64, error) {
|
||||
func (h Handler) copyBuffer(dst io.Writer, src io.Reader, buf []byte, logger *zap.Logger) (int64, error) {
|
||||
if len(buf) == 0 {
|
||||
buf = make([]byte, defaultBufferSize)
|
||||
}
|
||||
var written int64
|
||||
for {
|
||||
logger.Debug("waiting to read from upstream")
|
||||
nr, rerr := src.Read(buf)
|
||||
logger := logger.With(zap.Int("read", nr))
|
||||
logger.Debug("read from upstream", zap.Error(rerr))
|
||||
if rerr != nil && rerr != io.EOF && rerr != context.Canceled {
|
||||
// TODO: this could be useful to know (indeed, it revealed an error in our
|
||||
// fastcgi PoC earlier; but it's this single error report here that necessitates
|
||||
@@ -229,10 +247,15 @@ func (h Handler) copyBuffer(dst io.Writer, src io.Reader, buf []byte) (int64, er
|
||||
h.logger.Error("reading from backend", zap.Error(rerr))
|
||||
}
|
||||
if nr > 0 {
|
||||
logger.Debug("writing to downstream")
|
||||
nw, werr := dst.Write(buf[:nr])
|
||||
if nw > 0 {
|
||||
written += int64(nw)
|
||||
}
|
||||
logger.Debug("wrote to downstream",
|
||||
zap.Int("written", nw),
|
||||
zap.Int64("written_total", written),
|
||||
zap.Error(werr))
|
||||
if werr != nil {
|
||||
return written, fmt.Errorf("writing: %w", werr)
|
||||
}
|
||||
@@ -452,18 +475,22 @@ type maxLatencyWriter struct {
|
||||
mu sync.Mutex // protects t, flushPending, and dst.Flush
|
||||
t *time.Timer
|
||||
flushPending bool
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func (m *maxLatencyWriter) Write(p []byte) (n int, err error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
n, err = m.dst.Write(p)
|
||||
m.logger.Debug("wrote bytes", zap.Int("n", n), zap.Error(err))
|
||||
if m.latency < 0 {
|
||||
m.logger.Debug("flushing immediately")
|
||||
//nolint:errcheck
|
||||
m.flush()
|
||||
return
|
||||
}
|
||||
if m.flushPending {
|
||||
m.logger.Debug("delayed flush already pending")
|
||||
return
|
||||
}
|
||||
if m.t == nil {
|
||||
@@ -471,6 +498,7 @@ func (m *maxLatencyWriter) Write(p []byte) (n int, err error) {
|
||||
} else {
|
||||
m.t.Reset(m.latency)
|
||||
}
|
||||
m.logger.Debug("timer set for delayed flush", zap.Duration("duration", m.latency))
|
||||
m.flushPending = true
|
||||
return
|
||||
}
|
||||
@@ -479,8 +507,10 @@ func (m *maxLatencyWriter) delayedFlush() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if !m.flushPending { // if stop was called but AfterFunc already started this goroutine
|
||||
m.logger.Debug("delayed flush is not pending")
|
||||
return
|
||||
}
|
||||
m.logger.Debug("delayed flush")
|
||||
//nolint:errcheck
|
||||
m.flush()
|
||||
m.flushPending = false
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func TestHandlerCopyResponse(t *testing.T) {
|
||||
@@ -22,7 +24,7 @@ func TestHandlerCopyResponse(t *testing.T) {
|
||||
for _, d := range testdata {
|
||||
src := bytes.NewBuffer([]byte(d))
|
||||
dst.Reset()
|
||||
err := h.copyResponse(recorder, src, 0)
|
||||
err := h.copyResponse(recorder, src, 0, caddy.Log())
|
||||
if err != nil {
|
||||
t.Errorf("failed with error: %v", err)
|
||||
}
|
||||
|
||||
@@ -251,6 +251,8 @@ type AUpstreams struct {
|
||||
Versions *IPVersions `json:"versions,omitempty"`
|
||||
|
||||
resolver *net.Resolver
|
||||
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
@@ -261,7 +263,8 @@ func (AUpstreams) CaddyModule() caddy.ModuleInfo {
|
||||
}
|
||||
}
|
||||
|
||||
func (au *AUpstreams) Provision(_ caddy.Context) error {
|
||||
func (au *AUpstreams) Provision(ctx caddy.Context) error {
|
||||
au.logger = ctx.Logger()
|
||||
if au.Refresh == 0 {
|
||||
au.Refresh = caddy.Duration(time.Minute)
|
||||
}
|
||||
@@ -297,8 +300,8 @@ func (au *AUpstreams) Provision(_ caddy.Context) error {
|
||||
func (au AUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
|
||||
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
||||
|
||||
resolveIpv4 := au.Versions.IPv4 == nil || *au.Versions.IPv4
|
||||
resolveIpv6 := au.Versions.IPv6 == nil || *au.Versions.IPv6
|
||||
resolveIpv4 := au.Versions == nil || au.Versions.IPv4 == nil || *au.Versions.IPv4
|
||||
resolveIpv6 := au.Versions == nil || au.Versions.IPv6 == nil || *au.Versions.IPv6
|
||||
|
||||
// Map ipVersion early, so we can use it as part of the cache-key.
|
||||
// This should be fairly inexpensive and comes and the upside of
|
||||
@@ -343,6 +346,11 @@ func (au AUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
|
||||
name := repl.ReplaceAll(au.Name, "")
|
||||
port := repl.ReplaceAll(au.Port, "")
|
||||
|
||||
au.logger.Debug("refreshing A upstreams",
|
||||
zap.String("version", ipVersion),
|
||||
zap.String("name", name),
|
||||
zap.String("port", port))
|
||||
|
||||
ips, err := au.resolver.LookupIP(r.Context(), ipVersion, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -350,6 +358,8 @@ func (au AUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
|
||||
|
||||
upstreams := make([]Upstream, len(ips))
|
||||
for i, ip := range ips {
|
||||
au.logger.Debug("discovered A record",
|
||||
zap.String("ip", ip.String()))
|
||||
upstreams[i] = Upstream{
|
||||
Dial: net.JoinHostPort(ip.String(), port),
|
||||
}
|
||||
|
||||
@@ -22,6 +22,12 @@ import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterNamespace("http.handlers", []interface{}{
|
||||
(*MiddlewareHandler)(nil),
|
||||
})
|
||||
}
|
||||
|
||||
// Route consists of a set of rules for matching HTTP requests,
|
||||
// a list of handlers to execute, and optional flow control
|
||||
// parameters which customize the handling of HTTP requests
|
||||
|
||||
@@ -228,7 +228,6 @@ type Server struct {
|
||||
|
||||
server *http.Server
|
||||
h3server *http3.Server
|
||||
h3listeners []net.PacketConn // TODO: we have to hold these because quic-go won't close listeners it didn't create
|
||||
h2listeners []*http2Listener
|
||||
addresses []caddy.NetworkAddress
|
||||
|
||||
@@ -555,13 +554,7 @@ func (s *Server) findLastRouteWithHostMatcher() int {
|
||||
// the listener, with Server s as the handler.
|
||||
func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error {
|
||||
addr.Network = getHTTP3Network(addr.Network)
|
||||
lnAny, err := addr.Listen(s.ctx, 0, net.ListenConfig{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ln := lnAny.(net.PacketConn)
|
||||
|
||||
h3ln, err := caddy.ListenQUIC(ln, tlsCfg, &s.activeRequests)
|
||||
h3ln, err := addr.ListenQUIC(s.ctx, 0, net.ListenConfig{}, tlsCfg, &s.activeRequests)
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting HTTP/3 QUIC listener: %v", err)
|
||||
}
|
||||
@@ -579,8 +572,6 @@ func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error
|
||||
}
|
||||
}
|
||||
|
||||
s.h3listeners = append(s.h3listeners, ln)
|
||||
|
||||
//nolint:errcheck
|
||||
go s.h3server.ServeListener(h3ln)
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
)
|
||||
@@ -49,6 +52,29 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
||||
if !h.Args(&t.FileRoot) {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
case "extensions":
|
||||
if h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
if t.ExtensionsRaw != nil {
|
||||
return nil, h.Err("extensions already specified")
|
||||
}
|
||||
for nesting := h.Nesting(); h.NextBlock(nesting); {
|
||||
extensionModuleName := h.Val()
|
||||
modID := "http.handlers.templates.functions." + extensionModuleName
|
||||
unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cf, ok := unm.(CustomFunctions)
|
||||
if !ok {
|
||||
return nil, h.Errf("module %s (%T) does not provide template functions", modID, unm)
|
||||
}
|
||||
if t.ExtensionsRaw == nil {
|
||||
t.ExtensionsRaw = make(caddy.ModuleMap)
|
||||
}
|
||||
t.ExtensionsRaw[extensionModuleName] = caddyconfig.JSON(cf, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ import (
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
)
|
||||
@@ -46,7 +48,8 @@ func init() {
|
||||
//
|
||||
// ##### `.Args`
|
||||
//
|
||||
// A slice of arguments passed to this page/context, for example as the result of a `include`.
|
||||
// A slice of arguments passed to this page/context, for example
|
||||
// as the result of a [`include`](#include).
|
||||
//
|
||||
// ```
|
||||
// {{index .Args 0}} // first argument
|
||||
@@ -103,8 +106,8 @@ func init() {
|
||||
// Reads and returns the contents of another file, and parses it
|
||||
// as a template, adding any template definitions to the template
|
||||
// stack. If there are no definitions, the filepath will be the
|
||||
// definition name. Any {{ define }} blocks will be accessible by
|
||||
// {{ template }} or {{ block }}. Imports must happen before the
|
||||
// definition name. Any `{{ define }}` blocks will be accessible by
|
||||
// `{{ template }}` or `{{ block }}`. Imports must happen before the
|
||||
// template or block action is called. Note that the contents are
|
||||
// NOT escaped, so you should only import trusted template files.
|
||||
//
|
||||
@@ -125,12 +128,13 @@ func init() {
|
||||
//
|
||||
// Includes the contents of another file, rendering it in-place.
|
||||
// Optionally can pass key-value pairs as arguments to be accessed
|
||||
// by the included file. Note that the contents are NOT escaped,
|
||||
// so you should only include trusted template files.
|
||||
// by the included file. Use [`.Args N`](#args) to access the N-th
|
||||
// argument, 0-indexed. Note that the contents are NOT escaped, so
|
||||
// you should only include trusted template files.
|
||||
//
|
||||
// ```
|
||||
// {{include "path/to/file.html"}} // no arguments
|
||||
// {{include "path/to/file.html" "arg1" 2 "value 3"}} // with arguments
|
||||
// {{include "path/to/file.html" "arg0" 1 "value 2"}} // with arguments
|
||||
// ```
|
||||
//
|
||||
// ##### `readFile`
|
||||
@@ -145,7 +149,8 @@ func init() {
|
||||
//
|
||||
// ##### `listFiles`
|
||||
//
|
||||
// Returns a list of the files in the given directory, which is relative to the template context's file root.
|
||||
// Returns a list of the files in the given directory, which is relative
|
||||
// to the template context's file root.
|
||||
//
|
||||
// ```
|
||||
// {{listFiles "/mydir"}}
|
||||
@@ -165,12 +170,21 @@ func init() {
|
||||
//
|
||||
// ##### `.RemoteIP`
|
||||
//
|
||||
// Returns the client's IP address.
|
||||
// Returns the connection's IP address.
|
||||
//
|
||||
// ```
|
||||
// {{.RemoteIP}}
|
||||
// ```
|
||||
//
|
||||
// ##### `.ClientIP`
|
||||
//
|
||||
// Returns the real client's IP address, if `trusted_proxies` was configured,
|
||||
// otherwise returns the connection's IP address.
|
||||
//
|
||||
// ```
|
||||
// {{.ClientIP}}
|
||||
// ```
|
||||
//
|
||||
// ##### `.Req`
|
||||
//
|
||||
// Accesses the current HTTP request, which has various fields, including:
|
||||
@@ -186,7 +200,8 @@ func init() {
|
||||
//
|
||||
// ##### `.OriginalReq`
|
||||
//
|
||||
// Like .Req, except it accesses the original HTTP request before rewrites or other internal modifications.
|
||||
// Like [`.Req`](#req), except it accesses the original HTTP
|
||||
// request before rewrites or other internal modifications.
|
||||
//
|
||||
// ##### `.RespHeader.Add`
|
||||
//
|
||||
@@ -222,11 +237,13 @@ func init() {
|
||||
//
|
||||
// ##### `splitFrontMatter`
|
||||
//
|
||||
// Splits front matter out from the body. Front matter is metadata that appears at the very beginning of a file or string. Front matter can be in YAML, TOML, or JSON formats:
|
||||
// Splits front matter out from the body. Front matter is metadata that
|
||||
// appears at the very beginning of a file or string. Front matter can
|
||||
// be in YAML, TOML, or JSON formats:
|
||||
//
|
||||
// **TOML** front matter starts and ends with `+++`:
|
||||
//
|
||||
// ```
|
||||
// ```toml
|
||||
// +++
|
||||
// template = "blog"
|
||||
// title = "Blog Homepage"
|
||||
@@ -236,7 +253,7 @@ func init() {
|
||||
//
|
||||
// **YAML** is surrounded by `---`:
|
||||
//
|
||||
// ```
|
||||
// ```yaml
|
||||
// ---
|
||||
// template: blog
|
||||
// title: Blog Homepage
|
||||
@@ -246,14 +263,12 @@ func init() {
|
||||
//
|
||||
// **JSON** is simply `{` and `}`:
|
||||
//
|
||||
// ```
|
||||
//
|
||||
// {
|
||||
// "template": "blog",
|
||||
// "title": "Blog Homepage",
|
||||
// "sitename": "A Caddy site"
|
||||
// }
|
||||
//
|
||||
// ```json
|
||||
// {
|
||||
// "template": "blog",
|
||||
// "title": "Blog Homepage",
|
||||
// "sitename": "A Caddy site"
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// The resulting front matter will be made available like so:
|
||||
@@ -306,7 +321,12 @@ type Templates struct {
|
||||
// the opening and closing delimiters. Default: `["{{", "}}"]`
|
||||
Delimiters []string `json:"delimiters,omitempty"`
|
||||
|
||||
// Extensions adds functions to the template's func map. These often
|
||||
// act as components on web pages, for example.
|
||||
ExtensionsRaw caddy.ModuleMap `json:"match,omitempty" caddy:"namespace=http.handlers.templates.functions"`
|
||||
|
||||
customFuncs []template.FuncMap
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// Customfunctions is the interface for registering custom template functions.
|
||||
@@ -325,17 +345,14 @@ func (Templates) CaddyModule() caddy.ModuleInfo {
|
||||
|
||||
// Provision provisions t.
|
||||
func (t *Templates) Provision(ctx caddy.Context) error {
|
||||
fnModInfos := caddy.GetModules("http.handlers.templates.functions")
|
||||
customFuncs := make([]template.FuncMap, 0, len(fnModInfos))
|
||||
for _, modInfo := range fnModInfos {
|
||||
mod := modInfo.New()
|
||||
fnMod, ok := mod.(CustomFunctions)
|
||||
if !ok {
|
||||
return fmt.Errorf("module %q does not satisfy the CustomFunctions interface", modInfo.ID)
|
||||
}
|
||||
customFuncs = append(customFuncs, fnMod.CustomTemplateFunctions())
|
||||
t.logger = ctx.Logger()
|
||||
mods, err := ctx.LoadModule(t, "ExtensionsRaw")
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading template extensions: %v", err)
|
||||
}
|
||||
for _, modIface := range mods.(map[string]any) {
|
||||
t.customFuncs = append(t.customFuncs, modIface.(CustomFunctions).CustomTemplateFunctions())
|
||||
}
|
||||
t.customFuncs = customFuncs
|
||||
|
||||
if t.MIMETypes == nil {
|
||||
t.MIMETypes = defaultMIMETypes
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -37,6 +38,7 @@ import (
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
gmhtml "github.com/yuin/goldmark/renderer/html"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
@@ -57,7 +59,7 @@ type TemplateContext struct {
|
||||
// NewTemplate returns a new template intended to be evaluated with this
|
||||
// context, as it is initialized with configuration from this context.
|
||||
func (c *TemplateContext) NewTemplate(tplName string) *template.Template {
|
||||
c.tpl = template.New(tplName)
|
||||
c.tpl = template.New(tplName).Option("missingkey=zero")
|
||||
|
||||
// customize delimiters, if applicable
|
||||
if c.config != nil && len(c.config.Delimiters) == 2 {
|
||||
@@ -88,6 +90,7 @@ func (c *TemplateContext) NewTemplate(tplName string) *template.Template {
|
||||
"fileExists": c.funcFileExists,
|
||||
"httpError": c.funcHTTPError,
|
||||
"humanize": c.funcHumanize,
|
||||
"maybe": c.funcMaybe,
|
||||
})
|
||||
return c.tpl
|
||||
}
|
||||
@@ -188,6 +191,7 @@ func (c TemplateContext) funcHTTPInclude(uri string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
virtReq.Host = c.Req.Host
|
||||
virtReq.RemoteAddr = "127.0.0.1:10000" // https://github.com/caddyserver/caddy/issues/5835
|
||||
virtReq.Header = c.Req.Header.Clone()
|
||||
virtReq.Header.Set("Accept-Encoding", "identity") // https://github.com/caddyserver/caddy/issues/4352
|
||||
virtReq.Trailer = c.Req.Trailer.Clone()
|
||||
@@ -264,7 +268,7 @@ func (c TemplateContext) Cookie(name string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// RemoteIP gets the IP address of the client making the request.
|
||||
// RemoteIP gets the IP address of the connection's remote IP.
|
||||
func (c TemplateContext) RemoteIP() string {
|
||||
ip, _, err := net.SplitHostPort(c.Req.RemoteAddr)
|
||||
if err != nil {
|
||||
@@ -273,6 +277,18 @@ func (c TemplateContext) RemoteIP() string {
|
||||
return ip
|
||||
}
|
||||
|
||||
// ClientIP gets the IP address of the real client making the request
|
||||
// if the request is trusted (see trusted_proxies), otherwise returns
|
||||
// the connection's remote IP.
|
||||
func (c TemplateContext) ClientIP() string {
|
||||
address := caddyhttp.GetVar(c.Req.Context(), caddyhttp.ClientIPVarKey).(string)
|
||||
clientIP, _, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
clientIP = address // no port
|
||||
}
|
||||
return clientIP
|
||||
}
|
||||
|
||||
// Host returns the hostname portion of the Host header
|
||||
// from the HTTP request.
|
||||
func (c TemplateContext) Host() (string, error) {
|
||||
@@ -431,6 +447,14 @@ func (c TemplateContext) funcFileStat(filename string) (fs.FileInfo, error) {
|
||||
// funcHTTPError returns a structured HTTP handler error. EXPERIMENTAL; SUBJECT TO CHANGE.
|
||||
// Example usage: `{{if not (fileExists $includeFile)}}{{httpError 404}}{{end}}`
|
||||
func (c TemplateContext) funcHTTPError(statusCode int) (bool, error) {
|
||||
// Delete some headers that may have been set by the underlying
|
||||
// handler (such as file_server) which may break the error response.
|
||||
c.RespHeader.Header.Del("Content-Length")
|
||||
c.RespHeader.Header.Del("Content-Type")
|
||||
c.RespHeader.Header.Del("Etag")
|
||||
c.RespHeader.Header.Del("Last-Modified")
|
||||
c.RespHeader.Header.Del("Accept-Ranges")
|
||||
|
||||
return false, caddyhttp.Error(statusCode, nil)
|
||||
}
|
||||
|
||||
@@ -471,6 +495,51 @@ func (c TemplateContext) funcHumanize(formatType, data string) (string, error) {
|
||||
return "", fmt.Errorf("no know function was given")
|
||||
}
|
||||
|
||||
// funcMaybe invokes the plugged-in function named functionName if it is plugged in
|
||||
// (is a module in the 'http.handlers.templates.functions' namespace). If it is not
|
||||
// available, a log message is emitted.
|
||||
//
|
||||
// The first argument is the function name, and the rest of the arguments are
|
||||
// passed on to the actual function.
|
||||
//
|
||||
// This function is useful for executing templates that use components that may be
|
||||
// considered as optional in some cases (like during local development) where you do
|
||||
// not want to require everyone to have a custom Caddy build to be able to execute
|
||||
// your template.
|
||||
//
|
||||
// NOTE: This function is EXPERIMENTAL and subject to change or removal.
|
||||
func (c TemplateContext) funcMaybe(functionName string, args ...any) (any, error) {
|
||||
for _, funcMap := range c.CustomFuncs {
|
||||
if fn, ok := funcMap[functionName]; ok {
|
||||
val := reflect.ValueOf(fn)
|
||||
if val.Kind() != reflect.Func {
|
||||
continue
|
||||
}
|
||||
argVals := make([]reflect.Value, len(args))
|
||||
for i, arg := range args {
|
||||
argVals[i] = reflect.ValueOf(arg)
|
||||
}
|
||||
returnVals := val.Call(argVals)
|
||||
switch len(returnVals) {
|
||||
case 0:
|
||||
return "", nil
|
||||
case 1:
|
||||
return returnVals[0].Interface(), nil
|
||||
case 2:
|
||||
var err error
|
||||
if !returnVals[1].IsNil() {
|
||||
err = returnVals[1].Interface().(error)
|
||||
}
|
||||
return returnVals[0].Interface(), err
|
||||
default:
|
||||
return nil, fmt.Errorf("maybe %s: invalid number of return values: %d", functionName, len(returnVals))
|
||||
}
|
||||
}
|
||||
}
|
||||
c.config.logger.Named("maybe").Warn("template function could not be found; ignoring invocation", zap.String("name", functionName))
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// WrappedHeader wraps niladic functions so that they
|
||||
// can be used in templates. (Template functions must
|
||||
// return a value.)
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/acme/api"
|
||||
acmeNoSQL "github.com/smallstep/certificates/acme/db/nosql"
|
||||
|
||||
@@ -16,9 +16,11 @@ package caddytls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -496,7 +498,7 @@ func (iss *ACMEIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
// to see if a certificate can be obtained for name.
|
||||
// The certificate request should be denied if this
|
||||
// returns an error.
|
||||
func onDemandAskRequest(logger *zap.Logger, ask string, name string) error {
|
||||
func onDemandAskRequest(ctx context.Context, logger *zap.Logger, ask string, name string) error {
|
||||
askURL, err := url.Parse(ask)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing ask URL: %v", err)
|
||||
@@ -513,7 +515,17 @@ func onDemandAskRequest(logger *zap.Logger, ask string, name string) error {
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// logging out the client IP can be useful for servers that want to count
|
||||
// attempts from clients to detect patterns of abuse
|
||||
var clientIP string
|
||||
if hello, ok := ctx.Value(certmagic.ClientHelloInfoCtxKey).(*tls.ClientHelloInfo); ok && hello != nil {
|
||||
if remote := hello.Conn.RemoteAddr(); remote != nil {
|
||||
clientIP, _, _ = net.SplitHostPort(remote.String())
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("response from ask endpoint",
|
||||
zap.String("client_ip", clientIP),
|
||||
zap.String("domain", name),
|
||||
zap.String("url", askURLString),
|
||||
zap.Int("status", resp.StatusCode))
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -29,6 +30,18 @@ import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterNamespace("dns.provider", []interface{}{
|
||||
(*acmez.Solver)(nil),
|
||||
})
|
||||
caddy.RegisterNamespace("tls.get_certificate", []interface{}{
|
||||
(*certmagic.Manager)(nil),
|
||||
})
|
||||
caddy.RegisterNamespace("tls.issuance", []interface{}{
|
||||
(*certmagic.Issuer)(nil),
|
||||
})
|
||||
}
|
||||
|
||||
// AutomationConfig governs the automated management of TLS certificates.
|
||||
type AutomationConfig struct {
|
||||
// The list of automation policies. The first policy matching
|
||||
@@ -251,11 +264,11 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
|
||||
return fmt.Errorf("on-demand TLS cannot be enabled without an 'ask' endpoint to prevent abuse; please refer to documentation for details")
|
||||
}
|
||||
ond = &certmagic.OnDemandConfig{
|
||||
DecisionFunc: func(name string) error {
|
||||
DecisionFunc: func(ctx context.Context, name string) error {
|
||||
if tlsApp.Automation == nil || tlsApp.Automation.OnDemand == nil {
|
||||
return nil
|
||||
}
|
||||
if err := onDemandAskRequest(tlsApp.logger, tlsApp.Automation.OnDemand.Ask, name); err != nil {
|
||||
if err := onDemandAskRequest(ctx, tlsApp.logger, tlsApp.Automation.OnDemand.Ask, name); err != nil {
|
||||
// distinguish true errors from denials, because it's important to elevate actual errors
|
||||
if errors.Is(err, errAskDenied) {
|
||||
tlsApp.logger.Debug("certificate issuance denied",
|
||||
|
||||
@@ -58,7 +58,8 @@ nextChoice:
|
||||
if len(p.SerialNumber) > 0 {
|
||||
var found bool
|
||||
for _, sn := range p.SerialNumber {
|
||||
if cert.Leaf.SerialNumber.Cmp(&sn.Int) == 0 {
|
||||
snInt := sn.Int // avoid taking address of iteration variable (gosec warning)
|
||||
if cert.Leaf.SerialNumber.Cmp(&snInt) == 0 {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
//go:build cfgo
|
||||
|
||||
package caddytls
|
||||
|
||||
// This file adds support for X25519Kyber768Draft00, a post-quantum
|
||||
// key agreement that is currently being rolled out by Chrome [1]
|
||||
// and Cloudflare [2,3]. For more context, see the PR [4].
|
||||
//
|
||||
// [1] https://blog.chromium.org/2023/08/protecting-chrome-traffic-with-hybrid.html
|
||||
// [2] https://blog.cloudflare.com/post-quantum-for-all/
|
||||
// [3] https://blog.cloudflare.com/post-quantum-to-origins/
|
||||
// [4] https://github.com/caddyserver/caddy/pull/5852
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
)
|
||||
|
||||
func init() {
|
||||
SupportedCurves["X25519Kyber768Draft00"] = tls.X25519Kyber768Draft00
|
||||
defaultCurves = append(
|
||||
[]tls.CurveID{tls.X25519Kyber768Draft00},
|
||||
defaultCurves...,
|
||||
)
|
||||
}
|
||||
@@ -33,6 +33,9 @@ import (
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(LeafCertClientAuth{})
|
||||
caddy.RegisterNamespace("tls.handshake_match", []interface{}{
|
||||
(*ConnectionMatcher)(nil),
|
||||
})
|
||||
}
|
||||
|
||||
// ConnectionPolicies govern the establishment of TLS connections. It is
|
||||
|
||||
@@ -29,6 +29,26 @@ func init() {
|
||||
// FileLoader loads certificates and their associated keys from disk.
|
||||
type FileLoader []CertKeyFilePair
|
||||
|
||||
// Provision implements caddy.Provisioner.
|
||||
func (fl FileLoader) Provision(ctx caddy.Context) error {
|
||||
repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
||||
if !ok {
|
||||
repl = caddy.NewReplacer()
|
||||
}
|
||||
for k, pair := range fl {
|
||||
for i, tag := range pair.Tags {
|
||||
pair.Tags[i] = repl.ReplaceKnown(tag, "")
|
||||
}
|
||||
fl[k] = CertKeyFilePair{
|
||||
Certificate: repl.ReplaceKnown(pair.Certificate, ""),
|
||||
Key: repl.ReplaceKnown(pair.Key, ""),
|
||||
Format: repl.ReplaceKnown(pair.Format, ""),
|
||||
Tags: pair.Tags,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (FileLoader) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
@@ -87,4 +107,7 @@ func (fl FileLoader) LoadCertificates() ([]Certificate, error) {
|
||||
}
|
||||
|
||||
// Interface guard
|
||||
var _ CertificateLoader = (FileLoader)(nil)
|
||||
var (
|
||||
_ CertificateLoader = (FileLoader)(nil)
|
||||
_ caddy.Provisioner = (FileLoader)(nil)
|
||||
)
|
||||
|
||||
@@ -43,6 +43,18 @@ func (FolderLoader) CaddyModule() caddy.ModuleInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// Provision implements caddy.Provisioner.
|
||||
func (fl FolderLoader) Provision(ctx caddy.Context) error {
|
||||
repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
||||
if !ok {
|
||||
repl = caddy.NewReplacer()
|
||||
}
|
||||
for k, path := range fl {
|
||||
fl[k] = repl.ReplaceKnown(path, "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadCertificates loads all the certificates+keys in the directories
|
||||
// listed in fl from all files ending with .pem. This method of loading
|
||||
// certificates expects the certificate and key to be bundled into the
|
||||
@@ -146,4 +158,7 @@ func tlsCertFromCertAndKeyPEMBundle(bundle []byte) (tls.Certificate, error) {
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
var _ CertificateLoader = (FolderLoader)(nil)
|
||||
var (
|
||||
_ CertificateLoader = (FolderLoader)(nil)
|
||||
_ caddy.Provisioner = (FolderLoader)(nil)
|
||||
)
|
||||
|
||||
@@ -30,6 +30,25 @@ func init() {
|
||||
// of not needing to store them on disk at all.
|
||||
type PEMLoader []CertKeyPEMPair
|
||||
|
||||
// Provision implements caddy.Provisioner.
|
||||
func (pl PEMLoader) Provision(ctx caddy.Context) error {
|
||||
repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
||||
if !ok {
|
||||
repl = caddy.NewReplacer()
|
||||
}
|
||||
for k, pair := range pl {
|
||||
for i, tag := range pair.Tags {
|
||||
pair.Tags[i] = repl.ReplaceKnown(tag, "")
|
||||
}
|
||||
pl[k] = CertKeyPEMPair{
|
||||
CertificatePEM: repl.ReplaceKnown(pair.CertificatePEM, ""),
|
||||
KeyPEM: repl.ReplaceKnown(pair.KeyPEM, ""),
|
||||
Tags: pair.Tags,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (PEMLoader) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
@@ -69,4 +88,7 @@ func (pl PEMLoader) LoadCertificates() ([]Certificate, error) {
|
||||
}
|
||||
|
||||
// Interface guard
|
||||
var _ CertificateLoader = (PEMLoader)(nil)
|
||||
var (
|
||||
_ CertificateLoader = (PEMLoader)(nil)
|
||||
_ caddy.Provisioner = (PEMLoader)(nil)
|
||||
)
|
||||
|
||||
@@ -28,6 +28,12 @@ import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterNamespace("tls.stek", []interface{}{
|
||||
(*STEKProvider)(nil),
|
||||
})
|
||||
}
|
||||
|
||||
// SessionTicketService configures and manages TLS session tickets.
|
||||
type SessionTicketService struct {
|
||||
// KeySource is the method by which Caddy produces or obtains
|
||||
|
||||
@@ -52,6 +52,22 @@ func (StorageLoader) CaddyModule() caddy.ModuleInfo {
|
||||
func (sl *StorageLoader) Provision(ctx caddy.Context) error {
|
||||
sl.storage = ctx.Storage()
|
||||
sl.ctx = ctx
|
||||
|
||||
repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
||||
if !ok {
|
||||
repl = caddy.NewReplacer()
|
||||
}
|
||||
for k, pair := range sl.Pairs {
|
||||
for i, tag := range pair.Tags {
|
||||
pair.Tags[i] = repl.ReplaceKnown(tag, "")
|
||||
}
|
||||
sl.Pairs[k] = CertKeyFilePair{
|
||||
Certificate: repl.ReplaceKnown(pair.Certificate, ""),
|
||||
Key: repl.ReplaceKnown(pair.Key, ""),
|
||||
Format: repl.ReplaceKnown(pair.Format, ""),
|
||||
Tags: pair.Tags,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+28
-14
@@ -35,6 +35,10 @@ import (
|
||||
func init() {
|
||||
caddy.RegisterModule(TLS{})
|
||||
caddy.RegisterModule(AutomateLoader{})
|
||||
|
||||
caddy.RegisterNamespace("tls.certificates", []interface{}{
|
||||
(*CertificateLoader)(nil),
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -551,6 +555,10 @@ func (t *TLS) cleanStorageUnits() {
|
||||
storageCleanMu.Lock()
|
||||
defer storageCleanMu.Unlock()
|
||||
|
||||
// TODO: This check might not be needed anymore now that CertMagic syncs
|
||||
// and throttles storage cleaning globally across the cluster.
|
||||
// The original comment below might be outdated:
|
||||
//
|
||||
// If storage was cleaned recently, don't do it again for now. Although the ticker
|
||||
// calling this function drops missed ticks for us, config reloads discard the old
|
||||
// ticker and replace it with a new one, possibly invoking a cleaning to happen again
|
||||
@@ -563,21 +571,26 @@ func (t *TLS) cleanStorageUnits() {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := caddy.InstanceID()
|
||||
if err != nil {
|
||||
t.logger.Warn("unable to get instance ID; storage clean stamps will be incomplete", zap.Error(err))
|
||||
}
|
||||
options := certmagic.CleanStorageOptions{
|
||||
Logger: t.logger,
|
||||
InstanceID: id.String(),
|
||||
Interval: t.storageCleanInterval(),
|
||||
OCSPStaples: true,
|
||||
ExpiredCerts: true,
|
||||
ExpiredCertGracePeriod: 24 * time.Hour * 14,
|
||||
}
|
||||
|
||||
// avoid cleaning same storage more than once per cleaning cycle
|
||||
storagesCleaned := make(map[string]struct{})
|
||||
|
||||
// start with the default/global storage
|
||||
storage := t.ctx.Storage()
|
||||
storageStr := fmt.Sprintf("%v", storage)
|
||||
t.logger.Info("cleaning storage unit", zap.String("description", storageStr))
|
||||
certmagic.CleanStorage(t.ctx, storage, options)
|
||||
storagesCleaned[storageStr] = struct{}{}
|
||||
err = certmagic.CleanStorage(t.ctx, t.ctx.Storage(), options)
|
||||
if err != nil {
|
||||
// probably don't want to return early, since we should still
|
||||
// see if any other storages can get cleaned up
|
||||
t.logger.Error("could not clean default/global storage", zap.Error(err))
|
||||
}
|
||||
|
||||
// then clean each storage defined in ACME automation policies
|
||||
if t.Automation != nil {
|
||||
@@ -585,13 +598,9 @@ func (t *TLS) cleanStorageUnits() {
|
||||
if ap.storage == nil {
|
||||
continue
|
||||
}
|
||||
storageStr := fmt.Sprintf("%v", ap.storage)
|
||||
if _, ok := storagesCleaned[storageStr]; ok {
|
||||
continue
|
||||
if err := certmagic.CleanStorage(t.ctx, ap.storage, options); err != nil {
|
||||
t.logger.Error("could not clean storage configured in automation policy", zap.Error(err))
|
||||
}
|
||||
t.logger.Info("cleaning storage unit", zap.String("description", storageStr))
|
||||
certmagic.CleanStorage(t.ctx, ap.storage, options)
|
||||
storagesCleaned[storageStr] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,6 +656,11 @@ func (AutomateLoader) CaddyModule() caddy.ModuleInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// LoadCertificates is a stub so AutomateLoader can implement CertificateLoader
|
||||
func (AutomateLoader) LoadCertificates() ([]Certificate, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// CertCacheOptions configures the certificate cache.
|
||||
type CertCacheOptions struct {
|
||||
// Maximum number of certificates to allow in the
|
||||
|
||||
@@ -100,12 +100,15 @@ func (f *HashFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
// Filter filters the input field with the replacement value.
|
||||
func (f *HashFilter) Filter(in zapcore.Field) zapcore.Field {
|
||||
if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok {
|
||||
newArray := make(caddyhttp.LoggableStringArray, len(array))
|
||||
for i, s := range array {
|
||||
array[i] = hash(s)
|
||||
newArray[i] = hash(s)
|
||||
}
|
||||
in.Interface = newArray
|
||||
} else {
|
||||
in.String = hash(in.String)
|
||||
}
|
||||
|
||||
return in
|
||||
}
|
||||
|
||||
@@ -219,9 +222,11 @@ func (m *IPMaskFilter) Provision(ctx caddy.Context) error {
|
||||
// Filter filters the input field.
|
||||
func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field {
|
||||
if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok {
|
||||
newArray := make(caddyhttp.LoggableStringArray, len(array))
|
||||
for i, s := range array {
|
||||
array[i] = m.mask(s)
|
||||
newArray[i] = m.mask(s)
|
||||
}
|
||||
in.Interface = newArray
|
||||
} else {
|
||||
in.String = m.mask(in.String)
|
||||
}
|
||||
@@ -368,9 +373,23 @@ func (m *QueryFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
|
||||
// Filter filters the input field.
|
||||
func (m QueryFilter) Filter(in zapcore.Field) zapcore.Field {
|
||||
u, err := url.Parse(in.String)
|
||||
if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok {
|
||||
newArray := make(caddyhttp.LoggableStringArray, len(array))
|
||||
for i, s := range array {
|
||||
newArray[i] = m.processQueryString(s)
|
||||
}
|
||||
in.Interface = newArray
|
||||
} else {
|
||||
in.String = m.processQueryString(in.String)
|
||||
}
|
||||
|
||||
return in
|
||||
}
|
||||
|
||||
func (m QueryFilter) processQueryString(s string) string {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return in
|
||||
return s
|
||||
}
|
||||
|
||||
q := u.Query()
|
||||
@@ -392,9 +411,7 @@ func (m QueryFilter) Filter(in zapcore.Field) zapcore.Field {
|
||||
}
|
||||
|
||||
u.RawQuery = q.Encode()
|
||||
in.String = u.String()
|
||||
|
||||
return in
|
||||
return u.String()
|
||||
}
|
||||
|
||||
type cookieFilterAction struct {
|
||||
@@ -580,9 +597,11 @@ func (m *RegexpFilter) Provision(ctx caddy.Context) error {
|
||||
// Filter filters the input field with the replacement value if it matches the regexp.
|
||||
func (f *RegexpFilter) Filter(in zapcore.Field) zapcore.Field {
|
||||
if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok {
|
||||
newArray := make(caddyhttp.LoggableStringArray, len(array))
|
||||
for i, s := range array {
|
||||
array[i] = f.regexp.ReplaceAllString(s, f.Value)
|
||||
newArray[i] = f.regexp.ReplaceAllString(s, f.Value)
|
||||
}
|
||||
in.Interface = newArray
|
||||
} else {
|
||||
in.String = f.regexp.ReplaceAllString(in.String, f.Value)
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ func TestIPMaskMultiValue(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryFilter(t *testing.T) {
|
||||
func TestQueryFilterSingleValue(t *testing.T) {
|
||||
f := QueryFilter{[]queryFilterAction{
|
||||
{replaceAction, "foo", "REDACTED"},
|
||||
{replaceAction, "notexist", "REDACTED"},
|
||||
@@ -102,6 +102,40 @@ func TestQueryFilter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryFilterMultiValue(t *testing.T) {
|
||||
f := QueryFilter{
|
||||
Actions: []queryFilterAction{
|
||||
{Type: replaceAction, Parameter: "foo", Value: "REDACTED"},
|
||||
{Type: replaceAction, Parameter: "notexist", Value: "REDACTED"},
|
||||
{Type: deleteAction, Parameter: "bar"},
|
||||
{Type: deleteAction, Parameter: "notexist"},
|
||||
{Type: hashAction, Parameter: "hash"},
|
||||
},
|
||||
}
|
||||
|
||||
if f.Validate() != nil {
|
||||
t.Fatalf("the filter must be valid")
|
||||
}
|
||||
|
||||
out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{
|
||||
"/path1?foo=a&foo=b&bar=c&bar=d&baz=e&hash=hashed",
|
||||
"/path2?foo=c&foo=d&bar=e&bar=f&baz=g&hash=hashed",
|
||||
}})
|
||||
arr, ok := out.Interface.(caddyhttp.LoggableStringArray)
|
||||
if !ok {
|
||||
t.Fatalf("field is wrong type: %T", out.Interface)
|
||||
}
|
||||
|
||||
expected1 := "/path1?baz=e&foo=REDACTED&foo=REDACTED&hash=e3b0c442"
|
||||
expected2 := "/path2?baz=g&foo=REDACTED&foo=REDACTED&hash=e3b0c442"
|
||||
if arr[0] != expected1 {
|
||||
t.Fatalf("query parameters in entry 0 have not been filtered correctly: got %s, expected %s", arr[0], expected1)
|
||||
}
|
||||
if arr[1] != expected2 {
|
||||
t.Fatalf("query parameters in entry 1 have not been filtered correctly: got %s, expected %s", arr[1], expected2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateQueryFilter(t *testing.T) {
|
||||
f := QueryFilter{[]queryFilterAction{
|
||||
{},
|
||||
|
||||
+4
-1
@@ -319,7 +319,10 @@ func globalDefaultReplacements(key string) (any, bool) {
|
||||
case "time.now":
|
||||
return nowFunc(), true
|
||||
case "time.now.http":
|
||||
return nowFunc().Format(http.TimeFormat), true
|
||||
// According to the comment for http.TimeFormat, the timezone must be in UTC
|
||||
// to generate the correct format.
|
||||
// https://github.com/caddyserver/caddy/issues/5773
|
||||
return nowFunc().UTC().Format(http.TimeFormat), true
|
||||
case "time.now.common_log":
|
||||
return nowFunc().Format("02/Jan/2006:15:04:05 -0700"), true
|
||||
case "time.now.year":
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
var namespaceTypes map[string][]reflect.Type = make(map[string][]reflect.Type)
|
||||
|
||||
func RegisterNamespace(namespace string, vals []interface{}) {
|
||||
var types []reflect.Type
|
||||
for _, v := range vals {
|
||||
reflect.TypeOf(v).Elem()
|
||||
}
|
||||
if _, ok := namespaceTypes[namespace]; ok {
|
||||
panic("namespace is already registered")
|
||||
}
|
||||
namespaceTypes[namespace] = types
|
||||
}
|
||||
|
||||
// NamespaceTypes returns a copy of Caddy's namespace->type registry
|
||||
func NamespaceTypes() map[string][]reflect.Type {
|
||||
copy := make(map[string][]reflect.Type)
|
||||
for namespace, typeSlice := range namespaceTypes {
|
||||
copy[namespace] = typeSlice
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
// ConformsToNamespace validates the given module implements all the mandatory types of a given namespace
|
||||
func ConformsToNamespace(mod Module, namespace string) (bool, error) {
|
||||
modType := reflect.TypeOf(mod)
|
||||
for _, t := range namespaceTypes[namespace] {
|
||||
if !modType.Implements(t) {
|
||||
return false, fmt.Errorf("%s does not implement %s", modType, t)
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
+1
-1
@@ -112,7 +112,7 @@ func (up *UsagePool) LoadOrNew(key any, construct Constructor) (value any, loade
|
||||
// LoadOrStore loads the value associated with key from the pool if it
|
||||
// already exists, or stores it if it does not exist. It returns the
|
||||
// value that was either loaded or stored, and true if the value already
|
||||
// existed and was
|
||||
// existed and was loaded, false if the value didn't exist and was stored.
|
||||
func (up *UsagePool) LoadOrStore(key, val any) (value any, loaded bool) {
|
||||
var upv *usagePoolVal
|
||||
up.Lock()
|
||||
|
||||
Reference in New Issue
Block a user