Compare commits

..

5 Commits

Author SHA1 Message Date
Francis Lavoie 6d010189a5 Implement success ratio in health checks 2023-04-15 11:34:09 -04:00
Francis Lavoie 2c61b50b5f Add min_successes 2023-04-15 11:34:09 -04:00
Francis Lavoie c8b8c3a7b2 Add min_success_ratio WIP 2023-04-15 11:34:09 -04:00
Francis Lavoie c4b934f232 Add caddyhttp.Ratio type 2023-04-15 11:34:09 -04:00
Francis Lavoie cf69cd7b27 Add success_duration 2023-04-15 11:34:09 -04:00
183 changed files with 3241 additions and 8298 deletions
+3 -3
View File
@@ -7,7 +7,7 @@ The Caddy project would like to make sure that it stays on top of all practicall
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------- | ------------------ |
| 2.x | ✔️ | | 2.x | :white_check_mark: |
| 1.x | :x: | | 1.x | :x: |
| < 1.x | :x: | | < 1.x | :x: |
@@ -24,7 +24,7 @@ We do not accept reports if the steps imply or require a compromised system or t
Client-side exploits are out of scope. In other words, it is not a bug in Caddy if the web browser does something unsafe, even if the downloaded content was served by Caddy. (Those kinds of exploits can generally be mitigated by proper configuration of HTTP headers.) As a general rule, the content served by Caddy is not considered in scope because content is configurable by the site owner or the associated web application. Client-side exploits are out of scope. In other words, it is not a bug in Caddy if the web browser does something unsafe, even if the downloaded content was served by Caddy. (Those kinds of exploits can generally be mitigated by proper configuration of HTTP headers.) As a general rule, the content served by Caddy is not considered in scope because content is configurable by the site owner or the associated web application.
Security bugs in code dependencies (including Go's standard library) are out of scope. Instead, if a dependency has patched a relevant security bug, please feel free to open a public issue or pull request to update that dependency in our code. Security bugs in code dependencies are out of scope. Instead, if a dependency has patched a relevant security bug, please feel free to open a public issue or pull request to update that dependency in our code.
## Reporting a Vulnerability ## Reporting a Vulnerability
@@ -42,7 +42,7 @@ We'll need enough information to verify the bug and make a patch. To speed thing
- Specific minimal steps to reproduce the issue from scratch - Specific minimal steps to reproduce the issue from scratch
- A working patch - A working patch
Please DO NOT use containers, VMs, cloud instances or services, or any other complex infrastructure in your steps. Always prefer `curl -v` instead of web browsers. Please DO NOT use containers, VMs, cloud instances or services, or any other complex infrastructure in your steps. Always prefer `curl` instead of web browsers.
We consider publicly-registered domain names to be public information. This necessary in order to maintain the integrity of certificate transparency, public DNS, and other public trust systems. Do not redact domain names from your reports. The actual content of your domain name affects Caddy's behavior, so we need the exact domain name(s) to reproduce with, or your report will be ignored. We consider publicly-registered domain names to be public information. This necessary in order to maintain the integrity of certificate transparency, public DNS, and other public trust systems. Do not redact domain names from your reports. The actual content of your domain name affects Caddy's behavior, so we need the exact domain name(s) to reproduce with, or your report will be ignored.
+10 -16
View File
@@ -18,22 +18,17 @@ jobs:
# Default is true, cancels jobs for other platforms in the matrix if one fails # Default is true, cancels jobs for other platforms in the matrix if one fails
fail-fast: false fail-fast: false
matrix: matrix:
os: os: [ ubuntu-latest, macos-latest, windows-latest ]
- ubuntu-latest go: [ '1.19', '1.20' ]
- macos-latest
- windows-latest
go:
- '1.20'
- '1.21'
include: include:
# Set the minimum Go patch version for the given Go minor # Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }} # Usable via ${{ matrix.GO_SEMVER }}
- go: '1.20' - go: '1.19'
GO_SEMVER: '~1.20.6' GO_SEMVER: '~1.19.6'
- go: '1.21' - go: '1.20'
GO_SEMVER: '~1.21.0' GO_SEMVER: '~1.20.1'
# Set some variables per OS, usable via ${{ matrix.VAR }} # Set some variables per OS, usable via ${{ matrix.VAR }}
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing # CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
@@ -54,7 +49,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Install Go - name: Install Go
uses: actions/setup-go@v4 uses: actions/setup-go@v4
@@ -73,7 +68,6 @@ jobs:
- name: Print Go version and environment - name: Print Go version and environment
id: vars id: vars
shell: bash
run: | run: |
printf "Using go at: $(which go)\n" printf "Using go at: $(which go)\n"
printf "Go version: $(go version)\n" printf "Go version: $(go version)\n"
@@ -136,7 +130,7 @@ jobs:
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Run Tests - name: Run Tests
run: | run: |
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
@@ -162,9 +156,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v3
- uses: goreleaser/goreleaser-action@v5 - uses: goreleaser/goreleaser-action@v4
with: with:
version: latest version: latest
args: check args: check
+6 -20
View File
@@ -15,33 +15,20 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
goos: goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd']
- 'aix' go: [ '1.20' ]
- 'android'
- 'linux'
- 'solaris'
- 'illumos'
- 'dragonfly'
- 'freebsd'
- 'openbsd'
- 'plan9'
- 'windows'
- 'darwin'
- 'netbsd'
go:
- '1.21'
include: include:
# Set the minimum Go patch version for the given Go minor # Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }} # Usable via ${{ matrix.GO_SEMVER }}
- go: '1.21' - go: '1.20'
GO_SEMVER: '~1.21.0' GO_SEMVER: '~1.20.1'
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Install Go - name: Install Go
uses: actions/setup-go@v4 uses: actions/setup-go@v4
@@ -63,12 +50,11 @@ jobs:
env: env:
CGO_ENABLED: 0 CGO_ENABLED: 0
GOOS: ${{ matrix.goos }} GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goos == 'aix' && 'ppc64' || 'amd64' }}
shell: bash shell: bash
continue-on-error: true continue-on-error: true
working-directory: ./cmd/caddy working-directory: ./cmd/caddy
run: | run: |
GOOS=$GOOS GOARCH=$GOARCH go build -trimpath -o caddy-"$GOOS"-$GOARCH 2> /dev/null GOOS=$GOOS go build -trimpath -o caddy-"$GOOS"-amd64 2> /dev/null
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "::warning ::$GOOS Build Failed" echo "::warning ::$GOOS Build Failed"
exit 0 exit 0
+6 -26
View File
@@ -17,45 +17,25 @@ jobs:
# From https://github.com/golangci/golangci-lint-action # From https://github.com/golangci/golangci-lint-action
golangci: golangci:
permissions: permissions:
contents: read # for actions/checkout to fetch code contents: read # for actions/checkout to fetch code
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
name: lint name: lint
strategy: strategy:
matrix: matrix:
os: os: [ubuntu-latest, macos-latest, windows-latest]
- ubuntu-latest
- macos-latest
- windows-latest
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
with: with:
go-version: '~1.21.0' go-version: '~1.19.6'
check-latest: true check-latest: true
# Workaround for https://github.com/golangci/golangci-lint-action/issues/135
skip-pkg-cache: true
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v3
with: with:
version: v1.54 version: v1.50
# Workaround for https://github.com/golangci/golangci-lint-action/issues/135
skip-pkg-cache: true
# Windows times out frequently after about 5m50s if we don't set a longer timeout. # Windows times out frequently after about 5m50s if we don't set a longer timeout.
args: --timeout 10m args: --timeout 10m
# Optional: show only new issues if it's a pull request. The default value is `false`. # Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true # 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
+8 -10
View File
@@ -10,16 +10,14 @@ jobs:
name: Release name: Release
strategy: strategy:
matrix: matrix:
os: os: [ ubuntu-latest ]
- ubuntu-latest go: [ '1.20' ]
go:
- '1.21'
include: include:
# Set the minimum Go patch version for the given Go minor # Set the minimum Go patch version for the given Go minor
# Usable via ${{ matrix.GO_SEMVER }} # Usable via ${{ matrix.GO_SEMVER }}
- go: '1.21' - go: '1.20'
GO_SEMVER: '~1.21.0' GO_SEMVER: '~1.20.1'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233 # https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
@@ -32,7 +30,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -43,7 +41,7 @@ jobs:
check-latest: true check-latest: true
# Force fetch upstream tags -- because 65 minutes # Force fetch upstream tags -- because 65 minutes
# tl;dr: actions/checkout@v4 runs this line: # tl;dr: actions/checkout@v3 runs this line:
# git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/ # 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: # which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran:
# git fetch --prune --unshallow # git fetch --prune --unshallow
@@ -106,10 +104,10 @@ jobs:
run: syft version run: syft version
# GoReleaser will take care of publishing those artifacts into the release # GoReleaser will take care of publishing those artifacts into the release
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5 uses: goreleaser/goreleaser-action@v4
with: with:
version: latest version: latest
args: release --clean --timeout 60m args: release --rm-dist --timeout 60m
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.vars.outputs.version_tag }} TAG: ${{ steps.vars.outputs.version_tag }}
+1 -2
View File
@@ -10,8 +10,7 @@ jobs:
name: Release Published name: Release Published
strategy: strategy:
matrix: matrix:
os: os: [ ubuntu-latest ]
- ubuntu-latest
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
-2
View File
@@ -11,8 +11,6 @@ Caddyfile.*
# build artifacts and helpers # build artifacts and helpers
cmd/caddy/caddy cmd/caddy/caddy
cmd/caddy/caddy.exe cmd/caddy/caddy.exe
cmd/caddy/tmp/*.exe
cmd/caddy/.env
# mac specific # mac specific
.DS_Store .DS_Store
+10 -20
View File
@@ -2,27 +2,15 @@ linters-settings:
errcheck: errcheck:
ignore: fmt:.*,go.uber.org/zap/zapcore:^Add.* ignore: fmt:.*,go.uber.org/zap/zapcore:^Add.*
ignoretests: true 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: linters:
disable-all: true disable-all: true
enable: enable:
- bodyclose - bodyclose
- deadcode
- errcheck - errcheck
- gci - gofmt
- gofumpt - goimports
- gosec - gosec
- gosimple - gosimple
- govet - govet
@@ -30,9 +18,11 @@ linters:
- misspell - misspell
- prealloc - prealloc
- staticcheck - staticcheck
- structcheck
- typecheck - typecheck
- unconvert - unconvert
- unused - unused
- varcheck
# these are implicitly disabled: # these are implicitly disabled:
# - asciicheck # - asciicheck
# - depguard # - depguard
@@ -90,23 +80,23 @@ output:
issues: issues:
exclude-rules: exclude-rules:
# we aren't calling unknown URL # 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: linters:
- gosec - gosec
# as a web server that's expected to handle any template, this is totally in the hands of the user. # 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: linters:
- gosec - gosec
# we're shelling out to known commands, not relying on user-defined input. # 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: linters:
- gosec - gosec
# the choice of weakrand is deliberate, hence the named import "weakrand" # the choice of weakrand is deliberate, hence the named import "weakrand"
- path: modules/caddyhttp/reverseproxy/selectionpolicies.go - path: modules/caddyhttp/reverseproxy/selectionpolicies.go
text: 'G404' # G404: Insecure random number source (rand) text: "G404" # G404: Insecure random number source (rand)
linters: linters:
- gosec - gosec
- path: modules/caddyhttp/reverseproxy/streaming.go - path: modules/caddyhttp/reverseproxy/streaming.go
text: 'G404' # G404: Insecure random number source (rand) text: "G404" # G404: Insecure random number source (rand)
linters: linters:
- gosec - gosec
+10 -8
View File
@@ -43,7 +43,6 @@ builds:
- arm64 - arm64
- s390x - s390x
- ppc64le - ppc64le
- riscv64
goarm: goarm:
- "5" - "5"
- "6" - "6"
@@ -55,20 +54,14 @@ builds:
goarch: ppc64le goarch: ppc64le
- goos: darwin - goos: darwin
goarch: s390x goarch: s390x
- goos: darwin
goarch: riscv64
- goos: windows - goos: windows
goarch: ppc64le goarch: ppc64le
- goos: windows - goos: windows
goarch: s390x goarch: s390x
- goos: windows
goarch: riscv64
- goos: freebsd - goos: freebsd
goarch: ppc64le goarch: ppc64le
- goos: freebsd - goos: freebsd
goarch: s390x goarch: s390x
- goos: freebsd
goarch: riscv64
- goos: freebsd - goos: freebsd
goarch: arm goarch: arm
goarm: "5" goarm: "5"
@@ -113,10 +106,11 @@ archives:
{{- with .Mips }}_{{ . }}{{ end }} {{- with .Mips }}_{{ . }}{{ end }}
{{- if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }} {{- if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}
# package the 'caddy-build' directory into a tarball, # packge the 'caddy-build' directory into a tarball,
# allowing users to build the exact same set of files as ours. # allowing users to build the exact same set of files as ours.
- id: source - id: source
meta: true meta: true
rlcp: true
name_template: "{{ .ProjectName }}_{{ .Version }}_buildable-artifact" name_template: "{{ .ProjectName }}_{{ .Version }}_buildable-artifact"
files: files:
- src: LICENSE - src: LICENSE
@@ -133,6 +127,14 @@ source:
name_template: '{{ .ProjectName }}_{{ .Version }}_src' name_template: '{{ .ProjectName }}_{{ .Version }}_src'
format: 'tar.gz' format: 'tar.gz'
# This will make the destination paths be relative to the longest common
# path prefix between all the files matched and the source glob.
# Enabling this essentially mimic the behavior of nfpm's contents section.
# It will be the default by June 2023.
#
# Default: false
rlcp: true
# Additional files/template/globs you want to add to the source archive. # Additional files/template/globs you want to add to the source archive.
# #
# Default: empty. # Default: empty.
+1 -1
View File
@@ -87,7 +87,7 @@ See [our online documentation](https://caddyserver.com/docs/install) for other i
Requirements: Requirements:
- [Go 1.20 or newer](https://golang.org/dl/) - [Go 1.19 or newer](https://golang.org/dl/)
### For development ### For development
+5 -48
View File
@@ -55,7 +55,6 @@ func init() {
if env, exists := os.LookupEnv("CADDY_ADMIN"); exists { if env, exists := os.LookupEnv("CADDY_ADMIN"); exists {
DefaultAdminListen = env DefaultAdminListen = env
} }
RegisterNamespace("caddy.config_loaders", []interface{}{(*ConfigLoader)(nil)})
} }
// AdminConfig configures Caddy's API endpoint, which is used // AdminConfig configures Caddy's API endpoint, which is used
@@ -72,11 +71,6 @@ type AdminConfig struct {
// parsed by Caddy. Accepts placeholders. // parsed by Caddy. Accepts placeholders.
// Default: the value of the `CADDY_ADMIN` environment variable, // Default: the value of the `CADDY_ADMIN` environment variable,
// or `localhost:2019` otherwise. // or `localhost:2019` otherwise.
//
// Remember: When changing this value through a config reload,
// be sure to use the `--address` CLI flag to specify the current
// admin address if the currently-running admin endpoint is not
// the default address.
Listen string `json:"listen,omitempty"` Listen string `json:"listen,omitempty"`
// If true, CORS headers will be emitted, and requests to the // If true, CORS headers will be emitted, and requests to the
@@ -319,32 +313,7 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
// messages. If the requested URI does not include an Internet host // messages. If the requested URI does not include an Internet host
// name for the service being requested, then the Host header field MUST // name for the service being requested, then the Host header field MUST
// be given with an empty value." // be given with an empty value."
//
// UPDATE July 2023: Go broke this by patching a minor security bug in 1.20.6.
// Understandable, but frustrating. See:
// https://github.com/golang/go/issues/60374
// See also the discussion here:
// https://github.com/golang/go/issues/61431
//
// We can no longer conform to RFC 2616 Section 14.26 from either Go or curl
// in purity. (Curl allowed no host between 7.40 and 7.50, but now requires a
// bogus host; see https://superuser.com/a/925610.) If we disable Host/Origin
// security checks, the infosec community assures me that it is secure to do
// so, because:
// 1) Browsers do not allow access to unix sockets
// 2) DNS is irrelevant to unix sockets
//
// I am not quite ready to trust either of those external factors, so instead
// of disabling Host/Origin checks, we now allow specific Host values when
// accessing the admin endpoint over unix sockets. I definitely don't trust
// DNS (e.g. I don't trust 'localhost' to always resolve to the local host),
// and IP shouldn't even be used, but if it is for some reason, I think we can
// at least be reasonably assured that 127.0.0.1 and ::1 route to the local
// machine, meaning that a hypothetical browser origin would have to be on the
// local machine as well.
uniqueOrigins[""] = struct{}{} uniqueOrigins[""] = struct{}{}
uniqueOrigins["127.0.0.1"] = struct{}{}
uniqueOrigins["::1"] = struct{}{}
} else { } else {
uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{} uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{} uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
@@ -1042,9 +1011,9 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
id := parts[2] id := parts[2]
// map the ID to the expanded path // map the ID to the expanded path
rawCfgMu.RLock() currentCtxMu.RLock()
expanded, ok := rawCfgIndex[id] expanded, ok := rawCfgIndex[id]
rawCfgMu.RUnlock() defer currentCtxMu.RUnlock()
if !ok { if !ok {
return APIError{ return APIError{
HTTPStatus: http.StatusNotFound, HTTPStatus: http.StatusNotFound,
@@ -1197,27 +1166,15 @@ traverseLoop:
} }
case http.MethodPut: case http.MethodPut:
if _, ok := v[part]; ok { if _, ok := v[part]; ok {
return APIError{ return fmt.Errorf("[%s] key already exists: %s", path, part)
HTTPStatus: http.StatusConflict,
Err: fmt.Errorf("[%s] key already exists: %s", path, part),
}
} }
v[part] = val v[part] = val
case http.MethodPatch: case http.MethodPatch:
if _, ok := v[part]; !ok { if _, ok := v[part]; !ok {
return APIError{ return fmt.Errorf("[%s] key does not exist: %s", path, part)
HTTPStatus: http.StatusNotFound,
Err: fmt.Errorf("[%s] key does not exist: %s", path, part),
}
} }
v[part] = val v[part] = val
case http.MethodDelete: 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) delete(v, part)
default: default:
return fmt.Errorf("unrecognized method %s", method) return fmt.Errorf("unrecognized method %s", method)
@@ -1359,7 +1316,7 @@ var (
// will get deleted before the process gracefully exits. // will get deleted before the process gracefully exits.
func PIDFile(filename string) error { func PIDFile(filename string) error {
pid := []byte(strconv.Itoa(os.Getpid()) + "\n") pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
err := os.WriteFile(filename, pid, 0o600) err := os.WriteFile(filename, pid, 0600)
if err != nil { if err != nil {
return err return err
} }
-6
View File
@@ -75,12 +75,6 @@ func TestUnsyncedConfigAccess(t *testing.T) {
path: "/bar/qq", path: "/bar/qq",
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`, 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", method: "POST",
path: "/list", path: "/list",
+20 -56
View File
@@ -34,22 +34,12 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/caddyserver/caddy/v2/notify"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"github.com/google/uuid" "github.com/google/uuid"
"go.uber.org/zap" "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. // Config is the top (or beginning) of the Caddy configuration structure.
// Caddy config is expressed natively as a JSON document. If you prefer // 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) // not to work with JSON directly, there are [many config adapters](/docs/config-adapters)
@@ -81,15 +71,11 @@ type Config struct {
// module is `caddy.storage.file_system` (the local file system), // module is `caddy.storage.file_system` (the local file system),
// and the default path // and the default path
// [depends on the OS and environment](/docs/conventions#data-directory). // [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"` StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`
// AppsRaw are the apps that Caddy will load and run. The // AppsRaw are the apps that Caddy will load and run. The
// app module name is the key, and the app's config is the // app module name is the key, and the app's config is the
// associated value. // 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="` AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="`
apps map[string]App apps map[string]App
@@ -170,8 +156,8 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
return fmt.Errorf("method not allowed") return fmt.Errorf("method not allowed")
} }
rawCfgMu.Lock() currentCtxMu.Lock()
defer rawCfgMu.Unlock() defer currentCtxMu.Unlock()
if ifMatchHeader != "" { if ifMatchHeader != "" {
// expect the first and last character to be quotes // expect the first and last character to be quotes
@@ -271,8 +257,8 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
// readConfig traverses the current config to path // readConfig traverses the current config to path
// and writes its JSON encoding to out. // and writes its JSON encoding to out.
func readConfig(path string, out io.Writer) error { func readConfig(path string, out io.Writer) error {
rawCfgMu.RLock() currentCtxMu.RLock()
defer rawCfgMu.RUnlock() defer currentCtxMu.RUnlock()
return unsyncedConfigAccess(http.MethodGet, path, nil, out) return unsyncedConfigAccess(http.MethodGet, path, nil, out)
} }
@@ -319,7 +305,7 @@ func indexConfigObjects(ptr any, configPath string, index map[string]string) err
// it as the new config, replacing any other current config. // it as the new config, replacing any other current config.
// It does NOT update the raw config state, as this is a // It does NOT update the raw config state, as this is a
// lower-level function; most callers will want to use Load // lower-level function; most callers will want to use Load
// instead. A write lock on rawCfgMu is required! If // instead. A write lock on currentCtxMu is required! If
// allowPersist is false, it will not be persisted to disk, // allowPersist is false, it will not be persisted to disk,
// even if it is configured to. // even if it is configured to.
func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error { func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
@@ -354,10 +340,8 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
} }
// swap old context (including its config) with the new one // swap old context (including its config) with the new one
currentCtxMu.Lock()
oldCtx := currentCtx oldCtx := currentCtx
currentCtx = ctx currentCtx = ctx
currentCtxMu.Unlock()
// Stop, Cleanup each old app // Stop, Cleanup each old app
unsyncedStop(oldCtx) unsyncedStop(oldCtx)
@@ -370,13 +354,13 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
newCfg.Admin.Config.Persist == nil || newCfg.Admin.Config.Persist == nil ||
*newCfg.Admin.Config.Persist) { *newCfg.Admin.Config.Persist) {
dir := filepath.Dir(ConfigAutosavePath) dir := filepath.Dir(ConfigAutosavePath)
err := os.MkdirAll(dir, 0o700) err := os.MkdirAll(dir, 0700)
if err != nil { if err != nil {
Log().Error("unable to create folder for config autosave", Log().Error("unable to create folder for config autosave",
zap.String("dir", dir), zap.String("dir", dir),
zap.Error(err)) zap.Error(err))
} else { } else {
err := os.WriteFile(ConfigAutosavePath, cfgJSON, 0o600) err := os.WriteFile(ConfigAutosavePath, cfgJSON, 0600)
if err == nil { if err == nil {
Log().Info("autosaved config (load with --resume flag)", zap.String("file", ConfigAutosavePath)) Log().Info("autosaved config (load with --resume flag)", zap.String("file", ConfigAutosavePath))
} else { } else {
@@ -643,35 +627,22 @@ type ConfigLoader interface {
// stop the others. Stop should only be called // stop the others. Stop should only be called
// if not replacing with a new config. // if not replacing with a new config.
func Stop() error { func Stop() error {
currentCtxMu.RLock()
ctx := currentCtx
currentCtxMu.RUnlock()
rawCfgMu.Lock()
unsyncedStop(ctx)
currentCtxMu.Lock() currentCtxMu.Lock()
defer currentCtxMu.Unlock()
unsyncedStop(currentCtx)
currentCtx = Context{} currentCtx = Context{}
currentCtxMu.Unlock()
rawCfgJSON = nil rawCfgJSON = nil
rawCfgIndex = nil rawCfgIndex = nil
rawCfg[rawConfigKey] = nil rawCfg[rawConfigKey] = nil
rawCfgMu.Unlock()
return nil return nil
} }
// unsyncedStop stops ctx from running, but has // unsyncedStop stops cfg from running, but has
// no locking around ctx. It is a no-op if ctx has a // no locking around cfg. It is a no-op if cfg is
// nil cfg. If any app returns an error when stopping, // nil. If any app returns an error when stopping,
// it is logged and the function continues stopping // it is logged and the function continues stopping
// the next app. This function assumes all apps in // the next app. This function assumes all apps in
// ctx were successfully started first. // cfg were successfully started first.
//
// A lock on rawCfgMu is required, even though this
// function does not access rawCfg, that lock
// synchronizes the stop/start of apps.
func unsyncedStop(ctx Context) { func unsyncedStop(ctx Context) {
if ctx.cfg == nil { if ctx.cfg == nil {
return return
@@ -838,19 +809,14 @@ func ParseDuration(s string) (time.Duration, error) {
// regardless of storage configuration, since each instance is intended to // regardless of storage configuration, since each instance is intended to
// have its own unique ID. // have its own unique ID.
func InstanceID() (uuid.UUID, error) { func InstanceID() (uuid.UUID, error) {
appDataDir := AppDataDir() uuidFilePath := filepath.Join(AppDataDir(), "instance.uuid")
uuidFilePath := filepath.Join(appDataDir, "instance.uuid")
uuidFileBytes, err := os.ReadFile(uuidFilePath) uuidFileBytes, err := os.ReadFile(uuidFilePath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
uuid, err := uuid.NewRandom() uuid, err := uuid.NewRandom()
if err != nil { if err != nil {
return uuid, err return uuid, err
} }
err = os.MkdirAll(appDataDir, 0o600) err = os.WriteFile(uuidFilePath, []byte(uuid.String()), 0600)
if err != nil {
return uuid, err
}
err = os.WriteFile(uuidFilePath, []byte(uuid.String()), 0o600)
return uuid, err return uuid, err
} else if err != nil { } else if err != nil {
return [16]byte{}, err return [16]byte{}, err
@@ -1003,12 +969,14 @@ type CtxKey string
// This group of variables pertains to the current configuration. // This group of variables pertains to the current configuration.
var ( var (
// currentCtxMu protects everything in this var block.
currentCtxMu sync.RWMutex
// currentCtx is the root context for the currently-running // currentCtx is the root context for the currently-running
// configuration, which can be accessed through this value. // configuration, which can be accessed through this value.
// If the Config contained in this value is not nil, then // If the Config contained in this value is not nil, then
// a config is currently active/running. // a config is currently active/running.
currentCtx Context currentCtx Context
currentCtxMu sync.RWMutex
// rawCfg is the current, generic-decoded configuration; // rawCfg is the current, generic-decoded configuration;
// we initialize it as a map with one field ("config") // we initialize it as a map with one field ("config")
@@ -1026,10 +994,6 @@ var (
// rawCfgIndex is the map of user-assigned ID to expanded // rawCfgIndex is the map of user-assigned ID to expanded
// path, for converting /id/ paths to /config/ paths. // path, for converting /id/ paths to /config/ paths.
rawCfgIndex map[string]string rawCfgIndex map[string]string
// rawCfgMu protects all the rawCfg fields and also
// essentially synchronizes config changes/reloads.
rawCfgMu sync.RWMutex
) )
// errSameConfig is returned if the new config is the same // errSameConfig is returned if the new config is the same
+29 -13
View File
@@ -20,7 +20,6 @@ import (
"io" "io"
"log" "log"
"strconv" "strconv"
"strings"
) )
// Dispenser is a type that dispenses tokens, similarly to a lexer, // Dispenser is a type that dispenses tokens, similarly to a lexer,
@@ -106,7 +105,7 @@ func (d *Dispenser) nextOnSameLine() bool {
} }
curr := d.tokens[d.cursor] curr := d.tokens[d.cursor]
next := d.tokens[d.cursor+1] next := d.tokens[d.cursor+1]
if !isNextOnNewLine(curr, next) { if curr.File == next.File && curr.Line+curr.NumLineBreaks() == next.Line {
d.cursor++ d.cursor++
return true return true
} }
@@ -127,7 +126,7 @@ func (d *Dispenser) NextLine() bool {
} }
curr := d.tokens[d.cursor] curr := d.tokens[d.cursor]
next := d.tokens[d.cursor+1] next := d.tokens[d.cursor+1]
if isNextOnNewLine(curr, next) { if curr.File != next.File || curr.Line+curr.NumLineBreaks() < next.Line {
d.cursor++ d.cursor++
return true return true
} }
@@ -391,22 +390,22 @@ func (d *Dispenser) Reset() {
// an argument. // an argument.
func (d *Dispenser) ArgErr() error { func (d *Dispenser) ArgErr() error {
if d.Val() == "{" { 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 // SyntaxErr creates a generic syntax error which explains what was
// found and what was expected. // found and what was expected.
func (d *Dispenser) SyntaxErr(expected string) error { func (d *Dispenser) SyntaxErr(expected string) error {
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, "','")) msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected)
return errors.New(msg) return errors.New(msg)
} }
// EOFErr returns an error indicating that the dispenser reached // EOFErr returns an error indicating that the dispenser reached
// the end of the input when searching for the next token. // the end of the input when searching for the next token.
func (d *Dispenser) EOFErr() error { 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. // Err generates a custom parse-time error with a message of msg.
@@ -421,10 +420,7 @@ func (d *Dispenser) Errf(format string, args ...any) error {
// WrapErr takes an existing error and adds the Caddyfile file and line number. // WrapErr takes an existing error and adds the Caddyfile file and line number.
func (d *Dispenser) WrapErr(err error) error { func (d *Dispenser) WrapErr(err error) error {
if len(d.Token().imports) > 0 { return fmt.Errorf("%s:%d - Error during parsing: %w", d.File(), d.Line(), err)
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 // Delete deletes the current token and returns the updated slice
@@ -467,7 +463,17 @@ func (d *Dispenser) isNewLine() bool {
prev := d.tokens[d.cursor-1] prev := d.tokens[d.cursor-1]
curr := d.tokens[d.cursor] curr := d.tokens[d.cursor]
return isNextOnNewLine(prev, curr)
// If the previous token is from a different file,
// we can assume it's from a different line
if prev.File != curr.File {
return true
}
// If the previous token (incl line breaks) ends
// on a line earlier than the current token,
// then the current token is on a new line
return prev.Line+prev.NumLineBreaks() < curr.Line
} }
// isNextOnNewLine determines whether the current token is on a different // isNextOnNewLine determines whether the current token is on a different
@@ -483,5 +489,15 @@ func (d *Dispenser) isNextOnNewLine() bool {
curr := d.tokens[d.cursor] curr := d.tokens[d.cursor]
next := d.tokens[d.cursor+1] next := d.tokens[d.cursor+1]
return isNextOnNewLine(curr, next)
// If the next token is from a different file,
// we can assume it's from a different line
if curr.File != next.File {
return true
}
// If the current token (incl line breaks) ends
// on a line earlier than the next token,
// then the next token is on a new line
return curr.Line+curr.NumLineBreaks() < next.Line
} }
+5 -23
View File
@@ -19,9 +19,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"go.uber.org/zap"
) )
// parseVariadic determines if the token is a variadic placeholder, // parseVariadic determines if the token is a variadic placeholder,
@@ -40,7 +39,7 @@ func parseVariadic(token Token, argCount int) (bool, int, int) {
if argRange == "" { if argRange == "" {
caddy.Log().Named("caddyfile").Warn( caddy.Log().Named("caddyfile").Warn(
"Placeholder "+token.Text+" cannot have an empty index", "Placeholder "+token.Text+" cannot have an empty index",
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports)) zap.String("file", token.File+":"+strconv.Itoa(token.Line)))
return false, 0, 0 return false, 0, 0
} }
@@ -52,13 +51,6 @@ func parseVariadic(token Token, argCount int) (bool, int, int) {
return false, 0, 0 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 ( var (
startIndex = 0 startIndex = 0
endIndex = argCount endIndex = argCount
@@ -69,7 +61,7 @@ func parseVariadic(token Token, argCount int) (bool, int, int) {
if err != nil { if err != nil {
caddy.Log().Named("caddyfile").Warn( caddy.Log().Named("caddyfile").Warn(
"Variadic placeholder "+token.Text+" has an invalid start index", "Variadic placeholder "+token.Text+" has an invalid start index",
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports)) zap.String("file", token.File+":"+strconv.Itoa(token.Line)))
return false, 0, 0 return false, 0, 0
} }
} }
@@ -78,7 +70,7 @@ func parseVariadic(token Token, argCount int) (bool, int, int) {
if err != nil { if err != nil {
caddy.Log().Named("caddyfile").Warn( caddy.Log().Named("caddyfile").Warn(
"Variadic placeholder "+token.Text+" has an invalid end index", "Variadic placeholder "+token.Text+" has an invalid end index",
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports)) zap.String("file", token.File+":"+strconv.Itoa(token.Line)))
return false, 0, 0 return false, 0, 0
} }
} }
@@ -87,7 +79,7 @@ func parseVariadic(token Token, argCount int) (bool, int, int) {
if startIndex < 0 || startIndex > endIndex || endIndex > argCount { if startIndex < 0 || startIndex > endIndex || endIndex > argCount {
caddy.Log().Named("caddyfile").Warn( caddy.Log().Named("caddyfile").Warn(
"Variadic placeholder "+token.Text+" indices are out of bounds, only "+strconv.Itoa(argCount)+" argument(s) exist", "Variadic placeholder "+token.Text+" indices are out of bounds, only "+strconv.Itoa(argCount)+" argument(s) exist",
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports)) zap.String("file", token.File+":"+strconv.Itoa(token.Line)))
return false, 0, 0 return false, 0, 0
} }
return true, startIndex, endIndex return true, startIndex, endIndex
@@ -101,11 +93,6 @@ func makeArgsReplacer(args []string) *caddy.Replacer {
// TODO: Remove the deprecated {args.*} placeholder // TODO: Remove the deprecated {args.*} placeholder
// support at some point in the future // support at some point in the future
if matches := argsRegexpIndexDeprecated.FindStringSubmatch(key); len(matches) > 0 { 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]) value, err := strconv.Atoi(matches[1])
if err != nil { if err != nil {
caddy.Log().Named("caddyfile").Warn( caddy.Log().Named("caddyfile").Warn(
@@ -124,11 +111,6 @@ func makeArgsReplacer(args []string) *caddy.Replacer {
// Handle args[*] form // Handle args[*] form
if matches := argsRegexpIndex.FindStringSubmatch(key); len(matches) > 0 { 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], ":") { if strings.Contains(matches[1], ":") {
caddy.Log().Named("caddyfile").Warn( caddy.Log().Named("caddyfile").Warn(
"Variadic placeholder {args[" + matches[1] + "]} must be a token on its own") "Variadic placeholder {args[" + matches[1] + "]} must be a token on its own")
-3
View File
@@ -34,7 +34,6 @@ func (i *importGraph) addNode(name string) {
} }
i.nodes[name] = true i.nodes[name] = true
} }
func (i *importGraph) addNodes(names []string) { func (i *importGraph) addNodes(names []string) {
for _, name := range names { for _, name := range names {
i.addNode(name) i.addNode(name)
@@ -44,7 +43,6 @@ func (i *importGraph) addNodes(names []string) {
func (i *importGraph) removeNode(name string) { func (i *importGraph) removeNode(name string) {
delete(i.nodes, name) delete(i.nodes, name)
} }
func (i *importGraph) removeNodes(names []string) { func (i *importGraph) removeNodes(names []string) {
for _, name := range names { for _, name := range names {
i.removeNode(name) i.removeNode(name)
@@ -75,7 +73,6 @@ func (i *importGraph) addEdge(from, to string) error {
i.edges[from] = append(i.edges[from], to) i.edges[from] = append(i.edges[from], to)
return nil return nil
} }
func (i *importGraph) addEdges(from string, tos []string) error { func (i *importGraph) addEdges(from string, tos []string) error {
for _, to := range tos { for _, to := range tos {
err := i.addEdge(from, to) err := i.addEdge(from, to)
+21 -43
View File
@@ -39,7 +39,7 @@ type (
// Token represents a single parsable unit. // Token represents a single parsable unit.
Token struct { Token struct {
File string File string
imports []string origFile string
Line int Line int
Text string Text string
wasQuoted rune // enclosing quote character, if any wasQuoted rune // enclosing quote character, if any
@@ -137,32 +137,18 @@ func (l *lexer) next() (bool, error) {
} }
// detect whether we have the start of a heredoc // detect whether we have the start of a heredoc
if !(quoted || btQuoted) && !(inHeredoc || heredocEscaped) && if !inHeredoc && !heredocEscaped && len(val) > 1 && string(val[:2]) == "<<" {
len(val) > 1 && string(val[:2]) == "<<" { if ch == '<' {
// a space means it's just a regular token and not a heredoc return false, fmt.Errorf("too many '<' for heredoc on line #%d; only use two, for example <<END", l.line)
if ch == ' ' {
return makeToken(0), nil
} }
// skip CR, we only care about LF
if ch == '\r' { if ch == '\r' {
continue continue
} }
// after hitting a newline, we know that the heredoc marker // after hitting a newline, we know that the heredoc marker
// is the characters after the two << and the newline. // is the characters after the two << and the newline.
// we reset the val because the heredoc is syntax we don't // we reset the val because the heredoc is syntax we don't
// want to keep. // want to keep.
if ch == '\n' { 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:]) heredocMarker = string(val[2:])
if !heredocMarkerRegexp.Match([]byte(heredocMarker)) { 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) return false, fmt.Errorf("heredoc marker on line #%d must contain only alpha-numeric characters, dashes and underscores; got '%s'", l.line, heredocMarker)
@@ -335,6 +321,23 @@ func (l *lexer) finalizeHeredoc(val []rune, marker string) ([]rune, error) {
return []rune(out), nil return []rune(out), nil
} }
// originalFile gets original filename before import modification.
func (t Token) originalFile() string {
if t.origFile != "" {
return t.origFile
}
return t.File
}
// updateFile updates the token's source filename for error display
// and remembers the original filename. Used during "import" processing.
func (t *Token) updateFile(file string) {
if t.origFile == "" {
t.origFile = t.File
}
t.File = file
}
func (t Token) Quoted() bool { func (t Token) Quoted() bool {
return t.wasQuoted > 0 return t.wasQuoted > 0
} }
@@ -352,28 +355,3 @@ func (t Token) NumLineBreaks() int {
} }
var heredocMarkerRegexp = regexp.MustCompile("^[A-Za-z0-9_-]+$") var heredocMarkerRegexp = regexp.MustCompile("^[A-Za-z0-9_-]+$")
// isNextOnNewLine tests whether t2 is on a different line from t1
func isNextOnNewLine(t1, t2 Token) bool {
// If the second token is from a different file,
// we can assume it's from a different line
if t1.File != t2.File {
return true
}
// If the second token is from a different import chain,
// we can assume it's from a different line
if len(t1.imports) != len(t2.imports) {
return true
}
for i, im := range t1.imports {
if im != t2.imports[i] {
return true
}
}
// If the first token (incl line breaks) ends
// on a line earlier than the next token,
// then the second token is on a new line
return t1.Line+t1.NumLineBreaks() < t2.Line
}
+9 -50
View File
@@ -322,59 +322,15 @@ EOF same-line-arg
}, },
}, },
{ {
input: []byte(`escaped-heredoc \<< >>`), input: []byte(`heredoc <EOF
expected: []Token{
{Line: 1, Text: `escaped-heredoc`},
{Line: 1, Text: `<<`},
{Line: 1, Text: `>>`},
},
},
{
input: []byte(`not-a-heredoc <EOF
content content
EOF same-line-arg
`), `),
expected: []Token{ expected: []Token{
{Line: 1, Text: `not-a-heredoc`}, {Line: 1, Text: `heredoc`},
{Line: 1, Text: `<EOF`}, {Line: 1, Text: `<EOF`},
{Line: 2, Text: `content`}, {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`}, {Line: 3, Text: `same-line-arg`},
}, },
}, },
@@ -410,9 +366,12 @@ EOF same-line-arg
}, },
}, },
{ {
input: []byte("not-a-heredoc <<\n"), input: []byte(`heredoc <<HERE SAME LINE
content
HERE same-line-arg
`),
expectErr: true, expectErr: true,
errorMessage: "missing opening heredoc marker on line #1; must contain only alpha-numeric characters, dashes and underscores; got empty string", errorMessage: "heredoc marker on line #1 must contain only alpha-numeric characters, dashes and underscores; got 'HERE SAME LINE'",
}, },
{ {
input: []byte(`heredoc <<<EOF input: []byte(`heredoc <<<EOF
+19 -96
View File
@@ -22,9 +22,8 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"go.uber.org/zap"
) )
// Parse parses the input just enough to group tokens, in // Parse parses the input just enough to group tokens, in
@@ -149,6 +148,7 @@ func (p *parser) begin() error {
} }
err := p.addresses() err := p.addresses()
if err != nil { if err != nil {
return err return err
} }
@@ -159,25 +159,6 @@ func (p *parser) begin() error {
return nil return nil
} }
if ok, name := p.isNamedRoute(); ok {
// named routes only have one key, the route name
p.block.Keys = []string{name}
p.block.IsNamedRoute = true
// we just need a dummy leading token to ease parsing later
nameToken := p.Token()
nameToken.Text = name
// get all the tokens from the block, including the braces
tokens, err := p.blockTokens(true)
if err != nil {
return err
}
tokens = append([]Token{nameToken}, tokens...)
p.block.Segments = []Segment{tokens}
return nil
}
if ok, name := p.isSnippet(); ok { if ok, name := p.isSnippet(); ok {
if p.definedSnippets == nil { if p.definedSnippets == nil {
p.definedSnippets = map[string][]Token{} p.definedSnippets = map[string][]Token{}
@@ -186,7 +167,7 @@ func (p *parser) begin() error {
return p.Errf("redeclaration of previously declared snippet %s", name) return p.Errf("redeclaration of previously declared snippet %s", name)
} }
// consume all tokens til matched close brace // consume all tokens til matched close brace
tokens, err := p.blockTokens(false) tokens, err := p.snippetTokens()
if err != nil { if err != nil {
return err return err
} }
@@ -215,7 +196,7 @@ func (p *parser) addresses() error {
// special case: import directive replaces tokens during parse-time // special case: import directive replaces tokens during parse-time
if tkn == "import" && p.isNewLine() { if tkn == "import" && p.isNewLine() {
err := p.doImport(0) err := p.doImport()
if err != nil { if err != nil {
return err return err
} }
@@ -315,7 +296,7 @@ func (p *parser) directives() error {
// special case: import directive replaces tokens during parse-time // special case: import directive replaces tokens during parse-time
if p.Val() == "import" { if p.Val() == "import" {
err := p.doImport(1) err := p.doImport()
if err != nil { if err != nil {
return err return err
} }
@@ -341,7 +322,7 @@ func (p *parser) directives() error {
// is on the token before where the import directive was. In // is on the token before where the import directive was. In
// other words, call Next() to access the first token that was // other words, call Next() to access the first token that was
// imported. // imported.
func (p *parser) doImport(nesting int) error { func (p *parser) doImport() error {
// syntax checks // syntax checks
if !p.NextArg() { if !p.NextArg() {
return p.ArgErr() return p.ArgErr()
@@ -430,7 +411,7 @@ func (p *parser) doImport(nesting int) error {
nodes = matches nodes = matches
} }
nodeName := p.File() nodeName := p.Token().originalFile()
if p.Token().snippetName != "" { if p.Token().snippetName != "" {
nodeName += fmt.Sprintf(":%s", p.Token().snippetName) nodeName += fmt.Sprintf(":%s", p.Token().snippetName)
} }
@@ -444,55 +425,15 @@ func (p *parser) doImport(nesting int) error {
// copy the tokens so we don't overwrite p.definedSnippets // copy the tokens so we don't overwrite p.definedSnippets
tokensCopy := make([]Token, 0, len(importedTokens)) tokensCopy := make([]Token, 0, len(importedTokens))
var (
maybeSnippet bool
maybeSnippetId bool
index int
)
// run the argument replacer on the tokens // run the argument replacer on the tokens
// golang for range slice return a copy of value // golang for range slice return a copy of value
// similarly, append also copy value // similarly, append also copy value
for i, token := range importedTokens { for _, token := range importedTokens {
// update the token's imports to refer to import directive filename, line number and snippet name if there is one // set the token's file to refer to import directive line number and snippet name
if token.snippetName != "" { if token.snippetName != "" {
token.imports = append(token.imports, fmt.Sprintf("%s:%d (import %s)", p.File(), p.Line(), token.snippetName)) token.updateFile(fmt.Sprintf("%s:%d (import %s)", token.File, p.Line(), token.snippetName))
} else { } else {
token.imports = append(token.imports, fmt.Sprintf("%s:%d (import)", p.File(), p.Line())) token.updateFile(fmt.Sprintf("%s:%d (import)", token.File, p.Line()))
}
// naive way of determine snippets, as snippets definition can only follow name + block
// format, won't check for nesting correctness or any other error, that's what parser does.
if !maybeSnippet && nesting == 0 {
// first of the line
if i == 0 || isNextOnNewLine(tokensCopy[i-1], token) {
index = 0
} else {
index++
}
if index == 0 && len(token.Text) >= 3 && strings.HasPrefix(token.Text, "(") && strings.HasSuffix(token.Text, ")") {
maybeSnippetId = true
}
}
switch token.Text {
case "{":
nesting++
if index == 1 && maybeSnippetId && nesting == 1 {
maybeSnippet = true
maybeSnippetId = false
}
case "}":
nesting--
if nesting == 0 && maybeSnippet {
maybeSnippet = false
}
}
if maybeSnippet {
tokensCopy = append(tokensCopy, token)
continue
} }
foundVariadic, startIndex, endIndex := parseVariadic(token, len(args)) foundVariadic, startIndex, endIndex := parseVariadic(token, len(args))
@@ -566,6 +507,7 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
// are loaded into the current server block for later use // are loaded into the current server block for later use
// by directive setup functions. // by directive setup functions.
func (p *parser) directive() error { func (p *parser) directive() error {
// a segment is a list of tokens associated with this directive // a segment is a list of tokens associated with this directive
var segment Segment var segment Segment
@@ -578,9 +520,6 @@ func (p *parser) directive() error {
if !p.isNextOnNewLine() && p.Token().wasQuoted == 0 { if !p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
return p.Err("Unexpected next token after '{' on same line") return p.Err("Unexpected next token after '{' on same line")
} }
if p.isNewLine() {
return p.Err("Unexpected '{' on a new line; did you mean to place the '{' on the previous line?")
}
} else if p.Val() == "{}" { } else if p.Val() == "{}" {
if p.isNextOnNewLine() && p.Token().wasQuoted == 0 { if p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
return p.Err("Unexpected '{}' at end of line") return p.Err("Unexpected '{}' at end of line")
@@ -593,7 +532,7 @@ func (p *parser) directive() error {
} else if p.Val() == "}" && p.nesting == 0 { } else if p.Val() == "}" && p.nesting == 0 {
return p.Err("Unexpected '}' because no matching opening brace") return p.Err("Unexpected '}' because no matching opening brace")
} else if p.Val() == "import" && p.isNewLine() { } else if p.Val() == "import" && p.isNewLine() {
if err := p.doImport(1); err != nil { if err := p.doImport(); err != nil {
return err return err
} }
p.cursor-- // cursor is advanced when we continue, so roll back one more p.cursor-- // cursor is advanced when we continue, so roll back one more
@@ -634,15 +573,6 @@ func (p *parser) closeCurlyBrace() error {
return nil return nil
} }
func (p *parser) isNamedRoute() (bool, string) {
keys := p.block.Keys
// A named route block is a single key with parens, prefixed with &.
if len(keys) == 1 && strings.HasPrefix(keys[0], "&(") && strings.HasSuffix(keys[0], ")") {
return true, strings.TrimSuffix(keys[0][2:], ")")
}
return false, ""
}
func (p *parser) isSnippet() (bool, string) { func (p *parser) isSnippet() (bool, string) {
keys := p.block.Keys keys := p.block.Keys
// A snippet block is a single key with parens. Nothing else qualifies. // A snippet block is a single key with parens. Nothing else qualifies.
@@ -653,24 +583,18 @@ func (p *parser) isSnippet() (bool, string) {
} }
// read and store everything in a block for later replay. // read and store everything in a block for later replay.
func (p *parser) blockTokens(retainCurlies bool) ([]Token, error) { func (p *parser) snippetTokens() ([]Token, error) {
// block must have curlies. // snippet must have curlies.
err := p.openCurlyBrace() err := p.openCurlyBrace()
if err != nil { if err != nil {
return nil, err return nil, err
} }
nesting := 1 // count our own nesting nesting := 1 // count our own nesting in snippets
tokens := []Token{} tokens := []Token{}
if retainCurlies {
tokens = append(tokens, p.Token())
}
for p.Next() { for p.Next() {
if p.Val() == "}" { if p.Val() == "}" {
nesting-- nesting--
if nesting == 0 { if nesting == 0 {
if retainCurlies {
tokens = append(tokens, p.Token())
}
break break
} }
} }
@@ -690,10 +614,9 @@ func (p *parser) blockTokens(retainCurlies bool) ([]Token, error) {
// head of the server block with tokens, which are // head of the server block with tokens, which are
// grouped by segments. // grouped by segments.
type ServerBlock struct { type ServerBlock struct {
HasBraces bool HasBraces bool
Keys []string Keys []string
Segments []Segment Segments []Segment
IsNamedRoute bool
} }
// DispenseDirective returns a dispenser that contains // DispenseDirective returns a dispenser that contains
-42
View File
@@ -91,10 +91,6 @@ func TestParseVariadic(t *testing.T) {
input: "{args[0:10]}", input: "{args[0:10]}",
result: true, result: true,
}, },
{
input: "{args[0]}:{args[1]}:{args[2]}",
result: false,
},
} { } {
token := Token{ token := Token{
File: "test", File: "test",
@@ -297,14 +293,6 @@ func TestParseOneAndImport(t *testing.T) {
// Unexpected next token after '{' on same line // Unexpected next token after '{' on same line
{`localhost {`localhost
dir1 { a b }`, true, []string{"localhost"}, []int{}}, dir1 { a b }`, true, []string{"localhost"}, []int{}},
// Unexpected '{' on a new line
{`localhost
dir1
{
a b
}`, true, []string{"localhost"}, []int{}},
// Workaround with quotes // Workaround with quotes
{`localhost {`localhost
dir1 "{" a b "}"`, false, []string{"localhost"}, []int{5}}, dir1 "{" a b "}"`, false, []string{"localhost"}, []int{5}},
@@ -722,36 +710,6 @@ 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) { func TestSnippets(t *testing.T) {
p := testParser(` p := testParser(`
(common) { (common) {
+3 -17
View File
@@ -24,11 +24,10 @@ import (
"strings" "strings"
"unicode" "unicode"
"github.com/caddyserver/certmagic"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/certmagic"
) )
// mapAddressToServerBlocks returns a map of listener address to list of server // mapAddressToServerBlocks returns a map of listener address to list of server
@@ -78,8 +77,7 @@ import (
// multiple addresses to the same lists of server blocks (a many:many mapping). // multiple addresses to the same lists of server blocks (a many:many mapping).
// (Doing this is essentially a map-reduce technique.) // (Doing this is essentially a map-reduce technique.)
func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock, func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock,
options map[string]any, options map[string]any) (map[string][]serverBlock, error) {
) (map[string][]serverBlock, error) {
sbmap := make(map[string][]serverBlock) sbmap := make(map[string][]serverBlock)
for i, sblock := range originalServerBlocks { for i, sblock := range originalServerBlocks {
@@ -189,25 +187,13 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]se
// listenerAddrsForServerBlockKey essentially converts the Caddyfile // listenerAddrsForServerBlockKey essentially converts the Caddyfile
// site addresses to Caddy listener addresses for each server block. // site addresses to Caddy listener addresses for each server block.
func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string, func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string,
options map[string]any, options map[string]any) ([]string, error) {
) ([]string, error) {
addr, err := ParseAddress(key) addr, err := ParseAddress(key)
if err != nil { if err != nil {
return nil, fmt.Errorf("parsing key: %v", err) return nil, fmt.Errorf("parsing key: %v", err)
} }
addr = addr.Normalize() 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 // figure out the HTTP and HTTPS ports; either
// use defaults, or override with user config // use defaults, or override with user config
httpPort, httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPPort), strconv.Itoa(caddyhttp.DefaultHTTPSPort) httpPort, httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPPort), strconv.Itoa(caddyhttp.DefaultHTTPSPort)
+29 -84
View File
@@ -26,15 +26,14 @@ import (
"strings" "strings"
"time" "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"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
"go.uber.org/zap/zapcore"
) )
func init() { func init() {
@@ -49,7 +48,6 @@ func init() {
RegisterHandlerDirective("route", parseRoute) RegisterHandlerDirective("route", parseRoute)
RegisterHandlerDirective("handle", parseHandle) RegisterHandlerDirective("handle", parseHandle)
RegisterDirective("handle_errors", parseHandleErrors) RegisterDirective("handle_errors", parseHandleErrors)
RegisterHandlerDirective("invoke", parseInvoke)
RegisterDirective("log", parseLog) RegisterDirective("log", parseLog)
RegisterHandlerDirective("skip_log", parseSkipLog) RegisterHandlerDirective("skip_log", parseSkipLog)
} }
@@ -181,17 +179,17 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
case "protocols": case "protocols":
args := h.RemainingArgs() args := h.RemainingArgs()
if len(args) == 0 { if len(args) == 0 {
return nil, h.Errf("protocols requires one or two arguments") return nil, h.SyntaxErr("one or two protocols")
} }
if len(args) > 0 { if len(args) > 0 {
if _, ok := caddytls.SupportedProtocols[args[0]]; !ok { 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] cp.ProtocolMin = args[0]
} }
if len(args) > 1 { if len(args) > 1 {
if _, ok := caddytls.SupportedProtocols[args[1]]; !ok { 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] cp.ProtocolMax = args[1]
} }
@@ -199,7 +197,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
case "ciphers": case "ciphers":
for h.NextArg() { for h.NextArg() {
if !caddytls.CipherSuiteNameSupported(h.Val()) { 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()) cp.CipherSuites = append(cp.CipherSuites, h.Val())
} }
@@ -766,31 +764,9 @@ func parseHandleErrors(h Helper) ([]ConfigValue, error) {
}, nil }, nil
} }
// parseInvoke parses the invoke directive.
func parseInvoke(h Helper) (caddyhttp.MiddlewareHandler, error) {
h.Next() // consume directive
if !h.NextArg() {
return nil, h.ArgErr()
}
for h.Next() || h.NextBlock(0) {
return nil, h.ArgErr()
}
// remember that we're invoking this name
// to populate the server with these named routes
if h.State[namedRouteKey] == nil {
h.State[namedRouteKey] = map[string]struct{}{}
}
h.State[namedRouteKey].(map[string]struct{})[h.Val()] = struct{}{}
// return the handler
return &caddyhttp.Invoke{Name: h.Val()}, nil
}
// parseLog parses the log directive. Syntax: // parseLog parses the log directive. Syntax:
// //
// log <logger_name> { // log {
// hostnames <hostnames...>
// output <writer_module> ... // output <writer_module> ...
// format <encoder_module> ... // format <encoder_module> ...
// level <level> // level <level>
@@ -811,13 +787,11 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
var configValues []ConfigValue var configValues []ConfigValue
for h.Next() { for h.Next() {
// Logic below expects that a name is always present when a // Logic below expects that a name is always present when a
// global option is being parsed; or an optional override // global option is being parsed.
// is supported for access logs. var globalLogName string
var logName string
if parseAsGlobalOption { if parseAsGlobalOption {
if h.NextArg() { if h.NextArg() {
logName = h.Val() globalLogName = h.Val()
// Only a single argument is supported. // Only a single argument is supported.
if h.NextArg() { if h.NextArg() {
@@ -828,47 +802,26 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
// reference the default logger. See the // reference the default logger. See the
// setupNewDefault function in the logging // setupNewDefault function in the logging
// package for where this is configured. // package for where this is configured.
logName = caddy.DefaultLoggerName globalLogName = caddy.DefaultLoggerName
} }
// Verify this name is unused. // Verify this name is unused.
_, used := globalLogNames[logName] _, used := globalLogNames[globalLogName]
if used { if used {
return nil, h.Err("duplicate global log option for: " + logName) return nil, h.Err("duplicate global log option for: " + globalLogName)
} }
globalLogNames[logName] = struct{}{} globalLogNames[globalLogName] = struct{}{}
} else { } else {
// An optional override of the logger name can be provided; // No arguments are supported for the server block log directive
// otherwise a default will be used, like "log0", "log1", etc.
if h.NextArg() { if h.NextArg() {
logName = h.Val() return nil, h.ArgErr()
// Only a single argument is supported.
if h.NextArg() {
return nil, h.ArgErr()
}
} }
} }
cl := new(caddy.CustomLog) cl := new(caddy.CustomLog)
// allow overriding the current site block's hostnames for this logger;
// this is useful for setting up loggers per subdomain in a site block
// with a wildcard domain
customHostnames := []string{}
for h.NextBlock(0) { for h.NextBlock(0) {
switch h.Val() { switch h.Val() {
case "hostnames":
if parseAsGlobalOption {
return nil, h.Err("hostnames is not allowed in the log global options")
}
args := h.RemainingArgs()
if len(args) == 0 {
return nil, h.ArgErr()
}
customHostnames = append(customHostnames, args...)
case "output": case "output":
if !h.NextArg() { if !h.NextArg() {
return nil, h.ArgErr() return nil, h.ArgErr()
@@ -927,16 +880,18 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
} }
case "include": case "include":
// This configuration is only allowed in the global options
if !parseAsGlobalOption { if !parseAsGlobalOption {
return nil, h.Err("include is not allowed in the log directive") return nil, h.ArgErr()
} }
for h.NextArg() { for h.NextArg() {
cl.Include = append(cl.Include, h.Val()) cl.Include = append(cl.Include, h.Val())
} }
case "exclude": case "exclude":
// This configuration is only allowed in the global options
if !parseAsGlobalOption { if !parseAsGlobalOption {
return nil, h.Err("exclude is not allowed in the log directive") return nil, h.ArgErr()
} }
for h.NextArg() { for h.NextArg() {
cl.Exclude = append(cl.Exclude, h.Val()) cl.Exclude = append(cl.Exclude, h.Val())
@@ -948,34 +903,24 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
} }
var val namedCustomLog var val namedCustomLog
val.hostnames = customHostnames
isEmptyConfig := reflect.DeepEqual(cl, new(caddy.CustomLog))
// Skip handling of empty logging configs // Skip handling of empty logging configs
if !reflect.DeepEqual(cl, new(caddy.CustomLog)) {
if parseAsGlobalOption { if parseAsGlobalOption {
// Use indicated name for global log options // Use indicated name for global log options
val.name = logName val.name = globalLogName
} else { val.log = cl
if logName != "" { } else {
val.name = logName
} else if !isEmptyConfig {
// Construct a log name for server log streams // Construct a log name for server log streams
logCounter, ok := h.State["logCounter"].(int) logCounter, ok := h.State["logCounter"].(int)
if !ok { if !ok {
logCounter = 0 logCounter = 0
} }
val.name = fmt.Sprintf("log%d", logCounter) val.name = fmt.Sprintf("log%d", logCounter)
cl.Include = []string{"http.log.access." + val.name}
val.log = cl
logCounter++ logCounter++
h.State["logCounter"] = logCounter h.State["logCounter"] = logCounter
} }
if val.name != "" {
cl.Include = []string{"http.log.access." + val.name}
}
}
if !isEmptyConfig {
val.log = cl
} }
configValues = append(configValues, ConfigValue{ configValues = append(configValues, ConfigValue{
Class: "custom_log", Class: "custom_log",
+4 -99
View File
@@ -52,13 +52,12 @@ func TestLogDirectiveSyntax(t *testing.T) {
}, },
{ {
input: `:8080 { input: `:8080 {
log name-override { log invalid {
output file foo.log output file foo.log
} }
} }
`, `,
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.name-override"]},"name-override":{"writer":{"filename":"foo.log","output":"file"},"include":["http.log.access.name-override"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"name-override"}}}}}}`, expectError: true,
expectError: false,
}, },
} { } {
@@ -230,7 +229,7 @@ func TestImportErrorLine(t *testing.T) {
import t1 true import t1 true
}`, }`,
errorFunc: func(err error) bool { errorFunc: func(err error) bool {
return err != nil && strings.Contains(err.Error(), "Caddyfile:6 (import t1)") return err != nil && strings.Contains(err.Error(), "Caddyfile:6 (import t1):2")
}, },
}, },
{ {
@@ -241,101 +240,7 @@ func TestImportErrorLine(t *testing.T) {
import t1 true import t1 true
}`, }`,
errorFunc: func(err error) bool { errorFunc: func(err error) bool {
return err != nil && strings.Contains(err.Error(), "Caddyfile:5 (import t1)") return err != nil && strings.Contains(err.Error(), "Caddyfile:5 (import t1):2")
},
},
{
input: `
import testdata/import_variadic_snippet.txt
:8080 {
import t1 true
}`,
errorFunc: func(err error) bool {
return err == nil
},
},
{
input: `
import testdata/import_variadic_with_import.txt
:8080 {
import t1 true
import t2 true
}`,
errorFunc: func(err error) bool {
return err == nil
},
},
} {
adapter := caddyfile.Adapter{
ServerType: ServerType{},
}
_, _, err := adapter.Adapt([]byte(tc.input), nil)
if !tc.errorFunc(err) {
t.Errorf("Test %d error expectation failed, got %s", i, err)
continue
}
}
}
func TestNestedImport(t *testing.T) {
for i, tc := range []struct {
input string
errorFunc func(err error) bool
}{
{
input: `(t1) {
respond {args[0]} {args[1]}
}
(t2) {
import t1 {args[0]} 202
}
:8080 {
handle {
import t2 "foobar"
}
}`,
errorFunc: func(err error) bool {
return err == nil
},
},
{
input: `(t1) {
respond {args[:]}
}
(t2) {
import t1 {args[0]} {args[1]}
}
:8080 {
handle {
import t2 "foobar" 202
}
}`,
errorFunc: func(err error) bool {
return err == nil
},
},
{
input: `(t1) {
respond {args[0]} {args[1]}
}
(t2) {
import t1 {args[:]}
}
:8080 {
handle {
import t2 "foobar" 202
}
}`,
errorFunc: func(err error) bool {
return err == nil
}, },
}, },
} { } {
+1 -4
View File
@@ -65,7 +65,6 @@ var directiveOrder = []string{
"templates", "templates",
// special routing & dispatching directives // special routing & dispatching directives
"invoke",
"handle", "handle",
"handle_path", "handle_path",
"route", "route",
@@ -173,7 +172,6 @@ func (h Helper) Caddyfiles() []string {
for file := range files { for file := range files {
filesSlice = append(filesSlice, file) filesSlice = append(filesSlice, file)
} }
sort.Strings(filesSlice)
return filesSlice return filesSlice
} }
@@ -217,8 +215,7 @@ func (h Helper) ExtractMatcherSet() (caddy.ModuleMap, error) {
// NewRoute returns config values relevant to creating a new HTTP route. // NewRoute returns config values relevant to creating a new HTTP route.
func (h Helper) NewRoute(matcherSet caddy.ModuleMap, func (h Helper) NewRoute(matcherSet caddy.ModuleMap,
handler caddyhttp.MiddlewareHandler, handler caddyhttp.MiddlewareHandler) []ConfigValue {
) []ConfigValue {
mod, err := caddy.GetModule(caddy.GetModuleID(handler)) mod, err := caddy.GetModule(caddy.GetModuleID(handler))
if err != nil { if err != nil {
*h.warnings = append(*h.warnings, caddyconfig.Warning{ *h.warnings = append(*h.warnings, caddyconfig.Warning{
+81 -192
View File
@@ -17,21 +17,19 @@ package httpcaddyfile
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net"
"reflect" "reflect"
"regexp"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"go.uber.org/zap"
"golang.org/x/exp/slices"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddypki" "github.com/caddyserver/caddy/v2/modules/caddypki"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
"go.uber.org/zap"
) )
func init() { func init() {
@@ -50,13 +48,12 @@ type App struct {
} }
// ServerType can set up a config from an HTTP Caddyfile. // ServerType can set up a config from an HTTP Caddyfile.
type ServerType struct{} type ServerType struct {
}
// Setup makes a config from the tokens. // Setup makes a config from the tokens.
func (st ServerType) Setup( func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
inputServerBlocks []caddyfile.ServerBlock, options map[string]any) (*caddy.Config, []caddyconfig.Warning, error) {
options map[string]any,
) (*caddy.Config, []caddyconfig.Warning, error) {
var warnings []caddyconfig.Warning var warnings []caddyconfig.Warning
gc := counter{new(int)} gc := counter{new(int)}
state := make(map[string]any) state := make(map[string]any)
@@ -82,18 +79,41 @@ func (st ServerType) Setup(
return nil, warnings, err return nil, warnings, err
} }
// this will replace both static and user-defined placeholder shorthands // replace shorthand placeholders (which are convenient
// with actual identifiers used by Caddy // when writing a Caddyfile) with their actual placeholder
replacer := NewShorthandReplacer() // identifiers or variable names
replacer := strings.NewReplacer(placeholderShorthands()...)
originalServerBlocks, err = st.extractNamedRoutes(originalServerBlocks, options, &warnings, replacer) // these are placeholders that allow a user-defined final
if err != nil { // parameters, but we still want to provide a shorthand
return nil, warnings, err // 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 _, sb := range originalServerBlocks {
for i := range sb.block.Segments { for _, segment := range sb.block.Segments {
replacer.ApplyToSegment(&sb.block.Segments[i]) 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)
}
}
} }
if len(sb.block.Keys) == 0 { if len(sb.block.Keys) == 0 {
@@ -152,18 +172,6 @@ func (st ServerType) Setup(
result.directive = dir result.directive = dir
sb.pile[result.Class] = append(sb.pile[result.Class], result) sb.pile[result.Class] = append(sb.pile[result.Class], result)
} }
// specially handle named routes that were pulled out from
// the invoke directive, which could be nested anywhere within
// some subroutes in this directive; we add them to the pile
// for this server block
if state[namedRouteKey] != nil {
for name := range state[namedRouteKey].(map[string]struct{}) {
result := ConfigValue{Class: namedRouteKey, Value: name}
sb.pile[result.Class] = append(sb.pile[result.Class], result)
}
state[namedRouteKey] = nil
}
} }
} }
@@ -214,7 +222,7 @@ func (st ServerType) Setup(
if ncl.name == caddy.DefaultLoggerName { if ncl.name == caddy.DefaultLoggerName {
hasDefaultLog = true hasDefaultLog = true
} }
if _, ok := options["debug"]; ok && ncl.log != nil && ncl.log.Level == "" { if _, ok := options["debug"]; ok && ncl.log.Level == "" {
ncl.log.Level = zap.DebugLevel.CapitalString() ncl.log.Level = zap.DebugLevel.CapitalString()
} }
customLogs = append(customLogs, ncl) customLogs = append(customLogs, ncl)
@@ -297,21 +305,7 @@ func (st ServerType) Setup(
Logs: make(map[string]*caddy.CustomLog), Logs: make(map[string]*caddy.CustomLog),
} }
} }
// Add the default log first if defined, so that it doesn't
// accidentally get re-created below due to the Exclude logic
for _, ncl := range customLogs { for _, ncl := range customLogs {
if ncl.name == caddy.DefaultLoggerName && ncl.log != nil {
cfg.Logging.Logs[caddy.DefaultLoggerName] = ncl.log
break
}
}
// Add the rest of the custom logs
for _, ncl := range customLogs {
if ncl.log == nil || ncl.name == caddy.DefaultLoggerName {
continue
}
if ncl.name != "" { if ncl.name != "" {
cfg.Logging.Logs[ncl.name] = ncl.log cfg.Logging.Logs[ncl.name] = ncl.log
} }
@@ -325,16 +319,8 @@ func (st ServerType) Setup(
cfg.Logging.Logs[caddy.DefaultLoggerName] = defaultLog cfg.Logging.Logs[caddy.DefaultLoggerName] = defaultLog
} }
defaultLog.Exclude = append(defaultLog.Exclude, ncl.log.Include...) defaultLog.Exclude = append(defaultLog.Exclude, ncl.log.Include...)
// avoid duplicates by sorting + compacting
sort.Strings(defaultLog.Exclude)
defaultLog.Exclude = slices.Compact[[]string, string](defaultLog.Exclude)
} }
} }
// we may have not actually added anything, so remove if empty
if len(cfg.Logging.Logs) == 0 {
cfg.Logging = nil
}
} }
return cfg, warnings, nil return cfg, warnings, nil
@@ -417,81 +403,6 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
return serverBlocks[1:], nil return serverBlocks[1:], nil
} }
// extractNamedRoutes pulls out any named route server blocks
// so they don't get parsed as sites, and stores them in options
// for later.
func (ServerType) extractNamedRoutes(
serverBlocks []serverBlock,
options map[string]any,
warnings *[]caddyconfig.Warning,
replacer ShorthandReplacer,
) ([]serverBlock, error) {
namedRoutes := map[string]*caddyhttp.Route{}
gc := counter{new(int)}
state := make(map[string]any)
// copy the server blocks so we can
// splice out the named route ones
filtered := append([]serverBlock{}, serverBlocks...)
index := -1
for _, sb := range serverBlocks {
index++
if !sb.block.IsNamedRoute {
continue
}
// splice out this block, because we know it's not a real server
filtered = append(filtered[:index], filtered[index+1:]...)
index--
if len(sb.block.Segments) == 0 {
continue
}
wholeSegment := caddyfile.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{
Dispenser: caddyfile.NewDispenser(wholeSegment),
options: options,
warnings: warnings,
matcherDefs: nil,
parentBlock: sb.block,
groupCounter: gc,
State: state,
}
handler, err := ParseSegmentAsSubroute(h)
if err != nil {
return nil, err
}
subroute := handler.(*caddyhttp.Subroute)
route := caddyhttp.Route{}
if len(subroute.Routes) == 1 && len(subroute.Routes[0].MatcherSetsRaw) == 0 {
// if there's only one route with no matcher, then we can simplify
route.HandlersRaw = append(route.HandlersRaw, subroute.Routes[0].HandlersRaw[0])
} else {
// otherwise we need the whole subroute
route.HandlersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", subroute.CaddyModule().ID.Name(), h.warnings)}
}
namedRoutes[sb.block.Keys[0]] = &route
}
options["named_routes"] = namedRoutes
return filtered, nil
}
// serversFromPairings creates the servers for each pairing of addresses // serversFromPairings creates the servers for each pairing of addresses
// to server blocks. Each pairing is essentially a server definition. // to server blocks. Each pairing is essentially a server definition.
func (st *ServerType) serversFromPairings( func (st *ServerType) serversFromPairings(
@@ -502,7 +413,6 @@ func (st *ServerType) serversFromPairings(
) (map[string]*caddyhttp.Server, error) { ) (map[string]*caddyhttp.Server, error) {
servers := make(map[string]*caddyhttp.Server) servers := make(map[string]*caddyhttp.Server)
defaultSNI := tryString(options["default_sni"], warnings) defaultSNI := tryString(options["default_sni"], warnings)
fallbackSNI := tryString(options["fallback_sni"], warnings)
httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort) httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
if hp, ok := options["http_port"].(int); ok { if hp, ok := options["http_port"].(int); ok {
@@ -631,24 +541,6 @@ func (st *ServerType) serversFromPairings(
} }
} }
// add named routes to the server if 'invoke' was used inside of it
configuredNamedRoutes := options["named_routes"].(map[string]*caddyhttp.Route)
for _, sblock := range p.serverBlocks {
if len(sblock.pile[namedRouteKey]) == 0 {
continue
}
for _, value := range sblock.pile[namedRouteKey] {
if srv.NamedRoutes == nil {
srv.NamedRoutes = map[string]*caddyhttp.Route{}
}
name := value.Value.(string)
if configuredNamedRoutes[name] == nil {
return nil, fmt.Errorf("cannot invoke named route '%s', which was not defined", name)
}
srv.NamedRoutes[name] = configuredNamedRoutes[name]
}
}
// create a subroute for each site in the server block // create a subroute for each site in the server block
for _, sblock := range p.serverBlocks { for _, sblock := range p.serverBlocks {
matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock) matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock)
@@ -678,21 +570,14 @@ func (st *ServerType) serversFromPairings(
cp.DefaultSNI = defaultSNI cp.DefaultSNI = defaultSNI
break break
} }
if h == fallbackSNI {
hosts = append(hosts, "")
cp.FallbackSNI = fallbackSNI
break
}
} }
if len(hosts) > 0 { if len(hosts) > 0 {
slices.Sort(hosts) // for deterministic JSON output
cp.MatchersRaw = caddy.ModuleMap{ cp.MatchersRaw = caddy.ModuleMap{
"sni": caddyconfig.JSON(hosts, warnings), // make sure to match all hosts, not just auto-HTTPS-qualified ones "sni": caddyconfig.JSON(hosts, warnings), // make sure to match all hosts, not just auto-HTTPS-qualified ones
} }
} else { } else {
cp.DefaultSNI = defaultSNI cp.DefaultSNI = defaultSNI
cp.FallbackSNI = fallbackSNI
} }
// only append this policy if it actually changes something // only append this policy if it actually changes something
@@ -718,20 +603,10 @@ 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 // we'll need to remember if the address qualifies for auto-HTTPS, so we
// can add a TLS conn policy if necessary // can add a TLS conn policy if necessary
if addr.Scheme == "https" || if addr.Scheme == "https" ||
(addr.Scheme != "http" && addr.Port != httpPort && hasTLSEnabled) { (addr.Scheme != "http" && addr.Host != "" && addr.Port != httpPort) {
addressQualifiesForTLS = true addressQualifiesForTLS = true
} }
// predict whether auto-HTTPS will add the conn policy for us; if so, we // predict whether auto-HTTPS will add the conn policy for us; if so, we
@@ -780,30 +655,17 @@ func (st *ServerType) serversFromPairings(
sblockLogHosts := sblock.hostsFromKeys(true) sblockLogHosts := sblock.hostsFromKeys(true)
for _, cval := range sblock.pile["custom_log"] { for _, cval := range sblock.pile["custom_log"] {
ncl := cval.Value.(namedCustomLog) ncl := cval.Value.(namedCustomLog)
if sblock.hasHostCatchAllKey() && len(ncl.hostnames) == 0 { if sblock.hasHostCatchAllKey() {
// all requests for hosts not able to be listed should use // all requests for hosts not able to be listed should use
// this log because it's a catch-all-hosts server block // this log because it's a catch-all-hosts server block
srv.Logs.DefaultLoggerName = ncl.name srv.Logs.DefaultLoggerName = ncl.name
} else if len(ncl.hostnames) > 0 {
// if the logger overrides the hostnames, map that to the logger name
for _, h := range ncl.hostnames {
if srv.Logs.LoggerNames == nil {
srv.Logs.LoggerNames = make(map[string]string)
}
srv.Logs.LoggerNames[h] = ncl.name
}
} else { } else {
// otherwise, map each host to the logger name // map each host to the user's desired logger name
for _, h := range sblockLogHosts { for _, h := range sblockLogHosts {
if srv.Logs.LoggerNames == nil { if srv.Logs.LoggerNames == nil {
srv.Logs.LoggerNames = make(map[string]string) srv.Logs.LoggerNames = make(map[string]string)
} }
// strip the port from the host, if any srv.Logs.LoggerNames[h] = ncl.name
host, _, err := net.SplitHostPort(h)
if err != nil {
host = h
}
srv.Logs.LoggerNames[host] = ncl.name
} }
} }
} }
@@ -841,8 +703,8 @@ func (st *ServerType) serversFromPairings(
// policy missing for any HTTPS-enabled hosts, if so, add it... maybe? // policy missing for any HTTPS-enabled hosts, if so, add it... maybe?
if addressQualifiesForTLS && if addressQualifiesForTLS &&
!hasCatchAllTLSConnPolicy && !hasCatchAllTLSConnPolicy &&
(len(srv.TLSConnPolicies) > 0 || !autoHTTPSWillAddConnPolicy || defaultSNI != "" || fallbackSNI != "") { (len(srv.TLSConnPolicies) > 0 || !autoHTTPSWillAddConnPolicy || defaultSNI != "") {
srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{DefaultSNI: defaultSNI, FallbackSNI: fallbackSNI}) srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{DefaultSNI: defaultSNI})
} }
// tidy things up a bit // tidy things up a bit
@@ -1051,8 +913,8 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
subroute *caddyhttp.Subroute, subroute *caddyhttp.Subroute,
matcherSetsEnc []caddy.ModuleMap, matcherSetsEnc []caddy.ModuleMap,
p sbAddrAssociation, p sbAddrAssociation,
warnings *[]caddyconfig.Warning, warnings *[]caddyconfig.Warning) caddyhttp.RouteList {
) caddyhttp.RouteList {
// nothing to do if... there's nothing to do // nothing to do if... there's nothing to do
if len(matcherSetsEnc) == 0 && len(subroute.Routes) == 0 && subroute.Errors == nil { if len(matcherSetsEnc) == 0 && len(subroute.Routes) == 0 && subroute.Errors == nil {
return routeList return routeList
@@ -1114,7 +976,7 @@ func buildSubroute(routes []ConfigValue, groupCounter counter, needsSorting bool
if needsSorting { if needsSorting {
for _, val := range routes { for _, val := range routes {
if !directiveIsOrdered(val.directive) { if !directiveIsOrdered(val.directive) {
return nil, fmt.Errorf("directive '%s' is not an ordered HTTP handler, so it cannot be used here - try placing within a route block or using the order global option", val.directive) return nil, fmt.Errorf("directive '%s' is not an ordered HTTP handler, so it cannot be used here", val.directive)
} }
} }
@@ -1441,6 +1303,37 @@ func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (caddy.Modul
return msEncoded, nil 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 // WasReplacedPlaceholderShorthand checks if a token string was
// likely a replaced shorthand of the known Caddyfile placeholder // likely a replaced shorthand of the known Caddyfile placeholder
// replacement outputs. Useful to prevent some user-defined map // replacement outputs. Useful to prevent some user-defined map
@@ -1556,9 +1449,8 @@ func (c counter) nextGroup() string {
} }
type namedCustomLog struct { type namedCustomLog struct {
name string name string
hostnames []string log *caddy.CustomLog
log *caddy.CustomLog
} }
// sbAddrAssociation is a mapping from a list of // sbAddrAssociation is a mapping from a list of
@@ -1569,10 +1461,7 @@ type sbAddrAssociation struct {
serverBlocks []serverBlock serverBlocks []serverBlock
} }
const ( const matcherPrefix = "@"
matcherPrefix = "@"
namedRouteKey = "named_route"
)
// Interface guard // Interface guard
var _ caddyfile.ServerType = (*ServerType)(nil) var _ caddyfile.ServerType = (*ServerType)(nil)
+2 -4
View File
@@ -17,13 +17,12 @@ package httpcaddyfile
import ( import (
"strconv" "strconv"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
) )
func init() { func init() {
@@ -34,7 +33,6 @@ func init() {
RegisterGlobalOption("grace_period", parseOptDuration) RegisterGlobalOption("grace_period", parseOptDuration)
RegisterGlobalOption("shutdown_delay", parseOptDuration) RegisterGlobalOption("shutdown_delay", parseOptDuration)
RegisterGlobalOption("default_sni", parseOptSingleString) RegisterGlobalOption("default_sni", parseOptSingleString)
RegisterGlobalOption("fallback_sni", parseOptSingleString)
RegisterGlobalOption("order", parseOptOrder) RegisterGlobalOption("order", parseOptOrder)
RegisterGlobalOption("storage", parseOptStorage) RegisterGlobalOption("storage", parseOptStorage)
RegisterGlobalOption("storage_clean_interval", parseOptDuration) RegisterGlobalOption("storage_clean_interval", parseOptDuration)
+1
View File
@@ -174,6 +174,7 @@ func (st ServerType) buildPKIApp(
options map[string]any, options map[string]any,
warnings []caddyconfig.Warning, warnings []caddyconfig.Warning,
) (*caddypki.PKI, []caddyconfig.Warning, error) { ) (*caddypki.PKI, []caddyconfig.Warning, error) {
skipInstallTrust := false skipInstallTrust := false
if _, ok := options["skip_install_trust"]; ok { if _, ok := options["skip_install_trust"]; ok {
skipInstallTrust = true skipInstallTrust = true
+1 -10
View File
@@ -18,12 +18,11 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/dustin/go-humanize"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/dustin/go-humanize"
) )
// serverOptions collects server config overrides parsed from Caddyfile global options // serverOptions collects server config overrides parsed from Caddyfile global options
@@ -42,7 +41,6 @@ type serverOptions struct {
IdleTimeout caddy.Duration IdleTimeout caddy.Duration
KeepAliveInterval caddy.Duration KeepAliveInterval caddy.Duration
MaxHeaderBytes int MaxHeaderBytes int
EnableFullDuplex bool
Protocols []string Protocols []string
StrictSNIHost *bool StrictSNIHost *bool
TrustedProxiesRaw json.RawMessage TrustedProxiesRaw json.RawMessage
@@ -159,12 +157,6 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
} }
serverOpts.MaxHeaderBytes = int(size) serverOpts.MaxHeaderBytes = int(size)
case "enable_full_duplex":
if d.NextArg() {
return nil, d.ArgErr()
}
serverOpts.EnableFullDuplex = true
case "log_credentials": case "log_credentials":
if d.NextArg() { if d.NextArg() {
return nil, d.ArgErr() return nil, d.ArgErr()
@@ -335,7 +327,6 @@ func applyServerOptions(
server.IdleTimeout = opts.IdleTimeout server.IdleTimeout = opts.IdleTimeout
server.KeepAliveInterval = opts.KeepAliveInterval server.KeepAliveInterval = opts.KeepAliveInterval
server.MaxHeaderBytes = opts.MaxHeaderBytes server.MaxHeaderBytes = opts.MaxHeaderBytes
server.EnableFullDuplex = opts.EnableFullDuplex
server.Protocols = opts.Protocols server.Protocols = opts.Protocols
server.StrictSNIHost = opts.StrictSNIHost server.StrictSNIHost = opts.StrictSNIHost
server.TrustedProxiesRaw = opts.TrustedProxiesRaw server.TrustedProxiesRaw = opts.TrustedProxiesRaw
-93
View File
@@ -1,93 +0,0 @@
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)
}
}
}
}
@@ -1,9 +0,0 @@
(t2) {
respond 200 {
body {args[:]}
}
}
:8082 {
import t2 false
}
@@ -1,9 +0,0 @@
(t1) {
respond 200 {
body {args[:]}
}
}
:8081 {
import t1 false
}
@@ -1,15 +0,0 @@
(t1) {
respond 200 {
body {args[:]}
}
}
:8081 {
import t1 false
}
import import_variadic.txt
:8083 {
import t2 true
}
+3 -8
View File
@@ -23,13 +23,12 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
) )
func (st ServerType) buildTLSApp( func (st ServerType) buildTLSApp(
@@ -37,6 +36,7 @@ func (st ServerType) buildTLSApp(
options map[string]any, options map[string]any,
warnings []caddyconfig.Warning, warnings []caddyconfig.Warning,
) (*caddytls.TLS, []caddyconfig.Warning, error) { ) (*caddytls.TLS, []caddyconfig.Warning, error) {
tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)} tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}
var certLoaders []caddytls.CertificateLoader var certLoaders []caddytls.CertificateLoader
@@ -218,10 +218,6 @@ func (st ServerType) buildTLSApp(
if len(ap.Issuers) == 0 { if len(ap.Issuers) == 0 {
var internal, external []string var internal, external []string
for _, s := range ap.SubjectsRaw { for _, s := range ap.SubjectsRaw {
// do not create Issuers for Tailscale domains; they will be given a Manager instead
if strings.HasSuffix(strings.ToLower(s), ".ts.net") {
continue
}
if !certmagic.SubjectQualifiesForCert(s) { if !certmagic.SubjectQualifiesForCert(s) {
return nil, warnings, fmt.Errorf("subject does not qualify for certificate: '%s'", s) return nil, warnings, fmt.Errorf("subject does not qualify for certificate: '%s'", s)
} }
@@ -582,7 +578,6 @@ outer:
// eaten up by the one with subjects; and if both have subjects, we // eaten up by the one with subjects; and if both have subjects, we
// need to combine their lists // need to combine their lists
if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) && 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) && bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
aps[i].MustStaple == aps[j].MustStaple && aps[i].MustStaple == aps[j].MustStaple &&
aps[i].KeyType == aps[j].KeyType && aps[i].KeyType == aps[j].KeyType &&
+3 -19
View File
@@ -30,14 +30,8 @@ func init() {
caddy.RegisterModule(HTTPLoader{}) caddy.RegisterModule(HTTPLoader{})
} }
// HTTPLoader can load Caddy configs over HTTP(S). // HTTPLoader can load Caddy configs over HTTP(S). It can adapt the config
// // based on the Content-Type header of the HTTP response.
// If the response is not a JSON config, a config adapter must be specified
// either in the loader config (`adapter`), or in the Content-Type HTTP header
// returned in the HTTP response from the server. The Content-Type header is
// read just like the admin API's `/load` endpoint. Uf you don't have control
// over the HTTP server (but can still trust its response), you can override
// the Content-Type header by setting the `adapter` property in this config.
type HTTPLoader struct { type HTTPLoader struct {
// The method for the request. Default: GET // The method for the request. Default: GET
Method string `json:"method,omitempty"` Method string `json:"method,omitempty"`
@@ -51,11 +45,6 @@ type HTTPLoader struct {
// Maximum time allowed for a complete connection and request. // Maximum time allowed for a complete connection and request.
Timeout caddy.Duration `json:"timeout,omitempty"` Timeout caddy.Duration `json:"timeout,omitempty"`
// The name of the config adapter to use, if any. Only needed
// if the HTTP response is not a JSON config and if the server's
// Content-Type header is missing or incorrect.
Adapter string `json:"adapter,omitempty"`
TLS *struct { TLS *struct {
// Present this instance's managed remote identity credentials to the server. // Present this instance's managed remote identity credentials to the server.
UseServerIdentity bool `json:"use_server_identity,omitempty"` UseServerIdentity bool `json:"use_server_identity,omitempty"`
@@ -119,12 +108,7 @@ func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
return nil, err return nil, err
} }
// adapt the config based on either manually-configured adapter or server's response header result, warnings, err := adaptByContentType(resp.Header.Get("Content-Type"), body)
ct := resp.Header.Get("Content-Type")
if hl.Adapter != "" {
ct = "text/" + hl.Adapter
}
result, warnings, err := adaptByContentType(ct, body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+18 -3
View File
@@ -22,10 +22,9 @@ import (
"time" "time"
"github.com/aryann/difflib" "github.com/aryann/difflib"
"github.com/caddyserver/caddy/v2/caddyconfig"
caddycmd "github.com/caddyserver/caddy/v2/cmd" caddycmd "github.com/caddyserver/caddy/v2/cmd"
"github.com/caddyserver/caddy/v2/caddyconfig"
// plug in Caddy modules here // plug in Caddy modules here
_ "github.com/caddyserver/caddy/v2/modules/standard" _ "github.com/caddyserver/caddy/v2/modules/standard"
) )
@@ -64,6 +63,7 @@ type Tester struct {
// NewTester will create a new testing client with an attached cookie jar // NewTester will create a new testing client with an attached cookie jar
func NewTester(t *testing.T) *Tester { func NewTester(t *testing.T) *Tester {
jar, err := cookiejar.New(nil) jar, err := cookiejar.New(nil)
if err != nil { if err != nil {
t.Fatalf("failed to create cookiejar: %s", err) t.Fatalf("failed to create cookiejar: %s", err)
@@ -94,6 +94,7 @@ func timeElapsed(start time.Time, name string) {
// InitServer this will configure the server with a configurion of a specific // InitServer this will configure the server with a configurion of a specific
// type. The configType must be either "json" or the adapter type. // type. The configType must be either "json" or the adapter type.
func (tc *Tester) InitServer(rawConfig string, configType string) { func (tc *Tester) InitServer(rawConfig string, configType string) {
if err := tc.initServer(rawConfig, configType); err != nil { if err := tc.initServer(rawConfig, configType); err != nil {
tc.t.Logf("failed to load config: %s", err) tc.t.Logf("failed to load config: %s", err)
tc.t.Fail() tc.t.Fail()
@@ -107,6 +108,7 @@ func (tc *Tester) InitServer(rawConfig string, configType string) {
// InitServer this will configure the server with a configurion of a specific // InitServer this will configure the server with a configurion of a specific
// type. The configType must be either "json" or the adapter type. // type. The configType must be either "json" or the adapter type.
func (tc *Tester) initServer(rawConfig string, configType string) error { func (tc *Tester) initServer(rawConfig string, configType string) error {
if testing.Short() { if testing.Short() {
tc.t.SkipNow() tc.t.SkipNow()
return nil return nil
@@ -230,6 +232,7 @@ const initConfig = `{
// validateTestPrerequisites ensures the certificates are available in the // validateTestPrerequisites ensures the certificates are available in the
// designated path and Caddy sub-process is running. // designated path and Caddy sub-process is running.
func validateTestPrerequisites(t *testing.T) error { func validateTestPrerequisites(t *testing.T) error {
// check certificates are found // check certificates are found
for _, certName := range Default.Certifcates { for _, certName := range Default.Certifcates {
if _, err := os.Stat(getIntegrationDir() + certName); os.IsNotExist(err) { if _, err := os.Stat(getIntegrationDir() + certName); os.IsNotExist(err) {
@@ -281,6 +284,7 @@ func isCaddyAdminRunning() error {
} }
func getIntegrationDir() string { func getIntegrationDir() string {
_, filename, _, ok := runtime.Caller(1) _, filename, _, ok := runtime.Caller(1)
if !ok { if !ok {
panic("unable to determine the current file path") panic("unable to determine the current file path")
@@ -300,6 +304,7 @@ func prependCaddyFilePath(rawConfig string) string {
// CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally // CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally
func CreateTestingTransport() *http.Transport { func CreateTestingTransport() *http.Transport {
dialer := net.Dialer{ dialer := net.Dialer{
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
KeepAlive: 5 * time.Second, KeepAlive: 5 * time.Second,
@@ -327,6 +332,7 @@ func CreateTestingTransport() *http.Transport {
// AssertLoadError will load a config and expect an error // AssertLoadError will load a config and expect an error
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) { func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
tc := NewTester(t) tc := NewTester(t)
err := tc.initServer(rawConfig, configType) err := tc.initServer(rawConfig, configType)
@@ -337,6 +343,7 @@ func AssertLoadError(t *testing.T, rawConfig string, configType string, expected
// AssertRedirect makes a request and asserts the redirection happens // AssertRedirect makes a request and asserts the redirection happens
func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response { func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error { redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse return http.ErrUseLastResponse
} }
@@ -374,6 +381,7 @@ func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, e
// CompareAdapt adapts a config and then compares it against an expected result // 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 { func CompareAdapt(t *testing.T, filename, rawConfig string, adapterName string, expectedResponse string) bool {
cfgAdapter := caddyconfig.GetAdapter(adapterName) cfgAdapter := caddyconfig.GetAdapter(adapterName)
if cfgAdapter == nil { if cfgAdapter == nil {
t.Logf("unrecognized config adapter '%s'", adapterName) t.Logf("unrecognized config adapter '%s'", adapterName)
@@ -461,13 +469,14 @@ 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 // 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 { func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
resp, err := tc.Client.Do(req) resp, err := tc.Client.Do(req)
if err != nil { if err != nil {
tc.t.Fatalf("failed to call server %s", err) tc.t.Fatalf("failed to call server %s", err)
} }
if expectedStatusCode != resp.StatusCode { if expectedStatusCode != resp.StatusCode {
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.URL.RequestURI(), expectedStatusCode, resp.StatusCode) tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.RequestURI, expectedStatusCode, resp.StatusCode)
} }
return resp return resp
@@ -475,6 +484,7 @@ func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int)
// AssertResponse request a URI and assert the status code and the body contains a string // 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) { func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
resp := tc.AssertResponseCode(req, expectedStatusCode) resp := tc.AssertResponseCode(req, expectedStatusCode)
defer resp.Body.Close() defer resp.Body.Close()
@@ -496,6 +506,7 @@ func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expe
// AssertGetResponse GET a URI and expect a statusCode and body text // AssertGetResponse GET a URI and expect a statusCode and body text
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) { func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("GET", requestURI, nil) req, err := http.NewRequest("GET", requestURI, nil)
if err != nil { if err != nil {
tc.t.Fatalf("unable to create request %s", err) tc.t.Fatalf("unable to create request %s", err)
@@ -506,6 +517,7 @@ func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, e
// AssertDeleteResponse request a URI and expect a statusCode and body text // AssertDeleteResponse request a URI and expect a statusCode and body text
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) { func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
req, err := http.NewRequest("DELETE", requestURI, nil) req, err := http.NewRequest("DELETE", requestURI, nil)
if err != nil { if err != nil {
tc.t.Fatalf("unable to create request %s", err) tc.t.Fatalf("unable to create request %s", err)
@@ -516,6 +528,7 @@ func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int
// AssertPostResponseBody POST to a URI and assert the response code and body // 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) { 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) req, err := http.NewRequest("POST", requestURI, requestBody)
if err != nil { if err != nil {
tc.t.Errorf("failed to create request %s", err) tc.t.Errorf("failed to create request %s", err)
@@ -529,6 +542,7 @@ func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []str
// AssertPutResponseBody PUT to a URI and assert the response code and body // 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) { 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) req, err := http.NewRequest("PUT", requestURI, requestBody)
if err != nil { if err != nil {
tc.t.Errorf("failed to create request %s", err) tc.t.Errorf("failed to create request %s", err)
@@ -542,6 +556,7 @@ func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []stri
// AssertPatchResponseBody PATCH to a URI and assert the response code and body // 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) { 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) req, err := http.NewRequest("PATCH", requestURI, requestBody)
if err != nil { if err != nil {
tc.t.Errorf("failed to create request %s", err) tc.t.Errorf("failed to create request %s", err)
-33
View File
@@ -1,33 +0,0 @@
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"}
`)
}
@@ -1,37 +0,0 @@
:8443 {
tls internal {
on_demand
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8443"
],
"tls_connection_policies": [
{}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"issuers": [
{
"module": "internal"
}
],
"on_demand": true
}
]
}
}
}
}
@@ -11,7 +11,6 @@ encode gzip zstd {
header Content-Type application/xhtml+xml* header Content-Type application/xhtml+xml*
header Content-Type application/atom+xml* header Content-Type application/atom+xml*
header Content-Type application/rss+xml* header Content-Type application/rss+xml*
header Content-Type application/wasm*
header Content-Type image/svg+xml* header Content-Type image/svg+xml*
} }
} }
@@ -48,7 +47,6 @@ encode {
"application/xhtml+xml*", "application/xhtml+xml*",
"application/atom+xml*", "application/atom+xml*",
"application/rss+xml*", "application/rss+xml*",
"application/wasm*",
"image/svg+xml*" "image/svg+xml*"
] ]
}, },
@@ -69,11 +69,11 @@
} }
], ],
"on_demand": { "on_demand": {
"ask": "https://example.com",
"rate_limit": { "rate_limit": {
"interval": 30000000000, "interval": 30000000000,
"burst": 20 "burst": 20
} },
"ask": "https://example.com"
} }
}, },
"disable_ocsp_stapling": true "disable_ocsp_stapling": true
@@ -78,11 +78,11 @@
} }
], ],
"on_demand": { "on_demand": {
"ask": "https://example.com",
"rate_limit": { "rate_limit": {
"interval": 30000000000, "interval": 30000000000,
"burst": 20 "burst": 20
} },
"ask": "https://example.com"
}, },
"ocsp_interval": 172800000000000, "ocsp_interval": 172800000000000,
"renew_interval": 86400000000000, "renew_interval": 86400000000000,
@@ -71,11 +71,11 @@
} }
], ],
"on_demand": { "on_demand": {
"ask": "https://example.com",
"rate_limit": { "rate_limit": {
"interval": 30000000000, "interval": 30000000000,
"burst": 20 "burst": 20
} },
"ask": "https://example.com"
} }
} }
} }
@@ -11,7 +11,6 @@
idle 30s idle 30s
} }
max_header_size 100MB max_header_size 100MB
enable_full_duplex
log_credentials log_credentials
protocols h1 h2 h2c h3 protocols h1 h2 h2c h3
strict_sni_host strict_sni_host
@@ -46,7 +45,6 @@ foo.com {
"write_timeout": 30000000000, "write_timeout": 30000000000,
"idle_timeout": 30000000000, "idle_timeout": 30000000000,
"max_header_bytes": 100000000, "max_header_bytes": 100000000,
"enable_full_duplex": true,
"routes": [ "routes": [
{ {
"match": [ "match": [
@@ -17,8 +17,6 @@
+Link "Foo" +Link "Foo"
+Link "Bar" +Link "Bar"
} }
header >Set Defer
header >Replace Deferred Replacement
} }
---------- ----------
{ {
@@ -138,31 +136,6 @@
] ]
} }
} }
},
{
"handler": "headers",
"response": {
"deferred": true,
"set": {
"Set": [
"Defer"
]
}
}
},
{
"handler": "headers",
"response": {
"deferred": true,
"replace": {
"Replace": [
{
"replace": "Replacement",
"search_regexp": "Deferred"
}
]
}
}
} }
] ]
} }
@@ -1,154 +0,0 @@
&(first) {
@first path /first
vars @first first 1
respond "first"
}
&(second) {
respond "second"
}
:8881 {
invoke first
route {
invoke second
}
}
:8882 {
handle {
invoke second
}
}
:8883 {
respond "no invoke"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8881"
],
"routes": [
{
"handle": [
{
"handler": "invoke",
"name": "first"
},
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "invoke",
"name": "second"
}
]
}
]
}
]
}
],
"named_routes": {
"first": {
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"first": 1,
"handler": "vars"
}
],
"match": [
{
"path": [
"/first"
]
}
]
},
{
"handle": [
{
"body": "first",
"handler": "static_response"
}
]
}
]
}
]
},
"second": {
"handle": [
{
"body": "second",
"handler": "static_response"
}
]
}
}
},
"srv1": {
"listen": [
":8882"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "invoke",
"name": "second"
}
]
}
]
}
]
}
],
"named_routes": {
"second": {
"handle": [
{
"body": "second",
"handler": "static_response"
}
]
}
}
},
"srv2": {
"listen": [
":8883"
],
"routes": [
{
"handle": [
{
"body": "no invoke",
"handler": "static_response"
}
]
}
]
}
}
}
}
}
@@ -99,7 +99,7 @@ http://localhost:2020 {
}, },
"logs": { "logs": {
"logger_names": { "logger_names": {
"localhost": "" "localhost:2020": ""
}, },
"skip_unmapped_hosts": true "skip_unmapped_hosts": true
} }
@@ -1,109 +0,0 @@
*.example.com {
log {
hostnames foo.example.com bar.example.com
output file /foo-bar.txt
}
log {
hostnames baz.example.com
output file /baz.txt
}
}
example.com:8443 {
log {
output file /port.txt
}
}
----------
{
"logging": {
"logs": {
"default": {
"exclude": [
"http.log.access.log0",
"http.log.access.log1",
"http.log.access.log2"
]
},
"log0": {
"writer": {
"filename": "/foo-bar.txt",
"output": "file"
},
"include": [
"http.log.access.log0"
]
},
"log1": {
"writer": {
"filename": "/baz.txt",
"output": "file"
},
"include": [
"http.log.access.log1"
]
},
"log2": {
"writer": {
"filename": "/port.txt",
"output": "file"
},
"include": [
"http.log.access.log2"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"*.example.com"
]
}
],
"terminal": true
}
],
"logs": {
"logger_names": {
"bar.example.com": "log0",
"baz.example.com": "log1",
"foo.example.com": "log0"
}
}
},
"srv1": {
"listen": [
":8443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
],
"logs": {
"logger_names": {
"example.com": "log2"
}
}
}
}
}
}
}
@@ -1,86 +0,0 @@
{
log access-console {
include http.log.access.foo
output file access-localhost.log
format console
}
log access-json {
include http.log.access.foo
output file access-localhost.json
format json
}
}
http://localhost:8881 {
log foo
}
----------
{
"logging": {
"logs": {
"access-console": {
"writer": {
"filename": "access-localhost.log",
"output": "file"
},
"encoder": {
"format": "console"
},
"include": [
"http.log.access.foo"
]
},
"access-json": {
"writer": {
"filename": "access-localhost.json",
"output": "file"
},
"encoder": {
"format": "json"
},
"include": [
"http.log.access.foo"
]
},
"default": {
"exclude": [
"http.log.access.foo"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8881"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"localhost"
]
},
"logs": {
"logger_names": {
"localhost": "foo"
}
}
}
}
}
}
}
@@ -1,91 +0,0 @@
{
debug
log access-console {
include http.log.access.foo
output file access-localhost.log
format console
}
log access-json {
include http.log.access.foo
output file access-localhost.json
format json
}
}
http://localhost:8881 {
log foo
}
----------
{
"logging": {
"logs": {
"access-console": {
"writer": {
"filename": "access-localhost.log",
"output": "file"
},
"encoder": {
"format": "console"
},
"level": "DEBUG",
"include": [
"http.log.access.foo"
]
},
"access-json": {
"writer": {
"filename": "access-localhost.json",
"output": "file"
},
"encoder": {
"format": "json"
},
"level": "DEBUG",
"include": [
"http.log.access.foo"
]
},
"default": {
"level": "DEBUG",
"exclude": [
"http.log.access.foo"
]
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8881"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip": [
"localhost"
]
},
"logs": {
"logger_names": {
"localhost": "foo"
}
}
}
}
}
}
}
@@ -1,100 +0,0 @@
*.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
}
]
}
}
}
}
}
@@ -1,100 +0,0 @@
*.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
}
]
}
}
}
}
}
@@ -1,100 +0,0 @@
*.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
}
]
}
}
}
}
}
@@ -1,58 +0,0 @@
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,9 +6,6 @@ reverse_proxy 127.0.0.1:65535 {
X-Header-Key 95ca39e3cbe7 X-Header-Key 95ca39e3cbe7
X-Header-Keys VbG4NZwWnipo 335Q9/MhqcNU3s2TO X-Header-Keys VbG4NZwWnipo 335Q9/MhqcNU3s2TO
X-Empty-Value X-Empty-Value
Same-Key 1
Same-Key 2
X-System-Hostname {system.hostname}
} }
health_uri /health health_uri /health
} }
@@ -32,10 +29,6 @@ reverse_proxy 127.0.0.1:65535 {
"Host": [ "Host": [
"example.com" "example.com"
], ],
"Same-Key": [
"1",
"2"
],
"X-Empty-Value": [ "X-Empty-Value": [
"" ""
], ],
@@ -45,9 +38,6 @@ reverse_proxy 127.0.0.1:65535 {
"X-Header-Keys": [ "X-Header-Keys": [
"VbG4NZwWnipo", "VbG4NZwWnipo",
"335Q9/MhqcNU3s2TO" "335Q9/MhqcNU3s2TO"
],
"X-System-Hostname": [
"{system.hostname}"
] ]
}, },
"uri": "/health" "uri": "/health"
@@ -1,71 +0,0 @@
:8884
reverse_proxy 127.0.0.1:65535 127.0.0.1:35535 {
lb_policy weighted_round_robin 10 1
lb_retries 5
lb_try_duration 10s
lb_try_interval 500ms
lb_retry_match {
path /foo*
method POST
}
lb_retry_match path /bar*
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"load_balancing": {
"retries": 5,
"retry_match": [
{
"method": [
"POST"
],
"path": [
"/foo*"
]
},
{
"path": [
"/bar*"
]
}
],
"selection_policy": {
"policy": "weighted_round_robin",
"weights": [
10,
1
]
},
"try_duration": 10000000000,
"try_interval": 500000000
},
"upstreams": [
{
"dial": "127.0.0.1:65535"
},
{
"dial": "127.0.0.1:35535"
}
]
}
]
}
]
}
}
}
}
}
@@ -1,67 +0,0 @@
:8884 {
# Port range
reverse_proxy localhost:8001-8002
# Port range with placeholder
reverse_proxy {host}:8001-8002
# Port range with scheme
reverse_proxy https://localhost:8001-8002
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "localhost:8001"
},
{
"dial": "localhost:8002"
}
]
},
{
"handler": "reverse_proxy",
"upstreams": [
{
"dial": "{http.request.host}:8001"
},
{
"dial": "{http.request.host}:8002"
}
]
},
{
"handler": "reverse_proxy",
"transport": {
"protocol": "http",
"tls": {}
},
"upstreams": [
{
"dial": "localhost:8001"
},
{
"dial": "localhost:8002"
}
]
}
]
}
]
}
}
}
}
}
@@ -1,67 +0,0 @@
# 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"
]
}
]
}
}
}
}
-349
View File
@@ -135,352 +135,3 @@ func TestReplIndex(t *testing.T) {
// act and assert // act and assert
tester.AssertGetResponse("http://localhost:9080/", 200, "") 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)
}
}
-94
View File
@@ -1,94 +0,0 @@
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")
}
}
+3 -1
View File
@@ -176,7 +176,9 @@ func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
w.Header().Set("Cache-Control", "no-store") w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(200) w.WriteHeader(200)
http.NewResponseController(w).Flush() if f, ok := w.(http.Flusher); ok {
f.Flush()
}
buf := make([]byte, 4*1024) buf := make([]byte, 4*1024)
-22
View File
@@ -1,22 +0,0 @@
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)
}
+1 -29
View File
@@ -1,11 +1,7 @@
package caddycmd package caddycmd
import ( import (
"fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/caddyserver/caddy/v2"
) )
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
@@ -99,22 +95,15 @@ https://caddyserver.com/docs/running
// kind of annoying to have all the help text printed out if // kind of annoying to have all the help text printed out if
// caddy has an error provisioning its modules, for instance... // caddy has an error provisioning its modules, for instance...
SilenceUsage: true, SilenceUsage: true,
Version: onlyVersionText(),
} }
const fullDocsFooter = `Full documentation is available at: const fullDocsFooter = `Full documentation is available at:
https://caddyserver.com/docs/command-line` https://caddyserver.com/docs/command-line`
func init() { func init() {
rootCmd.SetVersionTemplate("{{.Version}}\n")
rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n") rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n")
} }
func onlyVersionText() string {
_, f := caddy.Version()
return f
}
func caddyCmdToCobra(caddyCmd Command) *cobra.Command { func caddyCmdToCobra(caddyCmd Command) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: caddyCmd.Name, Use: caddyCmd.Name,
@@ -134,24 +123,7 @@ func caddyCmdToCobra(caddyCmd Command) *cobra.Command {
// in a cobra command's RunE field. // in a cobra command's RunE field.
func WrapCommandFuncForCobra(f CommandFunc) func(cmd *cobra.Command, _ []string) error { func WrapCommandFuncForCobra(f CommandFunc) func(cmd *cobra.Command, _ []string) error {
return func(cmd *cobra.Command, _ []string) error { return func(cmd *cobra.Command, _ []string) error {
status, err := f(Flags{cmd.Flags()}) _, err := f(Flags{cmd.Flags()})
if status > 1 {
cmd.SilenceErrors = true
return &exitError{ExitCode: status, Err: err}
}
return 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()
}
+110 -184
View File
@@ -22,7 +22,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs"
"log" "log"
"net" "net"
"net/http" "net/http"
@@ -33,27 +32,18 @@ import (
"strings" "strings"
"github.com/aryann/difflib" "github.com/aryann/difflib"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/internal" "go.uber.org/zap"
) )
func cmdStart(fl Flags) (int, error) { func cmdStart(fl Flags) (int, error) {
configFlag := fl.String("config") startCmdConfigFlag := fl.String("config")
configAdapterFlag := fl.String("adapter") startCmdConfigAdapterFlag := fl.String("adapter")
pidfileFlag := fl.String("pidfile") startCmdPidfileFlag := fl.String("pidfile")
watchFlag := fl.Bool("watch") startCmdWatchFlag := fl.Bool("watch")
startCmdEnvfileFlag := fl.String("envfile")
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 // open a listener to which the child process will connect when
// it is ready to confirm that it has successfully started // it is ready to confirm that it has successfully started
@@ -74,23 +64,22 @@ func cmdStart(fl Flags) (int, error) {
// sure by giving it some random bytes and having it echo // sure by giving it some random bytes and having it echo
// them back to us) // them back to us)
cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String()) cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String())
if configFlag != "" { if startCmdConfigFlag != "" {
cmd.Args = append(cmd.Args, "--config", configFlag) cmd.Args = append(cmd.Args, "--config", startCmdConfigFlag)
} }
if startCmdEnvfileFlag != "" {
for _, envfile := range envfileFlag { cmd.Args = append(cmd.Args, "--envfile", startCmdEnvfileFlag)
cmd.Args = append(cmd.Args, "--envfile", envfile)
} }
if configAdapterFlag != "" { if startCmdConfigAdapterFlag != "" {
cmd.Args = append(cmd.Args, "--adapter", configAdapterFlag) cmd.Args = append(cmd.Args, "--adapter", startCmdConfigAdapterFlag)
} }
if watchFlag { if startCmdWatchFlag {
cmd.Args = append(cmd.Args, "--watch") cmd.Args = append(cmd.Args, "--watch")
} }
if pidfileFlag != "" { if startCmdPidfileFlag != "" {
cmd.Args = append(cmd.Args, "--pidfile", pidfileFlag) cmd.Args = append(cmd.Args, "--pidfile", startCmdPidfileFlag)
} }
stdinPipe, err := cmd.StdinPipe() stdinpipe, err := cmd.StdinPipe()
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
fmt.Errorf("creating stdin pipe: %v", err) fmt.Errorf("creating stdin pipe: %v", err)
@@ -102,8 +91,7 @@ func cmdStart(fl Flags) (int, error) {
expect := make([]byte, 32) expect := make([]byte, 32)
_, err = rand.Read(expect) _, err = rand.Read(expect)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup, fmt.Errorf("generating random confirmation bytes: %v", err)
fmt.Errorf("generating random confirmation bytes: %v", err)
} }
// begin writing the confirmation bytes to the child's // begin writing the confirmation bytes to the child's
@@ -111,15 +99,14 @@ func cmdStart(fl Flags) (int, error) {
// started yet, and writing synchronously would result // started yet, and writing synchronously would result
// in a deadlock // in a deadlock
go func() { go func() {
_, _ = stdinPipe.Write(expect) _, _ = stdinpipe.Write(expect)
stdinPipe.Close() stdinpipe.Close()
}() }()
// start the process // start the process
err = cmd.Start() err = cmd.Start()
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup, fmt.Errorf("starting caddy process: %v", err)
fmt.Errorf("starting caddy process: %v", err)
} }
// there are two ways we know we're done: either // there are two ways we know we're done: either
@@ -167,37 +154,41 @@ func cmdStart(fl Flags) (int, error) {
func cmdRun(fl Flags) (int, error) { func cmdRun(fl Flags) (int, error) {
caddy.TrapSignals() caddy.TrapSignals()
configFlag := fl.String("config") runCmdConfigFlag := fl.String("config")
configAdapterFlag := fl.String("adapter") runCmdConfigAdapterFlag := fl.String("adapter")
resumeFlag := fl.Bool("resume") runCmdResumeFlag := fl.Bool("resume")
printEnvFlag := fl.Bool("environ") runCmdLoadEnvfileFlag := fl.String("envfile")
watchFlag := fl.Bool("watch") runCmdPrintEnvFlag := fl.Bool("environ")
pidfileFlag := fl.String("pidfile") runCmdWatchFlag := fl.Bool("watch")
pingbackFlag := fl.String("pingback") runCmdPidfileFlag := fl.String("pidfile")
runCmdPingbackFlag := fl.String("pingback")
// load all additional envs as soon as possible // load all additional envs as soon as possible
err := handleEnvFileFlag(fl) if runCmdLoadEnvfileFlag != "" {
if err != nil { if err := loadEnvFromFile(runCmdLoadEnvfileFlag); err != nil {
return caddy.ExitCodeFailedStartup, err return caddy.ExitCodeFailedStartup,
fmt.Errorf("loading additional environment variables: %v", err)
}
} }
// if we are supposed to print the environment, do that first // if we are supposed to print the environment, do that first
if printEnvFlag { if runCmdPrintEnvFlag {
printEnvironment() printEnvironment()
} }
// load the config, depending on flags // load the config, depending on flags
var config []byte var config []byte
if resumeFlag { var err error
if runCmdResumeFlag {
config, err = os.ReadFile(caddy.ConfigAutosavePath) config, err = os.ReadFile(caddy.ConfigAutosavePath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
// not a bad error; just can't resume if autosave file doesn't exist // 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)) caddy.Log().Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath))
resumeFlag = false runCmdResumeFlag = false
} else if err != nil { } else if err != nil {
return caddy.ExitCodeFailedStartup, err return caddy.ExitCodeFailedStartup, err
} else { } else {
if configFlag == "" { if runCmdConfigFlag == "" {
caddy.Log().Info("resuming from last configuration", caddy.Log().Info("resuming from last configuration",
zap.String("autosave_file", caddy.ConfigAutosavePath)) zap.String("autosave_file", caddy.ConfigAutosavePath))
} else { } else {
@@ -210,19 +201,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 // we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
var configFile string var configFile string
if !resumeFlag { if !runCmdResumeFlag {
config, configFile, err = LoadConfig(configFlag, configAdapterFlag) config, configFile, err = LoadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, err return caddy.ExitCodeFailedStartup, err
} }
} }
// create pidfile now, in case loading config takes a while (issue #5477) // create pidfile now, in case loading config takes a while (issue #5477)
if pidfileFlag != "" { if runCmdPidfileFlag != "" {
err := caddy.PIDFile(pidfileFlag) err := caddy.PIDFile(runCmdPidfileFlag)
if err != nil { if err != nil {
caddy.Log().Error("unable to write PID file", caddy.Log().Error("unable to write PID file",
zap.String("pidfile", pidfileFlag), zap.String("pidfile", runCmdPidfileFlag),
zap.Error(err)) zap.Error(err))
} }
} }
@@ -236,13 +227,13 @@ func cmdRun(fl Flags) (int, error) {
// if we are to report to another process the successful start // if we are to report to another process the successful start
// of the server, do so now by echoing back contents of stdin // of the server, do so now by echoing back contents of stdin
if pingbackFlag != "" { if runCmdPingbackFlag != "" {
confirmationBytes, err := io.ReadAll(os.Stdin) confirmationBytes, err := io.ReadAll(os.Stdin)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading confirmation bytes from stdin: %v", err) fmt.Errorf("reading confirmation bytes from stdin: %v", err)
} }
conn, err := net.Dial("tcp", pingbackFlag) conn, err := net.Dial("tcp", runCmdPingbackFlag)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
fmt.Errorf("dialing confirmation address: %v", err) fmt.Errorf("dialing confirmation address: %v", err)
@@ -251,14 +242,14 @@ func cmdRun(fl Flags) (int, error) {
_, err = conn.Write(confirmationBytes) _, err = conn.Write(confirmationBytes)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
fmt.Errorf("writing confirmation bytes to %s: %v", pingbackFlag, err) fmt.Errorf("writing confirmation bytes to %s: %v", runCmdPingbackFlag, err)
} }
} }
// if enabled, reload config file automatically on changes // if enabled, reload config file automatically on changes
// (this better only be used in dev!) // (this better only be used in dev!)
if watchFlag { if runCmdWatchFlag {
go watchConfigFile(configFile, configAdapterFlag) go watchConfigFile(configFile, runCmdConfigAdapterFlag)
} }
// warn if the environment does not provide enough information about the disk // warn if the environment does not provide enough information about the disk
@@ -284,11 +275,11 @@ func cmdRun(fl Flags) (int, error) {
} }
func cmdStop(fl Flags) (int, error) { func cmdStop(fl Flags) (int, error) {
addressFlag := fl.String("address") addrFlag := fl.String("address")
configFlag := fl.String("config") configFlag := fl.String("config")
configAdapterFlag := fl.String("adapter") configAdapterFlag := fl.String("adapter")
adminAddr, err := DetermineAdminAPIAddress(addressFlag, nil, configFlag, configAdapterFlag) adminAddr, err := DetermineAdminAPIAddress(addrFlag, nil, configFlag, configAdapterFlag)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err) return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
} }
@@ -306,7 +297,7 @@ func cmdStop(fl Flags) (int, error) {
func cmdReload(fl Flags) (int, error) { func cmdReload(fl Flags) (int, error) {
configFlag := fl.String("config") configFlag := fl.String("config")
configAdapterFlag := fl.String("adapter") configAdapterFlag := fl.String("adapter")
addressFlag := fl.String("address") addrFlag := fl.String("address")
forceFlag := fl.Bool("force") forceFlag := fl.Bool("force")
// get the config in caddy's native format // get the config in caddy's native format
@@ -318,7 +309,7 @@ func cmdReload(fl Flags) (int, error) {
return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load") return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
} }
adminAddr, err := DetermineAdminAPIAddress(addressFlag, config, configFlag, configAdapterFlag) adminAddr, err := DetermineAdminAPIAddress(addrFlag, config, configFlag, configAdapterFlag)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err) return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
} }
@@ -420,60 +411,60 @@ func cmdListModules(fl Flags) (int, error) {
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
func cmdEnviron(fl Flags) (int, error) { func cmdEnviron(_ Flags) (int, error) {
// load all additional envs as soon as possible
err := handleEnvFileFlag(fl)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
printEnvironment() printEnvironment()
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
func cmdAdaptConfig(fl Flags) (int, error) { func cmdAdaptConfig(fl Flags) (int, error) {
inputFlag := fl.String("config") adaptCmdInputFlag := fl.String("config")
adapterFlag := fl.String("adapter") adaptCmdAdapterFlag := fl.String("adapter")
prettyFlag := fl.Bool("pretty") adaptCmdPrettyFlag := fl.Bool("pretty")
validateFlag := fl.Bool("validate") adaptCmdValidateFlag := fl.Bool("validate")
var err error // if no input file was specified, try a default
inputFlag, err = configFileWithRespectToDefault(caddy.Log(), inputFlag) // Caddyfile if the Caddyfile adapter is plugged in
if err != nil { if adaptCmdInputFlag == "" && caddyconfig.GetAdapter("caddyfile") != nil {
return caddy.ExitCodeFailedStartup, err _, 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)
}
} }
// load all additional envs as soon as possible if adaptCmdInputFlag == "" {
err = handleEnvFileFlag(fl) return caddy.ExitCodeFailedStartup,
if err != nil { fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
return caddy.ExitCodeFailedStartup, err
} }
if adaptCmdAdapterFlag == "" {
if adapterFlag == "" {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
fmt.Errorf("adapter name is required (use --adapt flag or leave unspecified for default)") fmt.Errorf("adapter name is required (use --adapt flag or leave unspecified for default)")
} }
cfgAdapter := caddyconfig.GetAdapter(adapterFlag) cfgAdapter := caddyconfig.GetAdapter(adaptCmdAdapterFlag)
if cfgAdapter == nil { if cfgAdapter == nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
fmt.Errorf("unrecognized config adapter: %s", adapterFlag) fmt.Errorf("unrecognized config adapter: %s", adaptCmdAdapterFlag)
} }
input, err := os.ReadFile(inputFlag) input, err := os.ReadFile(adaptCmdInputFlag)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading input file: %v", err) fmt.Errorf("reading input file: %v", err)
} }
opts := map[string]any{"filename": inputFlag} opts := map[string]any{"filename": adaptCmdInputFlag}
adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts) adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, err return caddy.ExitCodeFailedStartup, err
} }
if prettyFlag { if adaptCmdPrettyFlag {
var prettyBuf bytes.Buffer var prettyBuf bytes.Buffer
err = json.Indent(&prettyBuf, adaptedConfig, "", "\t") err = json.Indent(&prettyBuf, adaptedConfig, "", "\t")
if err != nil { if err != nil {
@@ -491,13 +482,13 @@ func cmdAdaptConfig(fl Flags) (int, error) {
if warn.Directive != "" { if warn.Directive != "" {
msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message) msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
} }
caddy.Log().Named(adapterFlag).Warn(msg, caddy.Log().Named(adaptCmdAdapterFlag).Warn(msg,
zap.String("file", warn.File), zap.String("file", warn.File),
zap.Int("line", warn.Line)) zap.Int("line", warn.Line))
} }
// validate output if requested // validate output if requested
if validateFlag { if adaptCmdValidateFlag {
var cfg *caddy.Config var cfg *caddy.Config
err = caddy.StrictUnmarshalJSON(adaptedConfig, &cfg) err = caddy.StrictUnmarshalJSON(adaptedConfig, &cfg)
if err != nil { if err != nil {
@@ -513,26 +504,19 @@ func cmdAdaptConfig(fl Flags) (int, error) {
} }
func cmdValidateConfig(fl Flags) (int, error) { func cmdValidateConfig(fl Flags) (int, error) {
configFlag := fl.String("config") validateCmdConfigFlag := fl.String("config")
adapterFlag := fl.String("adapter") validateCmdAdapterFlag := fl.String("adapter")
runCmdLoadEnvfileFlag := fl.String("envfile")
// load all additional envs as soon as possible // load all additional envs as soon as possible
err := handleEnvFileFlag(fl) if runCmdLoadEnvfileFlag != "" {
if err != nil { if err := loadEnvFromFile(runCmdLoadEnvfileFlag); err != nil {
return caddy.ExitCodeFailedStartup, err return caddy.ExitCodeFailedStartup,
fmt.Errorf("loading additional environment variables: %v", err)
}
} }
// use default config and ensure a config file is specified input, _, err := LoadConfig(validateCmdConfigFlag, validateCmdAdapterFlag)
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 { if err != nil {
return caddy.ExitCodeFailedStartup, err return caddy.ExitCodeFailedStartup, err
} }
@@ -555,13 +539,13 @@ func cmdValidateConfig(fl Flags) (int, error) {
} }
func cmdFmt(fl Flags) (int, error) { func cmdFmt(fl Flags) (int, error) {
configFile := fl.Arg(0) formatCmdConfigFile := fl.Arg(0)
if configFile == "" { if formatCmdConfigFile == "" {
configFile = "Caddyfile" formatCmdConfigFile = "Caddyfile"
} }
// as a special case, read from stdin if the file name is "-" // as a special case, read from stdin if the file name is "-"
if configFile == "-" { if formatCmdConfigFile == "-" {
input, err := io.ReadAll(os.Stdin) input, err := io.ReadAll(os.Stdin)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
@@ -571,7 +555,7 @@ func cmdFmt(fl Flags) (int, error) {
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
input, err := os.ReadFile(configFile) input, err := os.ReadFile(formatCmdConfigFile)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
fmt.Errorf("reading input file: %v", err) fmt.Errorf("reading input file: %v", err)
@@ -580,7 +564,7 @@ func cmdFmt(fl Flags) (int, error) {
output := caddyfile.Format(input) output := caddyfile.Format(input)
if fl.Bool("overwrite") { if fl.Bool("overwrite") {
if err := os.WriteFile(configFile, output, 0o600); err != nil { if err := os.WriteFile(formatCmdConfigFile, output, 0600); err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err) return caddy.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err)
} }
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
@@ -604,7 +588,7 @@ func cmdFmt(fl Flags) (int, error) {
fmt.Print(string(output)) fmt.Print(string(output))
} }
if warning, diff := caddyfile.FormattingDifference(configFile, input); diff { if warning, diff := caddyfile.FormattingDifference(formatCmdConfigFile, 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`, 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.File,
warning.Line, warning.Line,
@@ -614,25 +598,6 @@ func cmdFmt(fl Flags) (int, error) {
return caddy.ExitCodeSuccess, nil 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, // 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 // 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 // be assumed to be Content-Type application/json. The caller should close
@@ -645,17 +610,7 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
} }
origin := "http://" + parsedAddr.JoinHostPort(0) origin := "http://" + parsedAddr.JoinHostPort(0)
if parsedAddr.IsUnixNetwork() { if parsedAddr.IsUnixNetwork() {
origin = "http://127.0.0.1" // bogus host is a hack so that http.NewRequest() is happy origin = "http://unixsocket" // 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 // form the request
@@ -664,24 +619,20 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
return nil, fmt.Errorf("making request: %v", err) return nil, fmt.Errorf("making request: %v", err)
} }
if parsedAddr.IsUnixNetwork() { if parsedAddr.IsUnixNetwork() {
// We used to conform to RFC 2616 Section 14.26 which requires // When listening on a unix socket, the admin endpoint doesn't
// an empty host header when there is no host, as is the case // accept any Host header because there is no host:port for
// with unix sockets. However, Go required a Host value so we // a unix socket's address. The server's host check is fairly
// used a hack of a space character as the host (it would see // strict for security reasons, so we don't allow just any
// the Host was non-empty, then trim the space later). As of // Host header. For unix sockets, the Host header must be
// Go 1.20.6 (July 2023), this hack no longer works. See: // empty. Unfortunately, Go makes it impossible to make HTTP
// https://github.com/golang/go/issues/60374 // requests with an empty Host header... except with this one
// See also the discussion here: // weird trick. (Hopefully they don't fix it. It's already
// https://github.com/golang/go/issues/61431 // hard enough to use HTTP over unix sockets.)
// //
// After that, we now require a Host value of either 127.0.0.1 // An equivalent curl command would be something like:
// or ::1 if one is set. Above I choose to use 127.0.0.1. Even // $ curl --unix-socket caddy.sock http:/:$REQUEST_URI
// though the value should be completely irrelevant (it could be req.URL.Host = " "
// "srldkjfsd"), if for some reason the Host *is* used, at least req.Host = ""
// we can have some reasonable assurance it will stay on the local
// machine and that browsers, if they ever allow access to unix
// sockets, can still enforce CORS, ensuring it is still coming
// from the local machine.
} else { } else {
req.Header.Set("Origin", origin) req.Header.Set("Origin", origin)
} }
@@ -770,31 +721,6 @@ func DetermineAdminAPIAddress(address string, config []byte, configFile, configA
return caddy.DefaultAdminListen, nil 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 { type moduleInfo struct {
caddyModuleID string caddyModuleID string
goModule *debug.Module goModule *debug.Module
+30 -92
View File
@@ -21,10 +21,9 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/caddyserver/caddy/v2"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/cobra/doc" "github.com/spf13/cobra/doc"
"github.com/caddyserver/caddy/v2"
) )
// Command represents a subcommand. Name, Func, // Command represents a subcommand. Name, Func,
@@ -94,8 +93,8 @@ func init() {
Starts the Caddy process, optionally bootstrapped with an initial config file. Starts the Caddy process, optionally bootstrapped with an initial config file.
This command unblocks after the server starts running or fails to run. This command unblocks after the server starts running or fails to run.
If --envfile is specified, an environment file with environment variables If --envfile is specified, an environment file with environment variables in
in the KEY=VALUE format will be loaded into the Caddy process. the KEY=VALUE format will be loaded into the Caddy process.
On Windows, the spawned child process will remain attached to the terminal, so 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 closing the window will forcefully stop Caddy; to avoid forgetting this, try
@@ -104,7 +103,7 @@ using 'caddy run' instead to keep it in the foreground.
CobraFunc: func(cmd *cobra.Command) { CobraFunc: func(cmd *cobra.Command) {
cmd.Flags().StringP("config", "c", "", "Configuration file") cmd.Flags().StringP("config", "c", "", "Configuration file")
cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply") cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply")
cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load") cmd.Flags().StringP("envfile", "", "", "Environment file to load")
cmd.Flags().BoolP("watch", "w", false, "Reload changed config file automatically") 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.Flags().StringP("pidfile", "", "", "Path of file to which to write process ID")
cmd.RunE = WrapCommandFuncForCobra(cmdStart) cmd.RunE = WrapCommandFuncForCobra(cmdStart)
@@ -133,8 +132,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 that file will be loaded and used to configure Caddy, even without any command
line flags. line flags.
If --envfile is specified, an environment file with environment variables If --envfile is specified, an environment file with environment variables in
in the KEY=VALUE format will be loaded into the Caddy process. the KEY=VALUE format will be loaded into the Caddy process.
If --environ is specified, the environment as seen by the Caddy process will 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 be printed before starting. This is the same as the environ command but does
@@ -150,7 +149,7 @@ option in a local development environment.
CobraFunc: func(cmd *cobra.Command) { CobraFunc: func(cmd *cobra.Command) {
cmd.Flags().StringP("config", "c", "", "Configuration file") cmd.Flags().StringP("config", "c", "", "Configuration file")
cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply") cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply")
cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load") cmd.Flags().StringP("envfile", "", "", "Environment file to load")
cmd.Flags().BoolP("environ", "e", false, "Print environment") 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("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") cmd.Flags().BoolP("watch", "w", false, "Watch config file for changes and reload it automatically")
@@ -240,7 +239,6 @@ documentation: https://go.dev/doc/modules/version-numbers
RegisterCommand(Command{ RegisterCommand(Command{
Name: "environ", Name: "environ",
Usage: "[--envfile <path>]",
Short: "Prints the environment", Short: "Prints the environment",
Long: ` Long: `
Prints the environment as seen by this Caddy process. Prints the environment as seen by this Caddy process.
@@ -250,9 +248,6 @@ configuration uses environment variables (e.g. "{env.VARIABLE}") then
this command can be useful for verifying that the variables will have this command can be useful for verifying that the variables will have
the values you expect in your config. 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. Note that environments may be different depending on how you run Caddy.
Environments for Caddy instances started by service managers such as Environments for Caddy instances started by service managers such as
systemd are often different than the environment inherited from your systemd are often different than the environment inherited from your
@@ -263,15 +258,12 @@ by adding the "--environ" flag.
Environments may contain sensitive data. Environments may contain sensitive data.
`, `,
CobraFunc: func(cmd *cobra.Command) { Func: cmdEnviron,
cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
cmd.RunE = WrapCommandFuncForCobra(cmdEnviron)
},
}) })
RegisterCommand(Command{ RegisterCommand(Command{
Name: "adapt", Name: "adapt",
Usage: "--config <path> [--adapter <name>] [--pretty] [--validate] [--envfile <path>]", Usage: "--config <path> [--adapter <name>] [--pretty] [--validate]",
Short: "Adapts a configuration to Caddy's native JSON", Short: "Adapts a configuration to Caddy's native JSON",
Long: ` Long: `
Adapts a configuration to Caddy's native JSON format and writes the Adapts a configuration to Caddy's native JSON format and writes the
@@ -283,16 +275,12 @@ for human readability.
If --validate is used, the adapted config will be checked for validity. 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- If the config is invalid, an error will be printed to stderr and a non-
zero exit status will be returned. 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) { CobraFunc: func(cmd *cobra.Command) {
cmd.Flags().StringP("config", "c", "", "Configuration file to adapt (required)") cmd.Flags().StringP("config", "c", "", "Configuration file to adapt (required)")
cmd.Flags().StringP("adapter", "a", "caddyfile", "Name of config adapter") 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("pretty", "p", false, "Format the output for human readability")
cmd.Flags().BoolP("validate", "", false, "Validate the output") cmd.Flags().BoolP("validate", "", false, "Validate the output")
cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
cmd.RunE = WrapCommandFuncForCobra(cmdAdaptConfig) cmd.RunE = WrapCommandFuncForCobra(cmdAdaptConfig)
}, },
}) })
@@ -306,67 +294,17 @@ Loads and provisions the provided config, but does not start running it.
This reveals any errors with the configuration through the loading and This reveals any errors with the configuration through the loading and
provisioning stages. provisioning stages.
If --envfile is specified, an environment file with environment variables If --envfile is specified, an environment file with environment variables in
in the KEY=VALUE format will be loaded into the Caddy process. the KEY=VALUE format will be loaded into the Caddy process.
`, `,
CobraFunc: func(cmd *cobra.Command) { CobraFunc: func(cmd *cobra.Command) {
cmd.Flags().StringP("config", "c", "", "Input configuration file") cmd.Flags().StringP("config", "c", "", "Input configuration file")
cmd.Flags().StringP("adapter", "a", "", "Name of config adapter") cmd.Flags().StringP("adapter", "a", "", "Name of config adapter")
cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load") cmd.Flags().StringP("envfile", "", "", "Environment file to load")
cmd.RunE = WrapCommandFuncForCobra(cmdValidateConfig) cmd.RunE = WrapCommandFuncForCobra(cmdValidateConfig)
}, },
}) })
RegisterCommand(Command{
Name: "storage",
Short: "Commands for working with Caddy's storage (EXPERIMENTAL)",
Long: `
Allows exporting and importing Caddy's storage contents. The two commands can be
combined in a pipeline to transfer directly from one storage to another:
$ caddy storage export --config Caddyfile.old --output - |
> caddy storage import --config Caddyfile.new --input -
The - argument refers to stdout and stdin, respectively.
NOTE: When importing to or exporting from file_system storage (the default), the command
should be run as the user that owns the associated root path.
EXPERIMENTAL: May be changed or removed.
`,
CobraFunc: func(cmd *cobra.Command) {
exportCmd := &cobra.Command{
Use: "export --config <path> --output <path>",
Short: "Exports storage assets as a tarball",
Long: `
The contents of the configured storage module (TLS certificates, etc)
are exported via a tarball.
--output is required, - can be given for stdout.
`,
RunE: WrapCommandFuncForCobra(cmdExportStorage),
}
exportCmd.Flags().StringP("config", "c", "", "Input configuration file (required)")
exportCmd.Flags().StringP("output", "o", "", "Output path")
cmd.AddCommand(exportCmd)
importCmd := &cobra.Command{
Use: "import --config <path> --input <path>",
Short: "Imports storage assets from a tarball.",
Long: `
Imports storage assets to the configured storage module. The import file must be
a tar archive.
--input is required, - can be given for stdin.
`,
RunE: WrapCommandFuncForCobra(cmdImportStorage),
}
importCmd.Flags().StringP("config", "c", "", "Configuration file to load (required)")
importCmd.Flags().StringP("input", "i", "", "Tar of assets to load (required)")
cmd.AddCommand(importCmd)
},
})
RegisterCommand(Command{ RegisterCommand(Command{
Name: "fmt", Name: "fmt",
Usage: "[--overwrite] [--diff] [<path>]", Usage: "[--overwrite] [--diff] [<path>]",
@@ -413,7 +351,7 @@ latest versions. EXPERIMENTAL: May be changed or removed.
Short: "Adds Caddy packages (EXPERIMENTAL)", Short: "Adds Caddy packages (EXPERIMENTAL)",
Long: ` Long: `
Downloads an updated Caddy binary with the specified packages (module/plugin) Downloads an updated Caddy binary with the specified packages (module/plugin)
added. Retains existing packages. Returns an error if the any of packages are added. Retains existing packages. Returns an error if the any of packages are
already included. EXPERIMENTAL: May be changed or removed. already included. EXPERIMENTAL: May be changed or removed.
`, `,
CobraFunc: func(cmd *cobra.Command) { CobraFunc: func(cmd *cobra.Command) {
@@ -428,8 +366,8 @@ already included. EXPERIMENTAL: May be changed or removed.
Usage: "<packages...>", Usage: "<packages...>",
Short: "Removes Caddy packages (EXPERIMENTAL)", Short: "Removes Caddy packages (EXPERIMENTAL)",
Long: ` Long: `
Downloads an updated Caddy binaries without the specified packages (module/plugin). Downloads an updated Caddy binaries without the specified packages (module/plugin).
Returns an error if any of the packages are not included. Returns an error if any of the packages are not included.
EXPERIMENTAL: May be changed or removed. EXPERIMENTAL: May be changed or removed.
`, `,
CobraFunc: func(cmd *cobra.Command) { CobraFunc: func(cmd *cobra.Command) {
@@ -456,7 +394,7 @@ argument of --directory. If the directory does not exist, it will be created.
if dir == "" { if dir == "" {
return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required") return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required")
} }
if err := os.MkdirAll(dir, 0o755); err != nil { if err := os.MkdirAll(dir, 0755); err != nil {
return caddy.ExitCodeFailedQuit, err return caddy.ExitCodeFailedQuit, err
} }
if err := doc.GenManTree(rootCmd, &doc.GenManHeader{ if err := doc.GenManTree(rootCmd, &doc.GenManHeader{
@@ -475,40 +413,40 @@ argument of --directory. If the directory does not exist, it will be created.
Use: "completion [bash|zsh|fish|powershell]", Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script", Short: "Generate completion script",
Long: fmt.Sprintf(`To load completions: Long: fmt.Sprintf(`To load completions:
Bash: Bash:
$ source <(%[1]s completion bash) $ source <(%[1]s completion bash)
# To load completions for each session, execute once: # To load completions for each session, execute once:
# Linux: # Linux:
$ %[1]s completion bash > /etc/bash_completion.d/%[1]s $ %[1]s completion bash > /etc/bash_completion.d/%[1]s
# macOS: # macOS:
$ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s $ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
Zsh: Zsh:
# If shell completion is not already enabled in your environment, # If shell completion is not already enabled in your environment,
# you will need to enable it. You can execute the following once: # you will need to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc $ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load completions for each session, execute once: # To load completions for each session, execute once:
$ %[1]s completion zsh > "${fpath[1]}/_%[1]s" $ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
# You will need to start a new shell for this setup to take effect. # You will need to start a new shell for this setup to take effect.
fish: fish:
$ %[1]s completion fish | source $ %[1]s completion fish | source
# To load completions for each session, execute once: # To load completions for each session, execute once:
$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish $ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
PowerShell: PowerShell:
PS> %[1]s completion powershell | Out-String | Invoke-Expression PS> %[1]s completion powershell | Out-String | Invoke-Expression
# To load completions for every new session, run: # To load completions for every new session, run:
PS> %[1]s completion powershell > %[1]s.ps1 PS> %[1]s completion powershell > %[1]s.ps1
# and source this file from your PowerShell profile. # and source this file from your PowerShell profile.
+54 -47
View File
@@ -17,7 +17,6 @@ package caddycmd
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"errors"
"flag" "flag"
"fmt" "fmt"
"io" "io"
@@ -31,12 +30,11 @@ import (
"strings" "strings"
"time" "time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
) )
func init() { func init() {
@@ -64,10 +62,6 @@ func Main() {
} }
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
var exitError *exitError
if errors.As(err, &exitError) {
os.Exit(exitError.ExitCode)
}
os.Exit(1) os.Exit(1)
} }
} }
@@ -95,10 +89,6 @@ func handlePingbackConn(conn net.Conn, expect []byte) error {
// and returns the resulting JSON config bytes along with // and returns the resulting JSON config bytes along with
// the name of the loaded config file (if any). // the name of the loaded config file (if any).
func LoadConfig(configFile, adapterName string) ([]byte, string, error) { func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
return loadConfigWithLogger(caddy.Log(), configFile, adapterName)
}
func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([]byte, string, error) {
// specifying an adapter without a config file is ambiguous // specifying an adapter without a config file is ambiguous
if adapterName != "" && configFile == "" { if adapterName != "" && configFile == "" {
return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)") return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)")
@@ -117,14 +107,13 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
if err != nil { if err != nil {
return nil, "", fmt.Errorf("reading config file: %v", err) return nil, "", fmt.Errorf("reading config file: %v", err)
} }
if logger != nil { caddy.Log().Info("using provided configuration",
logger.Info("using provided configuration", zap.String("config_file", configFile),
zap.String("config_file", configFile), zap.String("config_adapter", adapterName))
zap.String("config_adapter", adapterName))
}
} else if adapterName == "" { } else if adapterName == "" {
// if the Caddyfile adapter is plugged in, we can try using an // as a special case when no config file or adapter
// adjacent Caddyfile by default // is specified, see if the Caddyfile adapter is
// plugged in, and if so, try using a default Caddyfile
cfgAdapter = caddyconfig.GetAdapter("caddyfile") cfgAdapter = caddyconfig.GetAdapter("caddyfile")
if cfgAdapter != nil { if cfgAdapter != nil {
config, err = os.ReadFile("Caddyfile") config, err = os.ReadFile("Caddyfile")
@@ -137,9 +126,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
} else { } else {
// success reading default Caddyfile // success reading default Caddyfile
configFile = "Caddyfile" configFile = "Caddyfile"
if logger != nil { caddy.Log().Info("using adjacent Caddyfile")
logger.Info("using adjacent Caddyfile")
}
} }
} }
} }
@@ -174,9 +161,7 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
if warn.Directive != "" { if warn.Directive != "" {
msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message) msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
} }
if logger != nil { caddy.Log().Warn(msg, zap.String("adapter", adapterName), zap.String("file", warn.File), zap.Int("line", warn.Line))
logger.Warn(msg, zap.String("adapter", adapterName), zap.String("file", warn.File), zap.Int("line", warn.Line))
}
} }
config = adaptedConfig config = adaptedConfig
} }
@@ -189,8 +174,6 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
// blocks indefinitely; it only quits if the poller has errors for // blocks indefinitely; it only quits if the poller has errors for
// long enough time. The filename passed in must be the actual // long enough time. The filename passed in must be the actual
// config file used, not one to be discovered. // config file used, not one to be discovered.
// Each second the config files is loaded and parsed into an object
// and is compared to the last config object that was loaded
func watchConfigFile(filename, adapterName string) { func watchConfigFile(filename, adapterName string) {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
@@ -206,36 +189,64 @@ func watchConfigFile(filename, adapterName string) {
With(zap.String("config_file", filename)) With(zap.String("config_file", filename))
} }
// get current config // get the initial timestamp on the config file
lastCfg, _, err := loadConfigWithLogger(nil, filename, adapterName) info, err := os.Stat(filename)
if err != nil { if err != nil {
logger().Error("unable to load latest config", zap.Error(err)) logger().Error("cannot watch config file", zap.Error(err))
return return
} }
lastModified := info.ModTime()
logger().Info("watching config file for changes") logger().Info("watching config file for changes")
// if the file disappears or something, we can
// stop polling if the error lasts long enough
var lastErr time.Time
finalError := func(err error) bool {
if lastErr.IsZero() {
lastErr = time.Now()
return false
}
if time.Since(lastErr) > 30*time.Second {
logger().Error("giving up watching config file; too many errors",
zap.Error(err))
return true
}
return false
}
// begin poller // begin poller
//nolint:staticcheck //nolint:staticcheck
for range time.Tick(1 * time.Second) { for range time.Tick(1 * time.Second) {
// get current config // get the file info
newCfg, _, err := loadConfigWithLogger(nil, filename, adapterName) info, err := os.Stat(filename)
if err != nil { if err != nil {
logger().Error("unable to load latest config", zap.Error(err)) if finalError(err) {
return return
} }
// if it hasn't changed, nothing to do
if bytes.Equal(lastCfg, newCfg) {
continue continue
} }
lastErr = time.Time{} // no error, so clear any memory of one
// if it hasn't changed, nothing to do
if !info.ModTime().After(lastModified) {
continue
}
logger().Info("config file changed; reloading") logger().Info("config file changed; reloading")
// remember the current config // remember this timestamp
lastCfg = newCfg lastModified = info.ModTime()
// load the contents of the file
config, _, err := LoadConfig(filename, adapterName)
if err != nil {
logger().Error("unable to load latest config", zap.Error(err))
continue
}
// apply the updated config // apply the updated config
err = caddy.Load(lastCfg, false) err = caddy.Load(config, false)
if err != nil { if err != nil {
logger().Error("applying latest config", zap.Error(err)) logger().Error("applying latest config", zap.Error(err))
continue continue
@@ -305,12 +316,8 @@ func loadEnvFromFile(envFile string) error {
} }
for k, v := range envMap { for k, v := range envMap {
// do not overwrite existing environment variables if err := os.Setenv(k, v); err != nil {
_, exists := os.LookupEnv(k) return fmt.Errorf("setting environment variables: %v", err)
if !exists {
if err := os.Setenv(k, v); err != nil {
return fmt.Errorf("setting environment variables: %v", err)
}
} }
} }
+1 -12
View File
@@ -22,15 +22,13 @@ import (
"net/url" "net/url"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"reflect" "reflect"
"runtime" "runtime"
"runtime/debug" "runtime/debug"
"strings" "strings"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"go.uber.org/zap"
) )
func cmdUpgrade(fl Flags) (int, error) { func cmdUpgrade(fl Flags) (int, error) {
@@ -104,15 +102,6 @@ func upgradeBuild(pluginPkgs map[string]struct{}, fl Flags) (int, error) {
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("retrieving current executable permission bits: %v", err) 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)) l.Info("this executable will be replaced", zap.String("path", thisExecPath))
// build the request URL to download this custom build // build the request URL to download this custom build
-222
View File
@@ -1,222 +0,0 @@
// 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 caddycmd
import (
"archive/tar"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"github.com/caddyserver/certmagic"
"github.com/caddyserver/caddy/v2"
)
type storVal struct {
StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`
}
// determineStorage returns the top-level storage module from the given config.
// It may return nil even if no error.
func determineStorage(configFile string, configAdapter string) (*storVal, error) {
cfg, _, err := LoadConfig(configFile, configAdapter)
if err != nil {
return nil, err
}
// storage defaults to FileStorage if not explicitly
// defined in the config, so the config can be valid
// json but unmarshaling will fail.
if !json.Valid(cfg) {
return nil, &json.SyntaxError{}
}
var tmpStruct storVal
err = json.Unmarshal(cfg, &tmpStruct)
if err != nil {
// default case, ignore the error
var jsonError *json.SyntaxError
if errors.As(err, &jsonError) {
return nil, nil
}
return nil, err
}
return &tmpStruct, nil
}
func cmdImportStorage(fl Flags) (int, error) {
importStorageCmdConfigFlag := fl.String("config")
importStorageCmdImportFile := fl.String("input")
if importStorageCmdConfigFlag == "" {
return caddy.ExitCodeFailedStartup, errors.New("--config is required")
}
if importStorageCmdImportFile == "" {
return caddy.ExitCodeFailedStartup, errors.New("--input is required")
}
// extract storage from config if possible
storageCfg, err := determineStorage(importStorageCmdConfigFlag, "")
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
// load specified storage or fallback to default
var stor certmagic.Storage
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
defer cancel()
if storageCfg != nil && storageCfg.StorageRaw != nil {
val, err := ctx.LoadModule(storageCfg, "StorageRaw")
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
stor, err = val.(caddy.StorageConverter).CertMagicStorage()
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
} else {
stor = caddy.DefaultStorage
}
// setup input
var f *os.File
if importStorageCmdImportFile == "-" {
f = os.Stdin
} else {
f, err = os.Open(importStorageCmdImportFile)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("opening input file: %v", err)
}
defer f.Close()
}
// store each archive element
tr := tar.NewReader(f)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return caddy.ExitCodeFailedQuit, fmt.Errorf("reading archive: %v", err)
}
b, err := io.ReadAll(tr)
if err != nil {
return caddy.ExitCodeFailedQuit, fmt.Errorf("reading archive: %v", err)
}
err = stor.Store(ctx, hdr.Name, b)
if err != nil {
return caddy.ExitCodeFailedQuit, fmt.Errorf("reading archive: %v", err)
}
}
fmt.Println("Successfully imported storage")
return caddy.ExitCodeSuccess, nil
}
func cmdExportStorage(fl Flags) (int, error) {
exportStorageCmdConfigFlag := fl.String("config")
exportStorageCmdOutputFlag := fl.String("output")
if exportStorageCmdConfigFlag == "" {
return caddy.ExitCodeFailedStartup, errors.New("--config is required")
}
if exportStorageCmdOutputFlag == "" {
return caddy.ExitCodeFailedStartup, errors.New("--output is required")
}
// extract storage from config if possible
storageCfg, err := determineStorage(exportStorageCmdConfigFlag, "")
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
// load specified storage or fallback to default
var stor certmagic.Storage
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
defer cancel()
if storageCfg != nil && storageCfg.StorageRaw != nil {
val, err := ctx.LoadModule(storageCfg, "StorageRaw")
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
stor, err = val.(caddy.StorageConverter).CertMagicStorage()
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
} else {
stor = caddy.DefaultStorage
}
// enumerate all keys
keys, err := stor.List(ctx, "", true)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
// setup output
var f *os.File
if exportStorageCmdOutputFlag == "-" {
f = os.Stdout
} else {
f, err = os.Create(exportStorageCmdOutputFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("opening output file: %v", err)
}
defer f.Close()
}
// `IsTerminal: true` keys hold the values we
// care about, write them out
tw := tar.NewWriter(f)
for _, k := range keys {
info, err := stor.Stat(ctx, k)
if err != nil {
return caddy.ExitCodeFailedQuit, err
}
if info.IsTerminal {
v, err := stor.Load(ctx, k)
if err != nil {
return caddy.ExitCodeFailedQuit, err
}
hdr := &tar.Header{
Name: k,
Mode: 0o600,
Size: int64(len(v)),
ModTime: info.Modified,
}
if err = tw.WriteHeader(hdr); err != nil {
return caddy.ExitCodeFailedQuit, fmt.Errorf("writing archive: %v", err)
}
if _, err = tw.Write(v); err != nil {
return caddy.ExitCodeFailedQuit, fmt.Errorf("writing archive: %v", err)
}
}
}
if err = tw.Close(); err != nil {
return caddy.ExitCodeFailedQuit, fmt.Errorf("writing archive: %v", err)
}
return caddy.ExitCodeSuccess, nil
}
+11 -22
View File
@@ -410,11 +410,6 @@ func (ctx Context) loadModuleInline(moduleNameKey, moduleScope string, raw json.
// called during the Provision/Validate phase to reference a // called during the Provision/Validate phase to reference a
// module's own host app (since the parent app module is still // module's own host app (since the parent app module is still
// in the process of being provisioned, it is not yet ready). // in the process of being provisioned, it is not yet ready).
//
// We return any type instead of the App type because it is NOT
// intended for the caller of this method to be the one to start
// or stop App modules. The caller is expected to assert to the
// concrete type.
func (ctx Context) App(name string) (any, error) { func (ctx Context) App(name string) (any, error) {
if app, ok := ctx.cfg.apps[name]; ok { if app, ok := ctx.cfg.apps[name]; ok {
return app, nil return app, nil
@@ -433,21 +428,18 @@ func (ctx Context) App(name string) (any, error) {
// AppIfConfigured returns an app by its name if it has been // AppIfConfigured returns an app by its name if it has been
// configured. Can be called instead of App() to avoid // configured. Can be called instead of App() to avoid
// instantiating an empty app when that's not desirable. If // instantiating an empty app when that's not desirable.
// the app has not been loaded, nil is returned. func (ctx Context) AppIfConfigured(name string) (any, error) {
// app, ok := ctx.cfg.apps[name]
// We return any type instead of the App type because it is not if !ok || app == nil {
// intended for the caller of this method to be the one to start return nil, nil
// or stop App modules. The caller is expected to assert to the
// concrete type.
func (ctx Context) AppIfConfigured(name string) any {
if ctx.cfg == nil {
// this can happen if the currently-active context
// is being accessed, but no config has successfully
// been loaded yet
return nil
} }
return ctx.cfg.apps[name]
appModule, err := ctx.App(name)
if err != nil {
return nil, err
}
return appModule, nil
} }
// Storage returns the configured Caddy storage implementation. // Storage returns the configured Caddy storage implementation.
@@ -488,9 +480,6 @@ func (ctx Context) Logger(module ...Module) *zap.Logger {
if len(module) > 0 { if len(module) > 0 {
mod = module[0] mod = module[0]
} }
if mod == nil {
return Log()
}
return ctx.cfg.Logging.Logger(mod) return ctx.cfg.Logging.Logger(mod)
} }
+77 -82
View File
@@ -1,72 +1,65 @@
module github.com/caddyserver/caddy/v2 module github.com/caddyserver/caddy/v2
go 1.20 go 1.19
require ( require (
github.com/BurntSushi/toml v1.3.2 github.com/BurntSushi/toml v1.2.1
github.com/Masterminds/sprig/v3 v3.2.3 github.com/Masterminds/sprig/v3 v3.2.3
github.com/alecthomas/chroma/v2 v2.9.1 github.com/alecthomas/chroma/v2 v2.5.0
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
github.com/caddyserver/certmagic v0.20.0 github.com/caddyserver/certmagic v0.17.2
github.com/dustin/go-humanize v1.0.1 github.com/dustin/go-humanize v1.0.1
github.com/go-chi/chi/v5 v5.0.10 github.com/go-chi/chi v4.1.2+incompatible
github.com/google/cel-go v0.15.1 github.com/google/cel-go v0.13.0
github.com/google/uuid v1.3.1 github.com/google/uuid v1.3.0
github.com/klauspost/compress v1.17.0 github.com/klauspost/compress v1.16.0
github.com/klauspost/cpuid/v2 v2.2.5 github.com/klauspost/cpuid/v2 v2.2.4
github.com/mholt/acmez v1.2.0 github.com/mastercactapus/proxyprotocol v0.0.4
github.com/prometheus/client_golang v1.15.1 github.com/mholt/acmez v1.1.0
github.com/quic-go/quic-go v0.40.0 github.com/prometheus/client_golang v1.14.0
github.com/smallstep/certificates v0.25.0 github.com/quic-go/quic-go v0.33.0
github.com/smallstep/certificates v0.23.2
github.com/smallstep/nosql v0.6.0 github.com/smallstep/nosql v0.6.0
github.com/smallstep/truststore v0.12.1 github.com/smallstep/truststore v0.12.1
github.com/spf13/cobra v1.7.0 github.com/spf13/cobra v1.6.1
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.4 github.com/tailscale/tscert v0.0.0-20230124224810-c6dc1f4049b2
github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046 github.com/yuin/goldmark v1.5.4
github.com/yuin/goldmark v1.5.6 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.40.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 go.opentelemetry.io/contrib/propagators/autoprop v0.40.0
go.opentelemetry.io/contrib/propagators/autoprop v0.42.0 go.opentelemetry.io/otel v1.14.0
go.opentelemetry.io/otel v1.21.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.14.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 go.opentelemetry.io/otel/sdk v1.14.0
go.opentelemetry.io/otel/sdk v1.21.0 go.uber.org/zap v1.24.0
go.uber.org/zap v1.25.0 golang.org/x/crypto v0.7.0
golang.org/x/crypto v0.14.0 golang.org/x/net v0.8.0
golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 golang.org/x/sync v0.1.0
golang.org/x/net v0.17.0 golang.org/x/term v0.6.0
golang.org/x/sync v0.4.0 google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4
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/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
cloud.google.com/go/iam v1.1.2 // indirect
github.com/Microsoft/go-winio v0.6.0 // 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/davecgh/go-spew v1.1.1 // indirect
github.com/fxamacker/cbor/v2 v2.5.0 // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/golang/glog v1.1.2 // indirect github.com/golang/glog v1.0.0 // indirect
github.com/google/certificate-transparency-go v1.1.6 // indirect github.com/golang/mock v1.6.0 // indirect
github.com/google/go-tpm v0.9.0 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/google/go-tspi v0.3.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect github.com/onsi/ginkgo/v2 v2.2.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/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/quic-go/qtls-go1-19 v0.2.1 // indirect
github.com/smallstep/go-attestation v0.4.4-0.20230627102604-cf579e53cbd2 // indirect github.com/quic-go/qtls-go1-20 v0.1.1 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect go.opentelemetry.io/contrib/propagators/aws v1.15.0 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.17.0 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.15.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.15.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect go.opentelemetry.io/contrib/propagators/ot v1.15.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.17.0 // indirect golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
go.uber.org/mock v0.3.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
) )
require ( require (
@@ -74,29 +67,30 @@ require (
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cenkalti/backoff/v4 v4.2.0 // indirect
github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/dgraph-io/badger v1.6.2 // indirect github.com/dgraph-io/badger v1.6.2 // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/go-kit/kit v0.10.0 // indirect github.com/go-kit/kit v0.10.0 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/huandu/xstrings v1.3.3 // indirect github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.12 // indirect github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.0 // indirect github.com/jackc/pgconn v1.14.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgio v1.0.0 // indirect
@@ -108,44 +102,45 @@ require (
github.com/libdns/libdns v0.2.1 // indirect github.com/libdns/libdns v0.2.1 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-isatty v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/micromdm/scep/v2 v2.1.0 // indirect github.com/micromdm/scep/v2 v2.1.0 // indirect
github.com/miekg/dns v1.1.55 // indirect github.com/miekg/dns v1.1.50 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/pires/go-proxyproto v0.7.0
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect
github.com/rs/xid v1.5.0 // indirect github.com/rs/xid v1.4.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/sirupsen/logrus v1.9.0 // indirect
github.com/slackhq/nebula v1.6.1 // indirect github.com/slackhq/nebula v1.6.1 // indirect
github.com/spf13/cast v1.4.1 // indirect github.com/spf13/cast v1.4.1 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/urfave/cli v1.22.14 // indirect github.com/urfave/cli v1.22.12 // indirect
go.etcd.io/bbolt v1.3.7 // indirect go.etcd.io/bbolt v1.3.7 // indirect
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0 // indirect
go.opentelemetry.io/otel/metric v1.21.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.14.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 go.opentelemetry.io/otel/metric v0.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.opentelemetry.io/otel/trace v1.14.0 // indirect
go.step.sm/cli-utils v0.8.0 // indirect go.opentelemetry.io/proto/otlp v0.19.0 // indirect
go.step.sm/crypto v0.35.1 go.step.sm/cli-utils v0.7.5 // indirect
go.step.sm/linkedca v0.20.1 // indirect go.step.sm/crypto v0.23.2
go.uber.org/multierr v1.11.0 // indirect go.step.sm/linkedca v0.19.0 // indirect
golang.org/x/mod v0.11.0 // indirect go.uber.org/atomic v1.9.0 // indirect
golang.org/x/sys v0.14.0 go.uber.org/multierr v1.9.0 // indirect
golang.org/x/text v0.13.0 // indirect golang.org/x/mod v0.8.0 // indirect
golang.org/x/tools v0.10.0 // indirect golang.org/x/sys v0.6.0
google.golang.org/grpc v1.59.0 // indirect golang.org/x/text v0.8.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect golang.org/x/tools v0.6.0 // indirect
google.golang.org/grpc v1.53.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect
howett.net/plist v1.0.0 // indirect howett.net/plist v1.0.0 // indirect
) )
+511 -194
View File
File diff suppressed because it is too large Load Diff
-56
View File
@@ -1,56 +0,0 @@
// 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
}
+9 -97
View File
@@ -30,34 +30,18 @@ func reuseUnixSocket(network, addr string) (any, error) {
return nil, nil return nil, nil
} }
func listenReusable(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (any, error) { func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (net.Listener, error) {
switch network { sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
case "udp", "udp4", "udp6", "unixgram": ln, err := config.Listen(ctx, network, address)
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 { if err != nil {
return nil, err return nil, err
} }
return &fakeClosePacketConn{sharedPacketConn: sharedPc.(*sharedPacketConn)}, nil return &sharedListener{Listener: ln, key: lnKey}, nil
})
default: if err != nil {
sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) { return nil, err
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 // fakeCloseListener is a private wrapper over a listener that
@@ -114,7 +98,7 @@ func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
// so that it's clear in the code that side-effects are shared with other // 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 // 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 // do anything with the error because all we could do is log it, but we
// explicitly assign it to nothing so we don't forget it's there if needed // expliclty assign it to nothing so we don't forget it's there if needed
_ = fcl.sharedListener.clearDeadline() _ = fcl.sharedListener.clearDeadline()
if netErr, ok := err.(net.Error); ok && netErr.Timeout() { if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
@@ -188,75 +172,3 @@ func (sl *sharedListener) setDeadline() error {
func (sl *sharedListener) Destruct() error { func (sl *sharedListener) Destruct() error {
return sl.Listener.Close() 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)
)
+4 -102
View File
@@ -22,10 +22,8 @@ package caddy
import ( import (
"context" "context"
"errors" "errors"
"io"
"io/fs" "io/fs"
"net" "net"
"os"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
@@ -89,7 +87,7 @@ func reuseUnixSocket(network, addr string) (any, error) {
return nil, nil return nil, nil
} }
func listenReusable(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (any, error) { func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (net.Listener, error) {
// wrap any Control function set by the user so we can also add our reusePort control without clobbering theirs // wrap any Control function set by the user so we can also add our reusePort control without clobbering theirs
oldControl := config.Control oldControl := config.Control
config.Control = func(network, address string, c syscall.RawConn) error { config.Control = func(network, address string, c syscall.RawConn) error {
@@ -105,44 +103,14 @@ func listenReusable(ctx context.Context, lnKey string, network, address string,
// we still put it in the listenerPool so we can count how many // we still put it in the listenerPool so we can count how many
// configs are using this socket; necessary to ensure we can know // configs are using this socket; necessary to ensure we can know
// whether to enforce shutdown delays, for example (see #5393). // whether to enforce shutdown delays, for example (see #5393).
var ln io.Closer ln, err := config.Listen(ctx, network, address)
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 { if err == nil {
listenerPool.LoadOrStore(lnKey, nil) listenerPool.LoadOrStore(lnKey, nil)
} }
// if new listener is a unix socket, make sure we can reuse it later
// (we do our own "unlink on close" -- not required, but more tidy)
one := int32(1)
if unix, ok := ln.(*net.UnixListener); ok {
unix.SetUnlinkOnClose(false)
ln = &unixListener{unix, lnKey, &one}
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, // lightly wrap the listener so that when it is closed,
// we can decrement the usage pool counter // we can decrement the usage pool counter
switch specificLn := ln.(type) { return deleteListener{ln, lnKey}, err
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. // reusePort sets SO_REUSEPORT. Ineffective for unix sockets.
@@ -151,7 +119,7 @@ func reusePort(network, address string, conn syscall.RawConn) error {
return nil return nil
} }
return conn.Control(func(descriptor uintptr) { return conn.Control(func(descriptor uintptr) {
if err := unix.SetsockoptInt(int(descriptor), unix.SOL_SOCKET, unixSOREUSEPORT, 1); err != nil { if err := unix.SetsockoptInt(int(descriptor), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
Log().Error("setting SO_REUSEPORT", Log().Error("setting SO_REUSEPORT",
zap.String("network", network), zap.String("network", network),
zap.String("address", address), zap.String("address", address),
@@ -161,56 +129,6 @@ func reusePort(network, address string, conn syscall.RawConn) error {
}) })
} }
type unixListener struct {
*net.UnixListener
mapKey string
count *int32 // accessed atomically
}
func (uln *unixListener) Close() error {
newCount := atomic.AddInt32(uln.count, -1)
if newCount == 0 {
defer func() {
addr := uln.Addr().String()
unixSocketsMu.Lock()
delete(unixSockets, uln.mapKey)
unixSocketsMu.Unlock()
_ = syscall.Unlink(addr)
}()
}
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 // deleteListener is a type that simply deletes itself
// from the listenerPool when it closes. It is used // from the listenerPool when it closes. It is used
// solely for the purpose of reference counting (i.e. // solely for the purpose of reference counting (i.e.
@@ -224,19 +142,3 @@ func (dl deleteListener) Close() error {
_, _ = listenerPool.Delete(dl.lnKey) _, _ = listenerPool.Delete(dl.lnKey)
return dl.Listener.Close() 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
}
-7
View File
@@ -1,7 +0,0 @@
//go:build unix && !freebsd
package caddy
import "golang.org/x/sys/unix"
const unixSOREUSEPORT = unix.SO_REUSEPORT
-7
View File
@@ -1,7 +0,0 @@
//go:build freebsd
package caddy
import "golang.org/x/sys/unix"
const unixSOREUSEPORT = unix.SO_REUSEPORT_LB
+170 -219
View File
@@ -20,7 +20,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs"
"net" "net"
"net/netip" "net/netip"
"os" "os"
@@ -28,21 +27,14 @@ import (
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"syscall"
"time" "time"
"github.com/quic-go/quic-go" "github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3" "github.com/quic-go/quic-go/http3"
"go.uber.org/zap" "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. // NetworkAddress represents one or more network addresses.
// It contains the individual components for a parsed network // It contains the individual components for a parsed network
// address of the form accepted by ParseNetworkAddress(). // address of the form accepted by ParseNetworkAddress().
@@ -154,44 +146,38 @@ 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) { func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
var ( var ln any
ln any var err error
err error
address string
unixFileMode fs.FileMode
isAbtractUnixSocket bool
)
// split unix socket addr early so lnKey address := na.JoinHostPort(portOffset)
// is independent of permissions bits
if na.IsUnixNetwork() {
var err error
address, unixFileMode, err = internal.SplitUnixSocketPermissionsBits(na.Host)
if err != nil {
return nil, err
}
isAbtractUnixSocket = strings.HasPrefix(address, "@")
} else {
address = na.JoinHostPort(portOffset)
}
// if this is a unix socket, see if we already have it open, // if this is a unix socket, see if we already have it open
// force socket permissions on it and return early
if socket, err := reuseUnixSocket(na.Network, address); socket != nil || err != nil { if socket, err := reuseUnixSocket(na.Network, address); socket != nil || err != nil {
if !isAbtractUnixSocket {
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 return socket, err
} }
lnKey := listenerKey(na.Network, address) 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
}
ln = &fakeClosePacketConn{sharedPacketConn: sharedPc.(*sharedPacketConn)}
}
if strings.HasPrefix(na.Network, "ip") { if strings.HasPrefix(na.Network, "ip") {
ln, err = config.ListenPacket(ctx, na.Network, address) ln, err = config.ListenPacket(ctx, na.Network, address)
} else {
ln, err = listenReusable(ctx, lnKey, na.Network, address, config)
} }
if err != nil { if err != nil {
return nil, err return nil, err
@@ -200,12 +186,17 @@ func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net
return nil, fmt.Errorf("unsupported network type: %s", na.Network) return nil, fmt.Errorf("unsupported network type: %s", na.Network)
} }
if IsUnixNetwork(na.Network) { // if new listener is a unix socket, make sure we can reuse it later
if !isAbtractUnixSocket { // (we do our own "unlink on close" -- not required, but more tidy)
if err := os.Chmod(address, unixFileMode); err != nil { one := int32(1)
return nil, fmt.Errorf("unable to set permissions (%s) on %s: %v", unixFileMode, address, err) switch unix := ln.(type) {
} case *net.UnixListener:
} unix.SetUnlinkOnClose(false)
ln = &unixListener{unix, lnKey, &one}
unixSockets[lnKey] = ln.(*unixListener)
case *net.UnixConn:
ln = &unixConn{unix, address, lnKey, &one}
unixSockets[lnKey] = ln.(*unixConn)
} }
return ln, nil return ln, nil
@@ -312,32 +303,22 @@ func IsUnixNetwork(netw string) bool {
// Network addresses are distinct from URLs and do not // Network addresses are distinct from URLs and do not
// use URL syntax. // use URL syntax.
func ParseNetworkAddress(addr string) (NetworkAddress, error) { func ParseNetworkAddress(addr string) (NetworkAddress, error) {
return ParseNetworkAddressWithDefaults(addr, "tcp", 0)
}
// ParseNetworkAddressWithDefaults is like ParseNetworkAddress but allows
// the default network and port to be specified.
func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort uint) (NetworkAddress, error) {
var host, port string var host, port string
network, host, port, err := SplitNetworkAddress(addr) network, host, port, err := SplitNetworkAddress(addr)
if err != nil { if err != nil {
return NetworkAddress{}, err return NetworkAddress{}, err
} }
if network == "" { if network == "" {
network = defaultNetwork network = "tcp"
} }
if IsUnixNetwork(network) { if IsUnixNetwork(network) {
_, _, err := internal.SplitUnixSocketPermissionsBits(host)
return NetworkAddress{ return NetworkAddress{
Network: network, Network: network,
Host: host, Host: host,
}, err }, nil
} }
var start, end uint64 var start, end uint64
if port == "" { if port != "" {
start = uint64(defaultPort)
end = uint64(defaultPort)
} else {
before, after, found := strings.Cut(port, "-") before, after, found := strings.Cut(port, "-")
if !found { if !found {
after = before after = before
@@ -453,93 +434,46 @@ func ListenPacket(network, addr string) (net.PacketConn, error) {
// unixgram will be used; otherwise, udp will be used). // unixgram will be used; otherwise, udp will be used).
// //
// NOTE: This API is EXPERIMENTAL and may be changed or removed. // NOTE: This API is EXPERIMENTAL and may be changed or removed.
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) {
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: sqs.getConfigForClient}
earlyLn, err := quic.ListenEarly(h3ln, 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
}
// 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 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. // 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) { func ListenQUIC(ln net.PacketConn, tlsConf *tls.Config, activeRequests *int64) (quic.EarlyListener, error) {
lnKey := listenerKey("quic+"+ln.LocalAddr().Network(), ln.LocalAddr().String()) lnKey := listenerKey("quic+"+ln.LocalAddr().Network(), ln.LocalAddr().String())
sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) { sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
sqs := newSharedQUICState(tlsConf, activeRequests) earlyLn, err := quic.ListenEarly(ln, http3.ConfigureTLSConfig(tlsConf), &quic.Config{
// http3.ConfigureTLSConfig only uses this field and tls App sets this field as well Allow0RTT: func(net.Addr) bool { return true },
//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 { RequireAddressValidation: func(clientAddr net.Addr) bool {
// TODO: make tunable? var highLoad bool
return sqs.getActiveRequests() > 1000 if activeRequests != nil {
highLoad = atomic.LoadInt64(activeRequests) > 1000 // TODO: make tunable?
}
return highLoad
}, },
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &sharedQuicListener{EarlyListener: earlyLn, sqs: sqs, key: lnKey}, nil return &sharedQuicListener{EarlyListener: earlyLn, key: lnKey}, nil
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
sql := sharedEarlyListener.(*sharedQuicListener) // TODO: to serve QUIC over a unix socket, currently we need to hold onto
// add current tls.Config and request counter to sqs, so GetConfigForClient will always return the latest tls.Config in case of context cancellation, // the underlying net.PacketConn (which we wrap as unixConn to keep count
// and the request counter will reflect current http server // of closes) because closing the quic.EarlyListener doesn't actually close
ctx, cancel := sql.sqs.addState(tlsConf, activeRequests) // 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
}
ctx, cancel := context.WithCancel(context.Background())
return &fakeCloseQuicListener{ return &fakeCloseQuicListener{
sharedQuicListener: sql, sharedQuicListener: sharedEarlyListener.(*sharedQuicListener),
uc: unix,
context: ctx, context: ctx,
contextCancel: cancel, contextCancel: cancel,
}, nil }, nil
@@ -551,101 +485,26 @@ func ListenerUsage(network, addr string) int {
return count return count
} }
// contextAndCancelFunc groups context and its cancelFunc
type contextAndCancelFunc struct {
context.Context
context.CancelFunc
}
// sharedQUICState manages GetConfigForClient and current number of active requests
// see issue: https://github.com/caddyserver/caddy/pull/4849
type sharedQUICState struct {
rmu sync.RWMutex
tlsConfs map[*tls.Config]contextAndCancelFunc
requestCounters map[*tls.Config]*int64
activeTlsConf *tls.Config
activeRequestsCounter *int64
}
// 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.addState(tlsConfig, activeRequests)
return sqtc
}
// getConfigForClient is used as tls.Config's GetConfigForClient field
func (sqs *sharedQUICState) getConfigForClient(ch *tls.ClientHelloInfo) (*tls.Config, error) {
sqs.rmu.RLock()
defer sqs.rmu.RUnlock()
return sqs.activeTlsConf.GetConfigForClient(ch)
}
// 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)
}
// 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
}
ctx, cancel := context.WithCancel(context.Background())
wrappedCancel := func() {
cancel()
sqs.rmu.Lock()
defer sqs.rmu.Unlock()
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, counter := range sqs.requestCounters {
sqs.activeTlsConf = tc
sqs.activeRequestsCounter = counter
break
}
}
}
sqs.tlsConfs[tlsConfig] = contextAndCancelFunc{ctx, wrappedCancel}
sqs.requestCounters[tlsConfig] = activeRequests
// there should be at most 2 tls.Configs
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
}
// sharedQuicListener is like sharedListener, but for quic.EarlyListeners. // sharedQuicListener is like sharedListener, but for quic.EarlyListeners.
type sharedQuicListener struct { type sharedQuicListener struct {
*quic.EarlyListener quic.EarlyListener
packetConn net.PacketConn // we have to hold these because quic-go won't close listeners it didn't create key string
sqs *sharedQUICState
key string
} }
// Destruct closes the underlying QUIC listener and its associated net.PacketConn. // Destruct closes the underlying QUIC listener.
func (sql *sharedQuicListener) Destruct() error { func (sql *sharedQuicListener) Destruct() error {
// close EarlyListener first to stop any operations being done to the net.PacketConn return sql.EarlyListener.Close()
_ = sql.EarlyListener.Close() }
// then close the net.PacketConn
return sql.packetConn.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()
} }
// fakeClosedErr returns an error value that is not temporary // fakeClosedErr returns an error value that is not temporary
@@ -667,9 +526,41 @@ func fakeClosedErr(l interface{ Addr() net.Addr }) error {
// socket is actually left open. // socket is actually left open.
var errFakeClosed = fmt.Errorf("listener 'closed' 😉") var errFakeClosed = fmt.Errorf("listener 'closed' 😉")
// fakeClosePacketConn is like fakeCloseListener, but for PacketConns.
type fakeClosePacketConn struct {
closed int32 // accessed atomically; belongs to this struct only
*sharedPacketConn // embedded, so we also become a net.PacketConn
}
func (fcpc *fakeClosePacketConn) Close() error {
if atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) {
_, _ = listenerPool.Delete(fcpc.sharedPacketConn.key)
}
return nil
}
// Supports QUIC implementation: https://github.com/caddyserver/caddy/issues/3998
func (fcpc fakeClosePacketConn) SetReadBuffer(bytes int) error {
if conn, ok := fcpc.PacketConn.(interface{ SetReadBuffer(int) error }); ok {
return conn.SetReadBuffer(bytes)
}
return fmt.Errorf("SetReadBuffer() not implemented for %T", fcpc.PacketConn)
}
// Supports QUIC implementation: https://github.com/caddyserver/caddy/issues/3998
func (fcpc fakeClosePacketConn) SyscallConn() (syscall.RawConn, error) {
if conn, ok := fcpc.PacketConn.(interface {
SyscallConn() (syscall.RawConn, error)
}); ok {
return conn.SyscallConn()
}
return nil, fmt.Errorf("SyscallConn() not implemented for %T", fcpc.PacketConn)
}
type fakeCloseQuicListener struct { type fakeCloseQuicListener struct {
closed int32 // accessed atomically; belongs to this struct only closed int32 // accessed atomically; belongs to this struct only
*sharedQuicListener // embedded, so we also become a quic.EarlyListener *sharedQuicListener // embedded, so we also become a quic.EarlyListener
uc *unixConn // underlying unix socket, if UDS
context context.Context context context.Context
contextCancel context.CancelFunc contextCancel context.CancelFunc
} }
@@ -696,6 +587,11 @@ func (fcql *fakeCloseQuicListener) Close() error {
if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) { if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) {
fcql.contextCancel() fcql.contextCancel()
_, _ = listenerPool.Delete(fcql.sharedQuicListener.key) _, _ = 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 return nil
} }
@@ -721,7 +617,54 @@ func RegisterNetwork(network string, getListener ListenerFunc) {
networkTypes[network] = getListener networkTypes[network] = getListener
} }
var unixSocketsMu sync.Mutex type unixListener struct {
*net.UnixListener
mapKey string
count *int32 // accessed atomically
}
func (uln *unixListener) Close() error {
newCount := atomic.AddInt32(uln.count, -1)
if newCount == 0 {
defer func() {
addr := uln.Addr().String()
unixSocketsMu.Lock()
delete(unixSockets, uln.mapKey)
unixSocketsMu.Unlock()
_ = syscall.Unlink(addr)
}()
}
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()
}
// 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
)
// getListenerFromPlugin returns a listener on the given network and address // getListenerFromPlugin returns a listener on the given network and address
// if a plugin has registered the network name. It may return (nil, nil) if // if a plugin has registered the network name. It may return (nil, nil) if
@@ -765,3 +708,11 @@ type ListenerWrapper interface {
var listenerPool = NewUsagePool() var listenerPool = NewUsagePool()
const maxPortSpan = 65535 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)
)
+21 -260
View File
@@ -17,8 +17,6 @@ package caddy
import ( import (
"reflect" "reflect"
"testing" "testing"
"github.com/caddyserver/caddy/v2/internal"
) )
func TestSplitNetworkAddress(t *testing.T) { func TestSplitNetworkAddress(t *testing.T) {
@@ -177,57 +175,47 @@ func TestJoinNetworkAddress(t *testing.T) {
func TestParseNetworkAddress(t *testing.T) { func TestParseNetworkAddress(t *testing.T) {
for i, tc := range []struct { for i, tc := range []struct {
input string input string
defaultNetwork string expectAddr NetworkAddress
defaultPort uint expectErr bool
expectAddr NetworkAddress
expectErr bool
}{ }{
{ {
input: "", input: "",
expectErr: true, expectErr: true,
}, },
{ {
input: ":", input: ":",
defaultNetwork: "udp",
expectAddr: NetworkAddress{ expectAddr: NetworkAddress{
Network: "udp", Network: "tcp",
}, },
}, },
{ {
input: "[::]", input: "[::]",
defaultNetwork: "udp",
defaultPort: 53,
expectAddr: NetworkAddress{ expectAddr: NetworkAddress{
Network: "udp", Network: "tcp",
Host: "::", Host: "::",
StartPort: 53,
EndPort: 53,
}, },
}, },
{ {
input: ":1234", input: ":1234",
defaultNetwork: "udp",
expectAddr: NetworkAddress{ expectAddr: NetworkAddress{
Network: "udp", Network: "tcp",
Host: "", Host: "",
StartPort: 1234, StartPort: 1234,
EndPort: 1234, EndPort: 1234,
}, },
}, },
{ {
input: "udp/:1234", input: "tcp/:1234",
defaultNetwork: "udp",
expectAddr: NetworkAddress{ expectAddr: NetworkAddress{
Network: "udp", Network: "tcp",
Host: "", Host: "",
StartPort: 1234, StartPort: 1234,
EndPort: 1234, EndPort: 1234,
}, },
}, },
{ {
input: "tcp6/:1234", input: "tcp6/:1234",
defaultNetwork: "tcp",
expectAddr: NetworkAddress{ expectAddr: NetworkAddress{
Network: "tcp6", Network: "tcp6",
Host: "", Host: "",
@@ -236,8 +224,7 @@ func TestParseNetworkAddress(t *testing.T) {
}, },
}, },
{ {
input: "tcp4/localhost:1234", input: "tcp4/localhost:1234",
defaultNetwork: "tcp",
expectAddr: NetworkAddress{ expectAddr: NetworkAddress{
Network: "tcp4", Network: "tcp4",
Host: "localhost", Host: "localhost",
@@ -246,16 +233,14 @@ func TestParseNetworkAddress(t *testing.T) {
}, },
}, },
{ {
input: "unix//foo/bar", input: "unix//foo/bar",
defaultNetwork: "tcp",
expectAddr: NetworkAddress{ expectAddr: NetworkAddress{
Network: "unix", Network: "unix",
Host: "/foo/bar", Host: "/foo/bar",
}, },
}, },
{ {
input: "localhost:1234-1234", input: "localhost:1234-1234",
defaultNetwork: "tcp",
expectAddr: NetworkAddress{ expectAddr: NetworkAddress{
Network: "tcp", Network: "tcp",
Host: "localhost", Host: "localhost",
@@ -264,139 +249,11 @@ func TestParseNetworkAddress(t *testing.T) {
}, },
}, },
{ {
input: "localhost:2-1", input: "localhost:2-1",
defaultNetwork: "tcp",
expectErr: true,
},
{
input: "localhost:0",
defaultNetwork: "tcp",
expectAddr: NetworkAddress{
Network: "tcp",
Host: "localhost",
StartPort: 0,
EndPort: 0,
},
},
{
input: "localhost:1-999999999999",
defaultNetwork: "tcp",
expectErr: true,
},
} {
actualAddr, err := ParseNetworkAddressWithDefaults(tc.input, tc.defaultNetwork, tc.defaultPort)
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 actualAddr.Network != tc.expectAddr.Network {
t.Errorf("Test %d: Expected network '%v' but got '%v'", i, tc.expectAddr, actualAddr)
}
if !reflect.DeepEqual(tc.expectAddr, actualAddr) {
t.Errorf("Test %d: Expected addresses %v but got %v", i, tc.expectAddr, actualAddr)
}
}
}
func TestParseNetworkAddressWithDefaults(t *testing.T) {
for i, tc := range []struct {
input string
defaultNetwork string
defaultPort uint
expectAddr NetworkAddress
expectErr bool
}{
{
input: "",
expectErr: true, expectErr: true,
}, },
{ {
input: ":", input: "localhost:0",
defaultNetwork: "udp",
expectAddr: NetworkAddress{
Network: "udp",
},
},
{
input: "[::]",
defaultNetwork: "udp",
defaultPort: 53,
expectAddr: NetworkAddress{
Network: "udp",
Host: "::",
StartPort: 53,
EndPort: 53,
},
},
{
input: ":1234",
defaultNetwork: "udp",
expectAddr: NetworkAddress{
Network: "udp",
Host: "",
StartPort: 1234,
EndPort: 1234,
},
},
{
input: "udp/:1234",
defaultNetwork: "udp",
expectAddr: NetworkAddress{
Network: "udp",
Host: "",
StartPort: 1234,
EndPort: 1234,
},
},
{
input: "tcp6/:1234",
defaultNetwork: "tcp",
expectAddr: NetworkAddress{
Network: "tcp6",
Host: "",
StartPort: 1234,
EndPort: 1234,
},
},
{
input: "tcp4/localhost:1234",
defaultNetwork: "tcp",
expectAddr: NetworkAddress{
Network: "tcp4",
Host: "localhost",
StartPort: 1234,
EndPort: 1234,
},
},
{
input: "unix//foo/bar",
defaultNetwork: "tcp",
expectAddr: NetworkAddress{
Network: "unix",
Host: "/foo/bar",
},
},
{
input: "localhost:1234-1234",
defaultNetwork: "tcp",
expectAddr: NetworkAddress{
Network: "tcp",
Host: "localhost",
StartPort: 1234,
EndPort: 1234,
},
},
{
input: "localhost:2-1",
defaultNetwork: "tcp",
expectErr: true,
},
{
input: "localhost:0",
defaultNetwork: "tcp",
expectAddr: NetworkAddress{ expectAddr: NetworkAddress{
Network: "tcp", Network: "tcp",
Host: "localhost", Host: "localhost",
@@ -405,12 +262,11 @@ func TestParseNetworkAddressWithDefaults(t *testing.T) {
}, },
}, },
{ {
input: "localhost:1-999999999999", input: "localhost:1-999999999999",
defaultNetwork: "tcp", expectErr: true,
expectErr: true,
}, },
} { } {
actualAddr, err := ParseNetworkAddressWithDefaults(tc.input, tc.defaultNetwork, tc.defaultPort) actualAddr, err := ParseNetworkAddress(tc.input)
if tc.expectErr && err == nil { if tc.expectErr && err == nil {
t.Errorf("Test %d: Expected error but got: %v", i, err) t.Errorf("Test %d: Expected error but got: %v", i, err)
} }
@@ -557,98 +413,3 @@ func TestExpand(t *testing.T) {
} }
} }
} }
func TestSplitUnixSocketPermissionsBits(t *testing.T) {
for i, tc := range []struct {
input string
expectNetwork string
expectPath string
expectFileMode string
expectErr bool
}{
{
input: "./foo.socket",
expectPath: "./foo.socket",
expectFileMode: "--w-------",
},
{
input: `.\relative\path.socket`,
expectPath: `.\relative\path.socket`,
expectFileMode: "--w-------",
},
{
// literal colon in resulting address
// and defaulting to 0200 bits
input: "./foo.socket:0666",
expectPath: "./foo.socket:0666",
expectFileMode: "--w-------",
},
{
input: "./foo.socket|0220",
expectPath: "./foo.socket",
expectFileMode: "--w--w----",
},
{
input: "/var/run/foo|222",
expectPath: "/var/run/foo",
expectFileMode: "--w--w--w-",
},
{
input: "./foo.socket|0660",
expectPath: "./foo.socket",
expectFileMode: "-rw-rw----",
},
{
input: "./foo.socket|0666",
expectPath: "./foo.socket",
expectFileMode: "-rw-rw-rw-",
},
{
input: "/var/run/foo|666",
expectPath: "/var/run/foo",
expectFileMode: "-rw-rw-rw-",
},
{
input: `c:\absolute\path.socket|220`,
expectPath: `c:\absolute\path.socket`,
expectFileMode: "--w--w----",
},
{
// symbolic permission representation is not supported for now
input: "./foo.socket|u=rw,g=rw,o=rw",
expectErr: true,
},
{
// octal (base-8) permission representation has to be between
// `0` for no read, no write, no exec (`---`) and
// `7` for read (4), write (2), exec (1) (`rwx` => `4+2+1 = 7`)
input: "./foo.socket|888",
expectErr: true,
},
{
// too many colons in address
input: "./foo.socket|123456|0660",
expectErr: true,
},
{
// owner is missing write perms
input: "./foo.socket|0522",
expectErr: true,
},
} {
actualPath, actualFileMode, err := internal.SplitUnixSocketPermissionsBits(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 actualPath != tc.expectPath {
t.Errorf("Test %d: Expected path '%s' but got '%s'", i, tc.expectPath, actualPath)
}
// fileMode.Perm().String() parses 0 to "----------"
if !tc.expectErr && actualFileMode.Perm().String() != tc.expectFileMode {
t.Errorf("Test %d: Expected perms '%s' but got '%s'", i, tc.expectFileMode, actualFileMode.Perm().String())
}
}
}
-8
View File
@@ -33,12 +33,6 @@ func init() {
RegisterModule(StdoutWriter{}) RegisterModule(StdoutWriter{})
RegisterModule(StderrWriter{}) RegisterModule(StderrWriter{})
RegisterModule(DiscardWriter{}) 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 // Logging facilitates logging within Caddy. The default log is
@@ -271,8 +265,6 @@ type BaseLog struct {
WriterRaw json.RawMessage `json:"writer,omitempty" caddy:"namespace=caddy.logging.writers inline_key=output"` 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. // 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"` EncoderRaw json.RawMessage `json:"encoder,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"`
// Level is the minimum level to emit, and is inclusive. // Level is the minimum level to emit, and is inclusive.
+1 -8
View File
@@ -3,11 +3,10 @@ package caddy
import ( import (
"net/http" "net/http"
"github.com/caddyserver/caddy/v2/internal/metrics"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
"github.com/caddyserver/caddy/v2/internal/metrics"
) )
// define and register the metrics used in this package. // define and register the metrics used in this package.
@@ -67,9 +66,3 @@ func (d *delegator) WriteHeader(code int) {
d.status = code d.status = code
d.ResponseWriter.WriteHeader(code) d.ResponseWriter.WriteHeader(code)
} }
// Unwrap returns the underlying ResponseWriter, necessary for
// http.ResponseController to work correctly.
func (d *delegator) Unwrap() http.ResponseWriter {
return d.ResponseWriter
}
+1 -2
View File
@@ -22,10 +22,9 @@ import (
"strings" "strings"
"time" "time"
"github.com/caddyserver/caddy/v2"
"github.com/google/uuid" "github.com/google/uuid"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
) )
func init() { func init() {
+20 -30
View File
@@ -20,19 +20,16 @@ import (
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"runtime"
"strconv" "strconv"
"strings"
"sync" "sync"
"time" "time"
"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"
"github.com/caddyserver/caddy/v2/modules/caddyevents" "github.com/caddyserver/caddy/v2/modules/caddyevents"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
"go.uber.org/zap"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
) )
func init() { func init() {
@@ -73,7 +70,7 @@ func init() {
// `{http.request.remote.host}` | The host (IP) part of the remote client's address // `{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.port}` | The port part of the remote client's address
// `{http.request.remote}` | The address of the remote client // `{http.request.remote}` | The address of the remote client
// `{http.request.scheme}` | The request scheme, typically `http` or `https` // `{http.request.scheme}` | The request scheme
// `{http.request.tls.version}` | The TLS version name // `{http.request.tls.version}` | The TLS version name
// `{http.request.tls.cipher_suite}` | The TLS cipher suite // `{http.request.tls.cipher_suite}` | The TLS cipher suite
// `{http.request.tls.resumed}` | The TLS connection resumed a previous connection // `{http.request.tls.resumed}` | The TLS connection resumed a previous connection
@@ -296,19 +293,11 @@ func (app *App) Provision(ctx caddy.Context) error {
if srv.Errors != nil { if srv.Errors != nil {
err := srv.Errors.Routes.Provision(ctx) err := srv.Errors.Routes.Provision(ctx)
if err != nil { if err != nil {
return fmt.Errorf("server %s: setting up error handling routes: %v", srvName, err) return fmt.Errorf("server %s: setting up server error handling routes: %v", srvName, err)
} }
srv.errorHandlerChain = srv.Errors.Routes.Compile(errorEmptyHandler) srv.errorHandlerChain = srv.Errors.Routes.Compile(errorEmptyHandler)
} }
// provision the named routes (they get compiled at runtime)
for name, route := range srv.NamedRoutes {
err := route.Provision(ctx, srv.Metrics)
if err != nil {
return fmt.Errorf("server %s: setting up named route '%s' handlers: %v", name, srvName, err)
}
}
// prepare the TLS connection policies // prepare the TLS connection policies
err = srv.TLSConnPolicies.Provision(ctx) err = srv.TLSConnPolicies.Provision(ctx)
if err != nil { if err != nil {
@@ -328,15 +317,9 @@ func (app *App) Provision(ctx caddy.Context) error {
// Validate ensures the app's configuration is valid. // Validate ensures the app's configuration is valid.
func (app *App) Validate() error { func (app *App) Validate() error {
isGo120 := strings.Contains(runtime.Version(), "go1.20")
// each server must use distinct listener addresses // each server must use distinct listener addresses
lnAddrs := make(map[string]string) lnAddrs := make(map[string]string)
for srvName, srv := range app.Servers { for srvName, srv := range app.Servers {
if isGo120 && srv.EnableFullDuplex {
app.logger.Warn("enable_full_duplex is not supported in Go 1.20, use a build made with Go 1.21 or later", zap.String("server", srvName))
}
for _, addr := range srv.Listen { for _, addr := range srv.Listen {
listenAddr, err := caddy.ParseNetworkAddress(addr) listenAddr, err := caddy.ParseNetworkAddress(addr)
if err != nil { if err != nil {
@@ -378,7 +361,11 @@ func (app *App) Start() error {
return context.WithValue(ctx, ConnCtxKey, c) return context.WithValue(ctx, ConnCtxKey, c)
}, },
} }
h2server := new(http2.Server) h2server := &http2.Server{
NewWriteScheduler: func() http2.WriteScheduler {
return http2.NewPriorityWriteScheduler(nil)
},
}
// disable HTTP/2, which we enabled by default during provisioning // disable HTTP/2, which we enabled by default during provisioning
if !srv.protocol("h2") { if !srv.protocol("h2") {
@@ -599,13 +586,16 @@ func (app *App) Stop() error {
return return
} }
// First close h3server then close listeners unlike stdlib for several reasons: // TODO: we have to manually close our listeners because quic-go won't
// 1, udp has only a single socket, once closed, no more data can be read and // close listeners it didn't create along with the server itself...
// written. In contrast, closing tcp listeners won't affect established connections. // see https://github.com/quic-go/quic-go/issues/3560
// This have something to do with graceful shutdown when upstream implements it. for _, el := range server.h3listeners {
// 2, h3server will only close listeners it's registered (quic listeners). Closing if err := el.Close(); err != nil {
// listener first and these listeners maybe unregistered thus won't be closed. caddy app.logger.Error("HTTP/3 listener close",
// distinguishes quic-listener and underlying datagram sockets. zap.Error(err),
zap.String("address", el.LocalAddr().String()))
}
}
// TODO: CloseGracefully, once implemented upstream (see https://github.com/quic-go/quic-go/issues/2103) // TODO: CloseGracefully, once implemented upstream (see https://github.com/quic-go/quic-go/issues/2103)
if err := server.h3server.Close(); err != nil { if err := server.h3server.Close(); err != nil {
+56 -67
View File
@@ -20,11 +20,10 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/caddyserver/certmagic"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/certmagic"
"go.uber.org/zap"
) )
// AutoHTTPSConfig is used to disable automatic HTTPS // AutoHTTPSConfig is used to disable automatic HTTPS
@@ -84,8 +83,6 @@ func (ahc AutoHTTPSConfig) Skipped(name string, skipSlice []string) bool {
// even servers to the app, which still need to be set up with the // even servers to the app, which still need to be set up with the
// rest of them during provisioning. // rest of them during provisioning.
func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) error { func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) error {
logger := app.logger.Named("auto_https")
// this map acts as a set to store the domain names // this map acts as a set to store the domain names
// for which we will manage certificates automatically // for which we will manage certificates automatically
uniqueDomainsForCerts := make(map[string]struct{}) uniqueDomainsForCerts := make(map[string]struct{})
@@ -117,13 +114,13 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
srv.AutoHTTPS = new(AutoHTTPSConfig) srv.AutoHTTPS = new(AutoHTTPSConfig)
} }
if srv.AutoHTTPS.Disabled { if srv.AutoHTTPS.Disabled {
logger.Warn("automatic HTTPS is completely disabled for server", zap.String("server_name", srvName)) app.logger.Warn("automatic HTTPS is completely disabled for server", zap.String("server_name", srvName))
continue continue
} }
// skip if all listeners use the HTTP port // skip if all listeners use the HTTP port
if !srv.listenersUseAnyPortOtherThan(app.httpPort()) { if !srv.listenersUseAnyPortOtherThan(app.httpPort()) {
logger.Warn("server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server", app.logger.Warn("server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server",
zap.String("server_name", srvName), zap.String("server_name", srvName),
zap.Int("http_port", app.httpPort()), zap.Int("http_port", app.httpPort()),
) )
@@ -137,7 +134,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
// needing to specify one empty policy to enable it // needing to specify one empty policy to enable it
if srv.TLSConnPolicies == nil && if srv.TLSConnPolicies == nil &&
!srv.listenersUseAnyPortOtherThan(app.httpsPort()) { !srv.listenersUseAnyPortOtherThan(app.httpsPort()) {
logger.Info("server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS", app.logger.Info("server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS",
zap.String("server_name", srvName), zap.String("server_name", srvName),
zap.Int("https_port", app.httpsPort()), zap.Int("https_port", app.httpsPort()),
) )
@@ -189,16 +186,22 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
// a deduplicated list of names for which to obtain certs // a deduplicated list of names for which to obtain certs
// (only if cert management not disabled for this server) // (only if cert management not disabled for this server)
if srv.AutoHTTPS.DisableCerts { if srv.AutoHTTPS.DisableCerts {
logger.Warn("skipping automated certificate management for server because it is disabled", zap.String("server_name", srvName)) app.logger.Warn("skipping automated certificate management for server because it is disabled", zap.String("server_name", srvName))
} else { } else {
for d := range serverDomainSet { for d := range serverDomainSet {
// the implicit Tailscale manager module will get its own certs at run-time
if isTailscaleDomain(d) {
continue
}
if certmagic.SubjectQualifiesForCert(d) && if certmagic.SubjectQualifiesForCert(d) &&
!srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) { !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) {
// if a certificate for this name is already loaded, // if a certificate for this name is already loaded,
// don't obtain another one for it, unless we are // don't obtain another one for it, unless we are
// supposed to ignore loaded certificates // supposed to ignore loaded certificates
if !srv.AutoHTTPS.IgnoreLoadedCerts && app.tlsApp.HasCertificateForSubject(d) { if !srv.AutoHTTPS.IgnoreLoadedCerts &&
logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded", len(app.tlsApp.AllMatchingCertificates(d)) > 0 {
app.logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded",
zap.String("domain", d), zap.String("domain", d),
zap.String("server_name", srvName), zap.String("server_name", srvName),
) )
@@ -209,7 +212,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
// can handle that, but as a courtesy, warn the user // can handle that, but as a courtesy, warn the user
if strings.Contains(d, "*") && if strings.Contains(d, "*") &&
strings.Count(strings.Trim(d, "."), ".") == 1 { strings.Count(strings.Trim(d, "."), ".") == 1 {
logger.Warn("most clients do not trust second-level wildcard certificates (*.tld)", app.logger.Warn("most clients do not trust second-level wildcard certificates (*.tld)",
zap.String("domain", d)) zap.String("domain", d))
} }
@@ -225,11 +228,11 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
// nothing left to do if auto redirects are disabled // nothing left to do if auto redirects are disabled
if srv.AutoHTTPS.DisableRedir { if srv.AutoHTTPS.DisableRedir {
logger.Warn("automatic HTTP->HTTPS redirects are disabled", zap.String("server_name", srvName)) app.logger.Warn("automatic HTTP->HTTPS redirects are disabled", zap.String("server_name", srvName))
continue continue
} }
logger.Info("enabling automatic HTTP->HTTPS redirects", zap.String("server_name", srvName)) app.logger.Info("enabling automatic HTTP->HTTPS redirects", zap.String("server_name", srvName))
// create HTTP->HTTPS redirects // create HTTP->HTTPS redirects
for _, listenAddr := range srv.Listen { for _, listenAddr := range srv.Listen {
@@ -269,15 +272,12 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
// we now have a list of all the unique names for which we need certs; // we now have a list of all the unique names for which we need certs;
// turn the set into a slice so that phase 2 can use it // turn the set into a slice so that phase 2 can use it
app.allCertDomains = make([]string, 0, len(uniqueDomainsForCerts)) app.allCertDomains = make([]string, 0, len(uniqueDomainsForCerts))
var internal, tailscale []string var internal []string
uniqueDomainsLoop: uniqueDomainsLoop:
for d := range uniqueDomainsForCerts { for d := range uniqueDomainsForCerts {
if !isTailscaleDomain(d) { // whether or not there is already an automation policy for this
// whether or not there is already an automation policy for this // name, we should add it to the list to manage a cert for it
// name, we should add it to the list to manage a cert for it, app.allCertDomains = append(app.allCertDomains, d)
// unless it's a Tailscale domain, because we don't manage those
app.allCertDomains = append(app.allCertDomains, d)
}
// some names we've found might already have automation policies // some names we've found might already have automation policies
// explicitly specified for them; we should exclude those from // explicitly specified for them; we should exclude those from
@@ -295,15 +295,13 @@ uniqueDomainsLoop:
// if no automation policy exists for the name yet, we // if no automation policy exists for the name yet, we
// will associate it with an implicit one // will associate it with an implicit one
if isTailscaleDomain(d) { if !certmagic.SubjectQualifiesForPublicCert(d) {
tailscale = append(tailscale, d)
} else if !certmagic.SubjectQualifiesForPublicCert(d) {
internal = append(internal, d) internal = append(internal, d)
} }
} }
// ensure there is an automation policy to handle these certs // ensure there is an automation policy to handle these certs
err := app.createAutomationPolicies(ctx, internal, tailscale) err := app.createAutomationPolicies(ctx, internal)
if err != nil { if err != nil {
return err return err
} }
@@ -426,10 +424,6 @@ redirServersLoop:
} }
} }
logger.Debug("adjusted config",
zap.Reflect("tls", app.tlsApp),
zap.Reflect("http", app))
return nil return nil
} }
@@ -472,7 +466,7 @@ func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route {
// automation policy exists, it will be shallow-copied and used as the // automation policy exists, it will be shallow-copied and used as the
// base for the new ones (this is important for preserving behavior the // base for the new ones (this is important for preserving behavior the
// user intends to be "defaults"). // user intends to be "defaults").
func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames, tailscaleNames []string) error { func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []string) error {
// before we begin, loop through the existing automation policies // before we begin, loop through the existing automation policies
// and, for any ACMEIssuers we find, make sure they're filled in // and, for any ACMEIssuers we find, make sure they're filled in
// with default values that might be specified in our HTTP app; also // with default values that might be specified in our HTTP app; also
@@ -486,22 +480,6 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames, tails
app.tlsApp.Automation = new(caddytls.AutomationConfig) app.tlsApp.Automation = new(caddytls.AutomationConfig)
} }
for _, ap := range app.tlsApp.Automation.Policies { for _, ap := range app.tlsApp.Automation.Policies {
// on-demand policies can have the tailscale manager added implicitly
// if there's no explicit manager configured -- for convenience
if ap.OnDemand && len(ap.Managers) == 0 {
var ts caddytls.Tailscale
if err := ts.Provision(ctx); err != nil {
return err
}
ap.Managers = []certmagic.Manager{ts}
// must reprovision the automation policy so that the underlying
// CertMagic config knows about the updated Managers
if err := ap.Provision(app.tlsApp); err != nil {
return fmt.Errorf("re-provisioning automation policy: %v", err)
}
}
// set up default issuer -- honestly, this is only // set up default issuer -- honestly, this is only
// really necessary because the HTTP app is opinionated // really necessary because the HTTP app is opinionated
// and has settings which could be inferred as new // and has settings which could be inferred as new
@@ -523,6 +501,22 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames, tails
} }
} }
// if no external managers were configured, enable
// implicit Tailscale support for convenience
if ap.Managers == nil {
ts, err := implicitTailscale(ctx)
if err != nil {
return err
}
ap.Managers = []certmagic.Manager{ts}
// must reprovision the automation policy so that the underlying
// CertMagic config knows about the updated Managers
if err := ap.Provision(app.tlsApp); err != nil {
return fmt.Errorf("re-provisioning automation policy: %v", err)
}
}
// while we're here, is this the catch-all/base policy? // while we're here, is this the catch-all/base policy?
if !foundBasePolicy && len(ap.SubjectsRaw) == 0 { if !foundBasePolicy && len(ap.SubjectsRaw) == 0 {
basePolicy = ap basePolicy = ap
@@ -535,6 +529,15 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames, tails
basePolicy = new(caddytls.AutomationPolicy) basePolicy = new(caddytls.AutomationPolicy)
} }
if basePolicy.Managers == nil {
// add implicit Tailscale integration, for harmless convenience
ts, err := implicitTailscale(ctx)
if err != nil {
return err
}
basePolicy.Managers = []certmagic.Manager{ts}
}
// if the basePolicy has an existing ACMEIssuer (particularly to // if the basePolicy has an existing ACMEIssuer (particularly to
// include any type that embeds/wraps an ACMEIssuer), let's use it // include any type that embeds/wraps an ACMEIssuer), let's use it
// (I guess we just use the first one?), otherwise we'll make one // (I guess we just use the first one?), otherwise we'll make one
@@ -639,27 +642,6 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames, tails
} }
} }
// tailscale names go in their own automation policies because
// they require on-demand TLS to be enabled, which we obviously
// can't enable for everything
if len(tailscaleNames) > 0 {
policyCopy := *basePolicy
newPolicy := &policyCopy
var ts caddytls.Tailscale
if err := ts.Provision(ctx); err != nil {
return err
}
newPolicy.SubjectsRaw = tailscaleNames
newPolicy.Issuers = nil
newPolicy.Managers = append(newPolicy.Managers, ts)
err := app.tlsApp.AddAutomationPolicy(newPolicy)
if err != nil {
return err
}
}
// we just changed a lot of stuff, so double-check that it's all good // we just changed a lot of stuff, so double-check that it's all good
err := app.tlsApp.Validate() err := app.tlsApp.Validate()
if err != nil { if err != nil {
@@ -738,6 +720,13 @@ func (app *App) automaticHTTPSPhase2() error {
return nil return nil
} }
// implicitTailscale returns a new and provisioned Tailscale module configured to be optional.
func implicitTailscale(ctx caddy.Context) (caddytls.Tailscale, error) {
ts := caddytls.Tailscale{Optional: true}
err := ts.Provision(ctx)
return ts, err
}
func isTailscaleDomain(name string) bool { func isTailscaleDomain(name string) bool {
return strings.HasSuffix(strings.ToLower(name), ".ts.net") return strings.HasSuffix(strings.ToLower(name), ".ts.net")
} }
+3 -8
View File
@@ -23,21 +23,16 @@ import (
"net/http" "net/http"
"strings" "strings"
"sync" "sync"
"time"
"golang.org/x/sync/singleflight"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"golang.org/x/sync/singleflight"
) )
func init() { func init() {
caddy.RegisterModule(HTTPBasicAuth{}) caddy.RegisterModule(HTTPBasicAuth{})
caddy.RegisterNamespace("http.authentication.hashes", []interface{}{ weakrand.Seed(time.Now().UnixNano())
(*Comparer)(nil),
})
caddy.RegisterNamespace("http.authentication.providers", []interface{}{
(*Authenticator)(nil),
})
} }
// HTTPBasicAuth facilitates HTTP basic authentication. // HTTPBasicAuth facilitates HTTP basic authentication.
+1 -2
View File
@@ -18,10 +18,9 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"go.uber.org/zap"
) )
func init() { func init() {
+2 -4
View File
@@ -22,12 +22,10 @@ import (
"os" "os"
"os/signal" "os/signal"
"github.com/caddyserver/caddy/v2"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/term" "golang.org/x/term"
caddycmd "github.com/caddyserver/caddy/v2/cmd"
"github.com/caddyserver/caddy/v2"
) )
func init() { func init() {
+1 -2
View File
@@ -18,10 +18,9 @@ import (
"crypto/subtle" "crypto/subtle"
"encoding/base64" "encoding/base64"
"github.com/caddyserver/caddy/v2"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/scrypt" "golang.org/x/crypto/scrypt"
"github.com/caddyserver/caddy/v2"
) )
func init() { func init() {
+80 -5
View File
@@ -17,6 +17,7 @@ package caddyhttp
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"net" "net"
"net/http" "net/http"
@@ -164,7 +165,7 @@ func (ws *WeakString) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// MarshalJSON marshals was a boolean if true or false, // MarshalJSON marshals as a boolean if true or false,
// a number if an integer, or a string otherwise. // a number if an integer, or a string otherwise.
func (ws WeakString) MarshalJSON() ([]byte, error) { func (ws WeakString) MarshalJSON() ([]byte, error) {
if ws == "true" { if ws == "true" {
@@ -204,6 +205,82 @@ func (ws WeakString) String() string {
return string(ws) return string(ws)
} }
// Ratio is a type that unmarshals a valid numerical ratio string.
// Valid formats are:
// - a/b as a fraction (a / b)
// - a:b as a ratio (a / a+b)
// - a floating point number
type Ratio float64
// UnmarshalJSON satisfies json.Unmarshaler according to
// this type's documentation.
func (r *Ratio) UnmarshalJSON(b []byte) error {
if len(b) == 0 {
return io.EOF
}
if b[0] == byte('"') && b[len(b)-1] == byte('"') {
if !strings.Contains(string(b), "/") && !strings.Contains(string(b), ":") {
return fmt.Errorf("ratio string '%s' did not contain a slash '/' or colon ':'", string(b[1:len(b)-1]))
}
if strings.Contains(string(b), "/") {
left, right, _ := strings.Cut(string(b[1:len(b)-1]), "/")
num, err := strconv.Atoi(left)
if err != nil {
return fmt.Errorf("failed parsing numerator as integer %s: %v", left, err)
}
denom, err := strconv.Atoi(right)
if err != nil {
return fmt.Errorf("failed parsing denominator as integer %s: %v", right, err)
}
*r = Ratio(float64(num) / float64(denom))
return nil
}
if strings.Contains(string(b), ":") {
left, right, _ := strings.Cut(string(b[1:len(b)-1]), ":")
num, err := strconv.Atoi(left)
if err != nil {
return fmt.Errorf("failed parsing numerator as integer %s: %v", left, err)
}
denom, err := strconv.Atoi(right)
if err != nil {
return fmt.Errorf("failed parsing denominator as integer %s: %v", right, err)
}
*r = Ratio(float64(num) / (float64(num) + float64(denom)))
return nil
}
return fmt.Errorf("invalid ratio string '%s'", string(b[1:len(b)-1]))
}
if bytes.Equal(b, []byte("null")) {
return nil
}
float, err := strconv.ParseFloat(string(b), 64)
if err != nil {
return fmt.Errorf("failed parsing ratio as float %s: %v", b, err)
}
*r = Ratio(float)
return nil
}
func ParseRatio(r string) (Ratio, error) {
if strings.Contains(r, "/") {
left, right, _ := strings.Cut(r, "/")
num, err := strconv.Atoi(left)
if err != nil {
return 0, fmt.Errorf("failed parsing numerator as integer %s: %v", left, err)
}
denom, err := strconv.Atoi(right)
if err != nil {
return 0, fmt.Errorf("failed parsing denominator as integer %s: %v", right, err)
}
return Ratio(float64(num) / float64(denom)), nil
}
float, err := strconv.ParseFloat(r, 64)
if err != nil {
return 0, fmt.Errorf("failed parsing ratio as float %s: %v", r, err)
}
return Ratio(float), nil
}
// StatusCodeMatches returns true if a real HTTP status code matches // StatusCodeMatches returns true if a real HTTP status code matches
// the configured status code, which may be either a real HTTP status // the configured status code, which may be either a real HTTP status
// code or an integer representing a class of codes (e.g. 4 for all // code or an integer representing a class of codes (e.g. 4 for all
@@ -307,7 +384,5 @@ const (
const separator = string(filepath.Separator) const separator = string(filepath.Separator)
// Interface guard // Interface guard
var ( var _ caddy.ListenerWrapper = (*tlsPlaceholderWrapper)(nil)
_ caddy.ListenerWrapper = (*tlsPlaceholderWrapper)(nil) var _ caddyfile.Unmarshaler = (*tlsPlaceholderWrapper)(nil)
_ caddyfile.Unmarshaler = (*tlsPlaceholderWrapper)(nil)
)
+76
View File
@@ -149,3 +149,79 @@ func TestCleanPath(t *testing.T) {
} }
} }
} }
func TestUnmarshalRatio(t *testing.T) {
for i, tc := range []struct {
input []byte
expect float64
errMsg string
}{
{
input: []byte("null"),
expect: 0,
},
{
input: []byte(`"1/3"`),
expect: float64(1) / float64(3),
},
{
input: []byte(`"1/100"`),
expect: float64(1) / float64(100),
},
{
input: []byte(`"3:2"`),
expect: 0.6,
},
{
input: []byte(`"99:1"`),
expect: 0.99,
},
{
input: []byte(`"1/100"`),
expect: float64(1) / float64(100),
},
{
input: []byte(`0.1`),
expect: 0.1,
},
{
input: []byte(`0.005`),
expect: 0.005,
},
{
input: []byte(`0`),
expect: 0,
},
{
input: []byte(`"0"`),
errMsg: `ratio string '0' did not contain a slash '/' or colon ':'`,
},
{
input: []byte(`a`),
errMsg: `failed parsing ratio as float a: strconv.ParseFloat: parsing "a": invalid syntax`,
},
{
input: []byte(`"a/1"`),
errMsg: `failed parsing numerator as integer a: strconv.Atoi: parsing "a": invalid syntax`,
},
{
input: []byte(`"1/a"`),
errMsg: `failed parsing denominator as integer a: strconv.Atoi: parsing "a": invalid syntax`,
},
} {
ratio := Ratio(0)
err := ratio.UnmarshalJSON(tc.input)
if err != nil {
if tc.errMsg != "" {
if tc.errMsg != err.Error() {
t.Fatalf("Test %d: expected error: %v, got: %v", i, tc.errMsg, err)
}
continue
}
t.Fatalf("Test %d: invalid ratio: %v", i, err)
}
if ratio != Ratio(tc.expect) {
t.Fatalf("Test %d: expected %v, got %v", i, tc.expect, ratio)
}
}
}
+4 -12
View File
@@ -25,6 +25,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/google/cel-go/cel" "github.com/google/cel-go/cel"
"github.com/google/cel-go/common" "github.com/google/cel-go/common"
"github.com/google/cel-go/common/operators" "github.com/google/cel-go/common/operators"
@@ -37,9 +39,6 @@ import (
"github.com/google/cel-go/parser" "github.com/google/cel-go/parser"
"go.uber.org/zap" "go.uber.org/zap"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
) )
func init() { func init() {
@@ -235,11 +234,9 @@ func (cr celHTTPRequest) Parent() interpreter.Activation {
func (cr celHTTPRequest) ConvertToNative(typeDesc reflect.Type) (any, error) { func (cr celHTTPRequest) ConvertToNative(typeDesc reflect.Type) (any, error) {
return cr.Request, nil return cr.Request, nil
} }
func (celHTTPRequest) ConvertToType(typeVal ref.Type) ref.Val { func (celHTTPRequest) ConvertToType(typeVal ref.Type) ref.Val {
panic("not implemented") panic("not implemented")
} }
func (cr celHTTPRequest) Equal(other ref.Val) ref.Val { func (cr celHTTPRequest) Equal(other ref.Val) ref.Val {
if o, ok := other.Value().(celHTTPRequest); ok { if o, ok := other.Value().(celHTTPRequest); ok {
return types.Bool(o.Request == cr.Request) return types.Bool(o.Request == cr.Request)
@@ -258,14 +255,9 @@ type celPkixName struct{ *pkix.Name }
func (pn celPkixName) ConvertToNative(typeDesc reflect.Type) (any, error) { func (pn celPkixName) ConvertToNative(typeDesc reflect.Type) (any, error) {
return pn.Name, nil return pn.Name, nil
} }
func (celPkixName) ConvertToType(typeVal ref.Type) ref.Val {
func (pn celPkixName) ConvertToType(typeVal ref.Type) ref.Val {
if typeVal.TypeName() == "string" {
return types.String(pn.Name.String())
}
panic("not implemented") panic("not implemented")
} }
func (pn celPkixName) Equal(other ref.Val) ref.Val { func (pn celPkixName) Equal(other ref.Val) ref.Val {
if o, ok := other.Value().(string); ok { if o, ok := other.Value().(string); ok {
return types.Bool(pn.Name.String() == o) return types.Bool(pn.Name.String() == o)
@@ -501,7 +493,7 @@ func celMatcherStringMacroExpander(funcName string) parser.MacroExpander {
} }
} }
// celMatcherJSONMacroExpander validates that the macro is called a single // celMatcherStringMacroExpander validates that the macro is called a single
// map literal argument. // map literal argument.
// //
// The following function call is returned: <funcName>(request, arg) // The following function call is returned: <funcName>(request, arg)
-26
View File
@@ -1,26 +0,0 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !go1.21
package caddyhttp
import (
"net/http"
)
func enableFullDuplex(w http.ResponseWriter) error {
// Do nothing, Go 1.20 and earlier do not support full duplex
return nil
}
-26
View File
@@ -1,26 +0,0 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build go1.21
package caddyhttp
import (
"net/http"
)
func enableFullDuplex(w http.ResponseWriter) error {
//nolint:bodyclose
return http.NewResponseController(w).EnableFullDuplex()
}
+12 -22
View File
@@ -37,9 +37,6 @@ import (
func init() { func init() {
caddy.RegisterModule(Encode{}) caddy.RegisterModule(Encode{})
caddy.RegisterNamespace("http.encoders", []interface{}{
(*Encoding)(nil),
})
} }
// Encode is a middleware which can encode responses. // Encode is a middleware which can encode responses.
@@ -96,7 +93,6 @@ func (enc *Encode) Provision(ctx caddy.Context) error {
"application/xhtml+xml*", "application/xhtml+xml*",
"application/atom+xml*", "application/atom+xml*",
"application/rss+xml*", "application/rss+xml*",
"application/wasm*",
"image/svg+xml*", "image/svg+xml*",
}, },
}, },
@@ -171,10 +167,10 @@ func (enc *Encode) openResponseWriter(encodingName string, w http.ResponseWriter
// initResponseWriter initializes the responseWriter instance // initResponseWriter initializes the responseWriter instance
// allocated in openResponseWriter, enabling mid-stack inlining. // allocated in openResponseWriter, enabling mid-stack inlining.
func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter) *responseWriter { func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter) *responseWriter {
if rww, ok := wrappedRW.(*caddyhttp.ResponseWriterWrapper); ok { if httpInterfaces, ok := wrappedRW.(caddyhttp.HTTPInterfaces); ok {
rw.ResponseWriter = rww rw.HTTPInterfaces = httpInterfaces
} else { } else {
rw.ResponseWriter = &caddyhttp.ResponseWriterWrapper{ResponseWriter: wrappedRW} rw.HTTPInterfaces = &caddyhttp.ResponseWriterWrapper{ResponseWriter: wrappedRW}
} }
rw.encodingName = encodingName rw.encodingName = encodingName
rw.config = enc rw.config = enc
@@ -186,7 +182,7 @@ func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, w
// using the encoding represented by encodingName and // using the encoding represented by encodingName and
// configured by config. // configured by config.
type responseWriter struct { type responseWriter struct {
http.ResponseWriter caddyhttp.HTTPInterfaces
encodingName string encodingName string
w Encoder w Encoder
config *Encode config *Encode
@@ -215,8 +211,7 @@ func (rw *responseWriter) Flush() {
// to rw.Write (see bug in #4314) // to rw.Write (see bug in #4314)
return return
} }
//nolint:bodyclose rw.HTTPInterfaces.Flush()
http.NewResponseController(rw.ResponseWriter).Flush()
} }
// Hijack implements http.Hijacker. It will flush status code if set. We don't track actual hijacked // Hijack implements http.Hijacker. It will flush status code if set. We don't track actual hijacked
@@ -224,12 +219,11 @@ func (rw *responseWriter) Flush() {
func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if !rw.wroteHeader { if !rw.wroteHeader {
if rw.statusCode != 0 { if rw.statusCode != 0 {
rw.ResponseWriter.WriteHeader(rw.statusCode) rw.HTTPInterfaces.WriteHeader(rw.statusCode)
} }
rw.wroteHeader = true rw.wroteHeader = true
} }
//nolint:bodyclose return rw.HTTPInterfaces.Hijack()
return http.NewResponseController(rw.ResponseWriter).Hijack()
} }
// Write writes to the response. If the response qualifies, // Write writes to the response. If the response qualifies,
@@ -266,7 +260,7 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
// by the standard library // by the standard library
if !rw.wroteHeader { if !rw.wroteHeader {
if rw.statusCode != 0 { if rw.statusCode != 0 {
rw.ResponseWriter.WriteHeader(rw.statusCode) rw.HTTPInterfaces.WriteHeader(rw.statusCode)
} }
rw.wroteHeader = true rw.wroteHeader = true
} }
@@ -274,7 +268,7 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
if rw.w != nil { if rw.w != nil {
return rw.w.Write(p) return rw.w.Write(p)
} else { } else {
return rw.ResponseWriter.Write(p) return rw.HTTPInterfaces.Write(p)
} }
} }
@@ -290,7 +284,7 @@ func (rw *responseWriter) Close() error {
// issue #5059, don't write status code if not set explicitly. // issue #5059, don't write status code if not set explicitly.
if rw.statusCode != 0 { if rw.statusCode != 0 {
rw.ResponseWriter.WriteHeader(rw.statusCode) rw.HTTPInterfaces.WriteHeader(rw.statusCode)
} }
rw.wroteHeader = true rw.wroteHeader = true
} }
@@ -305,18 +299,13 @@ func (rw *responseWriter) Close() error {
return err return err
} }
// Unwrap returns the underlying ResponseWriter.
func (rw *responseWriter) Unwrap() http.ResponseWriter {
return rw.ResponseWriter
}
// init should be called before we write a response, if rw.buf has contents. // init should be called before we write a response, if rw.buf has contents.
func (rw *responseWriter) init() { func (rw *responseWriter) init() {
if rw.Header().Get("Content-Encoding") == "" && isEncodeAllowed(rw.Header()) && if rw.Header().Get("Content-Encoding") == "" && isEncodeAllowed(rw.Header()) &&
rw.config.Match(rw) { rw.config.Match(rw) {
rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder) rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder)
rw.w.Reset(rw.ResponseWriter) rw.w.Reset(rw.HTTPInterfaces)
rw.Header().Del("Content-Length") // https://github.com/golang/go/issues/14975 rw.Header().Del("Content-Length") // https://github.com/golang/go/issues/14975
rw.Header().Set("Content-Encoding", rw.encodingName) rw.Header().Set("Content-Encoding", rw.encodingName)
rw.Header().Add("Vary", "Accept-Encoding") rw.Header().Add("Vary", "Accept-Encoding")
@@ -435,4 +424,5 @@ var (
_ caddy.Provisioner = (*Encode)(nil) _ caddy.Provisioner = (*Encode)(nil)
_ caddy.Validator = (*Encode)(nil) _ caddy.Validator = (*Encode)(nil)
_ caddyhttp.MiddlewareHandler = (*Encode)(nil) _ caddyhttp.MiddlewareHandler = (*Encode)(nil)
_ caddyhttp.HTTPInterfaces = (*responseWriter)(nil)
) )
+1 -2
View File
@@ -18,11 +18,10 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"github.com/klauspost/compress/gzip"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
"github.com/klauspost/compress/gzip"
) )
func init() { func init() {
+1 -2
View File
@@ -15,11 +15,10 @@
package caddyzstd package caddyzstd
import ( import (
"github.com/klauspost/compress/zstd"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
"github.com/klauspost/compress/zstd"
) )
func init() { func init() {
+6 -3
View File
@@ -15,24 +15,27 @@
package caddyhttp package caddyhttp
import ( import (
"errors"
"fmt" "fmt"
weakrand "math/rand" weakrand "math/rand"
"path" "path"
"runtime" "runtime"
"strings" "strings"
"time"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
) )
func init() {
weakrand.Seed(time.Now().UnixNano())
}
// Error is a convenient way for a Handler to populate the // Error is a convenient way for a Handler to populate the
// essential fields of a HandlerError. If err is itself a // essential fields of a HandlerError. If err is itself a
// HandlerError, then any essential fields that are not // HandlerError, then any essential fields that are not
// set will be populated. // set will be populated.
func Error(statusCode int, err error) HandlerError { func Error(statusCode int, err error) HandlerError {
const idLen = 9 const idLen = 9
var he HandlerError if he, ok := err.(HandlerError); ok {
if errors.As(err, &he) {
if he.ID == "" { if he.ID == "" {
he.ID = randString(idLen, true) he.ID = randString(idLen, true)
} }
+11 -18
View File
@@ -29,25 +29,18 @@ import (
"sync" "sync"
"text/template" "text/template"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/templates" "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 //go:embed browse.html
var BrowseTemplate string var defaultBrowseTemplate string
// Browse configures directory browsing. // Browse configures directory browsing.
type Browse struct { type Browse struct {
// Filename of the template to use instead of the embedded browse template. // Use this template file instead of the default browse template.
TemplateFile string `json:"template_file,omitempty"` TemplateFile string `json:"template_file,omitempty"`
} }
@@ -89,8 +82,8 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
// TODO: not entirely sure if path.Clean() is necessary here but seems like a safe plan (i.e. /%2e%2e%2f) - someone could verify this // calling path.Clean here prevents weird breadcrumbs when URL paths are sketchy like /%2e%2e%2f
listing, err := fsrv.loadDirectoryContents(r.Context(), dir.(fs.ReadDirFile), root, path.Clean(r.URL.EscapedPath()), repl) listing, err := fsrv.loadDirectoryContents(r.Context(), dir.(fs.ReadDirFile), root, path.Clean(r.URL.Path), repl)
switch { switch {
case os.IsPermission(err): case os.IsPermission(err):
return caddyhttp.Error(http.StatusForbidden, err) return caddyhttp.Error(http.StatusForbidden, err)
@@ -100,7 +93,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
return caddyhttp.Error(http.StatusInternalServerError, err) return caddyhttp.Error(http.StatusInternalServerError, err)
} }
fsrv.browseApplyQueryParams(w, r, listing) fsrv.browseApplyQueryParams(w, r, &listing)
buf := bufPool.Get().(*bytes.Buffer) buf := bufPool.Get().(*bytes.Buffer)
buf.Reset() buf.Reset()
@@ -120,7 +113,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
fs = http.Dir(repl.ReplaceAll(fsrv.Root, ".")) fs = http.Dir(repl.ReplaceAll(fsrv.Root, "."))
} }
tplCtx := &templateContext{ var tplCtx = &templateContext{
TemplateContext: templates.TemplateContext{ TemplateContext: templates.TemplateContext{
Root: fs, Root: fs,
Req: r, Req: r,
@@ -144,10 +137,10 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
return nil return nil
} }
func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, dir fs.ReadDirFile, root, urlPath string, repl *caddy.Replacer) (*browseTemplateContext, error) { func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, dir fs.ReadDirFile, root, urlPath string, repl *caddy.Replacer) (browseTemplateContext, error) {
files, err := dir.ReadDir(10000) // TODO: this limit should probably be configurable files, err := dir.ReadDir(10000) // TODO: this limit should probably be configurable
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
return nil, err return browseTemplateContext{}, err
} }
// user can presumably browse "up" to parent folder if path is longer than "/" // user can presumably browse "up" to parent folder if path is longer than "/"
@@ -211,7 +204,7 @@ func (fsrv *FileServer) makeBrowseTemplate(tplCtx *templateContext) (*template.T
} }
} else { } else {
tpl = tplCtx.NewTemplate("default_listing") tpl = tplCtx.NewTemplate("default_listing")
tpl, err = tpl.Parse(BrowseTemplate) tpl, err = tpl.Parse(defaultBrowseTemplate)
if err != nil { if err != nil {
return nil, fmt.Errorf("parsing default browse template: %v", err) return nil, fmt.Errorf("parsing default browse template: %v", err)
} }
@@ -244,7 +237,7 @@ func isSymlink(f fs.FileInfo) bool {
// features. // features.
type templateContext struct { type templateContext struct {
templates.TemplateContext templates.TemplateContext
*browseTemplateContext browseTemplateContext
} }
// bufPool is used to increase the efficiency of file listings. // bufPool is used to increase the efficiency of file listings.
+110 -329
View File
@@ -1,296 +1,90 @@
{{- define "icon"}} {{- define "icon"}}
{{- if .IsDir}} {{- if .IsDir}}
{{- if .IsSymlink}} <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-folder-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<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 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>
<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 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> </svg>
{{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp" ".tiff" ".bmp" ".heif" ".heic" ".svg"}} {{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp"}}
{{- if eq .Tpl.Layout "grid"}} <img src="{{html .Name}}">
<img loading="lazy" src="{{html .Name}}"> {{- else if .HasExt ".mp4" ".mov" ".mpeg" ".avi" ".ogg" ".webm"}}
{{- 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 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" ".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"> <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 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 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 d="M8 4l0 16"></path>
<path d="M16 4l0 16"/> <path d="M16 4l0 16"></path>
<path d="M4 8l4 0"/> <path d="M4 8l4 0"></path>
<path d="M4 16l4 0"/> <path d="M4 16l4 0"></path>
<path d="M4 12l16 0"/> <path d="M4 12l16 0"></path>
<path d="M16 8l4 0"/> <path d="M16 8l4 0"></path>
<path d="M16 16l4 0"/> <path d="M16 16l4 0"></path>
</svg> </svg>
{{- else if .HasExt ".mp3" ".m4a" ".aac" ".ogg" ".flac" ".wav" ".wma" ".midi" ".cda"}} {{- else if .HasExt ".mp3" ".flac" ".wav" ".wma"}}
<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"> <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 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 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 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 d="M9 17l0 -13l10 0l0 13"></path>
<path d="M9 8l10 0"/> <path d="M9 8l10 0"></path>
</svg> </svg>
{{- else if .HasExt ".pdf"}} {{- else if .HasExt ".pdf"}}
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-type-pdf" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M14 3v4a1 1 0 0 0 1 1h4"/> <path d="M10 8v8h2a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2h-2z"></path>
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"/> <path d="M3 12h2a2 2 0 1 0 0 -4h-2v8"></path>
<path d="M5 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6"/> <path d="M17 12h3"></path>
<path d="M17 18h2"/> <path d="M21 8h-4v8"></path>
<path d="M20 15h-3v6"/>
<path d="M11 15v6h1a2 2 0 0 0 2 -2v-2a2 2 0 0 0 -2 -2h-1z"/>
</svg> </svg>
{{- else if .HasExt ".csv" ".tsv"}} {{- else if .HasExt ".txt"}}
<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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M14 3v4a1 1 0 0 0 1 1h4"/> <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 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 d="M9 9l1 0"></path>
<path d="M9 13l6 0"/> <path d="M9 13l6 0"></path>
<path d="M9 17l6 0"/> <path d="M9 17l6 0"></path>
</svg> </svg>
{{- else if .HasExt ".xls" ".xlsx" ".ods" ".fods"}} {{- else if .HasExt ".zip" ".gz" ".xz" ".tar"}}
<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 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 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"> <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 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 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 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 d="M11 5l-1 0"></path>
<path d="M13 7l-1 0"/> <path d="M13 7l-1 0"></path>
<path d="M11 9l-1 0"/> <path d="M11 9l-1 0"></path>
<path d="M13 11l-1 0"/> <path d="M13 11l-1 0"></path>
<path d="M11 13l-1 0"/> <path d="M11 13l-1 0"></path>
<path d="M13 15l-1 0"/> <path d="M13 15l-1 0"></path>
</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 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 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 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 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 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 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 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> </svg>
{{- else if .HasExt ".md" ".mdown" ".markdown"}} {{- 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"> <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 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 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 d="M7 15v-6l2 2l2 -2v6"></path>
<path d="M14 13l2 2l2 -2m-2 2v-6"/> <path d="M14 13l2 2l2 -2m-2 2v-6"></path>
</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 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> </svg>
{{- else if .HasExt ".go"}} {{- 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"> <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 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 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 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 d="M5.5 15h-1.5"></path>
<path d="M6 9h-2"/> <path d="M6 9h-2"></path>
<path d="M5 12h-3"/> <path d="M5 12h-3"></path>
</svg>
{{- else if .HasExt ".html" ".htm"}}
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-type-html" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"/>
<path d="M2 21v-6"/>
<path d="M5 15v6"/>
<path d="M2 18h3"/>
<path d="M20 15v6h2"/>
<path d="M13 21v-6l2 3l2 -3v6"/>
<path d="M7.5 15h3"/>
<path d="M9 15v6"/>
</svg>
{{- else if .HasExt ".js"}}
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-type-js" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path 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 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-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 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 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 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 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> </svg>
{{- else}} {{- else}}
{{- if .IsSymlink}} {{- 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 21v-4a3 3 0 0 1 3 -3h5"/> <path d="M4 21v-4a3 3 0 0 1 3 -3h5"></path>
<path d="M9 17l3 -3l-3 -3"/> <path d="M9 17l3 -3l-3 -3"></path>
<path d="M14 3v4a1 1 0 0 0 1 1h4"/> <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 d="M5 11v-6a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-9.5"></path>
</svg> </svg>
{{- else}} {{- 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M14 3v4a1 1 0 0 0 1 1h4"/> <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 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>
</svg> </svg>
{{- end}} {{- end}}
{{- end}} {{- end}}
@@ -299,7 +93,6 @@
<html> <html>
<head> <head>
<title>{{html .Name}}</title> <title>{{html .Name}}</title>
<link rel="canonical" href="{{.Path}}/" />
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="color-scheme" content="light dark"> <meta name="color-scheme" content="light dark">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -311,7 +104,6 @@ body {
font-size: 16px; font-size: 16px;
text-rendering: optimizespeed; text-rendering: optimizespeed;
background-color: #f3f6f7; background-color: #f3f6f7;
min-height: 100vh;
} }
img, img,
@@ -409,7 +201,7 @@ header {
h1 { h1 {
font-size: 20px; font-size: 20px;
font-family: Poppins, system-ui, sans-serif; font-family: Poppins;
font-weight: normal; font-weight: normal;
white-space: nowrap; white-space: nowrap;
overflow-x: hidden; overflow-x: hidden;
@@ -592,14 +384,17 @@ td .go-up {
} }
.grid { .grid {
display: grid; display: flex;
grid-template-columns: repeat(auto-fill, minmax(16em, 1fr));
gap: 2px; gap: 2px;
flex-wrap: wrap;
} }
.grid .entry { .grid .entry {
position: relative; position: relative;
width: 100%; width: 20%;
min-width: 250px;
max-width: 400px;
flex: 1;
} }
.grid .entry a { .grid .entry a {
@@ -663,13 +458,8 @@ footer {
#filter { #filter {
max-width: 100px; max-width: 100px;
} }
.grid .entry {
max-width: initial;
}
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
html { html {
background: black; /* overscroll */ background: black; /* overscroll */
@@ -694,10 +484,6 @@ footer {
color: white; color: white;
} }
h1 {
color: white;
}
h1 a:hover { h1 a:hover {
background: hsl(213deg 100% 73% / 20%); background: hsl(213deg 100% 73% / 20%);
} }
@@ -765,16 +551,15 @@ footer {
stroke: #ccc !important; stroke: #ccc !important;
} }
} }
</style> </style>
{{- if eq .Layout "grid"}} {{- if eq .Layout "grid"}}
<style>.wrapper { max-width: none; } main { margin-top: 1px; }</style> <style>.wrapper { max-width: none; } main { margin-top: 1px; }</style>
{{- end}} {{- end}}
</head> </head>
<body onload="initPage()"> <body onload="initPage()">
<header> <header>
<div class="wrapper"> <div class="wrapper">
<div class="breadcrumbs">Folder Path</div> <div class="breadcrumbs">Folder Path</div>
<h1> <h1>
{{range $i, $crumb := .Breadcrumbs}}<a href="{{html $crumb.Link}}">{{html $crumb.Text}}</a>{{if ne $i 0}}/{{end}}{{end}} {{range $i, $crumb := .Breadcrumbs}}<a href="{{html $crumb.Link}}">{{html $crumb.Text}}</a>{{if ne $i 0}}/{{end}}{{end}}
</h1> </h1>
@@ -798,19 +583,19 @@ footer {
</div> </div>
<a href="javascript:queryParam('layout', '')" id="layout-list" class='layout{{if eq $.Layout "list" ""}}current{{end}}'> <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"> <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-layout-list" width="16" height="16" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"/> <path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></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 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>
</svg> </svg>
List List
</a> </a>
<a href="javascript:queryParam('layout', 'grid')" id="layout-grid" class='layout{{if eq $.Layout "grid"}}current{{end}}'> <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"> <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-layout-grid" width="16" height="16" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/> <path d="M4 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"></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 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 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 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>
</svg> </svg>
Grid Grid
</a> </a>
@@ -835,22 +620,22 @@ footer {
{{- if and (eq .Sort "namedirfirst") (ne .Order "desc")}} {{- 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"> <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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M18 14l-6 -6l-6 6h12"/> <path d="M18 14l-6 -6l-6 6h12"></path>
</svg> </svg>
</a> </a>
{{- else if and (eq .Sort "namedirfirst") (ne .Order "asc")}} {{- 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"> <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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 10l6 6l6 -6h-12"/> <path d="M6 10l6 6l6 -6h-12"></path>
</svg> </svg>
</a> </a>
{{- else}} {{- else}}
<a href="?sort=namedirfirst&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}" class="icon sort"> <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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M18 14l-6 -6l-6 6h12"/> <path d="M18 14l-6 -6l-6 6h12"></path>
</svg> </svg>
</a> </a>
{{- end}} {{- end}}
@@ -859,16 +644,16 @@ footer {
<a href="?sort=name&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}"> <a href="?sort=name&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
Name 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M18 14l-6 -6l-6 6h12"/> <path d="M18 14l-6 -6l-6 6h12"></path>
</svg> </svg>
</a> </a>
{{- else if and (eq .Sort "name") (ne .Order "asc")}} {{- 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}}"> <a href="?sort=name&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
Name 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 10l6 6l6 -6h-12"/> <path d="M6 10l6 6l6 -6h-12"></path>
</svg> </svg>
</a> </a>
{{- else}} {{- else}}
@@ -879,11 +664,11 @@ footer {
<div class="filter-container"> <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"> <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 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 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 d="M21 21l-6 -6"></path>
</svg> </svg>
<input type="search" placeholder="Search" id="filter" onkeyup='filter()'> <input type="text" placeholder="Search" id="filter" onkeyup='filter()'>
</div> </div>
</th> </th>
<th> <th>
@@ -891,16 +676,16 @@ footer {
<a href="?sort=size&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}"> <a href="?sort=size&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
Size 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M18 14l-6 -6l-6 6h12"/> <path d="M18 14l-6 -6l-6 6h12"></path>
</svg> </svg>
</a> </a>
{{- else if and (eq .Sort "size") (ne .Order "asc")}} {{- 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}}"> <a href="?sort=size&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
Size 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 10l6 6l6 -6h-12"/> <path d="M6 10l6 6l6 -6h-12"></path>
</svg> </svg>
</a> </a>
{{- else}} {{- else}}
@@ -914,16 +699,16 @@ footer {
<a href="?sort=time&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}"> <a href="?sort=time&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
Modified 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M18 14l-6 -6l-6 6h12"/> <path d="M18 14l-6 -6l-6 6h12"></path>
</svg> </svg>
</a> </a>
{{- else if and (eq .Sort "time") (ne .Order "asc")}} {{- 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}}"> <a href="?sort=time&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
Modified 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 10l6 6l6 -6h-12"/> <path d="M6 10l6 6l6 -6h-12"></path>
</svg> </svg>
</a> </a>
{{- else}} {{- else}}
@@ -942,8 +727,8 @@ footer {
<td> <td>
<a href=".."> <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"> <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 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 d="M18 18h-6a3 3 0 0 1 -3 -3v-10l-4 4m8 0l-4 -4"></path>
</svg> </svg>
<span class="go-up">Up</span> <span class="go-up">Up</span>
</a> </a>
@@ -1065,11 +850,11 @@ footer {
<script> <script>
const filterEl = document.getElementById('filter'); const filterEl = document.getElementById('filter');
filterEl?.focus({ preventScroll: true }); filterEl.focus({ preventScroll: true });
function initPage() { function initPage() {
// populate and evaluate filter // populate and evaluate filter
if (!filterEl?.value) { if (!filterEl.value) {
const filterParam = new URL(window.location.href).searchParams.get('filter'); const filterParam = new URL(window.location.href).searchParams.get('filter');
if (filterParam) { if (filterParam) {
filterEl.value = filterParam; filterEl.value = filterParam;
@@ -1084,15 +869,11 @@ footer {
}); });
document.querySelectorAll('.size').forEach(el => { document.querySelectorAll('.size').forEach(el => {
const size = Number(el.dataset.size); const size = Number(el.dataset.size);
const sizebar = el.querySelector('.sizebar-bar'); el.querySelector('.sizebar-bar').style.width = `${size/largest * 100}%`;
if (sizebar) {
sizebar.style.width = `${size/largest * 100}%`;
}
}); });
} }
function filter() { function filter() {
if (!filterEl) return;
const q = filterEl.value.trim().toLowerCase(); const q = filterEl.value.trim().toLowerCase();
document.querySelectorAll('tr.file').forEach(function(el) { document.querySelectorAll('tr.file').forEach(function(el) {
if (!q) { if (!q) {
@@ -1135,7 +916,7 @@ footer {
return; return;
} }
} }
e.textContent = d.toLocaleString(); e.textContent = d.toLocaleString([], {day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit"});
} }
var timeList = Array.prototype.slice.call(document.getElementsByTagName("time")); var timeList = Array.prototype.slice.call(document.getElementsByTagName("time"));
timeList.forEach(localizeDatetime); timeList.forEach(localizeDatetime);
@@ -25,23 +25,17 @@ import (
"strings" "strings"
"time" "time"
"github.com/dustin/go-humanize"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/dustin/go-humanize"
"go.uber.org/zap"
) )
func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEntry, canGoUp bool, root, urlPath string, repl *caddy.Replacer) *browseTemplateContext { func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEntry, canGoUp bool, root, urlPath string, repl *caddy.Replacer) browseTemplateContext {
filesToHide := fsrv.transformHidePaths(repl) filesToHide := fsrv.transformHidePaths(repl)
name, _ := url.PathUnescape(urlPath) var dirCount, fileCount int
fileInfos := []fileInfo{}
tplCtx := &browseTemplateContext{
Name: path.Base(name),
Path: urlPath,
CanGoUp: canGoUp,
}
for _, entry := range entries { for _, entry := range entries {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
@@ -67,9 +61,9 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEn
// add the slash after the escape of path to avoid escaping the slash as well // add the slash after the escape of path to avoid escaping the slash as well
if isDir { if isDir {
name += "/" name += "/"
tplCtx.NumDirs++ dirCount++
} else { } else {
tplCtx.NumFiles++ fileCount++
} }
size := info.Size() size := info.Size()
@@ -88,7 +82,7 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEn
u := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name u := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name
tplCtx.Items = append(tplCtx.Items, fileInfo{ fileInfos = append(fileInfos, fileInfo{
IsDir: isDir, IsDir: isDir,
IsSymlink: fileIsSymlink, IsSymlink: fileIsSymlink,
Name: name, Name: name,
@@ -96,11 +90,17 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEn
URL: u.String(), URL: u.String(),
ModTime: info.ModTime().UTC(), ModTime: info.ModTime().UTC(),
Mode: info.Mode(), Mode: info.Mode(),
Tpl: tplCtx, // a reference up to the template context is useful
}) })
} }
name, _ := url.PathUnescape(urlPath)
return tplCtx return browseTemplateContext{
Name: path.Base(name),
Path: urlPath,
CanGoUp: canGoUp,
Items: fileInfos,
NumDirs: dirCount,
NumFiles: fileCount,
}
} }
// browseTemplateContext provides the template context for directory listings. // browseTemplateContext provides the template context for directory listings.
@@ -230,9 +230,6 @@ type fileInfo struct {
Mode os.FileMode `json:"mode"` Mode os.FileMode `json:"mode"`
IsDir bool `json:"is_dir"` IsDir bool `json:"is_dir"`
IsSymlink bool `json:"is_symlink"` IsSymlink bool `json:"is_symlink"`
// a pointer to the template context is useful inside nested templates
Tpl *browseTemplateContext `json:"-"`
} }
// HasExt returns true if the filename has any of the given suffixes, case-insensitive. // HasExt returns true if the filename has any of the given suffixes, case-insensitive.
@@ -25,45 +25,6 @@ func TestBreadcrumbs(t *testing.T) {
}{ }{
{"", []crumb{}}, {"", []crumb{}},
{"/", []crumb{{Text: "/"}}}, {"/", []crumb{{Text: "/"}}},
{"/foo/", []crumb{
{Link: "../", Text: "/"},
{Link: "", Text: "foo"},
}},
{"/foo/bar/", []crumb{
{Link: "../../", Text: "/"},
{Link: "../", Text: "foo"},
{Link: "", Text: "bar"},
}},
{"/foo bar/", []crumb{
{Link: "../", Text: "/"},
{Link: "", Text: "foo bar"},
}},
{"/foo bar/baz/", []crumb{
{Link: "../../", Text: "/"},
{Link: "../", Text: "foo bar"},
{Link: "", Text: "baz"},
}},
{"/100%25 test coverage/is a lie/", []crumb{
{Link: "../../", Text: "/"},
{Link: "../", Text: "100% test coverage"},
{Link: "", Text: "is a lie"},
}},
{"/AC%2FDC/", []crumb{
{Link: "../", Text: "/"},
{Link: "", Text: "AC/DC"},
}},
{"/foo/%2e%2e%2f/bar", []crumb{
{Link: "../../../", Text: "/"},
{Link: "../../", Text: "foo"},
{Link: "../", Text: "../"},
{Link: "", Text: "bar"},
}},
{"/foo/../bar", []crumb{
{Link: "../../../", Text: "/"},
{Link: "../../", Text: "foo"},
{Link: "../", Text: ".."},
{Link: "", Text: "bar"},
}},
{"foo/bar/baz", []crumb{ {"foo/bar/baz", []crumb{
{Link: "../../", Text: "foo"}, {Link: "../../", Text: "foo"},
{Link: "../", Text: "bar"}, {Link: "../", Text: "bar"},
@@ -90,16 +51,16 @@ func TestBreadcrumbs(t *testing.T) {
}}, }},
} }
for testNum, d := range testdata { for _, d := range testdata {
l := browseTemplateContext{Path: d.path} l := browseTemplateContext{Path: d.path}
actual := l.Breadcrumbs() actual := l.Breadcrumbs()
if len(actual) != len(d.expected) { if len(actual) != len(d.expected) {
t.Errorf("Test %d: Got %d components but expected %d; got: %+v", testNum, len(actual), len(d.expected), actual) t.Errorf("wrong size output, got %d elements but expected %d", len(actual), len(d.expected))
continue continue
} }
for i, c := range actual { for i, c := range actual {
if c != d.expected[i] { if c != d.expected[i] {
t.Errorf("Test %d crumb %d: got %#v but expected %#v at index %d", testNum, i, c, d.expected[i], i) t.Errorf("got %#v but expected %#v at index %d", c, d.expected[i], i)
} }
} }
} }
+9 -78
View File
@@ -16,30 +16,24 @@ package fileserver
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io"
"log" "log"
"os"
"strconv" "strconv"
"time" "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/caddyserver/certmagic"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.uber.org/zap" "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() { func init() {
caddycmd.RegisterCommand(caddycmd.Command{ caddycmd.RegisterCommand(caddycmd.Command{
Name: "file-server", Name: "file-server",
Usage: "[--domain <example.com>] [--root <path>] [--listen <addr>] [--browse] [--access-log] [--precompressed]", Usage: "[--domain <example.com>] [--root <path>] [--listen <addr>] [--browse] [--access-log]",
Short: "Spins up a production-ready file server", Short: "Spins up a production-ready file server",
Long: ` Long: `
A simple but production-ready file server. Useful for quick deployments, A simple but production-ready file server. Useful for quick deployments,
@@ -52,31 +46,17 @@ 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 a public domain, ensure A/AAAA records are properly configured before
using this option. 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 If --browse is enabled, requests for folders without an index file will
respond with a file listing.`, respond with a file listing.`,
CobraFunc: func(cmd *cobra.Command) { CobraFunc: func(cmd *cobra.Command) {
cmd.Flags().StringP("domain", "d", "", "Domain name at which to serve the files") 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("root", "r", "", "The path to the root of the site")
cmd.Flags().StringP("listen", "l", "", "The address to which to bind the listener") cmd.Flags().StringP("listen", "", "", "The address to which to bind the listener")
cmd.Flags().BoolP("browse", "b", false, "Enable directory browsing") cmd.Flags().BoolP("browse", "b", false, "Enable directory browsing")
cmd.Flags().BoolP("templates", "t", false, "Enable template rendering") cmd.Flags().BoolP("templates", "t", false, "Enable template rendering")
cmd.Flags().BoolP("access-log", "a", false, "Enable the access log") cmd.Flags().BoolP("access-log", "", false, "Enable the access log")
cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs") 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.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, BrowseTemplate)
return err
},
})
}, },
}) })
} }
@@ -91,64 +71,15 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
templates := fs.Bool("templates") templates := fs.Bool("templates")
accessLog := fs.Bool("access-log") accessLog := fs.Bool("access-log")
debug := fs.Bool("debug") 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 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 { if templates {
handler := caddytpl.Templates{FileRoot: root} handler := caddytpl.Templates{FileRoot: root}
handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "templates", nil)) handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "templates", nil))
} }
handler := FileServer{Root: root} 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 { if browse {
handler.Browse = new(Browse) handler.Browse = new(Browse)
} }
@@ -210,7 +141,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
} }
} }
err = caddy.Run(cfg) err := caddy.Run(cfg)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, err return caddy.ExitCodeFailedStartup, err
} }
+4 -5
View File
@@ -26,6 +26,9 @@ import (
"strconv" "strconv"
"strings" "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/cel"
"github.com/google/cel-go/common" "github.com/google/cel-go/common"
"github.com/google/cel-go/common/operators" "github.com/google/cel-go/common/operators"
@@ -33,10 +36,6 @@ import (
"github.com/google/cel-go/parser" "github.com/google/cel-go/parser"
"go.uber.org/zap" "go.uber.org/zap"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" 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() { func init() {
@@ -559,7 +558,7 @@ func indexFold(haystack, needle string) int {
return -1 return -1
} }
// isCELTryFilesLiteral returns whether the expression resolves to a map literal containing // isCELMapLiteral returns whether the expression resolves to a map literal containing
// only string keys with or a placeholder call. // only string keys with or a placeholder call.
func isCELTryFilesLiteral(e *exprpb.Expr) bool { func isCELTryFilesLiteral(e *exprpb.Expr) bool {
switch e.GetExprKind().(type) { switch e.GetExprKind().(type) {

Some files were not shown because too many files have changed in this diff Show More