Compare commits

..

1 Commits

Author SHA1 Message Date
WeidiDeng 3cbbd1b62e fix: unwrap adapter modules to get underlying modules 2023-07-10 11:20:29 +08:00
153 changed files with 2517 additions and 4294 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
+5 -17
View File
@@ -15,32 +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']
- 'android' go: [ '1.20' ]
- '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
+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:
-1
View File
@@ -12,7 +12,6 @@ Caddyfile.*
cmd/caddy/caddy cmd/caddy/caddy
cmd/caddy/caddy.exe cmd/caddy/caddy.exe
cmd/caddy/tmp/*.exe cmd/caddy/tmp/*.exe
cmd/caddy/.env
# mac specific # mac specific
.DS_Store .DS_Store
+7 -20
View File
@@ -2,27 +2,14 @@ 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
- errcheck - errcheck
- gci - gofmt
- gofumpt - goimports
- gosec - gosec
- gosimple - gosimple
- govet - govet
@@ -90,23 +77,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 -42
View File
@@ -318,32 +318,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{}{}
@@ -1041,9 +1016,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,
@@ -1196,27 +1171,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)
@@ -1358,7 +1321,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",
+19 -37
View File
@@ -34,11 +34,10 @@ 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"
) )
// Config is the top (or beginning) of the Caddy configuration structure. // Config is the top (or beginning) of the Caddy configuration structure.
@@ -157,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
@@ -258,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)
} }
@@ -306,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 {
@@ -341,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)
@@ -357,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 {
@@ -630,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
@@ -832,7 +816,7 @@ func InstanceID() (uuid.UUID, error) {
if err != nil { if err != nil {
return uuid, err return uuid, err
} }
err = os.WriteFile(uuidFilePath, []byte(uuid.String()), 0o600) err = os.WriteFile(uuidFilePath, []byte(uuid.String()), 0600)
return uuid, err return uuid, err
} else if err != nil { } else if err != nil {
return [16]byte{}, err return [16]byte{}, err
@@ -985,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")
@@ -1008,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 -12
View File
@@ -106,7 +106,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 +127,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 +391,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', import chain: ['%s']", d.File(), d.Line(), d.Val(), expected, strings.Join(d.Token().imports, "','"))
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 +421,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, import chain: ['%s']", d.File(), d.Line(), err, strings.Join(d.Token().imports, "','"))
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 +464,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 +490,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
} }
+1 -19
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,
@@ -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
@@ -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)
+3 -42
View File
@@ -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)
@@ -352,28 +338,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
+3 -3
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
@@ -465,7 +464,7 @@ func (p *parser) doImport(nesting int) error {
// format, won't check for nesting correctness or any other error, that's what parser does. // format, won't check for nesting correctness or any other error, that's what parser does.
if !maybeSnippet && nesting == 0 { if !maybeSnippet && nesting == 0 {
// first of the line // first of the line
if i == 0 || isNextOnNewLine(tokensCopy[i-1], token) { if i == 0 || importedTokens[i-1].File != token.File || importedTokens[i-1].Line+importedTokens[i-1].NumLineBreaks() < token.Line {
index = 0 index = 0
} else { } else {
index++ index++
@@ -566,6 +565,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
-34
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",
@@ -722,36 +718,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) {
+7
View File
@@ -135,4 +135,11 @@ func (am adapterModule) CaddyModule() caddy.ModuleInfo {
} }
} }
// UnwrapAdapter return the original Adapter, this method must be exported
// to be type-assertable 🤷
// hack, https://github.com/caddyserver/caddy/issues/5621
func (am adapterModule) UnwrapAdapter() any {
return am.Adapter
}
var configAdapters = make(map[string]Adapter) var configAdapters = make(map[string]Adapter)
+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 -62
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() {
@@ -181,17 +180,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 +198,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())
} }
@@ -789,8 +788,7 @@ func parseInvoke(h Helper) (caddyhttp.MiddlewareHandler, error) {
// 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 +809,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 +824,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 +902,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 +925,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",
+2 -76
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,
}, },
} { } {
@@ -278,76 +277,3 @@ func TestImportErrorLine(t *testing.T) {
} }
} }
} }
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
},
},
} {
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
}
}
}
+1 -2
View File
@@ -217,8 +217,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{
+85 -80
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,7 +48,8 @@ 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(
@@ -82,18 +81,46 @@ func (st ServerType) Setup(
return nil, warnings, err return nil, warnings, err
} }
// this will replace both static and user-defined placeholder shorthands originalServerBlocks, err = st.extractNamedRoutes(originalServerBlocks, options, &warnings)
// with actual identifiers used by Caddy
replacer := NewShorthandReplacer()
originalServerBlocks, err = st.extractNamedRoutes(originalServerBlocks, options, &warnings, replacer)
if err != nil { if err != nil {
return nil, warnings, err return nil, warnings, err
} }
// replace shorthand placeholders (which are convenient
// when writing a Caddyfile) with their actual placeholder
// identifiers or variable names
replacer := strings.NewReplacer(placeholderShorthands()...)
// these are placeholders that allow a user-defined final
// parameters, but we still want to provide a shorthand
// for those, so we use a regexp to replace
regexpReplacements := []struct {
search *regexp.Regexp
replace string
}{
{regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"},
{regexp.MustCompile(`{cookie\.([\w-]*)}`), "{http.request.cookie.$1}"},
{regexp.MustCompile(`{labels\.([\w-]*)}`), "{http.request.host.labels.$1}"},
{regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"},
{regexp.MustCompile(`{file\.([\w-]*)}`), "{http.request.uri.path.file.$1}"},
{regexp.MustCompile(`{query\.([\w-]*)}`), "{http.request.uri.query.$1}"},
{regexp.MustCompile(`{re\.([\w-]*)\.([\w-]*)}`), "{http.regexp.$1.$2}"},
{regexp.MustCompile(`{vars\.([\w-]*)}`), "{http.vars.$1}"},
{regexp.MustCompile(`{rp\.([\w-\.]*)}`), "{http.reverse_proxy.$1}"},
{regexp.MustCompile(`{err\.([\w-\.]*)}`), "{http.error.$1}"},
{regexp.MustCompile(`{file_match\.([\w-]*)}`), "{http.matchers.file.$1}"},
}
for _, sb := range originalServerBlocks { for _, 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 {
@@ -214,7 +241,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 +324,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 +338,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
@@ -424,7 +429,6 @@ func (ServerType) extractNamedRoutes(
serverBlocks []serverBlock, serverBlocks []serverBlock,
options map[string]any, options map[string]any,
warnings *[]caddyconfig.Warning, warnings *[]caddyconfig.Warning,
replacer ShorthandReplacer,
) ([]serverBlock, error) { ) ([]serverBlock, error) {
namedRoutes := map[string]*caddyhttp.Route{} namedRoutes := map[string]*caddyhttp.Route{}
@@ -450,14 +454,11 @@ func (ServerType) extractNamedRoutes(
continue continue
} }
// zip up all the segments since ParseSegmentAsSubroute
// was designed to take a directive+
wholeSegment := caddyfile.Segment{} wholeSegment := caddyfile.Segment{}
for i := range sb.block.Segments { for _, segment := range sb.block.Segments {
// replace user-defined placeholder shorthands in extracted named routes wholeSegment = append(wholeSegment, segment...)
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{ h := Helper{
@@ -686,7 +687,6 @@ func (st *ServerType) serversFromPairings(
} }
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
} }
@@ -718,20 +718,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 +770,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
} }
} }
} }
@@ -1051,8 +1028,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
@@ -1441,6 +1418,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 +1564,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 +1576,8 @@ type sbAddrAssociation struct {
serverBlocks []serverBlock serverBlocks []serverBlock
} }
const ( const matcherPrefix = "@"
matcherPrefix = "@" const namedRouteKey = "named_route"
namedRouteKey = "named_route"
)
// Interface guard // Interface guard
var _ caddyfile.ServerType = (*ServerType)(nil) var _ caddyfile.ServerType = (*ServerType)(nil)
+2 -3
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() {
+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
-92
View File
@@ -1,92 +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}",
"{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)
}
}
}
}
+3 -4
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
@@ -582,7 +582,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": [
@@ -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,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)
+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 -42
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,13 +294,13 @@ 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)
}, },
}) })
@@ -413,7 +401,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 +416,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 +444,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 +463,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.
+7 -16
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)
} }
} }
@@ -123,8 +117,9 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
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")
@@ -305,12 +300,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)
}
} }
} }
+16 -15
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
@@ -185,6 +174,13 @@ func upgradeBuild(pluginPkgs map[string]struct{}, fl Flags) (int, error) {
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
func getModPkgPath(iface any) string {
if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr {
iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
}
return reflect.TypeOf(iface).PkgPath()
}
func getModules() (standard, nonstandard, unknown []moduleInfo, err error) { func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
bi, ok := debug.ReadBuildInfo() bi, ok := debug.ReadBuildInfo()
if !ok { if !ok {
@@ -206,10 +202,15 @@ func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
// not sure why), and since New() should return a pointer // not sure why), and since New() should return a pointer
// value, we need to dereference it first // value, we need to dereference it first
iface := any(modInfo.New()) iface := any(modInfo.New())
if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr { modPkgPath := getModPkgPath(iface)
iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
// Unwrap config adapters to get the underlying adapter modules, as config adapter modules are hacks anyway. https://github.com/caddyserver/caddy/issues/5621
// this method will only be called if it's from the built-in module to prevent abuse
if strings.HasPrefix(modPkgPath, caddy.ImportPath) {
if unwrapper, ok := iface.(interface{ UnwrapAdapter() any }); ok {
modPkgPath = getModPkgPath(unwrapper.UnwrapAdapter())
}
} }
modPkgPath := reflect.TypeOf(iface).PkgPath()
// now we find the Go module that the Caddy module's package // now we find the Go module that the Caddy module's package
// belongs to; we assume the Caddy module package path will // belongs to; we assume the Caddy module package path will
+2 -3
View File
@@ -23,9 +23,8 @@ import (
"io" "io"
"os" "os"
"github.com/caddyserver/certmagic"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/certmagic"
) )
type storVal struct { type storVal struct {
@@ -201,7 +200,7 @@ func cmdExportStorage(fl Flags) (int, error) {
hdr := &tar.Header{ hdr := &tar.Header{
Name: k, Name: k,
Mode: 0o600, Mode: 0600,
Size: int64(len(v)), Size: int64(len(v)),
} }
+11 -19
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.
+55 -55
View File
@@ -1,73 +1,71 @@
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.3.2
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.7.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.18.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.15.1
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.6
github.com/klauspost/cpuid/v2 v2.2.5 github.com/klauspost/cpuid/v2 v2.2.5
github.com/mastercactapus/proxyprotocol v0.0.4 github.com/mastercactapus/proxyprotocol v0.0.4
github.com/mholt/acmez v1.2.0 github.com/mholt/acmez v1.2.0
github.com/prometheus/client_golang v1.15.1 github.com/prometheus/client_golang v1.14.0
github.com/quic-go/quic-go v0.40.0 github.com/quic-go/quic-go v0.36.0
github.com/smallstep/certificates v0.25.0 github.com/smallstep/certificates v0.24.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.7.0
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.8.3
github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046 github.com/tailscale/tscert v0.0.0-20230509043813-4e9cb4f2b4ad
github.com/yuin/goldmark v1.5.6 github.com/yuin/goldmark v1.5.4
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0
go.opentelemetry.io/contrib/propagators/autoprop v0.42.0 go.opentelemetry.io/contrib/propagators/autoprop v0.42.0
go.opentelemetry.io/otel v1.21.0 go.opentelemetry.io/otel v1.16.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0
go.opentelemetry.io/otel/sdk v1.21.0 go.opentelemetry.io/otel/sdk v1.16.0
go.uber.org/zap v1.25.0 go.uber.org/zap v1.24.0
golang.org/x/crypto v0.14.0 golang.org/x/crypto v0.10.0
golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 golang.org/x/net v0.11.0
golang.org/x/net v0.17.0 golang.org/x/sync v0.3.0
golang.org/x/sync v0.4.0 golang.org/x/term v0.9.0
golang.org/x/term v0.13.0 google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1
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/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.1.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/certificate-transparency-go v1.1.4 // indirect
github.com/google/go-tpm v0.3.3 // indirect
github.com/google/go-tspi v0.3.0 // indirect github.com/google/go-tspi v0.3.0 // indirect
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // 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.3.2 // indirect
github.com/smallstep/go-attestation v0.4.4-0.20230627102604-cf579e53cbd2 // indirect github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
github.com/smallstep/go-attestation v0.4.4-0.20230509120429-e17291421738 // 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.17.0 // indirect go.opentelemetry.io/contrib/propagators/aws v1.17.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.17.0 // indirect go.opentelemetry.io/contrib/propagators/ot v1.17.0 // indirect
go.uber.org/mock v0.3.0 // indirect golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
) )
require ( require (
@@ -85,13 +83,13 @@ require (
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.4 // 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-20230315185526-52ccab3ef572 // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
@@ -110,7 +108,7 @@ require (
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.16 // 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.55 // indirect
@@ -118,34 +116,36 @@ require (
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/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.5.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.13 // 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.16.0 // indirect
go.opentelemetry.io/otel/metric v1.21.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 go.opentelemetry.io/otel/metric v1.16.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.opentelemetry.io/otel/trace v1.16.0
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.6 // indirect
go.step.sm/linkedca v0.20.1 // indirect go.step.sm/crypto v0.30.0
go.step.sm/linkedca v0.19.1 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/mod v0.11.0 // indirect golang.org/x/mod v0.11.0 // indirect
golang.org/x/sys v0.14.0 golang.org/x/sys v0.9.0
golang.org/x/text v0.13.0 // indirect golang.org/x/text v0.10.0 // indirect
golang.org/x/tools v0.10.0 // indirect golang.org/x/tools v0.10.0 // indirect
google.golang.org/grpc v1.59.0 // indirect google.golang.org/grpc v1.55.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect google.golang.org/protobuf v1.30.0 // 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
) )
+1001 -129
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)
)
+3 -72
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,14 +103,7 @@ 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)
} }
@@ -126,23 +117,9 @@ func listenReusable(ctx context.Context, lnKey string, network, address string,
unixSockets[lnKey] = ln.(*unixListener) 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.
@@ -181,36 +158,6 @@ func (uln *unixListener) Close() error {
return uln.UnixListener.Close() 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 +171,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
}
+212 -133
View File
@@ -28,13 +28,12 @@ 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"
) )
// NetworkAddress represents one or more network addresses. // NetworkAddress represents one or more network addresses.
@@ -148,19 +147,17 @@ 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 var address string
address string var unixFileMode fs.FileMode
unixFileMode fs.FileMode var isAbtractUnixSocket bool
isAbtractUnixSocket bool
)
// split unix socket addr early so lnKey // split unix socket addr early so lnKey
// is independent of permissions bits // is independent of permissions bits
if na.IsUnixNetwork() { if na.IsUnixNetwork() {
var err error var err error
address, unixFileMode, err = internal.SplitUnixSocketPermissionsBits(na.Host) address, unixFileMode, err = splitUnixSocketPermissionsBits(na.Host)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -176,16 +173,33 @@ func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net
if err := os.Chmod(address, unixFileMode); err != nil { if err := os.Chmod(address, unixFileMode); err != nil {
return nil, fmt.Errorf("unable to set permissions (%s) on %s: %v", unixFileMode, address, err) 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
@@ -194,6 +208,13 @@ 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)
} }
// TODO: Not 100% sure this is necessary, but we do this for net.UnixListener in listen_unix.go, so...
if unix, ok := ln.(*net.UnixConn); ok {
one := int32(1)
ln = &unixConn{unix, address, lnKey, &one}
unixSockets[lnKey] = unix
}
if IsUnixNetwork(na.Network) { if IsUnixNetwork(na.Network) {
if !isAbtractUnixSocket { if !isAbtractUnixSocket {
if err := os.Chmod(address, unixFileMode); err != nil { if err := os.Chmod(address, unixFileMode); err != nil {
@@ -297,6 +318,40 @@ func IsUnixNetwork(netw string) bool {
return strings.HasPrefix(netw, "unix") return strings.HasPrefix(netw, "unix")
} }
// Takes a unix socket address in the unusual "path|bits" format
// (e.g. /run/caddy.sock|0222) and tries to split it into
// socket path (host) and permissions bits (port). Colons (":")
// can't be used as separator, as socket paths on Windows may
// include a drive letter (e.g. `unix/c:\absolute\path.sock`).
// Permission bits will default to 0200 if none are specified.
// Throws an error, if the first carrying bit does not
// include write perms (e.g. `0422` or `022`).
// Symbolic permission representation (e.g. `u=w,g=w,o=w`)
// is not supported and will throw an error for now!
func splitUnixSocketPermissionsBits(addr string) (path string, fileMode fs.FileMode, err error) {
addrSplit := strings.SplitN(addr, "|", 2)
if len(addrSplit) == 2 {
// parse octal permission bit string as uint32
fileModeUInt64, err := strconv.ParseUint(addrSplit[1], 8, 32)
if err != nil {
return "", 0, fmt.Errorf("could not parse octal permission bits in %s: %v", addr, err)
}
fileMode = fs.FileMode(fileModeUInt64)
// FileMode.String() returns a string like `-rwxr-xr--` for `u=rwx,g=rx,o=r` (`0754`)
if string(fileMode.String()[2]) != "w" {
return "", 0, fmt.Errorf("owner of the socket requires '-w-' (write, octal: '2') permissions at least; got '%s' in %s", fileMode.String()[1:4], addr)
}
return addrSplit[0], fileMode, nil
}
// default to 0200 (symbolic: `u=w,g=,o=`)
// if no permission bits are specified
return addr, 0200, nil
}
// ParseNetworkAddress parses addr into its individual // ParseNetworkAddress parses addr into its individual
// components. The input string is expected to be of // components. The input string is expected to be of
// the form "network/host:port-range" where any part is // the form "network/host:port-range" where any part is
@@ -321,7 +376,7 @@ func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort ui
network = defaultNetwork network = defaultNetwork
} }
if IsUnixNetwork(network) { if IsUnixNetwork(network) {
_, _, err := internal.SplitUnixSocketPermissionsBits(host) _, _, err := splitUnixSocketPermissionsBits(host)
return NetworkAddress{ return NetworkAddress{
Network: network, Network: network,
Host: host, Host: host,
@@ -447,93 +502,53 @@ 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) (http3.QUICEarlyListener, 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) sqtc := newSharedQUICTLSConfig(tlsConf)
// http3.ConfigureTLSConfig only uses this field and tls App sets this field as well // http3.ConfigureTLSConfig only uses this field and tls App sets this field as well
//nolint:gosec //nolint:gosec
quicTlsConfig := &tls.Config{GetConfigForClient: sqs.getConfigForClient} quicTlsConfig := &tls.Config{GetConfigForClient: sqtc.getConfigForClient}
earlyLn, err := quic.ListenEarly(ln, http3.ConfigureTLSConfig(quicTlsConfig), &quic.Config{ earlyLn, err := quic.ListenEarly(ln, http3.ConfigureTLSConfig(quicTlsConfig), &quic.Config{
Allow0RTT: true, 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, sqtc: sqtc, key: lnKey}, nil
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
sql := sharedEarlyListener.(*sharedQuicListener) sql := sharedEarlyListener.(*sharedQuicListener)
// add current tls.Config and request counter to sqs, so GetConfigForClient will always return the latest tls.Config in case of context cancellation, // add current tls.Config to sqtc, 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.sqtc.addTLSConfig(tlsConf)
ctx, cancel := sql.sqs.addState(tlsConf, activeRequests)
// TODO: to serve QUIC over a unix socket, currently we need to hold onto
// the underlying net.PacketConn (which we wrap as unixConn to keep count
// of closes) because closing the quic.EarlyListener doesn't actually close
// the underlying PacketConn, but we need to for unix sockets since we dup
// the file descriptor and thus need to close the original; track issue:
// https://github.com/quic-go/quic-go/issues/3560#issuecomment-1258959608
var unix *unixConn
if uc, ok := ln.(*unixConn); ok {
unix = uc
}
return &fakeCloseQuicListener{ return &fakeCloseQuicListener{
sharedQuicListener: sql, sharedQuicListener: sql,
uc: unix,
context: ctx, context: ctx,
contextCancel: cancel, contextCancel: cancel,
}, nil }, nil
@@ -551,50 +566,38 @@ type contextAndCancelFunc struct {
context.CancelFunc context.CancelFunc
} }
// sharedQUICState manages GetConfigForClient and current number of active requests // sharedQUICTLSConfig manages GetConfigForClient
// see issue: https://github.com/caddyserver/caddy/pull/4849 // see issue: https://github.com/caddyserver/caddy/pull/4849
type sharedQUICState struct { type sharedQUICTLSConfig struct {
rmu sync.RWMutex rmu sync.RWMutex
tlsConfs map[*tls.Config]contextAndCancelFunc tlsConfs map[*tls.Config]contextAndCancelFunc
requestCounters map[*tls.Config]*int64 activeTlsConf *tls.Config
activeTlsConf *tls.Config
activeRequestsCounter *int64
} }
// newSharedQUICState creates a new sharedQUICState // newSharedQUICTLSConfig creates a new sharedQUICTLSConfig
func newSharedQUICState(tlsConfig *tls.Config, activeRequests *int64) *sharedQUICState { func newSharedQUICTLSConfig(tlsConfig *tls.Config) *sharedQUICTLSConfig {
sqtc := &sharedQUICState{ sqtc := &sharedQUICTLSConfig{
tlsConfs: make(map[*tls.Config]contextAndCancelFunc), tlsConfs: make(map[*tls.Config]contextAndCancelFunc),
requestCounters: make(map[*tls.Config]*int64), activeTlsConf: tlsConfig,
activeTlsConf: tlsConfig,
activeRequestsCounter: activeRequests,
} }
sqtc.addState(tlsConfig, activeRequests) sqtc.addTLSConfig(tlsConfig)
return sqtc return sqtc
} }
// getConfigForClient is used as tls.Config's GetConfigForClient field // getConfigForClient is used as tls.Config's GetConfigForClient field
func (sqs *sharedQUICState) getConfigForClient(ch *tls.ClientHelloInfo) (*tls.Config, error) { func (sqtc *sharedQUICTLSConfig) getConfigForClient(ch *tls.ClientHelloInfo) (*tls.Config, error) {
sqs.rmu.RLock() sqtc.rmu.RLock()
defer sqs.rmu.RUnlock() defer sqtc.rmu.RUnlock()
return sqs.activeTlsConf.GetConfigForClient(ch) return sqtc.activeTlsConf.GetConfigForClient(ch)
} }
// getActiveRequests returns the number of active requests // addTLSConfig adds tls.Config to the map if not present and returns the corresponding context and its cancelFunc
func (sqs *sharedQUICState) getActiveRequests() int64 { // so that when cancelled, the active tls.Config will change
// Prevent a race when a context is cancelled and active request counter is being changed func (sqtc *sharedQUICTLSConfig) addTLSConfig(tlsConfig *tls.Config) (context.Context, context.CancelFunc) {
sqs.rmu.RLock() sqtc.rmu.Lock()
defer sqs.rmu.RUnlock() defer sqtc.rmu.Unlock()
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 if cacc, ok := sqtc.tlsConfs[tlsConfig]; ok {
// 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 return cacc.Context, cacc.CancelFunc
} }
@@ -602,26 +605,23 @@ func (sqs *sharedQUICState) addState(tlsConfig *tls.Config, activeRequests *int6
wrappedCancel := func() { wrappedCancel := func() {
cancel() cancel()
sqs.rmu.Lock() sqtc.rmu.Lock()
defer sqs.rmu.Unlock() defer sqtc.rmu.Unlock()
delete(sqs.tlsConfs, tlsConfig) delete(sqtc.tlsConfs, tlsConfig)
delete(sqs.requestCounters, tlsConfig) if sqtc.activeTlsConf == tlsConfig {
if sqs.activeTlsConf == tlsConfig { // select another tls.Config, if there is none,
// select another tls.Config and request counter, if there is none,
// related sharedQuicListener will be destroyed anyway // related sharedQuicListener will be destroyed anyway
for tc, counter := range sqs.requestCounters { for tc := range sqtc.tlsConfs {
sqs.activeTlsConf = tc sqtc.activeTlsConf = tc
sqs.activeRequestsCounter = counter
break break
} }
} }
} }
sqs.tlsConfs[tlsConfig] = contextAndCancelFunc{ctx, wrappedCancel} sqtc.tlsConfs[tlsConfig] = contextAndCancelFunc{ctx, wrappedCancel}
sqs.requestCounters[tlsConfig] = activeRequests
// there should be at most 2 tls.Configs // there should be at most 2 tls.Configs
if len(sqs.tlsConfs) > 2 { if len(sqtc.tlsConfs) > 2 {
Log().Warn("quic listener tls configs are more than 2", zap.Int("number of configs", len(sqs.tlsConfs))) Log().Warn("quic listener tls configs are more than 2", zap.Int("number of configs", len(sqtc.tlsConfs)))
} }
return ctx, wrappedCancel return ctx, wrappedCancel
} }
@@ -629,17 +629,24 @@ func (sqs *sharedQUICState) addState(tlsConfig *tls.Config, activeRequests *int6
// 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 sqtc *sharedQUICTLSConfig
sqs *sharedQUICState key string
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
@@ -661,9 +668,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
} }
@@ -690,6 +729,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
} }
@@ -715,7 +759,34 @@ func RegisterNetwork(network string, getListener ListenerFunc) {
networkTypes[network] = getListener networkTypes[network] = getListener
} }
var unixSocketsMu sync.Mutex 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
@@ -759,3 +830,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)
)
+1 -3
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) {
@@ -636,7 +634,7 @@ func TestSplitUnixSocketPermissionsBits(t *testing.T) {
expectErr: true, expectErr: true,
}, },
} { } {
actualPath, actualFileMode, err := internal.SplitUnixSocketPermissionsBits(tc.input) actualPath, actualFileMode, err := splitUnixSocketPermissionsBits(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)
} }
+1 -2
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.
+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 -14
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
@@ -328,15 +325,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 +369,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") {
@@ -613,6 +608,17 @@ func (app *App) Stop() error {
zap.Error(err), zap.Error(err),
zap.Strings("addresses", server.Listen)) zap.Strings("addresses", server.Listen))
} }
// TODO: we have to manually close our listeners because quic-go won't
// close listeners it didn't create along with the server itself...
// see https://github.com/quic-go/quic-go/issues/3560
for _, el := range server.h3listeners {
if err := el.Close(); err != nil {
app.logger.Error("HTTP/3 listener close",
zap.Error(err),
zap.String("address", el.LocalAddr().String()))
}
}
} }
stopH2Listener := func(server *Server) { stopH2Listener := func(server *Server) {
defer finishedShutdown.Done() defer finishedShutdown.Done()
+4 -4
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
@@ -197,7 +196,8 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
// 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 &&
len(app.tlsApp.AllMatchingCertificates(d)) > 0 {
logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded", 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),
+4 -2
View File
@@ -23,14 +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{})
weakrand.Seed(time.Now().UnixNano())
} }
// 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() {
+2 -4
View File
@@ -307,7 +307,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)
)
+2 -7
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,12 @@ 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 (pn celPkixName) ConvertToType(typeVal ref.Type) ref.Val { func (pn celPkixName) ConvertToType(typeVal ref.Type) ref.Val {
if typeVal.TypeName() == "string" { if typeVal.TypeName() == "string" {
return types.String(pn.Name.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)
-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()
}
+13 -15
View File
@@ -93,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*",
}, },
}, },
@@ -168,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
@@ -183,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
@@ -212,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
@@ -221,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,
@@ -263,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
} }
@@ -271,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)
} }
} }
@@ -287,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
} }
@@ -304,7 +301,7 @@ func (rw *responseWriter) Close() error {
// Unwrap returns the underlying ResponseWriter. // Unwrap returns the underlying ResponseWriter.
func (rw *responseWriter) Unwrap() http.ResponseWriter { func (rw *responseWriter) Unwrap() http.ResponseWriter {
return rw.ResponseWriter return rw.HTTPInterfaces
} }
// 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.
@@ -313,7 +310,7 @@ func (rw *responseWriter) init() {
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")
@@ -432,4 +429,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)
} }
+5 -12
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"`
} }
@@ -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,
@@ -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)
} }
+198 -244
View File
@@ -1,296 +1,252 @@
{{- 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"/> </svg>
<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")}} {{- 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M15 21h-9a3 3 0 0 1 -3 -3v-1h10v2a2 2 0 0 0 4 0v-14a2 2 0 1 1 2 2h-2m2 -4h-11a3 3 0 0 0 -3 3v11"/> <path d="M15 21h-9a3 3 0 0 1 -3 -3v-1h10v2a2 2 0 0 0 4 0v-14a2 2 0 1 1 2 2h-2m2 -4h-11a3 3 0 0 0 -3 3v11"></path>
<path d="M9 7l4 0"/> <path d="M9 7l4 0"></path>
<path d="M9 11l4 0"/> <path d="M9 11l4 0"></path>
</svg> </svg>
{{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp" ".tiff" ".bmp" ".heif" ".heic" ".svg"}} {{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp" ".tiff" ".bmp" ".heif" ".heic"}}
{{- if eq .Tpl.Layout "grid"}} {{- if eq .Tpl.Layout "grid"}}
<img loading="lazy" src="{{html .Name}}"> <img src="{{html .Name}}">
{{- else}} {{- 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M15 8h.01"/> <path d="M15 8h.01"></path>
<path d="M3 6a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v12a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3v-12z"/> <path d="M3 6a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v12a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3v-12z"></path>
<path d="M3 16l5 -5c.928 -.893 2.072 -.893 3 0l5 5"/> <path d="M3 16l5 -5c.928 -.893 2.072 -.893 3 0l5 5"></path>
<path d="M14 14l1 -1c.928 -.893 2.072 -.893 3 0l3 3"/> <path d="M14 14l1 -1c.928 -.893 2.072 -.893 3 0l3 3"></path>
</svg> </svg>
{{- end}} {{- end}}
{{- else if .HasExt ".mp4" ".mov" ".m4v" ".mpeg" ".mpg" ".avi" ".ogg" ".webm" ".mkv" ".vob" ".gifv" ".3gp"}} {{- else if .HasExt ".mp4" ".mov" ".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" ".m4a" ".aac" ".ogg" ".flac" ".wav" ".wma" ".midi" ".cda"}}
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-music" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <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>
{{- else if .HasExt ".csv" ".tsv"}}
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-type-csv" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"/>
<path d="M7 16.5a1.5 1.5 0 0 0 -3 0v3a1.5 1.5 0 0 0 3 0"/>
<path d="M10 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"/>
<path d="M16 15l2 6l2 -6"/>
</svg> </svg>
{{- else if .HasExt ".txt" ".doc" ".docx" ".odt" ".fodt" ".rtf"}} {{- 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 ".xls" ".xlsx" ".ods" ".fods"}}
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-spreadsheet" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <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 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="M8 11h8v7h-8z"/> <path d="M8 11h8v7h-8z"></path>
<path d="M8 15h8"/> <path d="M8 15h8"></path>
<path d="M11 11v7"/> <path d="M11 11v7"></path>
</svg> </svg>
{{- else if .HasExt ".ppt" ".pptx" ".odp" ".fodp"}} {{- 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M9 12v-4"/> <path d="M9 12v-4"></path>
<path d="M15 12v-2"/> <path d="M15 12v-2"></path>
<path d="M12 12v-1"/> <path d="M12 12v-1"></path>
<path d="M3 4h18"/> <path d="M3 4h18"></path>
<path d="M4 4v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-10"/> <path d="M4 4v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-10"></path>
<path d="M12 16v4"/> <path d="M12 16v4"></path>
<path d="M9 20h6"/> <path d="M9 20h6"></path>
</svg> </svg>
{{- else if .HasExt ".zip" ".gz" ".xz" ".tar" ".7z" ".rar" ".xz" ".zst"}} {{- 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> </svg>
{{- else if .HasExt ".deb" ".dpkg"}} {{- 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 17c-2.397 -.943 -4 -3.153 -4 -5.635c0 -2.19 1.039 -3.14 1.604 -3.595c2.646 -2.133 6.396 -.27 6.396 3.23c0 2.5 -2.905 2.121 -3.5 1.5c-.595 -.621 -1 -1.5 -.5 -2.5"/> <path d="M12 17c-2.397 -.943 -4 -3.153 -4 -5.635c0 -2.19 1.039 -3.14 1.604 -3.595c2.646 -2.133 6.396 -.27 6.396 3.23c0 2.5 -2.905 2.121 -3.5 1.5c-.595 -.621 -1 -1.5 -.5 -2.5"></path>
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/> <path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"></path>
</svg> </svg>
{{- else if .HasExt ".rpm" ".exe" ".flatpak" ".appimage" ".jar" ".msi" ".apk"}} {{- 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5"/> <path d="M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5"></path>
<path d="M12 12l8 -4.5"/> <path d="M12 12l8 -4.5"></path>
<path d="M12 12l0 9"/> <path d="M12 12l0 9"></path>
<path d="M12 12l-8 -4.5"/> <path d="M12 12l-8 -4.5"></path>
<path d="M16 5.25l-8 4.5"/> <path d="M16 5.25l-8 4.5"></path>
</svg> </svg>
{{- else if .HasExt ".ps1"}} {{- 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4.887 20h11.868c.893 0 1.664 -.665 1.847 -1.592l2.358 -12c.212 -1.081 -.442 -2.14 -1.462 -2.366a1.784 1.784 0 0 0 -.385 -.042h-11.868c-.893 0 -1.664 .665 -1.847 1.592l-2.358 12c-.212 1.081 .442 2.14 1.462 2.366c.127 .028 .256 .042 .385 .042z"/> <path d="M4.887 20h11.868c.893 0 1.664 -.665 1.847 -1.592l2.358 -12c.212 -1.081 -.442 -2.14 -1.462 -2.366a1.784 1.784 0 0 0 -.385 -.042h-11.868c-.893 0 -1.664 .665 -1.847 1.592l-2.358 12c-.212 1.081 .442 2.14 1.462 2.366c.127 .028 .256 .042 .385 .042z"></path>
<path d="M9 8l4 4l-6 4"/> <path d="M9 8l4 4l-6 4"></path>
<path d="M12 16h3"/> <path d="M12 16h3"></path>
</svg> </svg>
{{- else if .HasExt ".py" ".pyc" ".pyo"}} {{- 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 9h-7a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h3"/> <path d="M12 9h-7a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h3"></path>
<path d="M12 15h7a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2h-3"/> <path d="M12 15h7a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2h-3"></path>
<path d="M8 9v-4a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v5a2 2 0 0 1 -2 2h-4a2 2 0 0 0 -2 2v5a2 2 0 0 0 2 2h4a2 2 0 0 0 2 -2v-4"/> <path d="M8 9v-4a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v5a2 2 0 0 1 -2 2h-4a2 2 0 0 0 -2 2v5a2 2 0 0 0 2 2h4a2 2 0 0 0 2 -2v-4"></path>
<path d="M11 6l0 .01"/> <path d="M11 6l0 .01"></path>
<path d="M13 18l0 .01"/> <path d="M13 18l0 .01"></path>
</svg> </svg>
{{- else if .HasExt ".bash" ".sh" ".com" ".bat" ".dll" ".so"}} {{- 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M17 20h-11a3 3 0 0 1 0 -6h11a3 3 0 0 0 0 6h1a3 3 0 0 0 3 -3v-11a2 2 0 0 0 -2 -2h-10a2 2 0 0 0 -2 2v8"/> <path d="M17 20h-11a3 3 0 0 1 0 -6h11a3 3 0 0 0 0 6h1a3 3 0 0 0 3 -3v-11a2 2 0 0 0 -2 -2h-10a2 2 0 0 0 -2 2v8"></path>
</svg> </svg>
{{- else if .HasExt ".dmg"}} {{- 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 4m0 1a1 1 0 0 1 1 -1h16a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1z"/> <path d="M3 4m0 1a1 1 0 0 1 1 -1h16a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1z"></path>
<path d="M7 8v1"/> <path d="M7 8v1"></path>
<path d="M17 8v1"/> <path d="M17 8v1"></path>
<path d="M12.5 4c-.654 1.486 -1.26 3.443 -1.5 9h2.5c-.19 2.867 .094 5.024 .5 7"/> <path d="M12.5 4c-.654 1.486 -1.26 3.443 -1.5 9h2.5c-.19 2.867 .094 5.024 .5 7"></path>
<path d="M7 15.5c3.667 2 6.333 2 10 0"/> <path d="M7 15.5c3.667 2 6.333 2 10 0"></path>
</svg> </svg>
{{- else if .HasExt ".iso" ".img"}} {{- 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/> <path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"></path>
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"/> <path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
<path d="M7 12a5 5 0 0 1 5 -5"/> <path d="M7 12a5 5 0 0 1 5 -5"></path>
<path d="M12 17a5 5 0 0 0 5 -5"/> <path d="M12 17a5 5 0 0 0 5 -5"></path>
</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> </svg>
{{- else if .HasExt ".ttf" ".otf" ".woff" ".woff2" ".eof"}} {{- 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"> <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 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="M11 18h2"/> <path d="M11 18h2"></path>
<path d="M12 18v-7"/> <path d="M12 18v-7"></path>
<path d="M9 12v-1h6v1"/> <path d="M9 12v-1h6v1"></path>
</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> </svg>
{{- else if .HasExt ".html" ".htm"}} {{- 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"> <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-html" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M14 3v4a1 1 0 0 0 1 1h4"/> <path d="M13 16v-8l2 5l2 -5v8"></path>
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"/> <path d="M1 16v-8"></path>
<path d="M2 21v-6"/> <path d="M5 8v8"></path>
<path d="M5 15v6"/> <path d="M1 12h4"></path>
<path d="M2 18h3"/> <path d="M7 8h4"></path>
<path d="M20 15v6h2"/> <path d="M9 8v8"></path>
<path d="M13 21v-6l2 3l2 -3v6"/> <path d="M20 8v8h3"></path>
<path d="M7.5 15h3"/>
<path d="M9 15v6"/>
</svg> </svg>
{{- else if .HasExt ".js"}} {{- 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"> <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-javascript" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M14 3v4a1 1 0 0 0 1 1h4"/> <path d="M20 4l-2 14.5l-6 2l-6 -2l-2 -14.5z"></path>
<path d="M3 15h3v4.5a1.5 1.5 0 0 1 -3 0"/> <path d="M7.5 8h3v8l-2 -1"></path>
<path d="M9 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"/> <path d="M16.5 8h-2.5a.5 .5 0 0 0 -.5 .5v3a.5 .5 0 0 0 .5 .5h1.423a.5 .5 0 0 1 .495 .57l-.418 2.93l-2 .5"></path>
<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> </svg>
{{- else if .HasExt ".json" ".json5" ".jsonc"}} {{- 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M20 16v-8l3 8v-8"/> <path d="M20 16v-8l3 8v-8"></path>
<path d="M15 8a2 2 0 0 1 2 2v4a2 2 0 1 1 -4 0v-4a2 2 0 0 1 2 -2z"/> <path d="M15 8a2 2 0 0 1 2 2v4a2 2 0 1 1 -4 0v-4a2 2 0 0 1 2 -2z"></path>
<path d="M1 8h3v6.5a1.5 1.5 0 0 1 -3 0v-.5"/> <path d="M1 8h3v6.5a1.5 1.5 0 0 1 -3 0v-.5"></path>
<path d="M7 15a1 1 0 0 0 1 1h1a1 1 0 0 0 1 -1v-2a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1h1a1 1 0 0 1 1 1"/> <path d="M7 15a1 1 0 0 0 1 1h1a1 1 0 0 0 1 -1v-2a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1h1a1 1 0 0 1 1 1"></path>
</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> </svg>
{{- else if .HasExt ".sql"}} {{- 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"> <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-sql" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M14 3v4a1 1 0 0 0 1 1h4"/> <path d="M12 8a2 2 0 0 1 2 2v4a2 2 0 1 1 -4 0v-4a2 2 0 0 1 2 -2z"></path>
<path d="M14 3v4a1 1 0 0 0 1 1h4"/> <path d="M17 8v8h4"></path>
<path d="M5 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"/> <path d="M13 15l1 1"></path>
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"/> <path d="M3 15a1 1 0 0 0 1 1h2a1 1 0 0 0 1 -1v-2a1 1 0 0 0 -1 -1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1"></path>
<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> </svg>
{{- else if .HasExt ".db" ".sqlite" ".bak" ".mdb"}} {{- 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0"/> <path d="M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0"></path>
<path d="M4 6v6a8 3 0 0 0 16 0v-6"/> <path d="M4 6v6a8 3 0 0 0 16 0v-6"></path>
<path d="M4 12v6a8 3 0 0 0 16 0v-6"/> <path d="M4 12v6a8 3 0 0 0 16 0v-6"></path>
</svg> </svg>
{{- else if .HasExt ".eml" ".email" ".mailbox" ".mbox" ".msg"}} {{- 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10z"/> <path d="M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10z"></path>
<path d="M3 7l9 6l9 -6"/> <path d="M3 7l9 6l9 -6"></path>
</svg> </svg>
{{- else if .HasExt ".crt" ".pem" ".x509" ".cer" ".ca-bundle"}} {{- 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M15 15m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"/> <path d="M15 15m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
<path d="M13 17.5v4.5l2 -1.5l2 1.5v-4.5"/> <path d="M13 17.5v4.5l2 -1.5l2 1.5v-4.5"></path>
<path d="M10 19h-5a2 2 0 0 1 -2 -2v-10c0 -1.1 .9 -2 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -1 1.73"/> <path d="M10 19h-5a2 2 0 0 1 -2 -2v-10c0 -1.1 .9 -2 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -1 1.73"></path>
<path d="M6 9l12 0"/> <path d="M6 9l12 0"></path>
<path d="M6 12l3 0"/> <path d="M6 12l3 0"></path>
<path d="M6 15l2 0"/> <path d="M6 15l2 0"></path>
</svg> </svg>
{{- else if .HasExt ".key" ".keystore" ".jks" ".p12" ".pfx" ".pub"}} {{- 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"> <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 stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M16.555 3.843l3.602 3.602a2.877 2.877 0 0 1 0 4.069l-2.643 2.643a2.877 2.877 0 0 1 -4.069 0l-.301 -.301l-6.558 6.558a2 2 0 0 1 -1.239 .578l-.175 .008h-1.172a1 1 0 0 1 -.993 -.883l-.007 -.117v-1.172a2 2 0 0 1 .467 -1.284l.119 -.13l.414 -.414h2v-2h2v-2l2.144 -2.144l-.301 -.301a2.877 2.877 0 0 1 0 -4.069l2.643 -2.643a2.877 2.877 0 0 1 4.069 0z"/> <path d="M16.555 3.843l3.602 3.602a2.877 2.877 0 0 1 0 4.069l-2.643 2.643a2.877 2.877 0 0 1 -4.069 0l-.301 -.301l-6.558 6.558a2 2 0 0 1 -1.239 .578l-.175 .008h-1.172a1 1 0 0 1 -.993 -.883l-.007 -.117v-1.172a2 2 0 0 1 .467 -1.284l.119 -.13l.414 -.414h2v-2h2v-2l2.144 -2.144l-.301 -.301a2.877 2.877 0 0 1 0 -4.069l2.643 -2.643a2.877 2.877 0 0 1 4.069 0z"></path>
<path d="M15 9h.01"/> <path d="M15 9h.01"></path>
</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 +255,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">
@@ -594,6 +549,8 @@ td .go-up {
.grid { .grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(16em, 1fr)); grid-template-columns: repeat(auto-fill, minmax(16em, 1fr));
justify-items: center;
align-items: start;
gap: 2px; gap: 2px;
} }
@@ -798,19 +755,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 +792,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 +816,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 +836,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 +848,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 +871,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 +899,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>
@@ -1084,10 +1041,7 @@ 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}%`;
}
}); });
} }
@@ -25,11 +25,10 @@ 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 {
+7 -20
View File
@@ -16,22 +16,18 @@ package fileserver
import ( import (
"encoding/json" "encoding/json"
"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"
caddytpl "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
) )
func init() { func init() {
@@ -55,21 +51,12 @@ 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.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
},
})
}, },
}) })
} }
+3 -4
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() {
+7 -26
View File
@@ -29,15 +29,17 @@ import (
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"time"
"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/encode" "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
"go.uber.org/zap"
) )
func init() { func init() {
weakrand.Seed(time.Now().UnixNano())
caddy.RegisterModule(FileServer{}) caddy.RegisterModule(FileServer{})
} }
@@ -60,23 +62,7 @@ func init() {
// requested directory does not have an index file, Caddy writes a // requested directory does not have an index file, Caddy writes a
// 404 response. Alternatively, file browsing can be enabled with // 404 response. Alternatively, file browsing can be enabled with
// the "browse" parameter which shows a list of files when directories // the "browse" parameter which shows a list of files when directories
// are requested if no index file is present. If "browse" is enabled, // are requested if no index file is present.
// Caddy may serve a JSON array of the dirctory listing when the `Accept`
// header mentions `application/json` with the following structure:
//
// [{
// "name": "",
// "size": 0,
// "url": "",
// "mod_time": "",
// "mode": 0,
// "is_dir": false,
// "is_symlink": false
// }]
//
// with the `url` being relative to the request path and `mod_time` in the RFC 3339 format
// with sub-second precision. For any other value for the `Accept` header, the
// respective browse template is executed with `Content-Type: text/html`.
// //
// By default, this handler will canonicalize URIs so that requests to // By default, this handler will canonicalize URIs so that requests to
// directories end with a slash, but requests to regular files do not. // directories end with a slash, but requests to regular files do not.
@@ -435,13 +421,8 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
// GET and HEAD, which is sensible for a static file server - reject // GET and HEAD, which is sensible for a static file server - reject
// any other methods (see issue #5166) // any other methods (see issue #5166)
if r.Method != http.MethodGet && r.Method != http.MethodHead { if r.Method != http.MethodGet && r.Method != http.MethodHead {
// if we're in an error context, then it doesn't make sense w.Header().Add("Allow", "GET, HEAD")
// to repeat the error; just continue because we're probably return caddyhttp.Error(http.StatusMethodNotAllowed, nil)
// trying to write an error page response (see issue #5703)
if _, ok := r.Context().Value(caddyhttp.ErrorCtxKey).(error); !ok {
w.Header().Add("Allow", "GET, HEAD")
return caddyhttp.Error(http.StatusMethodNotAllowed, nil)
}
} }
// set the Etag - note that a conditional If-None-Match request is handled // set the Etag - note that a conditional If-None-Match request is handled
+1 -1
View File
@@ -371,5 +371,5 @@ func (rww *responseWriterWrapper) Write(d []byte) (int, error) {
var ( var (
_ caddy.Provisioner = (*Handler)(nil) _ caddy.Provisioner = (*Handler)(nil)
_ caddyhttp.MiddlewareHandler = (*Handler)(nil) _ caddyhttp.MiddlewareHandler = (*Handler)(nil)
_ http.ResponseWriter = (*responseWriterWrapper)(nil) _ caddyhttp.HTTPInterfaces = (*responseWriterWrapper)(nil)
) )
+51 -69
View File
@@ -16,11 +16,10 @@ package caddyhttp
import ( import (
"bufio" "bufio"
"bytes"
"fmt" "fmt"
"io"
"net" "net"
"net/http" "net/http"
"sync"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -43,11 +42,7 @@ func init() {
// //
// This listener wrapper must be placed BEFORE the "tls" listener // This listener wrapper must be placed BEFORE the "tls" listener
// wrapper, for it to work properly. // wrapper, for it to work properly.
type HTTPRedirectListenerWrapper struct { type HTTPRedirectListenerWrapper struct{}
// MaxHeaderBytes is the maximum size to parse from a client's
// HTTP request headers. Default: 1 MB
MaxHeaderBytes int64 `json:"max_header_bytes,omitempty"`
}
func (HTTPRedirectListenerWrapper) CaddyModule() caddy.ModuleInfo { func (HTTPRedirectListenerWrapper) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
@@ -61,7 +56,7 @@ func (h *HTTPRedirectListenerWrapper) UnmarshalCaddyfile(d *caddyfile.Dispenser)
} }
func (h *HTTPRedirectListenerWrapper) WrapListener(l net.Listener) net.Listener { func (h *HTTPRedirectListenerWrapper) WrapListener(l net.Listener) net.Listener {
return &httpRedirectListener{l, h.MaxHeaderBytes} return &httpRedirectListener{l}
} }
// httpRedirectListener is listener that checks the first few bytes // httpRedirectListener is listener that checks the first few bytes
@@ -69,7 +64,6 @@ func (h *HTTPRedirectListenerWrapper) WrapListener(l net.Listener) net.Listener
// to respond to an HTTP request with a redirect. // to respond to an HTTP request with a redirect.
type httpRedirectListener struct { type httpRedirectListener struct {
net.Listener net.Listener
maxHeaderBytes int64
} }
// Accept waits for and returns the next connection to the listener, // Accept waits for and returns the next connection to the listener,
@@ -80,23 +74,16 @@ func (l *httpRedirectListener) Accept() (net.Conn, error) {
return nil, err return nil, err
} }
maxHeaderBytes := l.maxHeaderBytes
if maxHeaderBytes == 0 {
maxHeaderBytes = 1024 * 1024
}
return &httpRedirectConn{ return &httpRedirectConn{
Conn: c, Conn: c,
limit: maxHeaderBytes, r: bufio.NewReader(c),
r: bufio.NewReader(c),
}, nil }, nil
} }
type httpRedirectConn struct { type httpRedirectConn struct {
net.Conn net.Conn
once bool once sync.Once
limit int64 r *bufio.Reader
r *bufio.Reader
} }
// Read tries to peek at the first few bytes of the request, and if we get // Read tries to peek at the first few bytes of the request, and if we get
@@ -104,58 +91,53 @@ type httpRedirectConn struct {
// like an HTTP request, then we perform a HTTP->HTTPS redirect on the same // like an HTTP request, then we perform a HTTP->HTTPS redirect on the same
// port as the original connection. // port as the original connection.
func (c *httpRedirectConn) Read(p []byte) (int, error) { func (c *httpRedirectConn) Read(p []byte) (int, error) {
if c.once { var errReturn error
return c.r.Read(p) c.once.Do(func() {
} firstBytes, err := c.r.Peek(5)
// no need to use sync.Once - net.Conn is not read from concurrently. if err != nil {
c.once = true return
}
firstBytes, err := c.r.Peek(5) // If the request doesn't look like HTTP, then it's probably
if err != nil { // TLS bytes and we don't need to do anything.
return 0, err if !firstBytesLookLikeHTTP(firstBytes) {
return
}
// Parse the HTTP request, so we can get the Host and URL to redirect to.
req, err := http.ReadRequest(c.r)
if err != nil {
return
}
// Build the redirect response, using the same Host and URL,
// but replacing the scheme with https.
headers := make(http.Header)
headers.Add("Location", "https://"+req.Host+req.URL.String())
resp := &http.Response{
Proto: "HTTP/1.0",
Status: "308 Permanent Redirect",
StatusCode: 308,
ProtoMajor: 1,
ProtoMinor: 0,
Header: headers,
}
err = resp.Write(c.Conn)
if err != nil {
errReturn = fmt.Errorf("couldn't write HTTP->HTTPS redirect")
return
}
errReturn = fmt.Errorf("redirected HTTP request on HTTPS port")
c.Conn.Close()
})
if errReturn != nil {
return 0, errReturn
} }
// If the request doesn't look like HTTP, then it's probably return c.r.Read(p)
// TLS bytes, and we don't need to do anything.
if !firstBytesLookLikeHTTP(firstBytes) {
return c.r.Read(p)
}
// From now on, we can be almost certain the request is HTTP.
// The returned error will be non nil and caller are expected to
// close the connection.
// Set the read limit, io.MultiReader is needed because
// when resetting, *bufio.Reader discards buffered data.
buffered, _ := c.r.Peek(c.r.Buffered())
mr := io.MultiReader(bytes.NewReader(buffered), c.Conn)
c.r.Reset(io.LimitReader(mr, c.limit))
// Parse the HTTP request, so we can get the Host and URL to redirect to.
req, err := http.ReadRequest(c.r)
if err != nil {
return 0, fmt.Errorf("couldn't read HTTP request")
}
// Build the redirect response, using the same Host and URL,
// but replacing the scheme with https.
headers := make(http.Header)
headers.Add("Location", "https://"+req.Host+req.URL.String())
resp := &http.Response{
Proto: "HTTP/1.0",
Status: "308 Permanent Redirect",
StatusCode: 308,
ProtoMajor: 1,
ProtoMinor: 0,
Header: headers,
}
err = resp.Write(c.Conn)
if err != nil {
return 0, fmt.Errorf("couldn't write HTTP->HTTPS redirect")
}
return 0, fmt.Errorf("redirected HTTP request on HTTPS port")
} }
// firstBytesLookLikeHTTP reports whether a TLS record header // firstBytesLookLikeHTTP reports whether a TLS record header
+2 -3
View File
@@ -23,12 +23,11 @@ import (
"reflect" "reflect"
"strings" "strings"
"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/types/ref" "github.com/google/cel-go/common/types/ref"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
) )
// MatchRemoteIP matches requests by the remote IP address, // MatchRemoteIP matches requests by the remote IP address,
+1 -2
View File
@@ -20,10 +20,9 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/caddyserver/caddy/v2"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
"github.com/caddyserver/caddy/v2"
) )
// ServerLogConfig describes a server's logging configuration. If // ServerLogConfig describes a server's logging configuration. If
+6 -8
View File
@@ -25,17 +25,15 @@ import (
"path" "path"
"reflect" "reflect"
"regexp" "regexp"
"runtime"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"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/types" "github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/common/types/ref"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
) )
type ( type (
@@ -397,9 +395,7 @@ func (m MatchPath) Match(r *http.Request) bool {
// security risk (cry) if PHP files end up being served // security risk (cry) if PHP files end up being served
// as static files, exposing the source code, instead of // as static files, exposing the source code, instead of
// being matched by *.php to be treated as PHP scripts. // being matched by *.php to be treated as PHP scripts.
if runtime.GOOS == "windows" { // issue #5613 reqPath = strings.TrimRight(reqPath, ". ")
reqPath = strings.TrimRight(reqPath, ". ")
}
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
@@ -1393,7 +1389,9 @@ func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, er
return matcherSet, nil return matcherSet, nil
} }
var wordRE = regexp.MustCompile(`\w+`) var (
wordRE = regexp.MustCompile(`\w+`)
)
const regexpPlaceholderPrefix = "http.regexp" const regexpPlaceholderPrefix = "http.regexp"
+7 -5
View File
@@ -21,7 +21,6 @@ import (
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"os" "os"
"runtime"
"testing" "testing"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
@@ -254,6 +253,11 @@ func TestPathMatcher(t *testing.T) {
input: "/FOOOO", input: "/FOOOO",
expect: true, expect: true,
}, },
{
match: MatchPath{"*.php"},
input: "/foo/index.php. .",
expect: true,
},
{ {
match: MatchPath{"/foo/bar.txt"}, match: MatchPath{"/foo/bar.txt"},
input: "/foo/BAR.txt", input: "/foo/BAR.txt",
@@ -431,10 +435,8 @@ func TestPathMatcher(t *testing.T) {
func TestPathMatcherWindows(t *testing.T) { func TestPathMatcherWindows(t *testing.T) {
// only Windows has this bug where it will ignore // only Windows has this bug where it will ignore
// trailing dots and spaces in a filename // trailing dots and spaces in a filename, but we
if runtime.GOOS != "windows" { // test for it on all platforms to be more consistent
return
}
req := &http.Request{URL: &url.URL{Path: "/index.php . . .."}} req := &http.Request{URL: &url.URL{Path: "/index.php . . .."}}
repl := caddy.NewReplacer() repl := caddy.NewReplacer()
+1 -2
View File
@@ -6,10 +6,9 @@ import (
"sync" "sync"
"time" "time"
"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/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
"github.com/caddyserver/caddy/v2/internal/metrics"
) )
// Metrics configures metrics observations. // Metrics configures metrics observations.
@@ -19,9 +19,8 @@ import (
"net" "net"
"time" "time"
"github.com/mastercactapus/proxyprotocol"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/mastercactapus/proxyprotocol"
) )
// ListenerWrapper provides PROXY protocol support to Caddy by implementing // ListenerWrapper provides PROXY protocol support to Caddy by implementing
+2 -4
View File
@@ -19,11 +19,10 @@ import (
"net/http" "net/http"
"strings" "strings"
"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/headers" "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
"go.uber.org/zap"
) )
func init() { func init() {
@@ -252,6 +251,5 @@ const pushedLink = "http.handlers.push.pushed_link"
var ( var (
_ caddy.Provisioner = (*Handler)(nil) _ caddy.Provisioner = (*Handler)(nil)
_ caddyhttp.MiddlewareHandler = (*Handler)(nil) _ caddyhttp.MiddlewareHandler = (*Handler)(nil)
_ http.ResponseWriter = (*linkPusher)(nil) _ caddyhttp.HTTPInterfaces = (*linkPusher)(nil)
_ http.Pusher = (*linkPusher)(nil)
) )
+1 -2
View File
@@ -39,10 +39,9 @@ import (
"strings" "strings"
"time" "time"
"github.com/google/uuid"
"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/google/uuid"
) )
// NewTestReplacer creates a replacer for an http.Request // NewTestReplacer creates a replacer for an http.Request
+1 -2
View File
@@ -15,10 +15,9 @@
package requestbody package requestbody
import ( import (
"github.com/dustin/go-humanize"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/dustin/go-humanize"
) )
func init() { func init() {

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