mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-26 16:52:40 -04:00
Compare commits
101 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 | |||
| f11c3c9f5a | |||
| 936ee918ee | |||
| d6f86cccf5 | |||
| 2d7d806fcf | |||
| d8135505d3 | |||
| 11166889c5 | |||
| 080db93817 | |||
| a8492c064d | |||
| 6cdcc2a782 | |||
| fbb0ecfa32 | |||
| 5b9c850ab3 | |||
| b32f265eca | |||
| 431adc0980 | |||
| a8cc5d1a7d | |||
| 8d304a4566 | |||
| 65e33fc1ee | |||
| 9f34383c02 | |||
| b07b198764 | |||
| 51b1bfb125 | |||
| c049bab458 |
@@ -24,7 +24,7 @@ jobs:
|
||||
- windows-latest
|
||||
go:
|
||||
- '1.20'
|
||||
# - '1.21'
|
||||
- '1.21'
|
||||
|
||||
include:
|
||||
# Set the minimum Go patch version for the given Go minor
|
||||
@@ -32,8 +32,8 @@ jobs:
|
||||
- go: '1.20'
|
||||
GO_SEMVER: '~1.20.6'
|
||||
|
||||
# - go: '1.21'
|
||||
# GO_SEMVER: '~1.21.0'
|
||||
- go: '1.21'
|
||||
GO_SEMVER: '~1.21.0'
|
||||
|
||||
# Set some variables per OS, usable via ${{ matrix.VAR }}
|
||||
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
|
||||
@@ -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'
|
||||
@@ -28,19 +29,19 @@ jobs:
|
||||
- 'darwin'
|
||||
- 'netbsd'
|
||||
go:
|
||||
- '1.20'
|
||||
- '1.21'
|
||||
|
||||
include:
|
||||
# Set the minimum Go patch version for the given Go minor
|
||||
# Usable via ${{ matrix.GO_SEMVER }}
|
||||
- go: '1.20'
|
||||
GO_SEMVER: '~1.20.6'
|
||||
- go: '1.21'
|
||||
GO_SEMVER: '~1.21.0'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
|
||||
@@ -17,8 +17,8 @@ jobs:
|
||||
# From https://github.com/golangci/golangci-lint-action
|
||||
golangci:
|
||||
permissions:
|
||||
contents: read # for actions/checkout to fetch code
|
||||
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
|
||||
contents: read # for actions/checkout to fetch code
|
||||
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
|
||||
name: lint
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -28,10 +28,10 @@ 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.20.6'
|
||||
go-version: '~1.21.0'
|
||||
check-latest: true
|
||||
|
||||
# Workaround for https://github.com/golangci/golangci-lint-action/issues/135
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: v1.53
|
||||
version: v1.54
|
||||
|
||||
# Workaround for https://github.com/golangci/golangci-lint-action/issues/135
|
||||
skip-pkg-cache: true
|
||||
@@ -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
|
||||
|
||||
@@ -13,13 +13,13 @@ jobs:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
go:
|
||||
- '1.20'
|
||||
- '1.21'
|
||||
|
||||
include:
|
||||
# Set the minimum Go patch version for the given Go minor
|
||||
# Usable via ${{ matrix.GO_SEMVER }}
|
||||
- go: '1.20'
|
||||
GO_SEMVER: '~1.20.6'
|
||||
- go: '1.21'
|
||||
GO_SEMVER: '~1.21.0'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
|
||||
@@ -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,10 +106,10 @@ 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 --rm-dist --timeout 60m
|
||||
args: release --clean --timeout 60m
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ steps.vars.outputs.version_tag }}
|
||||
|
||||
@@ -12,6 +12,7 @@ Caddyfile.*
|
||||
cmd/caddy/caddy
|
||||
cmd/caddy/caddy.exe
|
||||
cmd/caddy/tmp/*.exe
|
||||
cmd/caddy/.env
|
||||
|
||||
# mac specific
|
||||
.DS_Store
|
||||
|
||||
+20
-7
@@ -2,14 +2,27 @@ linters-settings:
|
||||
errcheck:
|
||||
ignore: fmt:.*,go.uber.org/zap/zapcore:^Add.*
|
||||
ignoretests: true
|
||||
gci:
|
||||
sections:
|
||||
- standard # Standard section: captures all standard packages.
|
||||
- default # Default section: contains all imports that could not be matched to another section type.
|
||||
- prefix(github.com/caddyserver/caddy/v2/cmd) # ensure that this is always at the top and always has a line break.
|
||||
- prefix(github.com/caddyserver/caddy) # Custom section: groups all imports with the specified Prefix.
|
||||
# Skip generated files.
|
||||
# Default: true
|
||||
skip-generated: true
|
||||
# Enable custom order of sections.
|
||||
# If `true`, make the section order the same as the order of `sections`.
|
||||
# Default: false
|
||||
custom-order: true
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- bodyclose
|
||||
- errcheck
|
||||
- gofmt
|
||||
- goimports
|
||||
- gci
|
||||
- gofumpt
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
@@ -77,23 +90,23 @@ output:
|
||||
issues:
|
||||
exclude-rules:
|
||||
# we aren't calling unknown URL
|
||||
- text: "G107" # G107: Url provided to HTTP request as taint input
|
||||
- text: 'G107' # G107: Url provided to HTTP request as taint input
|
||||
linters:
|
||||
- gosec
|
||||
# as a web server that's expected to handle any template, this is totally in the hands of the user.
|
||||
- text: "G203" # G203: Use of unescaped data in HTML templates
|
||||
- text: 'G203' # G203: Use of unescaped data in HTML templates
|
||||
linters:
|
||||
- gosec
|
||||
# we're shelling out to known commands, not relying on user-defined input.
|
||||
- text: "G204" # G204: Audit use of command execution
|
||||
- text: 'G204' # G204: Audit use of command execution
|
||||
linters:
|
||||
- gosec
|
||||
# the choice of weakrand is deliberate, hence the named import "weakrand"
|
||||
- path: modules/caddyhttp/reverseproxy/selectionpolicies.go
|
||||
text: "G404" # G404: Insecure random number source (rand)
|
||||
text: 'G404' # G404: Insecure random number source (rand)
|
||||
linters:
|
||||
- gosec
|
||||
- path: modules/caddyhttp/reverseproxy/streaming.go
|
||||
text: "G404" # G404: Insecure random number source (rand)
|
||||
text: 'G404' # G404: Insecure random number source (rand)
|
||||
linters:
|
||||
- gosec
|
||||
|
||||
+8
-1
@@ -43,6 +43,7 @@ builds:
|
||||
- arm64
|
||||
- s390x
|
||||
- ppc64le
|
||||
- riscv64
|
||||
goarm:
|
||||
- "5"
|
||||
- "6"
|
||||
@@ -54,14 +55,20 @@ builds:
|
||||
goarch: ppc64le
|
||||
- goos: darwin
|
||||
goarch: s390x
|
||||
- goos: darwin
|
||||
goarch: riscv64
|
||||
- goos: windows
|
||||
goarch: ppc64le
|
||||
- goos: windows
|
||||
goarch: s390x
|
||||
- goos: windows
|
||||
goarch: riscv64
|
||||
- goos: freebsd
|
||||
goarch: ppc64le
|
||||
- goos: freebsd
|
||||
goarch: s390x
|
||||
- goos: freebsd
|
||||
goarch: riscv64
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
goarm: "5"
|
||||
@@ -106,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)
|
||||
@@ -1346,7 +1359,7 @@ var (
|
||||
// will get deleted before the process gracefully exits.
|
||||
func PIDFile(filename string) error {
|
||||
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
|
||||
err := os.WriteFile(filename, pid, 0600)
|
||||
err := os.WriteFile(filename, pid, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -34,12 +34,22 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/notify"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"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)
|
||||
@@ -71,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
|
||||
@@ -356,13 +370,13 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
||||
newCfg.Admin.Config.Persist == nil ||
|
||||
*newCfg.Admin.Config.Persist) {
|
||||
dir := filepath.Dir(ConfigAutosavePath)
|
||||
err := os.MkdirAll(dir, 0700)
|
||||
err := os.MkdirAll(dir, 0o700)
|
||||
if err != nil {
|
||||
Log().Error("unable to create folder for config autosave",
|
||||
zap.String("dir", dir),
|
||||
zap.Error(err))
|
||||
} else {
|
||||
err := os.WriteFile(ConfigAutosavePath, cfgJSON, 0600)
|
||||
err := os.WriteFile(ConfigAutosavePath, cfgJSON, 0o600)
|
||||
if err == nil {
|
||||
Log().Info("autosaved config (load with --resume flag)", zap.String("file", ConfigAutosavePath))
|
||||
} else {
|
||||
@@ -824,14 +838,19 @@ 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.WriteFile(uuidFilePath, []byte(uuid.String()), 0600)
|
||||
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 {
|
||||
return [16]byte{}, err
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,8 +19,9 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
// parseVariadic determines if the token is a variadic placeholder,
|
||||
@@ -51,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
|
||||
@@ -93,6 +101,11 @@ func makeArgsReplacer(args []string) *caddy.Replacer {
|
||||
// TODO: Remove the deprecated {args.*} placeholder
|
||||
// support at some point in the future
|
||||
if matches := argsRegexpIndexDeprecated.FindStringSubmatch(key); len(matches) > 0 {
|
||||
// What's matched may be a substring of the key
|
||||
if matches[0] != key {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
value, err := strconv.Atoi(matches[1])
|
||||
if err != nil {
|
||||
caddy.Log().Named("caddyfile").Warn(
|
||||
@@ -111,6 +124,11 @@ func makeArgsReplacer(args []string) *caddy.Replacer {
|
||||
|
||||
// Handle args[*] form
|
||||
if matches := argsRegexpIndex.FindStringSubmatch(key); len(matches) > 0 {
|
||||
// What's matched may be a substring of the key
|
||||
if matches[0] != key {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if strings.Contains(matches[1], ":") {
|
||||
caddy.Log().Named("caddyfile").Warn(
|
||||
"Variadic placeholder {args[" + matches[1] + "]} must be a token on its own")
|
||||
|
||||
@@ -34,6 +34,7 @@ func (i *importGraph) addNode(name string) {
|
||||
}
|
||||
i.nodes[name] = true
|
||||
}
|
||||
|
||||
func (i *importGraph) addNodes(names []string) {
|
||||
for _, name := range names {
|
||||
i.addNode(name)
|
||||
@@ -43,6 +44,7 @@ func (i *importGraph) addNodes(names []string) {
|
||||
func (i *importGraph) removeNode(name string) {
|
||||
delete(i.nodes, name)
|
||||
}
|
||||
|
||||
func (i *importGraph) removeNodes(names []string) {
|
||||
for _, name := range names {
|
||||
i.removeNode(name)
|
||||
@@ -73,6 +75,7 @@ func (i *importGraph) addEdge(from, to string) error {
|
||||
i.edges[from] = append(i.edges[from], to)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *importGraph) addEdges(from string, tos []string) error {
|
||||
for _, to := range tos {
|
||||
err := i.addEdge(from, to)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,8 +22,9 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
// Parse parses the input just enough to group tokens, in
|
||||
@@ -565,7 +566,6 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
|
||||
// are loaded into the current server block for later use
|
||||
// by directive setup functions.
|
||||
func (p *parser) directive() error {
|
||||
|
||||
// a segment is a list of tokens associated with this directive
|
||||
var segment Segment
|
||||
|
||||
|
||||
@@ -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",
|
||||
@@ -718,6 +722,36 @@ func TestEnvironmentReplacement(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportReplacementInJSONWithBrace(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
args []string
|
||||
input string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
args: []string{"123"},
|
||||
input: "{args[0]}",
|
||||
expect: "123",
|
||||
},
|
||||
{
|
||||
args: []string{"123"},
|
||||
input: `{"key":"{args[0]}"}`,
|
||||
expect: `{"key":"123"}`,
|
||||
},
|
||||
{
|
||||
args: []string{"123", "123"},
|
||||
input: `{"key":[{args[0]},{args[1]}]}`,
|
||||
expect: `{"key":[123,123]}`,
|
||||
},
|
||||
} {
|
||||
repl := makeArgsReplacer(test.args)
|
||||
actual := repl.ReplaceKnown(test.input, "")
|
||||
if actual != test.expect {
|
||||
t.Errorf("Test %d: Expected: '%s' but got '%s'", i, test.expect, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnippets(t *testing.T) {
|
||||
p := testParser(`
|
||||
(common) {
|
||||
|
||||
@@ -24,10 +24,11 @@ import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/certmagic"
|
||||
)
|
||||
|
||||
// mapAddressToServerBlocks returns a map of listener address to list of server
|
||||
@@ -77,7 +78,8 @@ import (
|
||||
// multiple addresses to the same lists of server blocks (a many:many mapping).
|
||||
// (Doing this is essentially a map-reduce technique.)
|
||||
func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock,
|
||||
options map[string]any) (map[string][]serverBlock, error) {
|
||||
options map[string]any,
|
||||
) (map[string][]serverBlock, error) {
|
||||
sbmap := make(map[string][]serverBlock)
|
||||
|
||||
for i, sblock := range originalServerBlocks {
|
||||
@@ -187,13 +189,25 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]se
|
||||
// listenerAddrsForServerBlockKey essentially converts the Caddyfile
|
||||
// site addresses to Caddy listener addresses for each server block.
|
||||
func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string,
|
||||
options map[string]any) ([]string, error) {
|
||||
options map[string]any,
|
||||
) ([]string, error) {
|
||||
addr, err := ParseAddress(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing key: %v", err)
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -26,14 +26,15 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/mholt/acmez/acme"
|
||||
"go.uber.org/zap/zapcore"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/mholt/acmez/acme"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -180,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]
|
||||
}
|
||||
@@ -198,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())
|
||||
}
|
||||
|
||||
@@ -217,7 +217,8 @@ func (h Helper) ExtractMatcherSet() (caddy.ModuleMap, error) {
|
||||
|
||||
// NewRoute returns config values relevant to creating a new HTTP route.
|
||||
func (h Helper) NewRoute(matcherSet caddy.ModuleMap,
|
||||
handler caddyhttp.MiddlewareHandler) []ConfigValue {
|
||||
handler caddyhttp.MiddlewareHandler,
|
||||
) []ConfigValue {
|
||||
mod, err := caddy.GetModule(caddy.GetModuleID(handler))
|
||||
if err != nil {
|
||||
*h.warnings = append(*h.warnings, caddyconfig.Warning{
|
||||
|
||||
@@ -17,20 +17,21 @@ package httpcaddyfile
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddypki"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -49,8 +50,7 @@ type App struct {
|
||||
}
|
||||
|
||||
// ServerType can set up a config from an HTTP Caddyfile.
|
||||
type ServerType struct {
|
||||
}
|
||||
type ServerType struct{}
|
||||
|
||||
// Setup makes a config from the tokens.
|
||||
func (st ServerType) Setup(
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1059,8 +1051,8 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
|
||||
subroute *caddyhttp.Subroute,
|
||||
matcherSetsEnc []caddy.ModuleMap,
|
||||
p sbAddrAssociation,
|
||||
warnings *[]caddyconfig.Warning) caddyhttp.RouteList {
|
||||
|
||||
warnings *[]caddyconfig.Warning,
|
||||
) caddyhttp.RouteList {
|
||||
// nothing to do if... there's nothing to do
|
||||
if len(matcherSetsEnc) == 0 && len(subroute.Routes) == 0 && subroute.Errors == nil {
|
||||
return routeList
|
||||
@@ -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
|
||||
@@ -1608,8 +1569,10 @@ type sbAddrAssociation struct {
|
||||
serverBlocks []serverBlock
|
||||
}
|
||||
|
||||
const matcherPrefix = "@"
|
||||
const namedRouteKey = "named_route"
|
||||
const (
|
||||
matcherPrefix = "@"
|
||||
namedRouteKey = "named_route"
|
||||
)
|
||||
|
||||
// Interface guard
|
||||
var _ caddyfile.ServerType = (*ServerType)(nil)
|
||||
|
||||
@@ -17,12 +17,13 @@ package httpcaddyfile
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/mholt/acmez/acme"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/mholt/acmez/acme"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -174,7 +174,6 @@ func (st ServerType) buildPKIApp(
|
||||
options map[string]any,
|
||||
warnings []caddyconfig.Warning,
|
||||
) (*caddypki.PKI, []caddyconfig.Warning, error) {
|
||||
|
||||
skipInstallTrust := false
|
||||
if _, ok := options["skip_install_trust"]; ok {
|
||||
skipInstallTrust = true
|
||||
|
||||
@@ -18,11 +18,12 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
// serverOptions collects server config overrides parsed from Caddyfile global options
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,12 +23,13 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/mholt/acmez/acme"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/mholt/acmez/acme"
|
||||
)
|
||||
|
||||
func (st ServerType) buildTLSApp(
|
||||
@@ -36,7 +37,6 @@ func (st ServerType) buildTLSApp(
|
||||
options map[string]any,
|
||||
warnings []caddyconfig.Warning,
|
||||
) (*caddytls.TLS, []caddyconfig.Warning, error) {
|
||||
|
||||
tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}
|
||||
var certLoaders []caddytls.CertificateLoader
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
+3
-18
@@ -22,9 +22,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/aryann/difflib"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
|
||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
// plug in Caddy modules here
|
||||
_ "github.com/caddyserver/caddy/v2/modules/standard"
|
||||
)
|
||||
@@ -63,7 +64,6 @@ type Tester struct {
|
||||
|
||||
// NewTester will create a new testing client with an attached cookie jar
|
||||
func NewTester(t *testing.T) *Tester {
|
||||
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create cookiejar: %s", err)
|
||||
@@ -94,7 +94,6 @@ func timeElapsed(start time.Time, name string) {
|
||||
// InitServer this will configure the server with a configurion of a specific
|
||||
// type. The configType must be either "json" or the adapter type.
|
||||
func (tc *Tester) InitServer(rawConfig string, configType string) {
|
||||
|
||||
if err := tc.initServer(rawConfig, configType); err != nil {
|
||||
tc.t.Logf("failed to load config: %s", err)
|
||||
tc.t.Fail()
|
||||
@@ -108,7 +107,6 @@ func (tc *Tester) InitServer(rawConfig string, configType string) {
|
||||
// InitServer this will configure the server with a configurion of a specific
|
||||
// type. The configType must be either "json" or the adapter type.
|
||||
func (tc *Tester) initServer(rawConfig string, configType string) error {
|
||||
|
||||
if testing.Short() {
|
||||
tc.t.SkipNow()
|
||||
return nil
|
||||
@@ -232,7 +230,6 @@ const initConfig = `{
|
||||
// validateTestPrerequisites ensures the certificates are available in the
|
||||
// designated path and Caddy sub-process is running.
|
||||
func validateTestPrerequisites(t *testing.T) error {
|
||||
|
||||
// check certificates are found
|
||||
for _, certName := range Default.Certifcates {
|
||||
if _, err := os.Stat(getIntegrationDir() + certName); os.IsNotExist(err) {
|
||||
@@ -284,7 +281,6 @@ func isCaddyAdminRunning() error {
|
||||
}
|
||||
|
||||
func getIntegrationDir() string {
|
||||
|
||||
_, filename, _, ok := runtime.Caller(1)
|
||||
if !ok {
|
||||
panic("unable to determine the current file path")
|
||||
@@ -304,7 +300,6 @@ func prependCaddyFilePath(rawConfig string) string {
|
||||
|
||||
// CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally
|
||||
func CreateTestingTransport() *http.Transport {
|
||||
|
||||
dialer := net.Dialer{
|
||||
Timeout: 5 * time.Second,
|
||||
KeepAlive: 5 * time.Second,
|
||||
@@ -332,7 +327,6 @@ func CreateTestingTransport() *http.Transport {
|
||||
|
||||
// AssertLoadError will load a config and expect an error
|
||||
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
|
||||
|
||||
tc := NewTester(t)
|
||||
|
||||
err := tc.initServer(rawConfig, configType)
|
||||
@@ -343,7 +337,6 @@ func AssertLoadError(t *testing.T, rawConfig string, configType string, expected
|
||||
|
||||
// AssertRedirect makes a request and asserts the redirection happens
|
||||
func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
|
||||
|
||||
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
@@ -381,7 +374,6 @@ func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, e
|
||||
|
||||
// CompareAdapt adapts a config and then compares it against an expected result
|
||||
func CompareAdapt(t *testing.T, filename, rawConfig string, adapterName string, expectedResponse string) bool {
|
||||
|
||||
cfgAdapter := caddyconfig.GetAdapter(adapterName)
|
||||
if cfgAdapter == nil {
|
||||
t.Logf("unrecognized config adapter '%s'", adapterName)
|
||||
@@ -469,14 +461,13 @@ func applyHeaders(t *testing.T, req *http.Request, requestHeaders []string) {
|
||||
|
||||
// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions
|
||||
func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
|
||||
|
||||
resp, err := tc.Client.Do(req)
|
||||
if err != nil {
|
||||
tc.t.Fatalf("failed to call server %s", err)
|
||||
}
|
||||
|
||||
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
|
||||
@@ -484,7 +475,6 @@ func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int)
|
||||
|
||||
// AssertResponse request a URI and assert the status code and the body contains a string
|
||||
func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||
|
||||
resp := tc.AssertResponseCode(req, expectedStatusCode)
|
||||
|
||||
defer resp.Body.Close()
|
||||
@@ -506,7 +496,6 @@ func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expe
|
||||
|
||||
// AssertGetResponse GET a URI and expect a statusCode and body text
|
||||
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||
|
||||
req, err := http.NewRequest("GET", requestURI, nil)
|
||||
if err != nil {
|
||||
tc.t.Fatalf("unable to create request %s", err)
|
||||
@@ -517,7 +506,6 @@ func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, e
|
||||
|
||||
// AssertDeleteResponse request a URI and expect a statusCode and body text
|
||||
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||
|
||||
req, err := http.NewRequest("DELETE", requestURI, nil)
|
||||
if err != nil {
|
||||
tc.t.Fatalf("unable to create request %s", err)
|
||||
@@ -528,7 +516,6 @@ func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int
|
||||
|
||||
// AssertPostResponseBody POST to a URI and assert the response code and body
|
||||
func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||
|
||||
req, err := http.NewRequest("POST", requestURI, requestBody)
|
||||
if err != nil {
|
||||
tc.t.Errorf("failed to create request %s", err)
|
||||
@@ -542,7 +529,6 @@ func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []str
|
||||
|
||||
// AssertPutResponseBody PUT to a URI and assert the response code and body
|
||||
func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||
|
||||
req, err := http.NewRequest("PUT", requestURI, requestBody)
|
||||
if err != nil {
|
||||
tc.t.Errorf("failed to create request %s", err)
|
||||
@@ -556,7 +542,6 @@ func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []stri
|
||||
|
||||
// AssertPatchResponseBody PATCH to a URI and assert the response code and body
|
||||
func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||
|
||||
req, err := http.NewRequest("PATCH", requestURI, requestBody)
|
||||
if err != nil {
|
||||
tc.t.Errorf("failed to create request %s", err)
|
||||
|
||||
@@ -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*"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -69,11 +69,11 @@
|
||||
}
|
||||
],
|
||||
"on_demand": {
|
||||
"ask": "https://example.com",
|
||||
"rate_limit": {
|
||||
"interval": 30000000000,
|
||||
"burst": 20
|
||||
},
|
||||
"ask": "https://example.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"disable_ocsp_stapling": true
|
||||
|
||||
@@ -78,11 +78,11 @@
|
||||
}
|
||||
],
|
||||
"on_demand": {
|
||||
"ask": "https://example.com",
|
||||
"rate_limit": {
|
||||
"interval": 30000000000,
|
||||
"burst": 20
|
||||
},
|
||||
"ask": "https://example.com"
|
||||
}
|
||||
},
|
||||
"ocsp_interval": 172800000000000,
|
||||
"renew_interval": 86400000000000,
|
||||
|
||||
@@ -71,11 +71,11 @@
|
||||
}
|
||||
],
|
||||
"on_demand": {
|
||||
"ask": "https://example.com",
|
||||
"rate_limit": {
|
||||
"interval": 30000000000,
|
||||
"burst": 20
|
||||
},
|
||||
"ask": "https://example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,100 @@
|
||||
*.sandbox.localhost {
|
||||
@sandboxPort {
|
||||
header_regexp first_label Host ^([0-9]{3})\.sandbox\.
|
||||
}
|
||||
handle @sandboxPort {
|
||||
reverse_proxy {re.first_label.1}
|
||||
}
|
||||
handle {
|
||||
redir {scheme}://application.localhost
|
||||
}
|
||||
}
|
||||
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"*.sandbox.localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"group": "group2",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "{http.regexp.first_label.1}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"match": [
|
||||
{
|
||||
"header_regexp": {
|
||||
"Host": {
|
||||
"name": "first_label",
|
||||
"pattern": "^([0-9]{3})\\.sandbox\\."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "group2",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "static_response",
|
||||
"headers": {
|
||||
"Location": [
|
||||
"{http.request.scheme}://application.localhost"
|
||||
]
|
||||
},
|
||||
"status_code": 302
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
*.sandbox.localhost {
|
||||
@sandboxPort {
|
||||
header_regexp port Host ^([0-9]{3})\.sandbox\.
|
||||
}
|
||||
handle @sandboxPort {
|
||||
reverse_proxy app:6{re.port.1}
|
||||
}
|
||||
handle {
|
||||
redir {scheme}://application.localhost
|
||||
}
|
||||
}
|
||||
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"*.sandbox.localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"group": "group2",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "app:6{http.regexp.port.1}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"match": [
|
||||
{
|
||||
"header_regexp": {
|
||||
"Host": {
|
||||
"name": "port",
|
||||
"pattern": "^([0-9]{3})\\.sandbox\\."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "group2",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "static_response",
|
||||
"headers": {
|
||||
"Location": [
|
||||
"{http.request.scheme}://application.localhost"
|
||||
]
|
||||
},
|
||||
"status_code": 302
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
*.sandbox.localhost {
|
||||
@sandboxPort {
|
||||
header_regexp port Host ^([0-9]{3})\.sandbox\.
|
||||
}
|
||||
handle @sandboxPort {
|
||||
reverse_proxy app:{re.port.1}
|
||||
}
|
||||
handle {
|
||||
redir {scheme}://application.localhost
|
||||
}
|
||||
}
|
||||
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"*.sandbox.localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"group": "group2",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "app:{http.regexp.port.1}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"match": [
|
||||
{
|
||||
"header_regexp": {
|
||||
"Host": {
|
||||
"name": "port",
|
||||
"pattern": "^([0-9]{3})\\.sandbox\\."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "group2",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "static_response",
|
||||
"headers": {
|
||||
"Location": [
|
||||
"{http.request.scheme}://application.localhost"
|
||||
]
|
||||
},
|
||||
"status_code": 302
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
+166
-96
@@ -22,6 +22,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -32,18 +33,27 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/aryann/difflib"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"go.uber.org/zap"
|
||||
"github.com/caddyserver/caddy/v2/internal"
|
||||
)
|
||||
|
||||
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
|
||||
@@ -64,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)
|
||||
@@ -91,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
|
||||
@@ -99,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
|
||||
@@ -154,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 {
|
||||
@@ -201,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))
|
||||
}
|
||||
}
|
||||
@@ -227,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)
|
||||
@@ -242,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
|
||||
@@ -275,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)
|
||||
}
|
||||
@@ -297,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
|
||||
@@ -309,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)
|
||||
}
|
||||
@@ -411,60 +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")
|
||||
|
||||
// if no input file was specified, try a default
|
||||
// Caddyfile if the Caddyfile adapter is plugged in
|
||||
if adaptCmdInputFlag == "" && caddyconfig.GetAdapter("caddyfile") != nil {
|
||||
_, err := os.Stat("Caddyfile")
|
||||
if err == nil {
|
||||
// default Caddyfile exists
|
||||
adaptCmdInputFlag = "Caddyfile"
|
||||
caddy.Log().Info("using adjacent Caddyfile")
|
||||
} else if !os.IsNotExist(err) {
|
||||
// default Caddyfile exists, but error accessing it
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("accessing default Caddyfile: %v", err)
|
||||
}
|
||||
var err error
|
||||
inputFlag, err = configFileWithRespectToDefault(caddy.Log(), inputFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
if adaptCmdInputFlag == "" {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
|
||||
// load all additional envs as soon as possible
|
||||
err = handleEnvFileFlag(fl)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
if adaptCmdAdapterFlag == "" {
|
||||
|
||||
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 {
|
||||
@@ -482,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 {
|
||||
@@ -504,19 +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)
|
||||
}
|
||||
err := handleEnvFileFlag(fl)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
input, _, err := LoadConfig(validateCmdConfigFlag, validateCmdAdapterFlag)
|
||||
// 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(configFlag, adapterFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
@@ -539,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,
|
||||
@@ -555,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)
|
||||
@@ -564,7 +580,7 @@ func cmdFmt(fl Flags) (int, error) {
|
||||
output := caddyfile.Format(input)
|
||||
|
||||
if fl.Bool("overwrite") {
|
||||
if err := os.WriteFile(formatCmdConfigFile, output, 0600); 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
|
||||
@@ -588,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,
|
||||
@@ -598,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
|
||||
@@ -611,6 +646,16 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
|
||||
origin := "http://" + parsedAddr.JoinHostPort(0)
|
||||
if parsedAddr.IsUnixNetwork() {
|
||||
origin = "http://127.0.0.1" // bogus host is a hack so that http.NewRequest() is happy
|
||||
|
||||
// the unix address at this point might still contain the optional
|
||||
// unix socket permissions, which are part of the address/host.
|
||||
// those need to be removed first, as they aren't part of the
|
||||
// resulting unix file path
|
||||
addr, _, err := internal.SplitUnixSocketPermissionsBits(parsedAddr.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parsedAddr.Host = addr
|
||||
}
|
||||
|
||||
// form the request
|
||||
@@ -725,6 +770,31 @@ func DetermineAdminAPIAddress(address string, config []byte, configFile, configA
|
||||
return caddy.DefaultAdminListen, nil
|
||||
}
|
||||
|
||||
// configFileWithRespectToDefault returns the filename to use for loading the config, based
|
||||
// on whether a config file is already specified and a supported default config file exists.
|
||||
func configFileWithRespectToDefault(logger *zap.Logger, configFile string) (string, error) {
|
||||
const defaultCaddyfile = "Caddyfile"
|
||||
|
||||
// if no input file was specified, try a default Caddyfile if the Caddyfile adapter is plugged in
|
||||
if configFile == "" && caddyconfig.GetAdapter("caddyfile") != nil {
|
||||
_, err := os.Stat(defaultCaddyfile)
|
||||
if err == nil {
|
||||
// default Caddyfile exists
|
||||
if logger != nil {
|
||||
logger.Info("using adjacent Caddyfile")
|
||||
}
|
||||
return defaultCaddyfile, nil
|
||||
}
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
// problem checking
|
||||
return configFile, fmt.Errorf("checking if default Caddyfile exists: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// default config file does not exist or is irrelevant
|
||||
return configFile, nil
|
||||
}
|
||||
|
||||
type moduleInfo struct {
|
||||
caddyModuleID string
|
||||
goModule *debug.Module
|
||||
|
||||
+25
-13
@@ -21,9 +21,10 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/cobra/doc"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
// Command represents a subcommand. Name, Func,
|
||||
@@ -93,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
|
||||
@@ -103,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)
|
||||
@@ -132,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
|
||||
@@ -149,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")
|
||||
@@ -239,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.
|
||||
@@ -248,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
|
||||
@@ -258,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
|
||||
@@ -275,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)
|
||||
},
|
||||
})
|
||||
@@ -294,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)
|
||||
},
|
||||
})
|
||||
@@ -444,7 +456,7 @@ argument of --directory. If the directory does not exist, it will be created.
|
||||
if dir == "" {
|
||||
return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required")
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return caddy.ExitCodeFailedQuit, err
|
||||
}
|
||||
if err := doc.GenManTree(rootCmd, &doc.GenManHeader{
|
||||
|
||||
+16
-7
@@ -17,6 +17,7 @@ package caddycmd
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -30,11 +31,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/spf13/pflag"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -62,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)
|
||||
}
|
||||
}
|
||||
@@ -117,9 +123,8 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
||||
zap.String("config_adapter", adapterName))
|
||||
}
|
||||
} else if adapterName == "" {
|
||||
// as a special case when no config file or adapter
|
||||
// is specified, see if the Caddyfile adapter is
|
||||
// plugged in, and if so, try using a default Caddyfile
|
||||
// if the Caddyfile adapter is plugged in, we can try using an
|
||||
// adjacent Caddyfile by default
|
||||
cfgAdapter = caddyconfig.GetAdapter("caddyfile")
|
||||
if cfgAdapter != nil {
|
||||
config, err = os.ReadFile("Caddyfile")
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-1
@@ -22,13 +22,15 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func cmdUpgrade(fl Flags) (int, error) {
|
||||
@@ -102,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
|
||||
|
||||
+6
-4
@@ -23,8 +23,9 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/certmagic"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
type storVal struct {
|
||||
@@ -199,9 +200,10 @@ func cmdExportStorage(fl Flags) (int, error) {
|
||||
}
|
||||
|
||||
hdr := &tar.Header{
|
||||
Name: k,
|
||||
Mode: 0600,
|
||||
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.1
|
||||
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.1
|
||||
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.11.0
|
||||
golang.org/x/crypto v0.14.0
|
||||
golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0
|
||||
golang.org/x/net v0.12.0
|
||||
golang.org/x/sync v0.3.0
|
||||
golang.org/x/term v0.10.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.0 // 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.10.0
|
||||
golang.org/x/text v0.11.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
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SplitUnixSocketPermissionsBits takes a unix socket address in the
|
||||
// unusual "path|bits" format (e.g. /run/caddy.sock|0222) and tries
|
||||
// to split it into socket path (host) and permissions bits (port).
|
||||
// Colons (":") can't be used as separator, as socket paths on Windows
|
||||
// may include a drive letter (e.g. `unix/c:\absolute\path.sock`).
|
||||
// Permission bits will default to 0200 if none are specified.
|
||||
// Throws an error, if the first carrying bit does not
|
||||
// include write perms (e.g. `0422` or `022`).
|
||||
// Symbolic permission representation (e.g. `u=w,g=w,o=w`)
|
||||
// is not supported and will throw an error for now!
|
||||
func SplitUnixSocketPermissionsBits(addr string) (path string, fileMode fs.FileMode, err error) {
|
||||
addrSplit := strings.SplitN(addr, "|", 2)
|
||||
|
||||
if len(addrSplit) == 2 {
|
||||
// parse octal permission bit string as uint32
|
||||
fileModeUInt64, err := strconv.ParseUint(addrSplit[1], 8, 32)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("could not parse octal permission bits in %s: %v", addr, err)
|
||||
}
|
||||
fileMode = fs.FileMode(fileModeUInt64)
|
||||
|
||||
// FileMode.String() returns a string like `-rwxr-xr--` for `u=rwx,g=rx,o=r` (`0754`)
|
||||
if string(fileMode.String()[2]) != "w" {
|
||||
return "", 0, fmt.Errorf("owner of the socket requires '-w-' (write, octal: '2') permissions at least; got '%s' in %s", fileMode.String()[1:4], addr)
|
||||
}
|
||||
|
||||
return addrSplit[0], fileMode, nil
|
||||
}
|
||||
|
||||
// default to 0200 (symbolic: `u=w,g=,o=`)
|
||||
// if no permission bits are specified
|
||||
return addr, 0o200, nil
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+143
-210
@@ -28,14 +28,21 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"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().
|
||||
@@ -147,17 +154,19 @@ 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
|
||||
if na.IsUnixNetwork() {
|
||||
var err error
|
||||
address, unixFileMode, err = splitUnixSocketPermissionsBits(na.Host)
|
||||
address, unixFileMode, err = internal.SplitUnixSocketPermissionsBits(na.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -173,34 +182,16 @@ func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net
|
||||
if err := os.Chmod(address, unixFileMode); err != nil {
|
||||
return nil, fmt.Errorf("unable to set permissions (%s) on %s: %v", unixFileMode, address, err)
|
||||
}
|
||||
|
||||
}
|
||||
return socket, err
|
||||
}
|
||||
|
||||
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
|
||||
@@ -209,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 {
|
||||
@@ -319,40 +303,6 @@ func IsUnixNetwork(netw string) bool {
|
||||
return strings.HasPrefix(netw, "unix")
|
||||
}
|
||||
|
||||
// Takes a unix socket address in the unusual "path|bits" format
|
||||
// (e.g. /run/caddy.sock|0222) and tries to split it into
|
||||
// socket path (host) and permissions bits (port). Colons (":")
|
||||
// can't be used as separator, as socket paths on Windows may
|
||||
// include a drive letter (e.g. `unix/c:\absolute\path.sock`).
|
||||
// Permission bits will default to 0200 if none are specified.
|
||||
// Throws an error, if the first carrying bit does not
|
||||
// include write perms (e.g. `0422` or `022`).
|
||||
// Symbolic permission representation (e.g. `u=w,g=w,o=w`)
|
||||
// is not supported and will throw an error for now!
|
||||
func splitUnixSocketPermissionsBits(addr string) (path string, fileMode fs.FileMode, err error) {
|
||||
addrSplit := strings.SplitN(addr, "|", 2)
|
||||
|
||||
if len(addrSplit) == 2 {
|
||||
// parse octal permission bit string as uint32
|
||||
fileModeUInt64, err := strconv.ParseUint(addrSplit[1], 8, 32)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("could not parse octal permission bits in %s: %v", addr, err)
|
||||
}
|
||||
fileMode = fs.FileMode(fileModeUInt64)
|
||||
|
||||
// FileMode.String() returns a string like `-rwxr-xr--` for `u=rwx,g=rx,o=r` (`0754`)
|
||||
if string(fileMode.String()[2]) != "w" {
|
||||
return "", 0, fmt.Errorf("owner of the socket requires '-w-' (write, octal: '2') permissions at least; got '%s' in %s", fileMode.String()[1:4], addr)
|
||||
}
|
||||
|
||||
return addrSplit[0], fileMode, nil
|
||||
}
|
||||
|
||||
// default to 0200 (symbolic: `u=w,g=,o=`)
|
||||
// if no permission bits are specified
|
||||
return addr, 0200, nil
|
||||
}
|
||||
|
||||
// ParseNetworkAddress parses addr into its individual
|
||||
// components. The input string is expected to be of
|
||||
// the form "network/host:port-range" where any part is
|
||||
@@ -377,7 +327,7 @@ func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort ui
|
||||
network = defaultNetwork
|
||||
}
|
||||
if IsUnixNetwork(network) {
|
||||
_, _, err := splitUnixSocketPermissionsBits(host)
|
||||
_, _, err := internal.SplitUnixSocketPermissionsBits(host)
|
||||
return NetworkAddress{
|
||||
Network: network,
|
||||
Host: host,
|
||||
@@ -503,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
|
||||
@@ -567,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
|
||||
}
|
||||
|
||||
@@ -606,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
|
||||
}
|
||||
@@ -630,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
|
||||
@@ -669,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
|
||||
}
|
||||
@@ -723,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
|
||||
}
|
||||
@@ -753,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
|
||||
@@ -824,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)
|
||||
)
|
||||
|
||||
+3
-1
@@ -17,6 +17,8 @@ package caddy
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/internal"
|
||||
)
|
||||
|
||||
func TestSplitNetworkAddress(t *testing.T) {
|
||||
@@ -634,7 +636,7 @@ func TestSplitUnixSocketPermissionsBits(t *testing.T) {
|
||||
expectErr: true,
|
||||
},
|
||||
} {
|
||||
actualPath, actualFileMode, err := splitUnixSocketPermissionsBits(tc.input)
|
||||
actualPath, actualFileMode, err := internal.SplitUnixSocketPermissionsBits(tc.input)
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("Test %d: Expected error but got: %v", i, err)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
+2
-1
@@ -3,10 +3,11 @@ package caddy
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/internal/metrics"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/internal/metrics"
|
||||
)
|
||||
|
||||
// define and register the metrics used in this package.
|
||||
|
||||
@@ -22,9 +22,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -26,12 +26,13 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyevents"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyevents"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -72,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
|
||||
@@ -377,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") {
|
||||
@@ -616,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()
|
||||
|
||||
@@ -20,10 +20,11 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
)
|
||||
|
||||
// AutoHTTPSConfig is used to disable automatic HTTPS
|
||||
|
||||
@@ -24,12 +24,20 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"golang.org/x/sync/singleflight"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
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.
|
||||
|
||||
@@ -18,9 +18,10 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -22,10 +22,12 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
|
||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -18,9 +18,10 @@ import (
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/crypto/scrypt"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -307,5 +307,7 @@ const (
|
||||
const separator = string(filepath.Separator)
|
||||
|
||||
// Interface guard
|
||||
var _ caddy.ListenerWrapper = (*tlsPlaceholderWrapper)(nil)
|
||||
var _ caddyfile.Unmarshaler = (*tlsPlaceholderWrapper)(nil)
|
||||
var (
|
||||
_ caddy.ListenerWrapper = (*tlsPlaceholderWrapper)(nil)
|
||||
_ caddyfile.Unmarshaler = (*tlsPlaceholderWrapper)(nil)
|
||||
)
|
||||
|
||||
@@ -25,8 +25,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common"
|
||||
"github.com/google/cel-go/common/operators"
|
||||
@@ -39,6 +37,9 @@ import (
|
||||
"github.com/google/cel-go/parser"
|
||||
"go.uber.org/zap"
|
||||
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -234,9 +235,11 @@ func (cr celHTTPRequest) Parent() interpreter.Activation {
|
||||
func (cr celHTTPRequest) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
||||
return cr.Request, nil
|
||||
}
|
||||
|
||||
func (celHTTPRequest) ConvertToType(typeVal ref.Type) ref.Val {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (cr celHTTPRequest) Equal(other ref.Val) ref.Val {
|
||||
if o, ok := other.Value().(celHTTPRequest); ok {
|
||||
return types.Bool(o.Request == cr.Request)
|
||||
@@ -255,12 +258,14 @@ type celPkixName struct{ *pkix.Name }
|
||||
func (pn celPkixName) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
||||
return pn.Name, nil
|
||||
}
|
||||
|
||||
func (pn celPkixName) ConvertToType(typeVal ref.Type) ref.Val {
|
||||
if typeVal.TypeName() == "string" {
|
||||
return types.String(pn.Name.String())
|
||||
}
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (pn celPkixName) Equal(other ref.Val) ref.Val {
|
||||
if o, ok := other.Value().(string); ok {
|
||||
return types.Bool(pn.Name.String() == o)
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func enableFullDuplex(w http.ResponseWriter) {
|
||||
func enableFullDuplex(w http.ResponseWriter) error {
|
||||
// Do nothing, Go 1.20 and earlier do not support full duplex
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func enableFullDuplex(w http.ResponseWriter) {
|
||||
http.NewResponseController(w).EnableFullDuplex()
|
||||
func enableFullDuplex(w http.ResponseWriter) error {
|
||||
//nolint:bodyclose
|
||||
return http.NewResponseController(w).EnableFullDuplex()
|
||||
}
|
||||
|
||||
@@ -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*",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -18,10 +18,11 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/klauspost/compress/gzip"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
||||
"github.com/klauspost/compress/gzip"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -15,10 +15,11 @@
|
||||
package caddyzstd
|
||||
|
||||
import (
|
||||
"github.com/klauspost/compress/zstd"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -29,18 +29,25 @@ import (
|
||||
"sync"
|
||||
"text/template"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
@@ -113,7 +120,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
|
||||
fs = http.Dir(repl.ReplaceAll(fsrv.Root, "."))
|
||||
}
|
||||
|
||||
var tplCtx = &templateContext{
|
||||
tplCtx := &templateContext{
|
||||
TemplateContext: templates.TemplateContext{
|
||||
Root: fs,
|
||||
Req: r,
|
||||
@@ -204,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,252 +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"}}
|
||||
{{- 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-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="M10 8v8h2a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2h-2z"></path>
|
||||
<path d="M3 12h2a2 2 0 1 0 0 -4h-2v8"></path>
|
||||
<path d="M17 12h3"></path>
|
||||
<path d="M21 8h-4v8"></path>
|
||||
<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 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 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-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="M13 16v-8l2 5l2 -5v8"></path>
|
||||
<path d="M1 16v-8"></path>
|
||||
<path d="M5 8v8"></path>
|
||||
<path d="M1 12h4"></path>
|
||||
<path d="M7 8h4"></path>
|
||||
<path d="M9 8v8"></path>
|
||||
<path d="M20 8v8h3"></path>
|
||||
<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 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-brand-javascript" 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 4l-2 14.5l-6 2l-6 -2l-2 -14.5z"></path>
|
||||
<path d="M7.5 8h3v8l-2 -1"></path>
|
||||
<path d="M16.5 8h-2.5a.5 .5 0 0 0 -.5 .5v3a.5 .5 0 0 0 .5 .5h1.423a.5 .5 0 0 1 .495 .57l-.418 2.93l-2 .5"></path>
|
||||
<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 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 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 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-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="M12 8a2 2 0 0 1 2 2v4a2 2 0 1 1 -4 0v-4a2 2 0 0 1 2 -2z"></path>
|
||||
<path d="M17 8v8h4"></path>
|
||||
<path d="M13 15l1 1"></path>
|
||||
<path d="M3 15a1 1 0 0 0 1 1h2a1 1 0 0 0 1 -1v-2a1 1 0 0 0 -1 -1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1"></path>
|
||||
<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 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}}
|
||||
@@ -255,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">
|
||||
@@ -753,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>
|
||||
@@ -790,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}}
|
||||
@@ -814,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}}
|
||||
@@ -834,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>
|
||||
@@ -846,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}}
|
||||
@@ -869,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}}
|
||||
@@ -897,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>
|
||||
@@ -1039,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}%`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -25,10 +25,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/dustin/go-humanize"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
)
|
||||
|
||||
func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEntry, canGoUp bool, root, urlPath string, repl *caddy.Replacer) *browseTemplateContext {
|
||||
|
||||
@@ -16,26 +16,30 @@ package fileserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
caddytpl "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/zap"
|
||||
|
||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||
|
||||
"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,
|
||||
@@ -48,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
|
||||
},
|
||||
})
|
||||
@@ -82,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)
|
||||
}
|
||||
@@ -152,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
|
||||
}
|
||||
|
||||
@@ -26,9 +26,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common"
|
||||
"github.com/google/cel-go/common/operators"
|
||||
@@ -36,6 +33,10 @@ import (
|
||||
"github.com/google/cel-go/parser"
|
||||
"go.uber.org/zap"
|
||||
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -30,13 +30,18 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterNamespace("http.precompressed", []interface{}{
|
||||
(*encode.Precompressed)(nil),
|
||||
})
|
||||
|
||||
caddy.RegisterModule(FileServer{})
|
||||
}
|
||||
|
||||
@@ -59,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.
|
||||
@@ -418,8 +439,13 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
|
||||
// GET and HEAD, which is sensible for a static file server - reject
|
||||
// any other methods (see issue #5166)
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||
w.Header().Add("Allow", "GET, HEAD")
|
||||
return caddyhttp.Error(http.StatusMethodNotAllowed, nil)
|
||||
// if we're in an error context, then it doesn't make sense
|
||||
// to repeat the error; just continue because we're probably
|
||||
// trying to write an error page response (see issue #5703)
|
||||
if _, ok := r.Context().Value(caddyhttp.ErrorCtxKey).(error); !ok {
|
||||
w.Header().Add("Allow", "GET, HEAD")
|
||||
return caddyhttp.Error(http.StatusMethodNotAllowed, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// set the Etag - note that a conditional If-None-Match request is handled
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,11 +23,12 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
)
|
||||
|
||||
// MatchRemoteIP matches requests by the remote IP address,
|
||||
|
||||
@@ -20,9 +20,10 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
// ServerLogConfig describes a server's logging configuration. If
|
||||
@@ -150,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
|
||||
|
||||
@@ -30,11 +30,12 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -196,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:"-"`
|
||||
}
|
||||
@@ -211,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.
|
||||
@@ -1392,9 +1398,7 @@ func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, er
|
||||
return matcherSet, nil
|
||||
}
|
||||
|
||||
var (
|
||||
wordRE = regexp.MustCompile(`\w+`)
|
||||
)
|
||||
var wordRE = regexp.MustCompile(`\w+`)
|
||||
|
||||
const regexpPlaceholderPrefix = "http.regexp"
|
||||
|
||||
|
||||
@@ -6,9 +6,10 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/internal/metrics"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/internal/metrics"
|
||||
)
|
||||
|
||||
// Metrics configures metrics observations.
|
||||
|
||||
@@ -15,12 +15,13 @@
|
||||
package proxyprotocol
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
goproxy "github.com/pires/go-proxyproto"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/mastercactapus/proxyprotocol"
|
||||
)
|
||||
|
||||
// ListenerWrapper provides PROXY protocol support to Caddy by implementing
|
||||
@@ -37,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")
|
||||
@@ -19,10 +19,11 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -39,9 +39,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// NewTestReplacer creates a replacer for an http.Request
|
||||
@@ -156,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
|
||||
|
||||
@@ -15,9 +15,10 @@
|
||||
package requestbody
|
||||
|
||||
import (
|
||||
"github.com/dustin/go-humanize"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -23,11 +23,46 @@ import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
type parsedAddr struct {
|
||||
network, scheme, host, port string
|
||||
valid bool
|
||||
}
|
||||
|
||||
func (p parsedAddr) dialAddr() string {
|
||||
if !p.valid {
|
||||
return ""
|
||||
}
|
||||
// for simplest possible config, we only need to include
|
||||
// the network portion if the user specified one
|
||||
if p.network != "" {
|
||||
return caddy.JoinNetworkAddress(p.network, p.host, p.port)
|
||||
}
|
||||
|
||||
// if the host is a placeholder, then we don't want to join with an empty port,
|
||||
// because that would just append an extra ':' at the end of the address.
|
||||
if p.port == "" && strings.Contains(p.host, "{") {
|
||||
return p.host
|
||||
}
|
||||
return net.JoinHostPort(p.host, p.port)
|
||||
}
|
||||
|
||||
func (p parsedAddr) rangedPort() bool {
|
||||
return strings.Contains(p.port, "-")
|
||||
}
|
||||
|
||||
func (p parsedAddr) replaceablePort() bool {
|
||||
return strings.Contains(p.port, "{") && strings.Contains(p.port, "}")
|
||||
}
|
||||
|
||||
func (p parsedAddr) isUnix() bool {
|
||||
return caddy.IsUnixNetwork(p.network)
|
||||
}
|
||||
|
||||
// parseUpstreamDialAddress parses configuration inputs for
|
||||
// the dial address, including support for a scheme in front
|
||||
// as a shortcut for the port number, and a network type,
|
||||
// for example 'unix' to dial a unix socket.
|
||||
func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
|
||||
func parseUpstreamDialAddress(upstreamAddr string) (parsedAddr, error) {
|
||||
var network, scheme, host, port string
|
||||
|
||||
if strings.Contains(upstreamAddr, "://") {
|
||||
@@ -35,7 +70,7 @@ func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
|
||||
// so we return a more user-friendly error message instead
|
||||
// to explain what to do instead
|
||||
if strings.Contains(upstreamAddr, "{") {
|
||||
return "", "", fmt.Errorf("due to parsing difficulties, placeholders are not allowed when an upstream address contains a scheme")
|
||||
return parsedAddr{}, fmt.Errorf("due to parsing difficulties, placeholders are not allowed when an upstream address contains a scheme")
|
||||
}
|
||||
|
||||
toURL, err := url.Parse(upstreamAddr)
|
||||
@@ -46,19 +81,19 @@ func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
|
||||
if strings.Contains(err.Error(), "invalid port") && strings.Contains(err.Error(), "-") {
|
||||
index := strings.LastIndex(upstreamAddr, ":")
|
||||
if index == -1 {
|
||||
return "", "", fmt.Errorf("parsing upstream URL: %v", err)
|
||||
return parsedAddr{}, fmt.Errorf("parsing upstream URL: %v", err)
|
||||
}
|
||||
portRange := upstreamAddr[index+1:]
|
||||
if strings.Count(portRange, "-") != 1 {
|
||||
return "", "", fmt.Errorf("parsing upstream URL: parse \"%v\": port range invalid: %v", upstreamAddr, portRange)
|
||||
return parsedAddr{}, fmt.Errorf("parsing upstream URL: parse \"%v\": port range invalid: %v", upstreamAddr, portRange)
|
||||
}
|
||||
toURL, err = url.Parse(strings.ReplaceAll(upstreamAddr, portRange, "0"))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("parsing upstream URL: %v", err)
|
||||
return parsedAddr{}, fmt.Errorf("parsing upstream URL: %v", err)
|
||||
}
|
||||
port = portRange
|
||||
} else {
|
||||
return "", "", fmt.Errorf("parsing upstream URL: %v", err)
|
||||
return parsedAddr{}, fmt.Errorf("parsing upstream URL: %v", err)
|
||||
}
|
||||
}
|
||||
if port == "" {
|
||||
@@ -69,18 +104,18 @@ func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
|
||||
// a backend and proxying to it, so we cannot allow extra components
|
||||
// in backend URLs
|
||||
if toURL.Path != "" || toURL.RawQuery != "" || toURL.Fragment != "" {
|
||||
return "", "", fmt.Errorf("for now, URLs for proxy upstreams only support scheme, host, and port components")
|
||||
return parsedAddr{}, fmt.Errorf("for now, URLs for proxy upstreams only support scheme, host, and port components")
|
||||
}
|
||||
|
||||
// ensure the port and scheme aren't in conflict
|
||||
if toURL.Scheme == "http" && port == "443" {
|
||||
return "", "", fmt.Errorf("upstream address has conflicting scheme (http://) and port (:443, the HTTPS port)")
|
||||
return parsedAddr{}, fmt.Errorf("upstream address has conflicting scheme (http://) and port (:443, the HTTPS port)")
|
||||
}
|
||||
if toURL.Scheme == "https" && port == "80" {
|
||||
return "", "", fmt.Errorf("upstream address has conflicting scheme (https://) and port (:80, the HTTP port)")
|
||||
return parsedAddr{}, fmt.Errorf("upstream address has conflicting scheme (https://) and port (:80, the HTTP port)")
|
||||
}
|
||||
if toURL.Scheme == "h2c" && port == "443" {
|
||||
return "", "", fmt.Errorf("upstream address has conflicting scheme (h2c://) and port (:443, the HTTPS port)")
|
||||
return parsedAddr{}, fmt.Errorf("upstream address has conflicting scheme (h2c://) and port (:443, the HTTPS port)")
|
||||
}
|
||||
|
||||
// if port is missing, attempt to infer from scheme
|
||||
@@ -112,18 +147,5 @@ func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
|
||||
network = "unix"
|
||||
scheme = "h2c"
|
||||
}
|
||||
|
||||
// for simplest possible config, we only need to include
|
||||
// the network portion if the user specified one
|
||||
if network != "" {
|
||||
return caddy.JoinNetworkAddress(network, host, port), scheme, nil
|
||||
}
|
||||
|
||||
// if the host is a placeholder, then we don't want to join with an empty port,
|
||||
// because that would just append an extra ':' at the end of the address.
|
||||
if port == "" && strings.Contains(host, "{") {
|
||||
return host, scheme, nil
|
||||
}
|
||||
|
||||
return net.JoinHostPort(host, port), scheme, nil
|
||||
return parsedAddr{network, scheme, host, port, true}, nil
|
||||
}
|
||||
|
||||
@@ -265,18 +265,18 @@ func TestParseUpstreamDialAddress(t *testing.T) {
|
||||
expectScheme: "h2c",
|
||||
},
|
||||
} {
|
||||
actualHostPort, actualScheme, err := parseUpstreamDialAddress(tc.input)
|
||||
actualAddr, err := parseUpstreamDialAddress(tc.input)
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("Test %d: Expected error but got %v", i, err)
|
||||
}
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error but got %v", i, err)
|
||||
}
|
||||
if actualHostPort != tc.expectHostPort {
|
||||
t.Errorf("Test %d: Expected host and port '%s' but got '%s'", i, tc.expectHostPort, actualHostPort)
|
||||
if actualAddr.dialAddr() != tc.expectHostPort {
|
||||
t.Errorf("Test %d: input %s: Expected host and port '%s' but got '%s'", i, tc.input, tc.expectHostPort, actualAddr.dialAddr())
|
||||
}
|
||||
if actualScheme != tc.expectScheme {
|
||||
t.Errorf("Test %d: Expected scheme '%s' but got '%s'", i, tc.expectScheme, actualScheme)
|
||||
if actualAddr.scheme != tc.expectScheme {
|
||||
t.Errorf("Test %d: Expected scheme '%s' but got '%s'", i, tc.expectScheme, actualAddr.scheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
@@ -28,7 +30,6 @@ import (
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -89,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...>
|
||||
@@ -146,7 +148,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
// appendUpstream creates an upstream for address and adds
|
||||
// it to the list.
|
||||
appendUpstream := func(address string) error {
|
||||
dialAddr, scheme, err := parseUpstreamDialAddress(address)
|
||||
pa, err := parseUpstreamDialAddress(address)
|
||||
if err != nil {
|
||||
return d.WrapErr(err)
|
||||
}
|
||||
@@ -154,21 +156,39 @@ 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
|
||||
if commonScheme != "" && scheme != commonScheme {
|
||||
return d.Errf("for now, all proxy upstreams must use the same scheme (transport protocol); expecting '%s://' but got '%s://'",
|
||||
commonScheme, scheme)
|
||||
}
|
||||
commonScheme = scheme
|
||||
|
||||
parsedAddr, err := caddy.ParseNetworkAddress(dialAddr)
|
||||
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)
|
||||
}
|
||||
commonScheme = pa.scheme
|
||||
|
||||
// if the port of upstream address contains a placeholder, only wrap it with the `Upstream` struct,
|
||||
// delaying actual resolution of the address until request time.
|
||||
if pa.replaceablePort() {
|
||||
h.Upstreams = append(h.Upstreams, &Upstream{Dial: pa.dialAddr()})
|
||||
return nil
|
||||
}
|
||||
parsedAddr, err := caddy.ParseNetworkAddress(pa.dialAddr())
|
||||
if err != nil {
|
||||
return d.WrapErr(err)
|
||||
}
|
||||
|
||||
if parsedAddr.StartPort == 0 && parsedAddr.EndPort == 0 {
|
||||
if pa.isUnix() || !pa.rangedPort() {
|
||||
// unix networks don't have ports
|
||||
h.Upstreams = append(h.Upstreams, &Upstream{
|
||||
Dial: dialAddr,
|
||||
Dial: pa.dialAddr(),
|
||||
})
|
||||
} else {
|
||||
// expand a port range into multiple upstreams
|
||||
@@ -186,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,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)
|
||||
@@ -532,18 +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
|
||||
@@ -757,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())
|
||||
}
|
||||
|
||||
@@ -21,15 +21,17 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/zap"
|
||||
|
||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -132,14 +134,14 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
|
||||
toAddresses := make([]string, len(to))
|
||||
var toScheme string
|
||||
for i, toLoc := range to {
|
||||
addr, scheme, err := parseUpstreamDialAddress(toLoc)
|
||||
addr, err := parseUpstreamDialAddress(toLoc)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid upstream address %s: %v", toLoc, err)
|
||||
}
|
||||
if scheme != "" && toScheme == "" {
|
||||
toScheme = scheme
|
||||
if addr.scheme != "" && toScheme == "" {
|
||||
toScheme = addr.scheme
|
||||
}
|
||||
toAddresses[i] = addr
|
||||
toAddresses[i] = addr.dialAddr()
|
||||
}
|
||||
|
||||
// proceed to build the handler and server
|
||||
@@ -284,7 +286,8 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
|
||||
|
||||
var false bool
|
||||
cfg := &caddy.Config{
|
||||
Admin: &caddy.AdminConfig{Disabled: true,
|
||||
Admin: &caddy.AdminConfig{
|
||||
Disabled: true,
|
||||
Config: &caddy.ConfigSettings{
|
||||
Persist: &false,
|
||||
},
|
||||
|
||||
@@ -251,7 +251,6 @@ func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Respons
|
||||
|
||||
// Get issues a GET request to the fcgi responder.
|
||||
func (c *client) Get(p map[string]string, body io.Reader, l int64) (resp *http.Response, err error) {
|
||||
|
||||
p["REQUEST_METHOD"] = "GET"
|
||||
p["CONTENT_LENGTH"] = strconv.FormatInt(l, 10)
|
||||
|
||||
@@ -260,7 +259,6 @@ func (c *client) Get(p map[string]string, body io.Reader, l int64) (resp *http.R
|
||||
|
||||
// Head issues a HEAD request to the fcgi responder.
|
||||
func (c *client) Head(p map[string]string) (resp *http.Response, err error) {
|
||||
|
||||
p["REQUEST_METHOD"] = "HEAD"
|
||||
p["CONTENT_LENGTH"] = "0"
|
||||
|
||||
@@ -269,7 +267,6 @@ func (c *client) Head(p map[string]string) (resp *http.Response, err error) {
|
||||
|
||||
// Options issues an OPTIONS request to the fcgi responder.
|
||||
func (c *client) Options(p map[string]string) (resp *http.Response, err error) {
|
||||
|
||||
p["REQUEST_METHOD"] = "OPTIONS"
|
||||
p["CONTENT_LENGTH"] = "0"
|
||||
|
||||
|
||||
@@ -24,13 +24,13 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
)
|
||||
|
||||
var noopLogger = zap.NewNop()
|
||||
|
||||
@@ -26,9 +26,10 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// HealthChecks configures active and passive health checks.
|
||||
@@ -357,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,12 +28,13 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pires/go-proxyproto"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/net/http2"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
"github.com/mastercactapus/proxyprotocol"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -206,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
|
||||
@@ -528,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
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user