mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-25 16:22:36 -04:00
Compare commits
196 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 18b346f6f9 | |||
| 52441e3037 | |||
| b825a10927 | |||
| 52f43d2f4c | |||
| 5e24e84288 | |||
| b16aba5c27 | |||
| 362f33daae | |||
| 3d7d60f7cf | |||
| dc12bd9743 | |||
| 56c6b3f673 | |||
| cbbd1df904 | |||
| 7d919af01b | |||
| 4a09cf0dc0 | |||
| b24ae63ea6 | |||
| 4173e2c77a | |||
| 18f34290d2 | |||
| 22eecdb90c | |||
| 4de2c1c65e | |||
| 878d491834 | |||
| 96f638eaad | |||
| 7e52db8280 | |||
| 3b3d678714 | |||
| ee358550e4 | |||
| 3f55efcfde | |||
| f71d779009 | |||
| d949caf459 | |||
| ac0ad4da84 | |||
| 4c10a05431 | |||
| fe2a02bf7a | |||
| 9fc55a9792 | |||
| 4e8245df0b | |||
| ac1f20b9e4 | |||
| 174c19a953 | |||
| c8559c4485 | |||
| 24b0ecc310 | |||
| 7c82e265da | |||
| 0900844c81 | |||
| 7984e6f6fd | |||
| d70608b656 | |||
| 1f60328e17 | |||
| 0e204b730a | |||
| fae195ac7e | |||
| 130f6d1f83 | |||
| 289934f3d1 | |||
| 3a3182fba3 | |||
| e8b8d4a8cd | |||
| a8586b05aa | |||
| 05dbe1c171 | |||
| 33d8d2c6b5 | |||
| 9c419f1e1a | |||
| b245ecd325 | |||
| 2a6859a5e4 | |||
| df99502977 | |||
| e0aaefab80 | |||
| fa5a579b60 | |||
| 88b4fbf244 | |||
| 5653c36bc2 | |||
| 4feac4d83c | |||
| 82c356f254 | |||
| 1405683c2b | |||
| 89c407aa34 | |||
| 58ab3a01a0 | |||
| a306c5f769 | |||
| 1e0dea59ef | |||
| 2cac3c5491 | |||
| f2ab7099db | |||
| 50cea4e263 | |||
| 1b73e3862d | |||
| c46ec3b500 | |||
| ed8bb13c5d | |||
| b7e472d548 | |||
| 7103ea096f | |||
| 888c6d7e93 | |||
| b377208ede | |||
| 4776f62caa | |||
| 38a7b6b3d0 | |||
| 84d5e1c5d6 | |||
| 288216e1fb | |||
| 10053f7570 | |||
| 0a6d3333b2 | |||
| 568fd2b286 | |||
| f11c3c9f5a | |||
| 936ee918ee | |||
| d6f86cccf5 | |||
| 2d7d806fcf | |||
| d8135505d3 | |||
| 11166889c5 | |||
| 080db93817 | |||
| a8492c064d | |||
| 6cdcc2a782 | |||
| fbb0ecfa32 | |||
| 5b9c850ab3 | |||
| b32f265eca | |||
| 431adc0980 | |||
| a8cc5d1a7d | |||
| 8d304a4566 | |||
| 65e33fc1ee | |||
| 9f34383c02 | |||
| b07b198764 | |||
| 51b1bfb125 | |||
| c049bab458 | |||
| e2fc08bd34 | |||
| 4aa4f3ac70 | |||
| 1913930783 | |||
| cd486c25d1 | |||
| e198c605bd | |||
| f66493efef | |||
| 5c51c1db2c | |||
| da23501457 | |||
| 94749e119a | |||
| d7d16360d4 | |||
| 4df27a20c8 | |||
| 18c309b5fa | |||
| e041962b66 | |||
| f45a6de20d | |||
| b51dc5d5d0 | |||
| f857b32d65 | |||
| 4e36b4c9d1 | |||
| 27bc16abed | |||
| bbe1952a59 | |||
| 0e2c7e1d35 | |||
| 7ceef91295 | |||
| 5dec11f2a0 | |||
| 66114cb155 | |||
| 7914ba3573 | |||
| dfe17c33ef | |||
| 710824c3ce | |||
| d8ae801068 | |||
| 119e8794bc | |||
| 22927e278d | |||
| 7a69ae7571 | |||
| 2b2addebb8 | |||
| 9563666bfb | |||
| 806341e089 | |||
| 0468508e92 | |||
| 415d1e7b6f | |||
| 1a36b06cd4 | |||
| 398c12ae9b | |||
| 361946eb0c | |||
| 424ae0f420 | |||
| 4548b7de8e | |||
| 3b19aa2b5a | |||
| 6a41b62e70 | |||
| 2ddb717144 | |||
| 56af1ceb32 | |||
| 4ba03c9d38 | |||
| 078f130a51 | |||
| 9c180a5988 | |||
| 467b7e3a9c | |||
| 31d75acc9c | |||
| 9cde715525 | |||
| 942fbb37ec | |||
| cee4441cb1 | |||
| 5bd9c49042 | |||
| cdd3884b32 | |||
| 2615c9c524 | |||
| 5336bc0fb6 | |||
| 29452647d8 | |||
| bd34cb6b4e | |||
| 2d236ead3e | |||
| 38cb587e0f | |||
| ca14b6edd9 | |||
| cbf16f6d9e | |||
| 13a37688dc | |||
| e8352aef38 | |||
| 36546cd8b9 | |||
| 75b690d248 | |||
| 52d7335c2b | |||
| 96919acc9d | |||
| e96aafe1ca | |||
| a02ecb0f88 | |||
| 5ebb7d496d | |||
| cfc85ae8ca | |||
| faf0399e80 | |||
| 808b05c3b4 | |||
| 12b2f22092 | |||
| 571fc034d3 | |||
| bef1a739db | |||
| 0de6064c3b | |||
| 774f228868 | |||
| b19946f6af | |||
| 335cd2e8a4 | |||
| 48598e1f2a | |||
| cdce452edc | |||
| f3e8b9d95f | |||
| c8032867b1 | |||
| 3f20a7c9f3 | |||
| 1af419e7ec | |||
| f0e3981774 | |||
| 1c9ea0113d | |||
| 2b04e09fa7 | |||
| 3443a8a056 | |||
| 2943c41884 | |||
| 53b6fab125 | |||
| c6ac350a3b | |||
| b301a3df70 |
+3
-3
@@ -7,7 +7,7 @@ The Caddy project would like to make sure that it stays on top of all practicall
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 2.x | :white_check_mark: |
|
||||
| 2.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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
|
||||
## 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
|
||||
- A working patch
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
+16
-10
@@ -18,17 +18,22 @@ jobs:
|
||||
# Default is true, cancels jobs for other platforms in the matrix if one fails
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
||||
go: [ '1.19', '1.20' ]
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macos-latest
|
||||
- windows-latest
|
||||
go:
|
||||
- '1.20'
|
||||
- '1.21'
|
||||
|
||||
include:
|
||||
# Set the minimum Go patch version for the given Go minor
|
||||
# Usable via ${{ matrix.GO_SEMVER }}
|
||||
- go: '1.19'
|
||||
GO_SEMVER: '~1.19.6'
|
||||
|
||||
- go: '1.20'
|
||||
GO_SEMVER: '~1.20.1'
|
||||
GO_SEMVER: '~1.20.6'
|
||||
|
||||
- go: '1.21'
|
||||
GO_SEMVER: '~1.21.0'
|
||||
|
||||
# Set some variables per OS, usable via ${{ matrix.VAR }}
|
||||
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
|
||||
@@ -49,7 +54,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
@@ -68,6 +73,7 @@ jobs:
|
||||
|
||||
- name: Print Go version and environment
|
||||
id: vars
|
||||
shell: bash
|
||||
run: |
|
||||
printf "Using go at: $(which go)\n"
|
||||
printf "Go version: $(go version)\n"
|
||||
@@ -130,7 +136,7 @@ jobs:
|
||||
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Run Tests
|
||||
run: |
|
||||
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
|
||||
@@ -156,9 +162,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: goreleaser/goreleaser-action@v4
|
||||
- uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
version: latest
|
||||
args: check
|
||||
|
||||
@@ -15,20 +15,33 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd']
|
||||
go: [ '1.20' ]
|
||||
goos:
|
||||
- 'aix'
|
||||
- 'android'
|
||||
- 'linux'
|
||||
- 'solaris'
|
||||
- 'illumos'
|
||||
- 'dragonfly'
|
||||
- 'freebsd'
|
||||
- 'openbsd'
|
||||
- 'plan9'
|
||||
- 'windows'
|
||||
- 'darwin'
|
||||
- 'netbsd'
|
||||
go:
|
||||
- '1.21'
|
||||
|
||||
include:
|
||||
# Set the minimum Go patch version for the given Go minor
|
||||
# Usable via ${{ matrix.GO_SEMVER }}
|
||||
- go: '1.20'
|
||||
GO_SEMVER: '~1.20.1'
|
||||
- go: '1.21'
|
||||
GO_SEMVER: '~1.21.0'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
@@ -50,11 +63,12 @@ jobs:
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goos == 'aix' && 'ppc64' || 'amd64' }}
|
||||
shell: bash
|
||||
continue-on-error: true
|
||||
working-directory: ./cmd/caddy
|
||||
run: |
|
||||
GOOS=$GOOS go build -trimpath -o caddy-"$GOOS"-amd64 2> /dev/null
|
||||
GOOS=$GOOS GOARCH=$GOARCH go build -trimpath -o caddy-"$GOOS"-$GOARCH 2> /dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "::warning ::$GOOS Build Failed"
|
||||
exit 0
|
||||
|
||||
@@ -17,25 +17,45 @@ jobs:
|
||||
# From https://github.com/golangci/golangci-lint-action
|
||||
golangci:
|
||||
permissions:
|
||||
contents: read # for actions/checkout to fetch code
|
||||
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
|
||||
contents: read # for actions/checkout to fetch code
|
||||
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
|
||||
name: lint
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macos-latest
|
||||
- windows-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '~1.19.6'
|
||||
go-version: '~1.21.0'
|
||||
check-latest: true
|
||||
|
||||
# Workaround for https://github.com/golangci/golangci-lint-action/issues/135
|
||||
skip-pkg-cache: true
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: v1.50
|
||||
version: v1.54
|
||||
|
||||
# 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.
|
||||
args: --timeout 10m
|
||||
|
||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||
# only-new-issues: true
|
||||
|
||||
govulncheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: govulncheck
|
||||
uses: golang/govulncheck-action@v1
|
||||
with:
|
||||
go-version-input: '~1.21.0'
|
||||
check-latest: true
|
||||
|
||||
@@ -10,14 +10,16 @@ jobs:
|
||||
name: Release
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
go: [ '1.20' ]
|
||||
os:
|
||||
- ubuntu-latest
|
||||
go:
|
||||
- '1.21'
|
||||
|
||||
include:
|
||||
# Set the minimum Go patch version for the given Go minor
|
||||
# Usable via ${{ matrix.GO_SEMVER }}
|
||||
- go: '1.20'
|
||||
GO_SEMVER: '~1.20.1'
|
||||
- go: '1.21'
|
||||
GO_SEMVER: '~1.21.0'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
|
||||
@@ -30,7 +32,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -41,7 +43,7 @@ jobs:
|
||||
check-latest: true
|
||||
|
||||
# Force fetch upstream tags -- because 65 minutes
|
||||
# tl;dr: actions/checkout@v3 runs this line:
|
||||
# tl;dr: actions/checkout@v4 runs this line:
|
||||
# git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/
|
||||
# which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran:
|
||||
# git fetch --prune --unshallow
|
||||
@@ -104,10 +106,10 @@ jobs:
|
||||
run: syft version
|
||||
# GoReleaser will take care of publishing those artifacts into the release
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
version: latest
|
||||
args: release --rm-dist --timeout 60m
|
||||
args: release --clean --timeout 60m
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ steps.vars.outputs.version_tag }}
|
||||
|
||||
@@ -10,7 +10,8 @@ jobs:
|
||||
name: Release Published
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest ]
|
||||
os:
|
||||
- ubuntu-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
|
||||
@@ -11,6 +11,8 @@ Caddyfile.*
|
||||
# build artifacts and helpers
|
||||
cmd/caddy/caddy
|
||||
cmd/caddy/caddy.exe
|
||||
cmd/caddy/tmp/*.exe
|
||||
cmd/caddy/.env
|
||||
|
||||
# mac specific
|
||||
.DS_Store
|
||||
|
||||
+20
-10
@@ -2,15 +2,27 @@ linters-settings:
|
||||
errcheck:
|
||||
ignore: fmt:.*,go.uber.org/zap/zapcore:^Add.*
|
||||
ignoretests: true
|
||||
gci:
|
||||
sections:
|
||||
- standard # Standard section: captures all standard packages.
|
||||
- default # Default section: contains all imports that could not be matched to another section type.
|
||||
- prefix(github.com/caddyserver/caddy/v2/cmd) # ensure that this is always at the top and always has a line break.
|
||||
- prefix(github.com/caddyserver/caddy) # Custom section: groups all imports with the specified Prefix.
|
||||
# Skip generated files.
|
||||
# Default: true
|
||||
skip-generated: true
|
||||
# Enable custom order of sections.
|
||||
# If `true`, make the section order the same as the order of `sections`.
|
||||
# Default: false
|
||||
custom-order: true
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- errcheck
|
||||
- gofmt
|
||||
- goimports
|
||||
- gci
|
||||
- gofumpt
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
@@ -18,11 +30,9 @@ linters:
|
||||
- misspell
|
||||
- prealloc
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unused
|
||||
- varcheck
|
||||
# these are implicitly disabled:
|
||||
# - asciicheck
|
||||
# - depguard
|
||||
@@ -80,23 +90,23 @@ output:
|
||||
issues:
|
||||
exclude-rules:
|
||||
# we aren't calling unknown URL
|
||||
- text: "G107" # G107: Url provided to HTTP request as taint input
|
||||
- text: 'G107' # G107: Url provided to HTTP request as taint input
|
||||
linters:
|
||||
- gosec
|
||||
# as a web server that's expected to handle any template, this is totally in the hands of the user.
|
||||
- text: "G203" # G203: Use of unescaped data in HTML templates
|
||||
- text: 'G203' # G203: Use of unescaped data in HTML templates
|
||||
linters:
|
||||
- gosec
|
||||
# we're shelling out to known commands, not relying on user-defined input.
|
||||
- text: "G204" # G204: Audit use of command execution
|
||||
- text: 'G204' # G204: Audit use of command execution
|
||||
linters:
|
||||
- gosec
|
||||
# the choice of weakrand is deliberate, hence the named import "weakrand"
|
||||
- path: modules/caddyhttp/reverseproxy/selectionpolicies.go
|
||||
text: "G404" # G404: Insecure random number source (rand)
|
||||
text: 'G404' # G404: Insecure random number source (rand)
|
||||
linters:
|
||||
- gosec
|
||||
- path: modules/caddyhttp/reverseproxy/streaming.go
|
||||
text: "G404" # G404: Insecure random number source (rand)
|
||||
text: 'G404' # G404: Insecure random number source (rand)
|
||||
linters:
|
||||
- gosec
|
||||
|
||||
+8
-10
@@ -43,6 +43,7 @@ builds:
|
||||
- arm64
|
||||
- s390x
|
||||
- ppc64le
|
||||
- riscv64
|
||||
goarm:
|
||||
- "5"
|
||||
- "6"
|
||||
@@ -54,14 +55,20 @@ builds:
|
||||
goarch: ppc64le
|
||||
- goos: darwin
|
||||
goarch: s390x
|
||||
- goos: darwin
|
||||
goarch: riscv64
|
||||
- goos: windows
|
||||
goarch: ppc64le
|
||||
- goos: windows
|
||||
goarch: s390x
|
||||
- goos: windows
|
||||
goarch: riscv64
|
||||
- goos: freebsd
|
||||
goarch: ppc64le
|
||||
- goos: freebsd
|
||||
goarch: s390x
|
||||
- goos: freebsd
|
||||
goarch: riscv64
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
goarm: "5"
|
||||
@@ -106,11 +113,10 @@ archives:
|
||||
{{- with .Mips }}_{{ . }}{{ end }}
|
||||
{{- if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}
|
||||
|
||||
# packge the 'caddy-build' directory into a tarball,
|
||||
# package the 'caddy-build' directory into a tarball,
|
||||
# allowing users to build the exact same set of files as ours.
|
||||
- id: source
|
||||
meta: true
|
||||
rlcp: true
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_buildable-artifact"
|
||||
files:
|
||||
- src: LICENSE
|
||||
@@ -127,14 +133,6 @@ source:
|
||||
name_template: '{{ .ProjectName }}_{{ .Version }}_src'
|
||||
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.
|
||||
#
|
||||
# Default: empty.
|
||||
|
||||
@@ -87,7 +87,7 @@ See [our online documentation](https://caddyserver.com/docs/install) for other i
|
||||
|
||||
Requirements:
|
||||
|
||||
- [Go 1.19 or newer](https://golang.org/dl/)
|
||||
- [Go 1.20 or newer](https://golang.org/dl/)
|
||||
|
||||
### For development
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ func init() {
|
||||
if env, exists := os.LookupEnv("CADDY_ADMIN"); exists {
|
||||
DefaultAdminListen = env
|
||||
}
|
||||
RegisterNamespace("caddy.config_loaders", []interface{}{(*ConfigLoader)(nil)})
|
||||
}
|
||||
|
||||
// AdminConfig configures Caddy's API endpoint, which is used
|
||||
@@ -71,6 +72,11 @@ type AdminConfig struct {
|
||||
// parsed by Caddy. Accepts placeholders.
|
||||
// Default: the value of the `CADDY_ADMIN` environment variable,
|
||||
// or `localhost:2019` otherwise.
|
||||
//
|
||||
// Remember: When changing this value through a config reload,
|
||||
// be sure to use the `--address` CLI flag to specify the current
|
||||
// admin address if the currently-running admin endpoint is not
|
||||
// the default address.
|
||||
Listen string `json:"listen,omitempty"`
|
||||
|
||||
// If true, CORS headers will be emitted, and requests to the
|
||||
@@ -313,7 +319,32 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
|
||||
// messages. If the requested URI does not include an Internet host
|
||||
// name for the service being requested, then the Host header field MUST
|
||||
// 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["127.0.0.1"] = struct{}{}
|
||||
uniqueOrigins["::1"] = struct{}{}
|
||||
} else {
|
||||
uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
|
||||
uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
|
||||
@@ -1011,9 +1042,9 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
|
||||
id := parts[2]
|
||||
|
||||
// map the ID to the expanded path
|
||||
currentCtxMu.RLock()
|
||||
rawCfgMu.RLock()
|
||||
expanded, ok := rawCfgIndex[id]
|
||||
defer currentCtxMu.RUnlock()
|
||||
rawCfgMu.RUnlock()
|
||||
if !ok {
|
||||
return APIError{
|
||||
HTTPStatus: http.StatusNotFound,
|
||||
@@ -1166,15 +1197,27 @@ traverseLoop:
|
||||
}
|
||||
case http.MethodPut:
|
||||
if _, ok := v[part]; ok {
|
||||
return fmt.Errorf("[%s] key already exists: %s", path, part)
|
||||
return APIError{
|
||||
HTTPStatus: http.StatusConflict,
|
||||
Err: fmt.Errorf("[%s] key already exists: %s", path, part),
|
||||
}
|
||||
}
|
||||
v[part] = val
|
||||
case http.MethodPatch:
|
||||
if _, ok := v[part]; !ok {
|
||||
return fmt.Errorf("[%s] key does not exist: %s", path, part)
|
||||
return APIError{
|
||||
HTTPStatus: http.StatusNotFound,
|
||||
Err: fmt.Errorf("[%s] key does not exist: %s", path, part),
|
||||
}
|
||||
}
|
||||
v[part] = val
|
||||
case http.MethodDelete:
|
||||
if _, ok := v[part]; !ok {
|
||||
return APIError{
|
||||
HTTPStatus: http.StatusNotFound,
|
||||
Err: fmt.Errorf("[%s] key does not exist: %s", path, part),
|
||||
}
|
||||
}
|
||||
delete(v, part)
|
||||
default:
|
||||
return fmt.Errorf("unrecognized method %s", method)
|
||||
@@ -1316,7 +1359,7 @@ var (
|
||||
// will get deleted before the process gracefully exits.
|
||||
func PIDFile(filename string) error {
|
||||
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
|
||||
err := os.WriteFile(filename, pid, 0600)
|
||||
err := os.WriteFile(filename, pid, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -75,6 +75,12 @@ func TestUnsyncedConfigAccess(t *testing.T) {
|
||||
path: "/bar/qq",
|
||||
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
|
||||
},
|
||||
{
|
||||
method: "DELETE",
|
||||
path: "/bar/qq",
|
||||
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/list",
|
||||
|
||||
@@ -34,12 +34,22 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/notify"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/notify"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterNamespace("", []interface{}{
|
||||
(*App)(nil),
|
||||
})
|
||||
RegisterNamespace("caddy.storage", []interface{}{
|
||||
(*StorageConverter)(nil),
|
||||
})
|
||||
}
|
||||
|
||||
// Config is the top (or beginning) of the Caddy configuration structure.
|
||||
// Caddy config is expressed natively as a JSON document. If you prefer
|
||||
// not to work with JSON directly, there are [many config adapters](/docs/config-adapters)
|
||||
@@ -71,11 +81,15 @@ type Config struct {
|
||||
// module is `caddy.storage.file_system` (the local file system),
|
||||
// and the default path
|
||||
// [depends on the OS and environment](/docs/conventions#data-directory).
|
||||
// A storage `module` should implement the following interfaces:
|
||||
// - [StorageConverter](https://pkg.go.dev/github.com/caddyserver/caddy/v2#StorageConverter)
|
||||
StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`
|
||||
|
||||
// AppsRaw are the apps that Caddy will load and run. The
|
||||
// app module name is the key, and the app's config is the
|
||||
// associated value.
|
||||
// An `app` should implement the following interfaces:
|
||||
// - [caddy.App](https://pkg.go.dev/github.com/caddyserver/caddy/v2?tab=doc#App)
|
||||
AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="`
|
||||
|
||||
apps map[string]App
|
||||
@@ -156,8 +170,8 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
|
||||
return fmt.Errorf("method not allowed")
|
||||
}
|
||||
|
||||
currentCtxMu.Lock()
|
||||
defer currentCtxMu.Unlock()
|
||||
rawCfgMu.Lock()
|
||||
defer rawCfgMu.Unlock()
|
||||
|
||||
if ifMatchHeader != "" {
|
||||
// expect the first and last character to be quotes
|
||||
@@ -257,8 +271,8 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
|
||||
// readConfig traverses the current config to path
|
||||
// and writes its JSON encoding to out.
|
||||
func readConfig(path string, out io.Writer) error {
|
||||
currentCtxMu.RLock()
|
||||
defer currentCtxMu.RUnlock()
|
||||
rawCfgMu.RLock()
|
||||
defer rawCfgMu.RUnlock()
|
||||
return unsyncedConfigAccess(http.MethodGet, path, nil, out)
|
||||
}
|
||||
|
||||
@@ -305,7 +319,7 @@ func indexConfigObjects(ptr any, configPath string, index map[string]string) err
|
||||
// it as the new config, replacing any other current config.
|
||||
// It does NOT update the raw config state, as this is a
|
||||
// lower-level function; most callers will want to use Load
|
||||
// instead. A write lock on currentCtxMu is required! If
|
||||
// instead. A write lock on rawCfgMu is required! If
|
||||
// allowPersist is false, it will not be persisted to disk,
|
||||
// even if it is configured to.
|
||||
func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
||||
@@ -340,8 +354,10 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
||||
}
|
||||
|
||||
// swap old context (including its config) with the new one
|
||||
currentCtxMu.Lock()
|
||||
oldCtx := currentCtx
|
||||
currentCtx = ctx
|
||||
currentCtxMu.Unlock()
|
||||
|
||||
// Stop, Cleanup each old app
|
||||
unsyncedStop(oldCtx)
|
||||
@@ -354,13 +370,13 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
||||
newCfg.Admin.Config.Persist == nil ||
|
||||
*newCfg.Admin.Config.Persist) {
|
||||
dir := filepath.Dir(ConfigAutosavePath)
|
||||
err := os.MkdirAll(dir, 0700)
|
||||
err := os.MkdirAll(dir, 0o700)
|
||||
if err != nil {
|
||||
Log().Error("unable to create folder for config autosave",
|
||||
zap.String("dir", dir),
|
||||
zap.Error(err))
|
||||
} else {
|
||||
err := os.WriteFile(ConfigAutosavePath, cfgJSON, 0600)
|
||||
err := os.WriteFile(ConfigAutosavePath, cfgJSON, 0o600)
|
||||
if err == nil {
|
||||
Log().Info("autosaved config (load with --resume flag)", zap.String("file", ConfigAutosavePath))
|
||||
} else {
|
||||
@@ -627,22 +643,35 @@ type ConfigLoader interface {
|
||||
// stop the others. Stop should only be called
|
||||
// if not replacing with a new config.
|
||||
func Stop() error {
|
||||
currentCtxMu.RLock()
|
||||
ctx := currentCtx
|
||||
currentCtxMu.RUnlock()
|
||||
|
||||
rawCfgMu.Lock()
|
||||
unsyncedStop(ctx)
|
||||
|
||||
currentCtxMu.Lock()
|
||||
defer currentCtxMu.Unlock()
|
||||
unsyncedStop(currentCtx)
|
||||
currentCtx = Context{}
|
||||
currentCtxMu.Unlock()
|
||||
|
||||
rawCfgJSON = nil
|
||||
rawCfgIndex = nil
|
||||
rawCfg[rawConfigKey] = nil
|
||||
rawCfgMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// unsyncedStop stops cfg from running, but has
|
||||
// no locking around cfg. It is a no-op if cfg is
|
||||
// nil. If any app returns an error when stopping,
|
||||
// unsyncedStop stops ctx from running, but has
|
||||
// no locking around ctx. It is a no-op if ctx has a
|
||||
// nil cfg. If any app returns an error when stopping,
|
||||
// it is logged and the function continues stopping
|
||||
// the next app. This function assumes all apps in
|
||||
// cfg were successfully started first.
|
||||
// ctx 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) {
|
||||
if ctx.cfg == nil {
|
||||
return
|
||||
@@ -809,14 +838,19 @@ func ParseDuration(s string) (time.Duration, error) {
|
||||
// regardless of storage configuration, since each instance is intended to
|
||||
// have its own unique ID.
|
||||
func InstanceID() (uuid.UUID, error) {
|
||||
uuidFilePath := filepath.Join(AppDataDir(), "instance.uuid")
|
||||
appDataDir := AppDataDir()
|
||||
uuidFilePath := filepath.Join(appDataDir, "instance.uuid")
|
||||
uuidFileBytes, err := os.ReadFile(uuidFilePath)
|
||||
if os.IsNotExist(err) {
|
||||
uuid, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return uuid, err
|
||||
}
|
||||
err = os.WriteFile(uuidFilePath, []byte(uuid.String()), 0600)
|
||||
err = os.MkdirAll(appDataDir, 0o600)
|
||||
if err != nil {
|
||||
return uuid, err
|
||||
}
|
||||
err = os.WriteFile(uuidFilePath, []byte(uuid.String()), 0o600)
|
||||
return uuid, err
|
||||
} else if err != nil {
|
||||
return [16]byte{}, err
|
||||
@@ -969,14 +1003,12 @@ type CtxKey string
|
||||
|
||||
// This group of variables pertains to the current configuration.
|
||||
var (
|
||||
// currentCtxMu protects everything in this var block.
|
||||
currentCtxMu sync.RWMutex
|
||||
|
||||
// currentCtx is the root context for the currently-running
|
||||
// configuration, which can be accessed through this value.
|
||||
// If the Config contained in this value is not nil, then
|
||||
// a config is currently active/running.
|
||||
currentCtx Context
|
||||
currentCtx Context
|
||||
currentCtxMu sync.RWMutex
|
||||
|
||||
// rawCfg is the current, generic-decoded configuration;
|
||||
// we initialize it as a map with one field ("config")
|
||||
@@ -994,6 +1026,10 @@ var (
|
||||
// rawCfgIndex is the map of user-assigned ID to expanded
|
||||
// path, for converting /id/ paths to /config/ paths.
|
||||
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
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Dispenser is a type that dispenses tokens, similarly to a lexer,
|
||||
@@ -105,7 +106,7 @@ func (d *Dispenser) nextOnSameLine() bool {
|
||||
}
|
||||
curr := d.tokens[d.cursor]
|
||||
next := d.tokens[d.cursor+1]
|
||||
if curr.File == next.File && curr.Line+curr.NumLineBreaks() == next.Line {
|
||||
if !isNextOnNewLine(curr, next) {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
@@ -126,7 +127,7 @@ func (d *Dispenser) NextLine() bool {
|
||||
}
|
||||
curr := d.tokens[d.cursor]
|
||||
next := d.tokens[d.cursor+1]
|
||||
if curr.File != next.File || curr.Line+curr.NumLineBreaks() < next.Line {
|
||||
if isNextOnNewLine(curr, next) {
|
||||
d.cursor++
|
||||
return true
|
||||
}
|
||||
@@ -390,22 +391,22 @@ func (d *Dispenser) Reset() {
|
||||
// an argument.
|
||||
func (d *Dispenser) ArgErr() error {
|
||||
if d.Val() == "{" {
|
||||
return d.Err("Unexpected token '{', expecting argument")
|
||||
return d.Err("unexpected token '{', expecting argument")
|
||||
}
|
||||
return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val())
|
||||
return d.Errf("wrong argument count or unexpected line ending after '%s'", d.Val())
|
||||
}
|
||||
|
||||
// SyntaxErr creates a generic syntax error which explains what was
|
||||
// found and what was expected.
|
||||
func (d *Dispenser) SyntaxErr(expected string) error {
|
||||
msg := fmt.Sprintf("%s:%d - Syntax error: Unexpected token '%s', expecting '%s'", d.File(), d.Line(), d.Val(), expected)
|
||||
msg := fmt.Sprintf("syntax error: unexpected token '%s', expecting '%s', at %s:%d import chain: ['%s']", d.Val(), expected, d.File(), d.Line(), strings.Join(d.Token().imports, "','"))
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
// EOFErr returns an error indicating that the dispenser reached
|
||||
// the end of the input when searching for the next token.
|
||||
func (d *Dispenser) EOFErr() error {
|
||||
return d.Errf("Unexpected EOF")
|
||||
return d.Errf("unexpected EOF")
|
||||
}
|
||||
|
||||
// Err generates a custom parse-time error with a message of msg.
|
||||
@@ -420,7 +421,10 @@ func (d *Dispenser) Errf(format string, args ...any) error {
|
||||
|
||||
// WrapErr takes an existing error and adds the Caddyfile file and line number.
|
||||
func (d *Dispenser) WrapErr(err error) error {
|
||||
return fmt.Errorf("%s:%d - Error during parsing: %w", d.File(), d.Line(), err)
|
||||
if len(d.Token().imports) > 0 {
|
||||
return fmt.Errorf("%w, at %s:%d import chain ['%s']", err, d.File(), d.Line(), strings.Join(d.Token().imports, "','"))
|
||||
}
|
||||
return fmt.Errorf("%w, at %s:%d", err, d.File(), d.Line())
|
||||
}
|
||||
|
||||
// Delete deletes the current token and returns the updated slice
|
||||
@@ -463,17 +467,7 @@ func (d *Dispenser) isNewLine() bool {
|
||||
|
||||
prev := d.tokens[d.cursor-1]
|
||||
curr := d.tokens[d.cursor]
|
||||
|
||||
// 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
|
||||
return isNextOnNewLine(prev, curr)
|
||||
}
|
||||
|
||||
// isNextOnNewLine determines whether the current token is on a different
|
||||
@@ -489,15 +483,5 @@ func (d *Dispenser) isNextOnNewLine() bool {
|
||||
|
||||
curr := d.tokens[d.cursor]
|
||||
next := d.tokens[d.cursor+1]
|
||||
|
||||
// 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
|
||||
return isNextOnNewLine(curr, next)
|
||||
}
|
||||
|
||||
@@ -19,8 +19,9 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
// parseVariadic determines if the token is a variadic placeholder,
|
||||
@@ -39,7 +40,7 @@ func parseVariadic(token Token, argCount int) (bool, int, int) {
|
||||
if argRange == "" {
|
||||
caddy.Log().Named("caddyfile").Warn(
|
||||
"Placeholder "+token.Text+" cannot have an empty index",
|
||||
zap.String("file", token.File+":"+strconv.Itoa(token.Line)))
|
||||
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
|
||||
return false, 0, 0
|
||||
}
|
||||
|
||||
@@ -51,6 +52,13 @@ func parseVariadic(token Token, argCount int) (bool, int, int) {
|
||||
return false, 0, 0
|
||||
}
|
||||
|
||||
// A valid token may contain several placeholders, and
|
||||
// they may be separated by ":". It's not variadic.
|
||||
// https://github.com/caddyserver/caddy/issues/5716
|
||||
if strings.Contains(start, "}") || strings.Contains(end, "{") {
|
||||
return false, 0, 0
|
||||
}
|
||||
|
||||
var (
|
||||
startIndex = 0
|
||||
endIndex = argCount
|
||||
@@ -61,7 +69,7 @@ func parseVariadic(token Token, argCount int) (bool, int, int) {
|
||||
if err != nil {
|
||||
caddy.Log().Named("caddyfile").Warn(
|
||||
"Variadic placeholder "+token.Text+" has an invalid start index",
|
||||
zap.String("file", token.File+":"+strconv.Itoa(token.Line)))
|
||||
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
|
||||
return false, 0, 0
|
||||
}
|
||||
}
|
||||
@@ -70,7 +78,7 @@ func parseVariadic(token Token, argCount int) (bool, int, int) {
|
||||
if err != nil {
|
||||
caddy.Log().Named("caddyfile").Warn(
|
||||
"Variadic placeholder "+token.Text+" has an invalid end index",
|
||||
zap.String("file", token.File+":"+strconv.Itoa(token.Line)))
|
||||
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
|
||||
return false, 0, 0
|
||||
}
|
||||
}
|
||||
@@ -79,7 +87,7 @@ func parseVariadic(token Token, argCount int) (bool, int, int) {
|
||||
if startIndex < 0 || startIndex > endIndex || endIndex > argCount {
|
||||
caddy.Log().Named("caddyfile").Warn(
|
||||
"Variadic placeholder "+token.Text+" indices are out of bounds, only "+strconv.Itoa(argCount)+" argument(s) exist",
|
||||
zap.String("file", token.File+":"+strconv.Itoa(token.Line)))
|
||||
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
|
||||
return false, 0, 0
|
||||
}
|
||||
return true, startIndex, endIndex
|
||||
@@ -93,6 +101,11 @@ func makeArgsReplacer(args []string) *caddy.Replacer {
|
||||
// TODO: Remove the deprecated {args.*} placeholder
|
||||
// support at some point in the future
|
||||
if matches := argsRegexpIndexDeprecated.FindStringSubmatch(key); len(matches) > 0 {
|
||||
// What's matched may be a substring of the key
|
||||
if matches[0] != key {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
value, err := strconv.Atoi(matches[1])
|
||||
if err != nil {
|
||||
caddy.Log().Named("caddyfile").Warn(
|
||||
@@ -111,6 +124,11 @@ func makeArgsReplacer(args []string) *caddy.Replacer {
|
||||
|
||||
// Handle args[*] form
|
||||
if matches := argsRegexpIndex.FindStringSubmatch(key); len(matches) > 0 {
|
||||
// What's matched may be a substring of the key
|
||||
if matches[0] != key {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if strings.Contains(matches[1], ":") {
|
||||
caddy.Log().Named("caddyfile").Warn(
|
||||
"Variadic placeholder {args[" + matches[1] + "]} must be a token on its own")
|
||||
|
||||
@@ -34,6 +34,7 @@ func (i *importGraph) addNode(name string) {
|
||||
}
|
||||
i.nodes[name] = true
|
||||
}
|
||||
|
||||
func (i *importGraph) addNodes(names []string) {
|
||||
for _, name := range names {
|
||||
i.addNode(name)
|
||||
@@ -43,6 +44,7 @@ func (i *importGraph) addNodes(names []string) {
|
||||
func (i *importGraph) removeNode(name string) {
|
||||
delete(i.nodes, name)
|
||||
}
|
||||
|
||||
func (i *importGraph) removeNodes(names []string) {
|
||||
for _, name := range names {
|
||||
i.removeNode(name)
|
||||
@@ -73,6 +75,7 @@ func (i *importGraph) addEdge(from, to string) error {
|
||||
i.edges[from] = append(i.edges[from], to)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *importGraph) addEdges(from string, tos []string) error {
|
||||
for _, to := range tos {
|
||||
err := i.addEdge(from, to)
|
||||
|
||||
@@ -39,7 +39,7 @@ type (
|
||||
// Token represents a single parsable unit.
|
||||
Token struct {
|
||||
File string
|
||||
origFile string
|
||||
imports []string
|
||||
Line int
|
||||
Text string
|
||||
wasQuoted rune // enclosing quote character, if any
|
||||
@@ -137,18 +137,32 @@ func (l *lexer) next() (bool, error) {
|
||||
}
|
||||
|
||||
// detect whether we have the start of a heredoc
|
||||
if !inHeredoc && !heredocEscaped && len(val) > 1 && string(val[:2]) == "<<" {
|
||||
if ch == '<' {
|
||||
return false, fmt.Errorf("too many '<' for heredoc on line #%d; only use two, for example <<END", l.line)
|
||||
if !(quoted || btQuoted) && !(inHeredoc || heredocEscaped) &&
|
||||
len(val) > 1 && string(val[:2]) == "<<" {
|
||||
// a space means it's just a regular token and not a heredoc
|
||||
if ch == ' ' {
|
||||
return makeToken(0), nil
|
||||
}
|
||||
|
||||
// skip CR, we only care about LF
|
||||
if ch == '\r' {
|
||||
continue
|
||||
}
|
||||
|
||||
// after hitting a newline, we know that the heredoc marker
|
||||
// is the characters after the two << and the newline.
|
||||
// we reset the val because the heredoc is syntax we don't
|
||||
// want to keep.
|
||||
if ch == '\n' {
|
||||
if len(val) == 2 {
|
||||
return false, fmt.Errorf("missing opening heredoc marker on line #%d; must contain only alpha-numeric characters, dashes and underscores; got empty string", l.line)
|
||||
}
|
||||
|
||||
// check if there's too many <
|
||||
if string(val[:3]) == "<<<" {
|
||||
return false, fmt.Errorf("too many '<' for heredoc on line #%d; only use two, for example <<END", l.line)
|
||||
}
|
||||
|
||||
heredocMarker = string(val[2:])
|
||||
if !heredocMarkerRegexp.Match([]byte(heredocMarker)) {
|
||||
return false, fmt.Errorf("heredoc marker on line #%d must contain only alpha-numeric characters, dashes and underscores; got '%s'", l.line, heredocMarker)
|
||||
@@ -321,23 +335,6 @@ func (l *lexer) finalizeHeredoc(val []rune, marker string) ([]rune, error) {
|
||||
return []rune(out), nil
|
||||
}
|
||||
|
||||
// originalFile gets original filename before import modification.
|
||||
func (t Token) originalFile() string {
|
||||
if t.origFile != "" {
|
||||
return t.origFile
|
||||
}
|
||||
return t.File
|
||||
}
|
||||
|
||||
// updateFile updates the token's source filename for error display
|
||||
// and remembers the original filename. Used during "import" processing.
|
||||
func (t *Token) updateFile(file string) {
|
||||
if t.origFile == "" {
|
||||
t.origFile = t.File
|
||||
}
|
||||
t.File = file
|
||||
}
|
||||
|
||||
func (t Token) Quoted() bool {
|
||||
return t.wasQuoted > 0
|
||||
}
|
||||
@@ -355,3 +352,28 @@ func (t Token) NumLineBreaks() int {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -322,15 +322,59 @@ EOF same-line-arg
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte(`heredoc <EOF
|
||||
input: []byte(`escaped-heredoc \<< >>`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `escaped-heredoc`},
|
||||
{Line: 1, Text: `<<`},
|
||||
{Line: 1, Text: `>>`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte(`not-a-heredoc <EOF
|
||||
content
|
||||
EOF same-line-arg
|
||||
`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `heredoc`},
|
||||
{Line: 1, Text: `not-a-heredoc`},
|
||||
{Line: 1, Text: `<EOF`},
|
||||
{Line: 2, Text: `content`},
|
||||
{Line: 3, Text: `EOF`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte(`not-a-heredoc <<<EOF content`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `not-a-heredoc`},
|
||||
{Line: 1, Text: `<<<EOF`},
|
||||
{Line: 1, Text: `content`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte(`not-a-heredoc "<<" ">>"`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `not-a-heredoc`},
|
||||
{Line: 1, Text: `<<`},
|
||||
{Line: 1, Text: `>>`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte(`not-a-heredoc << >>`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `not-a-heredoc`},
|
||||
{Line: 1, Text: `<<`},
|
||||
{Line: 1, Text: `>>`},
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte(`not-a-heredoc <<HERE SAME LINE
|
||||
content
|
||||
HERE same-line-arg
|
||||
`),
|
||||
expected: []Token{
|
||||
{Line: 1, Text: `not-a-heredoc`},
|
||||
{Line: 1, Text: `<<HERE`},
|
||||
{Line: 1, Text: `SAME`},
|
||||
{Line: 1, Text: `LINE`},
|
||||
{Line: 2, Text: `content`},
|
||||
{Line: 3, Text: `HERE`},
|
||||
{Line: 3, Text: `same-line-arg`},
|
||||
},
|
||||
},
|
||||
@@ -366,12 +410,9 @@ EOF same-line-arg
|
||||
},
|
||||
},
|
||||
{
|
||||
input: []byte(`heredoc <<HERE SAME LINE
|
||||
content
|
||||
HERE same-line-arg
|
||||
`),
|
||||
input: []byte("not-a-heredoc <<\n"),
|
||||
expectErr: true,
|
||||
errorMessage: "heredoc marker on line #1 must contain only alpha-numeric characters, dashes and underscores; got 'HERE SAME LINE'",
|
||||
errorMessage: "missing opening heredoc marker on line #1; must contain only alpha-numeric characters, dashes and underscores; got empty string",
|
||||
},
|
||||
{
|
||||
input: []byte(`heredoc <<<EOF
|
||||
|
||||
@@ -22,8 +22,9 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
// Parse parses the input just enough to group tokens, in
|
||||
@@ -148,7 +149,6 @@ func (p *parser) begin() error {
|
||||
}
|
||||
|
||||
err := p.addresses()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -159,6 +159,25 @@ func (p *parser) begin() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ok, name := p.isNamedRoute(); ok {
|
||||
// named routes only have one key, the route name
|
||||
p.block.Keys = []string{name}
|
||||
p.block.IsNamedRoute = true
|
||||
|
||||
// we just need a dummy leading token to ease parsing later
|
||||
nameToken := p.Token()
|
||||
nameToken.Text = name
|
||||
|
||||
// get all the tokens from the block, including the braces
|
||||
tokens, err := p.blockTokens(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tokens = append([]Token{nameToken}, tokens...)
|
||||
p.block.Segments = []Segment{tokens}
|
||||
return nil
|
||||
}
|
||||
|
||||
if ok, name := p.isSnippet(); ok {
|
||||
if p.definedSnippets == nil {
|
||||
p.definedSnippets = map[string][]Token{}
|
||||
@@ -167,7 +186,7 @@ func (p *parser) begin() error {
|
||||
return p.Errf("redeclaration of previously declared snippet %s", name)
|
||||
}
|
||||
// consume all tokens til matched close brace
|
||||
tokens, err := p.snippetTokens()
|
||||
tokens, err := p.blockTokens(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -196,7 +215,7 @@ func (p *parser) addresses() error {
|
||||
|
||||
// special case: import directive replaces tokens during parse-time
|
||||
if tkn == "import" && p.isNewLine() {
|
||||
err := p.doImport()
|
||||
err := p.doImport(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -296,7 +315,7 @@ func (p *parser) directives() error {
|
||||
|
||||
// special case: import directive replaces tokens during parse-time
|
||||
if p.Val() == "import" {
|
||||
err := p.doImport()
|
||||
err := p.doImport(1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -322,7 +341,7 @@ func (p *parser) directives() error {
|
||||
// is on the token before where the import directive was. In
|
||||
// other words, call Next() to access the first token that was
|
||||
// imported.
|
||||
func (p *parser) doImport() error {
|
||||
func (p *parser) doImport(nesting int) error {
|
||||
// syntax checks
|
||||
if !p.NextArg() {
|
||||
return p.ArgErr()
|
||||
@@ -411,7 +430,7 @@ func (p *parser) doImport() error {
|
||||
nodes = matches
|
||||
}
|
||||
|
||||
nodeName := p.Token().originalFile()
|
||||
nodeName := p.File()
|
||||
if p.Token().snippetName != "" {
|
||||
nodeName += fmt.Sprintf(":%s", p.Token().snippetName)
|
||||
}
|
||||
@@ -425,15 +444,55 @@ func (p *parser) doImport() error {
|
||||
// copy the tokens so we don't overwrite p.definedSnippets
|
||||
tokensCopy := make([]Token, 0, len(importedTokens))
|
||||
|
||||
var (
|
||||
maybeSnippet bool
|
||||
maybeSnippetId bool
|
||||
index int
|
||||
)
|
||||
|
||||
// run the argument replacer on the tokens
|
||||
// golang for range slice return a copy of value
|
||||
// similarly, append also copy value
|
||||
for _, token := range importedTokens {
|
||||
// set the token's file to refer to import directive line number and snippet name
|
||||
for i, token := range importedTokens {
|
||||
// update the token's imports to refer to import directive filename, line number and snippet name if there is one
|
||||
if token.snippetName != "" {
|
||||
token.updateFile(fmt.Sprintf("%s:%d (import %s)", token.File, p.Line(), token.snippetName))
|
||||
token.imports = append(token.imports, fmt.Sprintf("%s:%d (import %s)", p.File(), p.Line(), token.snippetName))
|
||||
} else {
|
||||
token.updateFile(fmt.Sprintf("%s:%d (import)", token.File, p.Line()))
|
||||
token.imports = append(token.imports, fmt.Sprintf("%s:%d (import)", p.File(), p.Line()))
|
||||
}
|
||||
|
||||
// naive way of determine snippets, as snippets definition can only follow name + block
|
||||
// format, won't check for nesting correctness or any other error, that's what parser does.
|
||||
if !maybeSnippet && nesting == 0 {
|
||||
// first of the line
|
||||
if i == 0 || isNextOnNewLine(tokensCopy[i-1], token) {
|
||||
index = 0
|
||||
} else {
|
||||
index++
|
||||
}
|
||||
|
||||
if index == 0 && len(token.Text) >= 3 && strings.HasPrefix(token.Text, "(") && strings.HasSuffix(token.Text, ")") {
|
||||
maybeSnippetId = true
|
||||
}
|
||||
}
|
||||
|
||||
switch token.Text {
|
||||
case "{":
|
||||
nesting++
|
||||
if index == 1 && maybeSnippetId && nesting == 1 {
|
||||
maybeSnippet = true
|
||||
maybeSnippetId = false
|
||||
}
|
||||
case "}":
|
||||
nesting--
|
||||
if nesting == 0 && maybeSnippet {
|
||||
maybeSnippet = false
|
||||
}
|
||||
}
|
||||
|
||||
if maybeSnippet {
|
||||
tokensCopy = append(tokensCopy, token)
|
||||
continue
|
||||
}
|
||||
|
||||
foundVariadic, startIndex, endIndex := parseVariadic(token, len(args))
|
||||
@@ -507,7 +566,6 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
|
||||
// are loaded into the current server block for later use
|
||||
// by directive setup functions.
|
||||
func (p *parser) directive() error {
|
||||
|
||||
// a segment is a list of tokens associated with this directive
|
||||
var segment Segment
|
||||
|
||||
@@ -520,6 +578,9 @@ func (p *parser) directive() error {
|
||||
if !p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
|
||||
return p.Err("Unexpected next token after '{' on same line")
|
||||
}
|
||||
if p.isNewLine() {
|
||||
return p.Err("Unexpected '{' on a new line; did you mean to place the '{' on the previous line?")
|
||||
}
|
||||
} else if p.Val() == "{}" {
|
||||
if p.isNextOnNewLine() && p.Token().wasQuoted == 0 {
|
||||
return p.Err("Unexpected '{}' at end of line")
|
||||
@@ -532,7 +593,7 @@ func (p *parser) directive() error {
|
||||
} else if p.Val() == "}" && p.nesting == 0 {
|
||||
return p.Err("Unexpected '}' because no matching opening brace")
|
||||
} else if p.Val() == "import" && p.isNewLine() {
|
||||
if err := p.doImport(); err != nil {
|
||||
if err := p.doImport(1); err != nil {
|
||||
return err
|
||||
}
|
||||
p.cursor-- // cursor is advanced when we continue, so roll back one more
|
||||
@@ -573,6 +634,15 @@ func (p *parser) closeCurlyBrace() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *parser) isNamedRoute() (bool, string) {
|
||||
keys := p.block.Keys
|
||||
// A named route block is a single key with parens, prefixed with &.
|
||||
if len(keys) == 1 && strings.HasPrefix(keys[0], "&(") && strings.HasSuffix(keys[0], ")") {
|
||||
return true, strings.TrimSuffix(keys[0][2:], ")")
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func (p *parser) isSnippet() (bool, string) {
|
||||
keys := p.block.Keys
|
||||
// A snippet block is a single key with parens. Nothing else qualifies.
|
||||
@@ -583,18 +653,24 @@ func (p *parser) isSnippet() (bool, string) {
|
||||
}
|
||||
|
||||
// read and store everything in a block for later replay.
|
||||
func (p *parser) snippetTokens() ([]Token, error) {
|
||||
// snippet must have curlies.
|
||||
func (p *parser) blockTokens(retainCurlies bool) ([]Token, error) {
|
||||
// block must have curlies.
|
||||
err := p.openCurlyBrace()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nesting := 1 // count our own nesting in snippets
|
||||
nesting := 1 // count our own nesting
|
||||
tokens := []Token{}
|
||||
if retainCurlies {
|
||||
tokens = append(tokens, p.Token())
|
||||
}
|
||||
for p.Next() {
|
||||
if p.Val() == "}" {
|
||||
nesting--
|
||||
if nesting == 0 {
|
||||
if retainCurlies {
|
||||
tokens = append(tokens, p.Token())
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -614,9 +690,10 @@ func (p *parser) snippetTokens() ([]Token, error) {
|
||||
// head of the server block with tokens, which are
|
||||
// grouped by segments.
|
||||
type ServerBlock struct {
|
||||
HasBraces bool
|
||||
Keys []string
|
||||
Segments []Segment
|
||||
HasBraces bool
|
||||
Keys []string
|
||||
Segments []Segment
|
||||
IsNamedRoute bool
|
||||
}
|
||||
|
||||
// DispenseDirective returns a dispenser that contains
|
||||
|
||||
@@ -91,6 +91,10 @@ func TestParseVariadic(t *testing.T) {
|
||||
input: "{args[0:10]}",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
input: "{args[0]}:{args[1]}:{args[2]}",
|
||||
result: false,
|
||||
},
|
||||
} {
|
||||
token := Token{
|
||||
File: "test",
|
||||
@@ -293,6 +297,14 @@ func TestParseOneAndImport(t *testing.T) {
|
||||
// Unexpected next token after '{' on same line
|
||||
{`localhost
|
||||
dir1 { a b }`, true, []string{"localhost"}, []int{}},
|
||||
|
||||
// Unexpected '{' on a new line
|
||||
{`localhost
|
||||
dir1
|
||||
{
|
||||
a b
|
||||
}`, true, []string{"localhost"}, []int{}},
|
||||
|
||||
// Workaround with quotes
|
||||
{`localhost
|
||||
dir1 "{" a b "}"`, false, []string{"localhost"}, []int{5}},
|
||||
@@ -710,6 +722,36 @@ func TestEnvironmentReplacement(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportReplacementInJSONWithBrace(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
args []string
|
||||
input string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
args: []string{"123"},
|
||||
input: "{args[0]}",
|
||||
expect: "123",
|
||||
},
|
||||
{
|
||||
args: []string{"123"},
|
||||
input: `{"key":"{args[0]}"}`,
|
||||
expect: `{"key":"123"}`,
|
||||
},
|
||||
{
|
||||
args: []string{"123", "123"},
|
||||
input: `{"key":[{args[0]},{args[1]}]}`,
|
||||
expect: `{"key":[123,123]}`,
|
||||
},
|
||||
} {
|
||||
repl := makeArgsReplacer(test.args)
|
||||
actual := repl.ReplaceKnown(test.input, "")
|
||||
if actual != test.expect {
|
||||
t.Errorf("Test %d: Expected: '%s' but got '%s'", i, test.expect, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSnippets(t *testing.T) {
|
||||
p := testParser(`
|
||||
(common) {
|
||||
|
||||
@@ -24,10 +24,11 @@ import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/certmagic"
|
||||
)
|
||||
|
||||
// mapAddressToServerBlocks returns a map of listener address to list of server
|
||||
@@ -77,7 +78,8 @@ import (
|
||||
// multiple addresses to the same lists of server blocks (a many:many mapping).
|
||||
// (Doing this is essentially a map-reduce technique.)
|
||||
func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock,
|
||||
options map[string]any) (map[string][]serverBlock, error) {
|
||||
options map[string]any,
|
||||
) (map[string][]serverBlock, error) {
|
||||
sbmap := make(map[string][]serverBlock)
|
||||
|
||||
for i, sblock := range originalServerBlocks {
|
||||
@@ -187,13 +189,25 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]se
|
||||
// listenerAddrsForServerBlockKey essentially converts the Caddyfile
|
||||
// site addresses to Caddy listener addresses for each server block.
|
||||
func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string,
|
||||
options map[string]any) ([]string, error) {
|
||||
options map[string]any,
|
||||
) ([]string, error) {
|
||||
addr, err := ParseAddress(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing key: %v", err)
|
||||
}
|
||||
addr = addr.Normalize()
|
||||
|
||||
switch addr.Scheme {
|
||||
case "wss":
|
||||
return nil, fmt.Errorf("the scheme wss:// is only supported in browsers; use https:// instead")
|
||||
case "ws":
|
||||
return nil, fmt.Errorf("the scheme ws:// is only supported in browsers; use http:// instead")
|
||||
case "https", "http", "":
|
||||
// Do nothing or handle the valid schemes
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported URL scheme %s://", addr.Scheme)
|
||||
}
|
||||
|
||||
// figure out the HTTP and HTTPS ports; either
|
||||
// use defaults, or override with user config
|
||||
httpPort, httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPPort), strconv.Itoa(caddyhttp.DefaultHTTPSPort)
|
||||
|
||||
@@ -26,14 +26,15 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/mholt/acmez/acme"
|
||||
"go.uber.org/zap/zapcore"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/mholt/acmez/acme"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -48,6 +49,7 @@ func init() {
|
||||
RegisterHandlerDirective("route", parseRoute)
|
||||
RegisterHandlerDirective("handle", parseHandle)
|
||||
RegisterDirective("handle_errors", parseHandleErrors)
|
||||
RegisterHandlerDirective("invoke", parseInvoke)
|
||||
RegisterDirective("log", parseLog)
|
||||
RegisterHandlerDirective("skip_log", parseSkipLog)
|
||||
}
|
||||
@@ -179,17 +181,17 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||
case "protocols":
|
||||
args := h.RemainingArgs()
|
||||
if len(args) == 0 {
|
||||
return nil, h.SyntaxErr("one or two protocols")
|
||||
return nil, h.Errf("protocols requires one or two arguments")
|
||||
}
|
||||
if len(args) > 0 {
|
||||
if _, ok := caddytls.SupportedProtocols[args[0]]; !ok {
|
||||
return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[0])
|
||||
return nil, h.Errf("wrong protocol name or protocol not supported: '%s'", args[0])
|
||||
}
|
||||
cp.ProtocolMin = args[0]
|
||||
}
|
||||
if len(args) > 1 {
|
||||
if _, ok := caddytls.SupportedProtocols[args[1]]; !ok {
|
||||
return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[1])
|
||||
return nil, h.Errf("wrong protocol name or protocol not supported: '%s'", args[1])
|
||||
}
|
||||
cp.ProtocolMax = args[1]
|
||||
}
|
||||
@@ -197,7 +199,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||
case "ciphers":
|
||||
for h.NextArg() {
|
||||
if !caddytls.CipherSuiteNameSupported(h.Val()) {
|
||||
return nil, h.Errf("Wrong cipher suite name or cipher suite not supported: '%s'", h.Val())
|
||||
return nil, h.Errf("wrong cipher suite name or cipher suite not supported: '%s'", h.Val())
|
||||
}
|
||||
cp.CipherSuites = append(cp.CipherSuites, h.Val())
|
||||
}
|
||||
@@ -764,9 +766,31 @@ func parseHandleErrors(h Helper) ([]ConfigValue, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseInvoke parses the invoke directive.
|
||||
func parseInvoke(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||
h.Next() // consume directive
|
||||
if !h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
for h.Next() || h.NextBlock(0) {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
|
||||
// remember that we're invoking this name
|
||||
// to populate the server with these named routes
|
||||
if h.State[namedRouteKey] == nil {
|
||||
h.State[namedRouteKey] = map[string]struct{}{}
|
||||
}
|
||||
h.State[namedRouteKey].(map[string]struct{})[h.Val()] = struct{}{}
|
||||
|
||||
// return the handler
|
||||
return &caddyhttp.Invoke{Name: h.Val()}, nil
|
||||
}
|
||||
|
||||
// parseLog parses the log directive. Syntax:
|
||||
//
|
||||
// log {
|
||||
// log <logger_name> {
|
||||
// hostnames <hostnames...>
|
||||
// output <writer_module> ...
|
||||
// format <encoder_module> ...
|
||||
// level <level>
|
||||
@@ -787,11 +811,13 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
|
||||
var configValues []ConfigValue
|
||||
for h.Next() {
|
||||
// Logic below expects that a name is always present when a
|
||||
// global option is being parsed.
|
||||
var globalLogName string
|
||||
// global option is being parsed; or an optional override
|
||||
// is supported for access logs.
|
||||
var logName string
|
||||
|
||||
if parseAsGlobalOption {
|
||||
if h.NextArg() {
|
||||
globalLogName = h.Val()
|
||||
logName = h.Val()
|
||||
|
||||
// Only a single argument is supported.
|
||||
if h.NextArg() {
|
||||
@@ -802,26 +828,47 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
|
||||
// reference the default logger. See the
|
||||
// setupNewDefault function in the logging
|
||||
// package for where this is configured.
|
||||
globalLogName = caddy.DefaultLoggerName
|
||||
logName = caddy.DefaultLoggerName
|
||||
}
|
||||
|
||||
// Verify this name is unused.
|
||||
_, used := globalLogNames[globalLogName]
|
||||
_, used := globalLogNames[logName]
|
||||
if used {
|
||||
return nil, h.Err("duplicate global log option for: " + globalLogName)
|
||||
return nil, h.Err("duplicate global log option for: " + logName)
|
||||
}
|
||||
globalLogNames[globalLogName] = struct{}{}
|
||||
globalLogNames[logName] = struct{}{}
|
||||
} else {
|
||||
// No arguments are supported for the server block log directive
|
||||
// An optional override of the logger name can be provided;
|
||||
// otherwise a default will be used, like "log0", "log1", etc.
|
||||
if h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
logName = h.Val()
|
||||
|
||||
// Only a single argument is supported.
|
||||
if h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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":
|
||||
if !h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
@@ -880,18 +927,16 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
|
||||
}
|
||||
|
||||
case "include":
|
||||
// This configuration is only allowed in the global options
|
||||
if !parseAsGlobalOption {
|
||||
return nil, h.ArgErr()
|
||||
return nil, h.Err("include is not allowed in the log directive")
|
||||
}
|
||||
for h.NextArg() {
|
||||
cl.Include = append(cl.Include, h.Val())
|
||||
}
|
||||
|
||||
case "exclude":
|
||||
// This configuration is only allowed in the global options
|
||||
if !parseAsGlobalOption {
|
||||
return nil, h.ArgErr()
|
||||
return nil, h.Err("exclude is not allowed in the log directive")
|
||||
}
|
||||
for h.NextArg() {
|
||||
cl.Exclude = append(cl.Exclude, h.Val())
|
||||
@@ -903,24 +948,34 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
|
||||
}
|
||||
|
||||
var val namedCustomLog
|
||||
val.hostnames = customHostnames
|
||||
|
||||
isEmptyConfig := reflect.DeepEqual(cl, new(caddy.CustomLog))
|
||||
|
||||
// Skip handling of empty logging configs
|
||||
if !reflect.DeepEqual(cl, new(caddy.CustomLog)) {
|
||||
if parseAsGlobalOption {
|
||||
// Use indicated name for global log options
|
||||
val.name = globalLogName
|
||||
val.log = cl
|
||||
} else {
|
||||
|
||||
if parseAsGlobalOption {
|
||||
// Use indicated name for global log options
|
||||
val.name = logName
|
||||
} else {
|
||||
if logName != "" {
|
||||
val.name = logName
|
||||
} else if !isEmptyConfig {
|
||||
// Construct a log name for server log streams
|
||||
logCounter, ok := h.State["logCounter"].(int)
|
||||
if !ok {
|
||||
logCounter = 0
|
||||
}
|
||||
val.name = fmt.Sprintf("log%d", logCounter)
|
||||
cl.Include = []string{"http.log.access." + val.name}
|
||||
val.log = cl
|
||||
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{
|
||||
Class: "custom_log",
|
||||
|
||||
@@ -52,12 +52,13 @@ func TestLogDirectiveSyntax(t *testing.T) {
|
||||
},
|
||||
{
|
||||
input: `:8080 {
|
||||
log invalid {
|
||||
log name-override {
|
||||
output file foo.log
|
||||
}
|
||||
}
|
||||
`,
|
||||
expectError: true,
|
||||
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: false,
|
||||
},
|
||||
} {
|
||||
|
||||
@@ -229,7 +230,7 @@ func TestImportErrorLine(t *testing.T) {
|
||||
import t1 true
|
||||
}`,
|
||||
errorFunc: func(err error) bool {
|
||||
return err != nil && strings.Contains(err.Error(), "Caddyfile:6 (import t1):2")
|
||||
return err != nil && strings.Contains(err.Error(), "Caddyfile:6 (import t1)")
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -240,7 +241,101 @@ func TestImportErrorLine(t *testing.T) {
|
||||
import t1 true
|
||||
}`,
|
||||
errorFunc: func(err error) bool {
|
||||
return err != nil && strings.Contains(err.Error(), "Caddyfile:5 (import t1):2")
|
||||
return err != nil && strings.Contains(err.Error(), "Caddyfile:5 (import t1)")
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `
|
||||
import testdata/import_variadic_snippet.txt
|
||||
:8080 {
|
||||
import t1 true
|
||||
}`,
|
||||
errorFunc: func(err error) bool {
|
||||
return err == nil
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `
|
||||
import testdata/import_variadic_with_import.txt
|
||||
:8080 {
|
||||
import t1 true
|
||||
import t2 true
|
||||
}`,
|
||||
errorFunc: func(err error) bool {
|
||||
return err == nil
|
||||
},
|
||||
},
|
||||
} {
|
||||
adapter := caddyfile.Adapter{
|
||||
ServerType: ServerType{},
|
||||
}
|
||||
|
||||
_, _, err := adapter.Adapt([]byte(tc.input), nil)
|
||||
|
||||
if !tc.errorFunc(err) {
|
||||
t.Errorf("Test %d error expectation failed, got %s", i, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNestedImport(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
input string
|
||||
errorFunc func(err error) bool
|
||||
}{
|
||||
{
|
||||
input: `(t1) {
|
||||
respond {args[0]} {args[1]}
|
||||
}
|
||||
|
||||
(t2) {
|
||||
import t1 {args[0]} 202
|
||||
}
|
||||
|
||||
:8080 {
|
||||
handle {
|
||||
import t2 "foobar"
|
||||
}
|
||||
}`,
|
||||
errorFunc: func(err error) bool {
|
||||
return err == nil
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `(t1) {
|
||||
respond {args[:]}
|
||||
}
|
||||
|
||||
(t2) {
|
||||
import t1 {args[0]} {args[1]}
|
||||
}
|
||||
|
||||
:8080 {
|
||||
handle {
|
||||
import t2 "foobar" 202
|
||||
}
|
||||
}`,
|
||||
errorFunc: func(err error) bool {
|
||||
return err == nil
|
||||
},
|
||||
},
|
||||
{
|
||||
input: `(t1) {
|
||||
respond {args[0]} {args[1]}
|
||||
}
|
||||
|
||||
(t2) {
|
||||
import t1 {args[:]}
|
||||
}
|
||||
|
||||
:8080 {
|
||||
handle {
|
||||
import t2 "foobar" 202
|
||||
}
|
||||
}`,
|
||||
errorFunc: func(err error) bool {
|
||||
return err == nil
|
||||
},
|
||||
},
|
||||
} {
|
||||
|
||||
@@ -65,6 +65,7 @@ var directiveOrder = []string{
|
||||
"templates",
|
||||
|
||||
// special routing & dispatching directives
|
||||
"invoke",
|
||||
"handle",
|
||||
"handle_path",
|
||||
"route",
|
||||
@@ -172,6 +173,7 @@ func (h Helper) Caddyfiles() []string {
|
||||
for file := range files {
|
||||
filesSlice = append(filesSlice, file)
|
||||
}
|
||||
sort.Strings(filesSlice)
|
||||
return filesSlice
|
||||
}
|
||||
|
||||
@@ -215,7 +217,8 @@ func (h Helper) ExtractMatcherSet() (caddy.ModuleMap, error) {
|
||||
|
||||
// NewRoute returns config values relevant to creating a new HTTP route.
|
||||
func (h Helper) NewRoute(matcherSet caddy.ModuleMap,
|
||||
handler caddyhttp.MiddlewareHandler) []ConfigValue {
|
||||
handler caddyhttp.MiddlewareHandler,
|
||||
) []ConfigValue {
|
||||
mod, err := caddy.GetModule(caddy.GetModuleID(handler))
|
||||
if err != nil {
|
||||
*h.warnings = append(*h.warnings, caddyconfig.Warning{
|
||||
|
||||
@@ -17,19 +17,21 @@ package httpcaddyfile
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddypki"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -48,12 +50,13 @@ type App struct {
|
||||
}
|
||||
|
||||
// ServerType can set up a config from an HTTP Caddyfile.
|
||||
type ServerType struct {
|
||||
}
|
||||
type ServerType struct{}
|
||||
|
||||
// Setup makes a config from the tokens.
|
||||
func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
||||
options map[string]any) (*caddy.Config, []caddyconfig.Warning, error) {
|
||||
func (st ServerType) Setup(
|
||||
inputServerBlocks []caddyfile.ServerBlock,
|
||||
options map[string]any,
|
||||
) (*caddy.Config, []caddyconfig.Warning, error) {
|
||||
var warnings []caddyconfig.Warning
|
||||
gc := counter{new(int)}
|
||||
state := make(map[string]any)
|
||||
@@ -79,41 +82,18 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
||||
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()...)
|
||||
// this will replace both static and user-defined placeholder shorthands
|
||||
// with actual identifiers used by Caddy
|
||||
replacer := NewShorthandReplacer()
|
||||
|
||||
// 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}"},
|
||||
originalServerBlocks, err = st.extractNamedRoutes(originalServerBlocks, options, &warnings, replacer)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
for _, sb := range originalServerBlocks {
|
||||
for _, segment := range sb.block.Segments {
|
||||
for i := 0; i < len(segment); i++ {
|
||||
// simple string replacements
|
||||
segment[i].Text = replacer.Replace(segment[i].Text)
|
||||
// complex regexp replacements
|
||||
for _, r := range regexpReplacements {
|
||||
segment[i].Text = r.search.ReplaceAllString(segment[i].Text, r.replace)
|
||||
}
|
||||
}
|
||||
for i := range sb.block.Segments {
|
||||
replacer.ApplyToSegment(&sb.block.Segments[i])
|
||||
}
|
||||
|
||||
if len(sb.block.Keys) == 0 {
|
||||
@@ -172,6 +152,18 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
||||
result.directive = dir
|
||||
sb.pile[result.Class] = append(sb.pile[result.Class], result)
|
||||
}
|
||||
|
||||
// specially handle named routes that were pulled out from
|
||||
// the invoke directive, which could be nested anywhere within
|
||||
// some subroutes in this directive; we add them to the pile
|
||||
// for this server block
|
||||
if state[namedRouteKey] != nil {
|
||||
for name := range state[namedRouteKey].(map[string]struct{}) {
|
||||
result := ConfigValue{Class: namedRouteKey, Value: name}
|
||||
sb.pile[result.Class] = append(sb.pile[result.Class], result)
|
||||
}
|
||||
state[namedRouteKey] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +214,7 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
||||
if ncl.name == caddy.DefaultLoggerName {
|
||||
hasDefaultLog = true
|
||||
}
|
||||
if _, ok := options["debug"]; ok && ncl.log.Level == "" {
|
||||
if _, ok := options["debug"]; ok && ncl.log != nil && ncl.log.Level == "" {
|
||||
ncl.log.Level = zap.DebugLevel.CapitalString()
|
||||
}
|
||||
customLogs = append(customLogs, ncl)
|
||||
@@ -305,7 +297,21 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
||||
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 {
|
||||
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 != "" {
|
||||
cfg.Logging.Logs[ncl.name] = ncl.log
|
||||
}
|
||||
@@ -319,8 +325,16 @@ func (st ServerType) Setup(inputServerBlocks []caddyfile.ServerBlock,
|
||||
cfg.Logging.Logs[caddy.DefaultLoggerName] = defaultLog
|
||||
}
|
||||
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
|
||||
@@ -403,6 +417,81 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
|
||||
return serverBlocks[1:], nil
|
||||
}
|
||||
|
||||
// extractNamedRoutes pulls out any named route server blocks
|
||||
// so they don't get parsed as sites, and stores them in options
|
||||
// for later.
|
||||
func (ServerType) extractNamedRoutes(
|
||||
serverBlocks []serverBlock,
|
||||
options map[string]any,
|
||||
warnings *[]caddyconfig.Warning,
|
||||
replacer ShorthandReplacer,
|
||||
) ([]serverBlock, error) {
|
||||
namedRoutes := map[string]*caddyhttp.Route{}
|
||||
|
||||
gc := counter{new(int)}
|
||||
state := make(map[string]any)
|
||||
|
||||
// copy the server blocks so we can
|
||||
// splice out the named route ones
|
||||
filtered := append([]serverBlock{}, serverBlocks...)
|
||||
index := -1
|
||||
|
||||
for _, sb := range serverBlocks {
|
||||
index++
|
||||
if !sb.block.IsNamedRoute {
|
||||
continue
|
||||
}
|
||||
|
||||
// splice out this block, because we know it's not a real server
|
||||
filtered = append(filtered[:index], filtered[index+1:]...)
|
||||
index--
|
||||
|
||||
if len(sb.block.Segments) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
wholeSegment := caddyfile.Segment{}
|
||||
for i := range sb.block.Segments {
|
||||
// replace user-defined placeholder shorthands in extracted named routes
|
||||
replacer.ApplyToSegment(&sb.block.Segments[i])
|
||||
|
||||
// zip up all the segments since ParseSegmentAsSubroute
|
||||
// was designed to take a directive+
|
||||
wholeSegment = append(wholeSegment, sb.block.Segments[i]...)
|
||||
}
|
||||
|
||||
h := Helper{
|
||||
Dispenser: caddyfile.NewDispenser(wholeSegment),
|
||||
options: options,
|
||||
warnings: warnings,
|
||||
matcherDefs: nil,
|
||||
parentBlock: sb.block,
|
||||
groupCounter: gc,
|
||||
State: state,
|
||||
}
|
||||
|
||||
handler, err := ParseSegmentAsSubroute(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
subroute := handler.(*caddyhttp.Subroute)
|
||||
route := caddyhttp.Route{}
|
||||
|
||||
if len(subroute.Routes) == 1 && len(subroute.Routes[0].MatcherSetsRaw) == 0 {
|
||||
// if there's only one route with no matcher, then we can simplify
|
||||
route.HandlersRaw = append(route.HandlersRaw, subroute.Routes[0].HandlersRaw[0])
|
||||
} else {
|
||||
// otherwise we need the whole subroute
|
||||
route.HandlersRaw = []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", subroute.CaddyModule().ID.Name(), h.warnings)}
|
||||
}
|
||||
|
||||
namedRoutes[sb.block.Keys[0]] = &route
|
||||
}
|
||||
options["named_routes"] = namedRoutes
|
||||
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
// serversFromPairings creates the servers for each pairing of addresses
|
||||
// to server blocks. Each pairing is essentially a server definition.
|
||||
func (st *ServerType) serversFromPairings(
|
||||
@@ -413,6 +502,7 @@ func (st *ServerType) serversFromPairings(
|
||||
) (map[string]*caddyhttp.Server, error) {
|
||||
servers := make(map[string]*caddyhttp.Server)
|
||||
defaultSNI := tryString(options["default_sni"], warnings)
|
||||
fallbackSNI := tryString(options["fallback_sni"], warnings)
|
||||
|
||||
httpPort := strconv.Itoa(caddyhttp.DefaultHTTPPort)
|
||||
if hp, ok := options["http_port"].(int); ok {
|
||||
@@ -541,6 +631,24 @@ func (st *ServerType) serversFromPairings(
|
||||
}
|
||||
}
|
||||
|
||||
// add named routes to the server if 'invoke' was used inside of it
|
||||
configuredNamedRoutes := options["named_routes"].(map[string]*caddyhttp.Route)
|
||||
for _, sblock := range p.serverBlocks {
|
||||
if len(sblock.pile[namedRouteKey]) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, value := range sblock.pile[namedRouteKey] {
|
||||
if srv.NamedRoutes == nil {
|
||||
srv.NamedRoutes = map[string]*caddyhttp.Route{}
|
||||
}
|
||||
name := value.Value.(string)
|
||||
if configuredNamedRoutes[name] == nil {
|
||||
return nil, fmt.Errorf("cannot invoke named route '%s', which was not defined", name)
|
||||
}
|
||||
srv.NamedRoutes[name] = configuredNamedRoutes[name]
|
||||
}
|
||||
}
|
||||
|
||||
// create a subroute for each site in the server block
|
||||
for _, sblock := range p.serverBlocks {
|
||||
matcherSetsEnc, err := st.compileEncodedMatcherSets(sblock)
|
||||
@@ -570,14 +678,21 @@ func (st *ServerType) serversFromPairings(
|
||||
cp.DefaultSNI = defaultSNI
|
||||
break
|
||||
}
|
||||
if h == fallbackSNI {
|
||||
hosts = append(hosts, "")
|
||||
cp.FallbackSNI = fallbackSNI
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(hosts) > 0 {
|
||||
slices.Sort(hosts) // for deterministic JSON output
|
||||
cp.MatchersRaw = caddy.ModuleMap{
|
||||
"sni": caddyconfig.JSON(hosts, warnings), // make sure to match all hosts, not just auto-HTTPS-qualified ones
|
||||
}
|
||||
} else {
|
||||
cp.DefaultSNI = defaultSNI
|
||||
cp.FallbackSNI = fallbackSNI
|
||||
}
|
||||
|
||||
// only append this policy if it actually changes something
|
||||
@@ -603,10 +718,20 @@ func (st *ServerType) serversFromPairings(
|
||||
}
|
||||
}
|
||||
|
||||
// If TLS is specified as directive, it will also result in 1 or more connection policy being created
|
||||
// Thus, catch-all address with non-standard port, e.g. :8443, can have TLS enabled without
|
||||
// specifying prefix "https://"
|
||||
// Second part of the condition is to allow creating TLS conn policy even though `auto_https` has been disabled
|
||||
// ensuring compatibility with behavior described in below link
|
||||
// https://caddy.community/t/making-sense-of-auto-https-and-why-disabling-it-still-serves-https-instead-of-http/9761
|
||||
createdTLSConnPolicies, ok := sblock.pile["tls.connection_policy"]
|
||||
hasTLSEnabled := (ok && len(createdTLSConnPolicies) > 0) ||
|
||||
(addr.Host != "" && srv.AutoHTTPS != nil && !sliceContains(srv.AutoHTTPS.Skip, addr.Host))
|
||||
|
||||
// we'll need to remember if the address qualifies for auto-HTTPS, so we
|
||||
// can add a TLS conn policy if necessary
|
||||
if addr.Scheme == "https" ||
|
||||
(addr.Scheme != "http" && addr.Host != "" && addr.Port != httpPort) {
|
||||
(addr.Scheme != "http" && addr.Port != httpPort && hasTLSEnabled) {
|
||||
addressQualifiesForTLS = true
|
||||
}
|
||||
// predict whether auto-HTTPS will add the conn policy for us; if so, we
|
||||
@@ -655,18 +780,31 @@ func (st *ServerType) serversFromPairings(
|
||||
sblockLogHosts := sblock.hostsFromKeys(true)
|
||||
for _, cval := range sblock.pile["custom_log"] {
|
||||
ncl := cval.Value.(namedCustomLog)
|
||||
if sblock.hasHostCatchAllKey() {
|
||||
if sblock.hasHostCatchAllKey() && len(ncl.hostnames) == 0 {
|
||||
// all requests for hosts not able to be listed should use
|
||||
// this log because it's a catch-all-hosts server block
|
||||
srv.Logs.DefaultLoggerName = ncl.name
|
||||
} else {
|
||||
// map each host to the user's desired logger name
|
||||
for _, h := range sblockLogHosts {
|
||||
} 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 {
|
||||
// otherwise, map each host to the logger name
|
||||
for _, h := range sblockLogHosts {
|
||||
if srv.Logs.LoggerNames == nil {
|
||||
srv.Logs.LoggerNames = make(map[string]string)
|
||||
}
|
||||
// strip the port from the host, if any
|
||||
host, _, err := net.SplitHostPort(h)
|
||||
if err != nil {
|
||||
host = h
|
||||
}
|
||||
srv.Logs.LoggerNames[host] = ncl.name
|
||||
}
|
||||
}
|
||||
}
|
||||
if srv.Logs != nil && len(sblock.pile["custom_log"]) == 0 {
|
||||
@@ -703,8 +841,8 @@ func (st *ServerType) serversFromPairings(
|
||||
// policy missing for any HTTPS-enabled hosts, if so, add it... maybe?
|
||||
if addressQualifiesForTLS &&
|
||||
!hasCatchAllTLSConnPolicy &&
|
||||
(len(srv.TLSConnPolicies) > 0 || !autoHTTPSWillAddConnPolicy || defaultSNI != "") {
|
||||
srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{DefaultSNI: defaultSNI})
|
||||
(len(srv.TLSConnPolicies) > 0 || !autoHTTPSWillAddConnPolicy || defaultSNI != "" || fallbackSNI != "") {
|
||||
srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{DefaultSNI: defaultSNI, FallbackSNI: fallbackSNI})
|
||||
}
|
||||
|
||||
// tidy things up a bit
|
||||
@@ -913,8 +1051,8 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
|
||||
subroute *caddyhttp.Subroute,
|
||||
matcherSetsEnc []caddy.ModuleMap,
|
||||
p sbAddrAssociation,
|
||||
warnings *[]caddyconfig.Warning) caddyhttp.RouteList {
|
||||
|
||||
warnings *[]caddyconfig.Warning,
|
||||
) caddyhttp.RouteList {
|
||||
// nothing to do if... there's nothing to do
|
||||
if len(matcherSetsEnc) == 0 && len(subroute.Routes) == 0 && subroute.Errors == nil {
|
||||
return routeList
|
||||
@@ -976,7 +1114,7 @@ func buildSubroute(routes []ConfigValue, groupCounter counter, needsSorting bool
|
||||
if needsSorting {
|
||||
for _, val := range routes {
|
||||
if !directiveIsOrdered(val.directive) {
|
||||
return nil, fmt.Errorf("directive '%s' is not an ordered HTTP handler, so it cannot be used here", val.directive)
|
||||
return nil, fmt.Errorf("directive '%s' is not an ordered HTTP handler, so it cannot be used here - try placing within a route block or using the order global option", val.directive)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1303,37 +1441,6 @@ func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (caddy.Modul
|
||||
return msEncoded, nil
|
||||
}
|
||||
|
||||
// placeholderShorthands returns a slice of old-new string pairs,
|
||||
// where the left of the pair is a placeholder shorthand that may
|
||||
// be used in the Caddyfile, and the right is the replacement.
|
||||
func placeholderShorthands() []string {
|
||||
return []string{
|
||||
"{dir}", "{http.request.uri.path.dir}",
|
||||
"{file}", "{http.request.uri.path.file}",
|
||||
"{host}", "{http.request.host}",
|
||||
"{hostport}", "{http.request.hostport}",
|
||||
"{port}", "{http.request.port}",
|
||||
"{method}", "{http.request.method}",
|
||||
"{path}", "{http.request.uri.path}",
|
||||
"{query}", "{http.request.uri.query}",
|
||||
"{remote}", "{http.request.remote}",
|
||||
"{remote_host}", "{http.request.remote.host}",
|
||||
"{remote_port}", "{http.request.remote.port}",
|
||||
"{scheme}", "{http.request.scheme}",
|
||||
"{uri}", "{http.request.uri}",
|
||||
"{tls_cipher}", "{http.request.tls.cipher_suite}",
|
||||
"{tls_version}", "{http.request.tls.version}",
|
||||
"{tls_client_fingerprint}", "{http.request.tls.client.fingerprint}",
|
||||
"{tls_client_issuer}", "{http.request.tls.client.issuer}",
|
||||
"{tls_client_serial}", "{http.request.tls.client.serial}",
|
||||
"{tls_client_subject}", "{http.request.tls.client.subject}",
|
||||
"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
|
||||
"{tls_client_certificate_der_base64}", "{http.request.tls.client.certificate_der_base64}",
|
||||
"{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
|
||||
"{client_ip}", "{http.vars.client_ip}",
|
||||
}
|
||||
}
|
||||
|
||||
// WasReplacedPlaceholderShorthand checks if a token string was
|
||||
// likely a replaced shorthand of the known Caddyfile placeholder
|
||||
// replacement outputs. Useful to prevent some user-defined map
|
||||
@@ -1449,8 +1556,9 @@ func (c counter) nextGroup() string {
|
||||
}
|
||||
|
||||
type namedCustomLog struct {
|
||||
name string
|
||||
log *caddy.CustomLog
|
||||
name string
|
||||
hostnames []string
|
||||
log *caddy.CustomLog
|
||||
}
|
||||
|
||||
// sbAddrAssociation is a mapping from a list of
|
||||
@@ -1461,7 +1569,10 @@ type sbAddrAssociation struct {
|
||||
serverBlocks []serverBlock
|
||||
}
|
||||
|
||||
const matcherPrefix = "@"
|
||||
const (
|
||||
matcherPrefix = "@"
|
||||
namedRouteKey = "named_route"
|
||||
)
|
||||
|
||||
// Interface guard
|
||||
var _ caddyfile.ServerType = (*ServerType)(nil)
|
||||
|
||||
@@ -17,12 +17,13 @@ package httpcaddyfile
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/mholt/acmez/acme"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/mholt/acmez/acme"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -33,6 +34,7 @@ func init() {
|
||||
RegisterGlobalOption("grace_period", parseOptDuration)
|
||||
RegisterGlobalOption("shutdown_delay", parseOptDuration)
|
||||
RegisterGlobalOption("default_sni", parseOptSingleString)
|
||||
RegisterGlobalOption("fallback_sni", parseOptSingleString)
|
||||
RegisterGlobalOption("order", parseOptOrder)
|
||||
RegisterGlobalOption("storage", parseOptStorage)
|
||||
RegisterGlobalOption("storage_clean_interval", parseOptDuration)
|
||||
|
||||
@@ -174,7 +174,6 @@ func (st ServerType) buildPKIApp(
|
||||
options map[string]any,
|
||||
warnings []caddyconfig.Warning,
|
||||
) (*caddypki.PKI, []caddyconfig.Warning, error) {
|
||||
|
||||
skipInstallTrust := false
|
||||
if _, ok := options["skip_install_trust"]; ok {
|
||||
skipInstallTrust = true
|
||||
|
||||
@@ -18,11 +18,12 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
||||
// serverOptions collects server config overrides parsed from Caddyfile global options
|
||||
@@ -41,6 +42,7 @@ type serverOptions struct {
|
||||
IdleTimeout caddy.Duration
|
||||
KeepAliveInterval caddy.Duration
|
||||
MaxHeaderBytes int
|
||||
EnableFullDuplex bool
|
||||
Protocols []string
|
||||
StrictSNIHost *bool
|
||||
TrustedProxiesRaw json.RawMessage
|
||||
@@ -157,6 +159,12 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
||||
}
|
||||
serverOpts.MaxHeaderBytes = int(size)
|
||||
|
||||
case "enable_full_duplex":
|
||||
if d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
serverOpts.EnableFullDuplex = true
|
||||
|
||||
case "log_credentials":
|
||||
if d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
@@ -327,6 +335,7 @@ func applyServerOptions(
|
||||
server.IdleTimeout = opts.IdleTimeout
|
||||
server.KeepAliveInterval = opts.KeepAliveInterval
|
||||
server.MaxHeaderBytes = opts.MaxHeaderBytes
|
||||
server.EnableFullDuplex = opts.EnableFullDuplex
|
||||
server.Protocols = opts.Protocols
|
||||
server.StrictSNIHost = opts.StrictSNIHost
|
||||
server.TrustedProxiesRaw = opts.TrustedProxiesRaw
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package httpcaddyfile
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
)
|
||||
|
||||
type ComplexShorthandReplacer struct {
|
||||
search *regexp.Regexp
|
||||
replace string
|
||||
}
|
||||
|
||||
type ShorthandReplacer struct {
|
||||
complex []ComplexShorthandReplacer
|
||||
simple *strings.Replacer
|
||||
}
|
||||
|
||||
func NewShorthandReplacer() ShorthandReplacer {
|
||||
// replace shorthand placeholders (which are convenient
|
||||
// when writing a Caddyfile) with their actual placeholder
|
||||
// identifiers or variable names
|
||||
replacer := strings.NewReplacer(placeholderShorthands()...)
|
||||
|
||||
// these are placeholders that allow a user-defined final
|
||||
// parameters, but we still want to provide a shorthand
|
||||
// for those, so we use a regexp to replace
|
||||
regexpReplacements := []ComplexShorthandReplacer{
|
||||
{regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"},
|
||||
{regexp.MustCompile(`{cookie\.([\w-]*)}`), "{http.request.cookie.$1}"},
|
||||
{regexp.MustCompile(`{labels\.([\w-]*)}`), "{http.request.host.labels.$1}"},
|
||||
{regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"},
|
||||
{regexp.MustCompile(`{file\.([\w-]*)}`), "{http.request.uri.path.file.$1}"},
|
||||
{regexp.MustCompile(`{query\.([\w-]*)}`), "{http.request.uri.query.$1}"},
|
||||
{regexp.MustCompile(`{re\.([\w-]*)\.([\w-]*)}`), "{http.regexp.$1.$2}"},
|
||||
{regexp.MustCompile(`{vars\.([\w-]*)}`), "{http.vars.$1}"},
|
||||
{regexp.MustCompile(`{rp\.([\w-\.]*)}`), "{http.reverse_proxy.$1}"},
|
||||
{regexp.MustCompile(`{err\.([\w-\.]*)}`), "{http.error.$1}"},
|
||||
{regexp.MustCompile(`{file_match\.([\w-]*)}`), "{http.matchers.file.$1}"},
|
||||
}
|
||||
|
||||
return ShorthandReplacer{
|
||||
complex: regexpReplacements,
|
||||
simple: replacer,
|
||||
}
|
||||
}
|
||||
|
||||
// placeholderShorthands returns a slice of old-new string pairs,
|
||||
// where the left of the pair is a placeholder shorthand that may
|
||||
// be used in the Caddyfile, and the right is the replacement.
|
||||
func placeholderShorthands() []string {
|
||||
return []string{
|
||||
"{dir}", "{http.request.uri.path.dir}",
|
||||
"{file}", "{http.request.uri.path.file}",
|
||||
"{host}", "{http.request.host}",
|
||||
"{hostport}", "{http.request.hostport}",
|
||||
"{port}", "{http.request.port}",
|
||||
"{method}", "{http.request.method}",
|
||||
"{path}", "{http.request.uri.path}",
|
||||
"{query}", "{http.request.uri.query}",
|
||||
"{remote}", "{http.request.remote}",
|
||||
"{remote_host}", "{http.request.remote.host}",
|
||||
"{remote_port}", "{http.request.remote.port}",
|
||||
"{scheme}", "{http.request.scheme}",
|
||||
"{uri}", "{http.request.uri}",
|
||||
"{uuid}", "{http.request.uuid}",
|
||||
"{tls_cipher}", "{http.request.tls.cipher_suite}",
|
||||
"{tls_version}", "{http.request.tls.version}",
|
||||
"{tls_client_fingerprint}", "{http.request.tls.client.fingerprint}",
|
||||
"{tls_client_issuer}", "{http.request.tls.client.issuer}",
|
||||
"{tls_client_serial}", "{http.request.tls.client.serial}",
|
||||
"{tls_client_subject}", "{http.request.tls.client.subject}",
|
||||
"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
|
||||
"{tls_client_certificate_der_base64}", "{http.request.tls.client.certificate_der_base64}",
|
||||
"{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
|
||||
"{client_ip}", "{http.vars.client_ip}",
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyToSegment replaces shorthand placeholder to its full placeholder, understandable by Caddy.
|
||||
func (s ShorthandReplacer) ApplyToSegment(segment *caddyfile.Segment) {
|
||||
if segment != nil {
|
||||
for i := 0; i < len(*segment); i++ {
|
||||
// simple string replacements
|
||||
(*segment)[i].Text = s.simple.Replace((*segment)[i].Text)
|
||||
// complex regexp replacements
|
||||
for _, r := range s.complex {
|
||||
(*segment)[i].Text = r.search.ReplaceAllString((*segment)[i].Text, r.replace)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
(t2) {
|
||||
respond 200 {
|
||||
body {args[:]}
|
||||
}
|
||||
}
|
||||
|
||||
:8082 {
|
||||
import t2 false
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
(t1) {
|
||||
respond 200 {
|
||||
body {args[:]}
|
||||
}
|
||||
}
|
||||
|
||||
:8081 {
|
||||
import t1 false
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
(t1) {
|
||||
respond 200 {
|
||||
body {args[:]}
|
||||
}
|
||||
}
|
||||
|
||||
:8081 {
|
||||
import t1 false
|
||||
}
|
||||
|
||||
import import_variadic.txt
|
||||
|
||||
:8083 {
|
||||
import t2 true
|
||||
}
|
||||
@@ -23,12 +23,13 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/mholt/acmez/acme"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/mholt/acmez/acme"
|
||||
)
|
||||
|
||||
func (st ServerType) buildTLSApp(
|
||||
@@ -36,7 +37,6 @@ func (st ServerType) buildTLSApp(
|
||||
options map[string]any,
|
||||
warnings []caddyconfig.Warning,
|
||||
) (*caddytls.TLS, []caddyconfig.Warning, error) {
|
||||
|
||||
tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}
|
||||
var certLoaders []caddytls.CertificateLoader
|
||||
|
||||
@@ -218,6 +218,10 @@ func (st ServerType) buildTLSApp(
|
||||
if len(ap.Issuers) == 0 {
|
||||
var internal, external []string
|
||||
for _, s := range ap.SubjectsRaw {
|
||||
// do not create Issuers for Tailscale domains; they will be given a Manager instead
|
||||
if strings.HasSuffix(strings.ToLower(s), ".ts.net") {
|
||||
continue
|
||||
}
|
||||
if !certmagic.SubjectQualifiesForCert(s) {
|
||||
return nil, warnings, fmt.Errorf("subject does not qualify for certificate: '%s'", s)
|
||||
}
|
||||
@@ -578,6 +582,7 @@ outer:
|
||||
// eaten up by the one with subjects; and if both have subjects, we
|
||||
// need to combine their lists
|
||||
if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) &&
|
||||
reflect.DeepEqual(aps[i].ManagersRaw, aps[j].ManagersRaw) &&
|
||||
bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
|
||||
aps[i].MustStaple == aps[j].MustStaple &&
|
||||
aps[i].KeyType == aps[j].KeyType &&
|
||||
|
||||
@@ -30,8 +30,14 @@ func init() {
|
||||
caddy.RegisterModule(HTTPLoader{})
|
||||
}
|
||||
|
||||
// HTTPLoader can load Caddy configs over HTTP(S). It can adapt the config
|
||||
// based on the Content-Type header of the HTTP response.
|
||||
// HTTPLoader can load Caddy configs over HTTP(S).
|
||||
//
|
||||
// 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 {
|
||||
// The method for the request. Default: GET
|
||||
Method string `json:"method,omitempty"`
|
||||
@@ -45,6 +51,11 @@ type HTTPLoader struct {
|
||||
// Maximum time allowed for a complete connection and request.
|
||||
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 {
|
||||
// Present this instance's managed remote identity credentials to the server.
|
||||
UseServerIdentity bool `json:"use_server_identity,omitempty"`
|
||||
@@ -108,7 +119,12 @@ func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, warnings, err := adaptByContentType(resp.Header.Get("Content-Type"), body)
|
||||
// adapt the config based on either manually-configured adapter or server's response header
|
||||
ct := resp.Header.Get("Content-Type")
|
||||
if hl.Adapter != "" {
|
||||
ct = "text/" + hl.Adapter
|
||||
}
|
||||
result, warnings, err := adaptByContentType(ct, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+3
-18
@@ -22,9 +22,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/aryann/difflib"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
|
||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
// plug in Caddy modules here
|
||||
_ "github.com/caddyserver/caddy/v2/modules/standard"
|
||||
)
|
||||
@@ -63,7 +64,6 @@ type Tester struct {
|
||||
|
||||
// NewTester will create a new testing client with an attached cookie jar
|
||||
func NewTester(t *testing.T) *Tester {
|
||||
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create cookiejar: %s", err)
|
||||
@@ -94,7 +94,6 @@ func timeElapsed(start time.Time, name string) {
|
||||
// InitServer this will configure the server with a configurion of a specific
|
||||
// type. The configType must be either "json" or the adapter type.
|
||||
func (tc *Tester) InitServer(rawConfig string, configType string) {
|
||||
|
||||
if err := tc.initServer(rawConfig, configType); err != nil {
|
||||
tc.t.Logf("failed to load config: %s", err)
|
||||
tc.t.Fail()
|
||||
@@ -108,7 +107,6 @@ func (tc *Tester) InitServer(rawConfig string, configType string) {
|
||||
// InitServer this will configure the server with a configurion of a specific
|
||||
// type. The configType must be either "json" or the adapter type.
|
||||
func (tc *Tester) initServer(rawConfig string, configType string) error {
|
||||
|
||||
if testing.Short() {
|
||||
tc.t.SkipNow()
|
||||
return nil
|
||||
@@ -232,7 +230,6 @@ const initConfig = `{
|
||||
// validateTestPrerequisites ensures the certificates are available in the
|
||||
// designated path and Caddy sub-process is running.
|
||||
func validateTestPrerequisites(t *testing.T) error {
|
||||
|
||||
// check certificates are found
|
||||
for _, certName := range Default.Certifcates {
|
||||
if _, err := os.Stat(getIntegrationDir() + certName); os.IsNotExist(err) {
|
||||
@@ -284,7 +281,6 @@ func isCaddyAdminRunning() error {
|
||||
}
|
||||
|
||||
func getIntegrationDir() string {
|
||||
|
||||
_, filename, _, ok := runtime.Caller(1)
|
||||
if !ok {
|
||||
panic("unable to determine the current file path")
|
||||
@@ -304,7 +300,6 @@ func prependCaddyFilePath(rawConfig string) string {
|
||||
|
||||
// CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally
|
||||
func CreateTestingTransport() *http.Transport {
|
||||
|
||||
dialer := net.Dialer{
|
||||
Timeout: 5 * time.Second,
|
||||
KeepAlive: 5 * time.Second,
|
||||
@@ -332,7 +327,6 @@ func CreateTestingTransport() *http.Transport {
|
||||
|
||||
// AssertLoadError will load a config and expect an error
|
||||
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
|
||||
|
||||
tc := NewTester(t)
|
||||
|
||||
err := tc.initServer(rawConfig, configType)
|
||||
@@ -343,7 +337,6 @@ func AssertLoadError(t *testing.T, rawConfig string, configType string, expected
|
||||
|
||||
// AssertRedirect makes a request and asserts the redirection happens
|
||||
func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
|
||||
|
||||
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
@@ -381,7 +374,6 @@ func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, e
|
||||
|
||||
// CompareAdapt adapts a config and then compares it against an expected result
|
||||
func CompareAdapt(t *testing.T, filename, rawConfig string, adapterName string, expectedResponse string) bool {
|
||||
|
||||
cfgAdapter := caddyconfig.GetAdapter(adapterName)
|
||||
if cfgAdapter == nil {
|
||||
t.Logf("unrecognized config adapter '%s'", adapterName)
|
||||
@@ -469,14 +461,13 @@ func applyHeaders(t *testing.T, req *http.Request, requestHeaders []string) {
|
||||
|
||||
// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions
|
||||
func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
|
||||
|
||||
resp, err := tc.Client.Do(req)
|
||||
if err != nil {
|
||||
tc.t.Fatalf("failed to call server %s", err)
|
||||
}
|
||||
|
||||
if expectedStatusCode != resp.StatusCode {
|
||||
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.RequestURI, expectedStatusCode, resp.StatusCode)
|
||||
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.URL.RequestURI(), expectedStatusCode, resp.StatusCode)
|
||||
}
|
||||
|
||||
return resp
|
||||
@@ -484,7 +475,6 @@ func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int)
|
||||
|
||||
// AssertResponse request a URI and assert the status code and the body contains a string
|
||||
func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||
|
||||
resp := tc.AssertResponseCode(req, expectedStatusCode)
|
||||
|
||||
defer resp.Body.Close()
|
||||
@@ -506,7 +496,6 @@ func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expe
|
||||
|
||||
// AssertGetResponse GET a URI and expect a statusCode and body text
|
||||
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||
|
||||
req, err := http.NewRequest("GET", requestURI, nil)
|
||||
if err != nil {
|
||||
tc.t.Fatalf("unable to create request %s", err)
|
||||
@@ -517,7 +506,6 @@ func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, e
|
||||
|
||||
// AssertDeleteResponse request a URI and expect a statusCode and body text
|
||||
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||
|
||||
req, err := http.NewRequest("DELETE", requestURI, nil)
|
||||
if err != nil {
|
||||
tc.t.Fatalf("unable to create request %s", err)
|
||||
@@ -528,7 +516,6 @@ func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int
|
||||
|
||||
// AssertPostResponseBody POST to a URI and assert the response code and body
|
||||
func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||
|
||||
req, err := http.NewRequest("POST", requestURI, requestBody)
|
||||
if err != nil {
|
||||
tc.t.Errorf("failed to create request %s", err)
|
||||
@@ -542,7 +529,6 @@ func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []str
|
||||
|
||||
// AssertPutResponseBody PUT to a URI and assert the response code and body
|
||||
func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||
|
||||
req, err := http.NewRequest("PUT", requestURI, requestBody)
|
||||
if err != nil {
|
||||
tc.t.Errorf("failed to create request %s", err)
|
||||
@@ -556,7 +542,6 @@ func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []stri
|
||||
|
||||
// AssertPatchResponseBody PATCH to a URI and assert the response code and body
|
||||
func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||
|
||||
req, err := http.NewRequest("PATCH", requestURI, requestBody)
|
||||
if err != nil {
|
||||
tc.t.Errorf("failed to create request %s", err)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddytest"
|
||||
)
|
||||
|
||||
func TestACMEServerDirectory(t *testing.T) {
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(`
|
||||
{
|
||||
skip_install_trust
|
||||
local_certs
|
||||
admin localhost:2999
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
pki {
|
||||
ca local {
|
||||
name "Caddy Local Authority"
|
||||
}
|
||||
}
|
||||
}
|
||||
acme.localhost:9443 {
|
||||
acme_server
|
||||
}
|
||||
`, "caddyfile")
|
||||
tester.AssertGetResponse(
|
||||
"https://acme.localhost:9443/acme/local/directory",
|
||||
200,
|
||||
`{"newNonce":"https://acme.localhost:9443/acme/local/new-nonce","newAccount":"https://acme.localhost:9443/acme/local/new-account","newOrder":"https://acme.localhost:9443/acme/local/new-order","revokeCert":"https://acme.localhost:9443/acme/local/revoke-cert","keyChange":"https://acme.localhost:9443/acme/local/key-change"}
|
||||
`)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
:8443 {
|
||||
tls internal {
|
||||
on_demand
|
||||
}
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":8443"
|
||||
],
|
||||
"tls_connection_policies": [
|
||||
{}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"issuers": [
|
||||
{
|
||||
"module": "internal"
|
||||
}
|
||||
],
|
||||
"on_demand": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ encode gzip zstd {
|
||||
header Content-Type application/xhtml+xml*
|
||||
header Content-Type application/atom+xml*
|
||||
header Content-Type application/rss+xml*
|
||||
header Content-Type application/wasm*
|
||||
header Content-Type image/svg+xml*
|
||||
}
|
||||
}
|
||||
@@ -47,6 +48,7 @@ encode {
|
||||
"application/xhtml+xml*",
|
||||
"application/atom+xml*",
|
||||
"application/rss+xml*",
|
||||
"application/wasm*",
|
||||
"image/svg+xml*"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -69,11 +69,11 @@
|
||||
}
|
||||
],
|
||||
"on_demand": {
|
||||
"ask": "https://example.com",
|
||||
"rate_limit": {
|
||||
"interval": 30000000000,
|
||||
"burst": 20
|
||||
},
|
||||
"ask": "https://example.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"disable_ocsp_stapling": true
|
||||
|
||||
@@ -78,11 +78,11 @@
|
||||
}
|
||||
],
|
||||
"on_demand": {
|
||||
"ask": "https://example.com",
|
||||
"rate_limit": {
|
||||
"interval": 30000000000,
|
||||
"burst": 20
|
||||
},
|
||||
"ask": "https://example.com"
|
||||
}
|
||||
},
|
||||
"ocsp_interval": 172800000000000,
|
||||
"renew_interval": 86400000000000,
|
||||
|
||||
@@ -71,11 +71,11 @@
|
||||
}
|
||||
],
|
||||
"on_demand": {
|
||||
"ask": "https://example.com",
|
||||
"rate_limit": {
|
||||
"interval": 30000000000,
|
||||
"burst": 20
|
||||
},
|
||||
"ask": "https://example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
idle 30s
|
||||
}
|
||||
max_header_size 100MB
|
||||
enable_full_duplex
|
||||
log_credentials
|
||||
protocols h1 h2 h2c h3
|
||||
strict_sni_host
|
||||
@@ -45,6 +46,7 @@ foo.com {
|
||||
"write_timeout": 30000000000,
|
||||
"idle_timeout": 30000000000,
|
||||
"max_header_bytes": 100000000,
|
||||
"enable_full_duplex": true,
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
+Link "Foo"
|
||||
+Link "Bar"
|
||||
}
|
||||
header >Set Defer
|
||||
header >Replace Deferred Replacement
|
||||
}
|
||||
----------
|
||||
{
|
||||
@@ -136,6 +138,31 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "headers",
|
||||
"response": {
|
||||
"deferred": true,
|
||||
"set": {
|
||||
"Set": [
|
||||
"Defer"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "headers",
|
||||
"response": {
|
||||
"deferred": true,
|
||||
"replace": {
|
||||
"Replace": [
|
||||
{
|
||||
"replace": "Replacement",
|
||||
"search_regexp": "Deferred"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
&(first) {
|
||||
@first path /first
|
||||
vars @first first 1
|
||||
respond "first"
|
||||
}
|
||||
|
||||
&(second) {
|
||||
respond "second"
|
||||
}
|
||||
|
||||
:8881 {
|
||||
invoke first
|
||||
route {
|
||||
invoke second
|
||||
}
|
||||
}
|
||||
|
||||
:8882 {
|
||||
handle {
|
||||
invoke second
|
||||
}
|
||||
}
|
||||
|
||||
:8883 {
|
||||
respond "no invoke"
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":8881"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "invoke",
|
||||
"name": "first"
|
||||
},
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "invoke",
|
||||
"name": "second"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"named_routes": {
|
||||
"first": {
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"first": 1,
|
||||
"handler": "vars"
|
||||
}
|
||||
],
|
||||
"match": [
|
||||
{
|
||||
"path": [
|
||||
"/first"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"body": "first",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"second": {
|
||||
"handle": [
|
||||
{
|
||||
"body": "second",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"srv1": {
|
||||
"listen": [
|
||||
":8882"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "invoke",
|
||||
"name": "second"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"named_routes": {
|
||||
"second": {
|
||||
"handle": [
|
||||
{
|
||||
"body": "second",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"srv2": {
|
||||
"listen": [
|
||||
":8883"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"body": "no invoke",
|
||||
"handler": "static_response"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ http://localhost:2020 {
|
||||
},
|
||||
"logs": {
|
||||
"logger_names": {
|
||||
"localhost:2020": ""
|
||||
"localhost": ""
|
||||
},
|
||||
"skip_unmapped_hosts": true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
*.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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
*.sandbox.localhost {
|
||||
@sandboxPort {
|
||||
header_regexp first_label Host ^([0-9]{3})\.sandbox\.
|
||||
}
|
||||
handle @sandboxPort {
|
||||
reverse_proxy {re.first_label.1}
|
||||
}
|
||||
handle {
|
||||
redir {scheme}://application.localhost
|
||||
}
|
||||
}
|
||||
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"*.sandbox.localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"group": "group2",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "{http.regexp.first_label.1}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"match": [
|
||||
{
|
||||
"header_regexp": {
|
||||
"Host": {
|
||||
"name": "first_label",
|
||||
"pattern": "^([0-9]{3})\\.sandbox\\."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "group2",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "static_response",
|
||||
"headers": {
|
||||
"Location": [
|
||||
"{http.request.scheme}://application.localhost"
|
||||
]
|
||||
},
|
||||
"status_code": 302
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
*.sandbox.localhost {
|
||||
@sandboxPort {
|
||||
header_regexp port Host ^([0-9]{3})\.sandbox\.
|
||||
}
|
||||
handle @sandboxPort {
|
||||
reverse_proxy app:6{re.port.1}
|
||||
}
|
||||
handle {
|
||||
redir {scheme}://application.localhost
|
||||
}
|
||||
}
|
||||
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"*.sandbox.localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"group": "group2",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "app:6{http.regexp.port.1}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"match": [
|
||||
{
|
||||
"header_regexp": {
|
||||
"Host": {
|
||||
"name": "port",
|
||||
"pattern": "^([0-9]{3})\\.sandbox\\."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "group2",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "static_response",
|
||||
"headers": {
|
||||
"Location": [
|
||||
"{http.request.scheme}://application.localhost"
|
||||
]
|
||||
},
|
||||
"status_code": 302
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
*.sandbox.localhost {
|
||||
@sandboxPort {
|
||||
header_regexp port Host ^([0-9]{3})\.sandbox\.
|
||||
}
|
||||
handle @sandboxPort {
|
||||
reverse_proxy app:{re.port.1}
|
||||
}
|
||||
handle {
|
||||
redir {scheme}://application.localhost
|
||||
}
|
||||
}
|
||||
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"*.sandbox.localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"group": "group2",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "app:{http.regexp.port.1}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"match": [
|
||||
{
|
||||
"header_regexp": {
|
||||
"Host": {
|
||||
"name": "port",
|
||||
"pattern": "^([0-9]{3})\\.sandbox\\."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "group2",
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "static_response",
|
||||
"headers": {
|
||||
"Location": [
|
||||
"{http.request.scheme}://application.localhost"
|
||||
]
|
||||
},
|
||||
"status_code": 302
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
https://example.com {
|
||||
reverse_proxy https://localhost:54321 {
|
||||
request_buffers unlimited
|
||||
response_buffers unlimited
|
||||
}
|
||||
}
|
||||
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"example.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"request_buffers": -1,
|
||||
"response_buffers": -1,
|
||||
"transport": {
|
||||
"protocol": "http",
|
||||
"tls": {}
|
||||
},
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "localhost:54321"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,9 @@ reverse_proxy 127.0.0.1:65535 {
|
||||
X-Header-Key 95ca39e3cbe7
|
||||
X-Header-Keys VbG4NZwWnipo 335Q9/MhqcNU3s2TO
|
||||
X-Empty-Value
|
||||
Same-Key 1
|
||||
Same-Key 2
|
||||
X-System-Hostname {system.hostname}
|
||||
}
|
||||
health_uri /health
|
||||
}
|
||||
@@ -29,6 +32,10 @@ reverse_proxy 127.0.0.1:65535 {
|
||||
"Host": [
|
||||
"example.com"
|
||||
],
|
||||
"Same-Key": [
|
||||
"1",
|
||||
"2"
|
||||
],
|
||||
"X-Empty-Value": [
|
||||
""
|
||||
],
|
||||
@@ -38,6 +45,9 @@ reverse_proxy 127.0.0.1:65535 {
|
||||
"X-Header-Keys": [
|
||||
"VbG4NZwWnipo",
|
||||
"335Q9/MhqcNU3s2TO"
|
||||
],
|
||||
"X-System-Hostname": [
|
||||
"{system.hostname}"
|
||||
]
|
||||
},
|
||||
"uri": "/health"
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
:8884
|
||||
|
||||
reverse_proxy 127.0.0.1:65535 127.0.0.1:35535 {
|
||||
lb_policy weighted_round_robin 10 1
|
||||
lb_retries 5
|
||||
lb_try_duration 10s
|
||||
lb_try_interval 500ms
|
||||
lb_retry_match {
|
||||
path /foo*
|
||||
method POST
|
||||
}
|
||||
lb_retry_match path /bar*
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":8884"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"load_balancing": {
|
||||
"retries": 5,
|
||||
"retry_match": [
|
||||
{
|
||||
"method": [
|
||||
"POST"
|
||||
],
|
||||
"path": [
|
||||
"/foo*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"/bar*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"selection_policy": {
|
||||
"policy": "weighted_round_robin",
|
||||
"weights": [
|
||||
10,
|
||||
1
|
||||
]
|
||||
},
|
||||
"try_duration": 10000000000,
|
||||
"try_interval": 500000000
|
||||
},
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "127.0.0.1:65535"
|
||||
},
|
||||
{
|
||||
"dial": "127.0.0.1:35535"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
:8884 {
|
||||
# Port range
|
||||
reverse_proxy localhost:8001-8002
|
||||
|
||||
# Port range with placeholder
|
||||
reverse_proxy {host}:8001-8002
|
||||
|
||||
# Port range with scheme
|
||||
reverse_proxy https://localhost:8001-8002
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":8884"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "localhost:8001"
|
||||
},
|
||||
{
|
||||
"dial": "localhost:8002"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "{http.request.host}:8001"
|
||||
},
|
||||
{
|
||||
"dial": "{http.request.host}:8002"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"transport": {
|
||||
"protocol": "http",
|
||||
"tls": {}
|
||||
},
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "localhost:8001"
|
||||
},
|
||||
{
|
||||
"dial": "localhost:8002"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
# example from https://caddy.community/t/21415
|
||||
a.com {
|
||||
tls {
|
||||
get_certificate http http://foo.com/get
|
||||
}
|
||||
}
|
||||
|
||||
b.com {
|
||||
}
|
||||
----------
|
||||
{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"a.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
},
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"b.com"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [
|
||||
{
|
||||
"subjects": [
|
||||
"a.com"
|
||||
],
|
||||
"get_certificate": [
|
||||
{
|
||||
"url": "http://foo.com/get",
|
||||
"via": "http"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"subjects": [
|
||||
"b.com"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,3 +135,352 @@ func TestReplIndex(t *testing.T) {
|
||||
// act and assert
|
||||
tester.AssertGetResponse("http://localhost:9080/", 200, "")
|
||||
}
|
||||
|
||||
func TestInvalidPrefix(t *testing.T) {
|
||||
type testCase struct {
|
||||
config, expectedError string
|
||||
}
|
||||
|
||||
failureCases := []testCase{
|
||||
{
|
||||
config: `wss://localhost`,
|
||||
expectedError: `the scheme wss:// is only supported in browsers; use https:// instead`,
|
||||
},
|
||||
{
|
||||
config: `ws://localhost`,
|
||||
expectedError: `the scheme ws:// is only supported in browsers; use http:// instead`,
|
||||
},
|
||||
{
|
||||
config: `someInvalidPrefix://localhost`,
|
||||
expectedError: "unsupported URL scheme someinvalidprefix://",
|
||||
},
|
||||
{
|
||||
config: `h2c://localhost`,
|
||||
expectedError: `unsupported URL scheme h2c://`,
|
||||
},
|
||||
{
|
||||
config: `localhost, wss://localhost`,
|
||||
expectedError: `the scheme wss:// is only supported in browsers; use https:// instead`,
|
||||
},
|
||||
{
|
||||
config: `localhost {
|
||||
reverse_proxy ws://localhost"
|
||||
}`,
|
||||
expectedError: `the scheme ws:// is only supported in browsers; use http:// instead`,
|
||||
},
|
||||
{
|
||||
config: `localhost {
|
||||
reverse_proxy someInvalidPrefix://localhost"
|
||||
}`,
|
||||
expectedError: `unsupported URL scheme someinvalidprefix://`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, failureCase := range failureCases {
|
||||
caddytest.AssertLoadError(t, failureCase.config, "caddyfile", failureCase.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidPrefix(t *testing.T) {
|
||||
type testCase struct {
|
||||
rawConfig, expectedResponse string
|
||||
}
|
||||
|
||||
successCases := []testCase{
|
||||
{
|
||||
"localhost",
|
||||
`{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
"https://localhost",
|
||||
`{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
"http://localhost",
|
||||
`{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":80"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
`localhost {
|
||||
reverse_proxy http://localhost:3000
|
||||
}`,
|
||||
`{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "localhost:3000"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
`localhost {
|
||||
reverse_proxy https://localhost:3000
|
||||
}`,
|
||||
`{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"transport": {
|
||||
"protocol": "http",
|
||||
"tls": {}
|
||||
},
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "localhost:3000"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
`localhost {
|
||||
reverse_proxy h2c://localhost:3000
|
||||
}`,
|
||||
`{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"transport": {
|
||||
"protocol": "http",
|
||||
"versions": [
|
||||
"h2c",
|
||||
"2"
|
||||
]
|
||||
},
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "localhost:3000"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
`localhost {
|
||||
reverse_proxy localhost:3000
|
||||
}`,
|
||||
`{
|
||||
"apps": {
|
||||
"http": {
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [
|
||||
":443"
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"match": [
|
||||
{
|
||||
"host": [
|
||||
"localhost"
|
||||
]
|
||||
}
|
||||
],
|
||||
"handle": [
|
||||
{
|
||||
"handler": "subroute",
|
||||
"routes": [
|
||||
{
|
||||
"handle": [
|
||||
{
|
||||
"handler": "reverse_proxy",
|
||||
"upstreams": [
|
||||
{
|
||||
"dial": "localhost:3000"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"terminal": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, successCase := range successCases {
|
||||
caddytest.AssertAdapt(t, successCase.rawConfig, "caddyfile", successCase.expectedResponse)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/caddytest"
|
||||
)
|
||||
|
||||
func setupListenerWrapperTest(t *testing.T, handlerFunc http.HandlerFunc) *caddytest.Tester {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to listen: %s", err)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", handlerFunc)
|
||||
srv := &http.Server{
|
||||
Handler: mux,
|
||||
}
|
||||
go srv.Serve(l)
|
||||
t.Cleanup(func() {
|
||||
_ = srv.Close()
|
||||
_ = l.Close()
|
||||
})
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(fmt.Sprintf(`
|
||||
{
|
||||
skip_install_trust
|
||||
admin localhost:2999
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
local_certs
|
||||
servers :9443 {
|
||||
listener_wrappers {
|
||||
http_redirect
|
||||
tls
|
||||
}
|
||||
}
|
||||
}
|
||||
localhost {
|
||||
reverse_proxy %s
|
||||
}
|
||||
`, l.Addr().String()), "caddyfile")
|
||||
return tester
|
||||
}
|
||||
|
||||
func TestHTTPRedirectWrapperWithLargeUpload(t *testing.T) {
|
||||
const uploadSize = (1024 * 1024) + 1 // 1 MB + 1 byte
|
||||
// 1 more than an MB
|
||||
body := make([]byte, uploadSize)
|
||||
rand.New(rand.NewSource(0)).Read(body)
|
||||
|
||||
tester := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) {
|
||||
buf := new(bytes.Buffer)
|
||||
_, err := buf.ReadFrom(request.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read body: %s", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(buf.Bytes(), body) {
|
||||
t.Fatalf("body not the same")
|
||||
}
|
||||
|
||||
writer.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
resp, err := tester.Client.Post("https://localhost:9443", "application/octet-stream", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to post: %s", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
t.Fatalf("unexpected status: %d != %d", resp.StatusCode, http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLargeHttpRequest(t *testing.T) {
|
||||
tester := setupListenerWrapperTest(t, func(writer http.ResponseWriter, request *http.Request) {
|
||||
t.Fatal("not supposed to handle a request")
|
||||
})
|
||||
|
||||
// We never read the body in any way, set an extra long header instead.
|
||||
req, _ := http.NewRequest("POST", "http://localhost:9443", nil)
|
||||
req.Header.Set("Long-Header", strings.Repeat("X", 1024*1024))
|
||||
_, err := tester.Client.Do(req)
|
||||
if err == nil {
|
||||
t.Fatal("not supposed to succeed")
|
||||
}
|
||||
}
|
||||
@@ -176,9 +176,7 @@ func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
|
||||
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.WriteHeader(200)
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
http.NewResponseController(w).Flush()
|
||||
|
||||
buf := make([]byte, 4*1024)
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
_ "github.com/caddyserver/caddy/v2/modules/standard"
|
||||
)
|
||||
|
||||
// Validates that Caddy's registered internal modules implement the necessary interfaces of their
|
||||
// respective namespaces
|
||||
func TestTypes(t *testing.T) {
|
||||
var i int
|
||||
for _, v := range caddy.Modules() {
|
||||
mod, _ := caddy.GetModule(v)
|
||||
if ok, err := caddy.ConformsToNamespace(mod.New(), mod.ID.Namespace()); !ok {
|
||||
t.Errorf("%s", err)
|
||||
}
|
||||
i++
|
||||
}
|
||||
t.Logf("Passed through %d modules", i)
|
||||
}
|
||||
+29
-1
@@ -1,7 +1,11 @@
|
||||
package caddycmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
@@ -95,15 +99,22 @@ https://caddyserver.com/docs/running
|
||||
// kind of annoying to have all the help text printed out if
|
||||
// caddy has an error provisioning its modules, for instance...
|
||||
SilenceUsage: true,
|
||||
Version: onlyVersionText(),
|
||||
}
|
||||
|
||||
const fullDocsFooter = `Full documentation is available at:
|
||||
https://caddyserver.com/docs/command-line`
|
||||
|
||||
func init() {
|
||||
rootCmd.SetVersionTemplate("{{.Version}}\n")
|
||||
rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n")
|
||||
}
|
||||
|
||||
func onlyVersionText() string {
|
||||
_, f := caddy.Version()
|
||||
return f
|
||||
}
|
||||
|
||||
func caddyCmdToCobra(caddyCmd Command) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: caddyCmd.Name,
|
||||
@@ -123,7 +134,24 @@ func caddyCmdToCobra(caddyCmd Command) *cobra.Command {
|
||||
// in a cobra command's RunE field.
|
||||
func WrapCommandFuncForCobra(f CommandFunc) func(cmd *cobra.Command, _ []string) error {
|
||||
return func(cmd *cobra.Command, _ []string) error {
|
||||
_, err := f(Flags{cmd.Flags()})
|
||||
status, err := f(Flags{cmd.Flags()})
|
||||
if status > 1 {
|
||||
cmd.SilenceErrors = true
|
||||
return &exitError{ExitCode: status, Err: err}
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// exitError carries the exit code from CommandFunc to Main()
|
||||
type exitError struct {
|
||||
ExitCode int
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *exitError) Error() string {
|
||||
if e.Err == nil {
|
||||
return fmt.Sprintf("exiting with status %d", e.ExitCode)
|
||||
}
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
+184
-110
@@ -22,6 +22,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -32,18 +33,27 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/aryann/difflib"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"go.uber.org/zap"
|
||||
"github.com/caddyserver/caddy/v2/internal"
|
||||
)
|
||||
|
||||
func cmdStart(fl Flags) (int, error) {
|
||||
startCmdConfigFlag := fl.String("config")
|
||||
startCmdConfigAdapterFlag := fl.String("adapter")
|
||||
startCmdPidfileFlag := fl.String("pidfile")
|
||||
startCmdWatchFlag := fl.Bool("watch")
|
||||
startCmdEnvfileFlag := fl.String("envfile")
|
||||
configFlag := fl.String("config")
|
||||
configAdapterFlag := fl.String("adapter")
|
||||
pidfileFlag := fl.String("pidfile")
|
||||
watchFlag := fl.Bool("watch")
|
||||
|
||||
var err error
|
||||
var envfileFlag []string
|
||||
envfileFlag, err = fl.GetStringSlice("envfile")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("reading envfile flag: %v", err)
|
||||
}
|
||||
|
||||
// open a listener to which the child process will connect when
|
||||
// it is ready to confirm that it has successfully started
|
||||
@@ -64,22 +74,23 @@ func cmdStart(fl Flags) (int, error) {
|
||||
// sure by giving it some random bytes and having it echo
|
||||
// them back to us)
|
||||
cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String())
|
||||
if startCmdConfigFlag != "" {
|
||||
cmd.Args = append(cmd.Args, "--config", startCmdConfigFlag)
|
||||
if configFlag != "" {
|
||||
cmd.Args = append(cmd.Args, "--config", configFlag)
|
||||
}
|
||||
if startCmdEnvfileFlag != "" {
|
||||
cmd.Args = append(cmd.Args, "--envfile", startCmdEnvfileFlag)
|
||||
|
||||
for _, envfile := range envfileFlag {
|
||||
cmd.Args = append(cmd.Args, "--envfile", envfile)
|
||||
}
|
||||
if startCmdConfigAdapterFlag != "" {
|
||||
cmd.Args = append(cmd.Args, "--adapter", startCmdConfigAdapterFlag)
|
||||
if configAdapterFlag != "" {
|
||||
cmd.Args = append(cmd.Args, "--adapter", configAdapterFlag)
|
||||
}
|
||||
if startCmdWatchFlag {
|
||||
if watchFlag {
|
||||
cmd.Args = append(cmd.Args, "--watch")
|
||||
}
|
||||
if startCmdPidfileFlag != "" {
|
||||
cmd.Args = append(cmd.Args, "--pidfile", startCmdPidfileFlag)
|
||||
if pidfileFlag != "" {
|
||||
cmd.Args = append(cmd.Args, "--pidfile", pidfileFlag)
|
||||
}
|
||||
stdinpipe, err := cmd.StdinPipe()
|
||||
stdinPipe, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("creating stdin pipe: %v", err)
|
||||
@@ -91,7 +102,8 @@ func cmdStart(fl Flags) (int, error) {
|
||||
expect := make([]byte, 32)
|
||||
_, err = rand.Read(expect)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("generating random confirmation bytes: %v", err)
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("generating random confirmation bytes: %v", err)
|
||||
}
|
||||
|
||||
// begin writing the confirmation bytes to the child's
|
||||
@@ -99,14 +111,15 @@ func cmdStart(fl Flags) (int, error) {
|
||||
// started yet, and writing synchronously would result
|
||||
// in a deadlock
|
||||
go func() {
|
||||
_, _ = stdinpipe.Write(expect)
|
||||
stdinpipe.Close()
|
||||
_, _ = stdinPipe.Write(expect)
|
||||
stdinPipe.Close()
|
||||
}()
|
||||
|
||||
// start the process
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("starting caddy process: %v", err)
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("starting caddy process: %v", err)
|
||||
}
|
||||
|
||||
// there are two ways we know we're done: either
|
||||
@@ -154,41 +167,37 @@ func cmdStart(fl Flags) (int, error) {
|
||||
func cmdRun(fl Flags) (int, error) {
|
||||
caddy.TrapSignals()
|
||||
|
||||
runCmdConfigFlag := fl.String("config")
|
||||
runCmdConfigAdapterFlag := fl.String("adapter")
|
||||
runCmdResumeFlag := fl.Bool("resume")
|
||||
runCmdLoadEnvfileFlag := fl.String("envfile")
|
||||
runCmdPrintEnvFlag := fl.Bool("environ")
|
||||
runCmdWatchFlag := fl.Bool("watch")
|
||||
runCmdPidfileFlag := fl.String("pidfile")
|
||||
runCmdPingbackFlag := fl.String("pingback")
|
||||
configFlag := fl.String("config")
|
||||
configAdapterFlag := fl.String("adapter")
|
||||
resumeFlag := fl.Bool("resume")
|
||||
printEnvFlag := fl.Bool("environ")
|
||||
watchFlag := fl.Bool("watch")
|
||||
pidfileFlag := fl.String("pidfile")
|
||||
pingbackFlag := fl.String("pingback")
|
||||
|
||||
// load all additional envs as soon as possible
|
||||
if runCmdLoadEnvfileFlag != "" {
|
||||
if err := loadEnvFromFile(runCmdLoadEnvfileFlag); err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("loading additional environment variables: %v", err)
|
||||
}
|
||||
err := handleEnvFileFlag(fl)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
// if we are supposed to print the environment, do that first
|
||||
if runCmdPrintEnvFlag {
|
||||
if printEnvFlag {
|
||||
printEnvironment()
|
||||
}
|
||||
|
||||
// load the config, depending on flags
|
||||
var config []byte
|
||||
var err error
|
||||
if runCmdResumeFlag {
|
||||
if resumeFlag {
|
||||
config, err = os.ReadFile(caddy.ConfigAutosavePath)
|
||||
if os.IsNotExist(err) {
|
||||
// not a bad error; just can't resume if autosave file doesn't exist
|
||||
caddy.Log().Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath))
|
||||
runCmdResumeFlag = false
|
||||
resumeFlag = false
|
||||
} else if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
} else {
|
||||
if runCmdConfigFlag == "" {
|
||||
if configFlag == "" {
|
||||
caddy.Log().Info("resuming from last configuration",
|
||||
zap.String("autosave_file", caddy.ConfigAutosavePath))
|
||||
} else {
|
||||
@@ -201,19 +210,19 @@ func cmdRun(fl Flags) (int, error) {
|
||||
}
|
||||
// we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
|
||||
var configFile string
|
||||
if !runCmdResumeFlag {
|
||||
config, configFile, err = LoadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
|
||||
if !resumeFlag {
|
||||
config, configFile, err = LoadConfig(configFlag, configAdapterFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
}
|
||||
|
||||
// create pidfile now, in case loading config takes a while (issue #5477)
|
||||
if runCmdPidfileFlag != "" {
|
||||
err := caddy.PIDFile(runCmdPidfileFlag)
|
||||
if pidfileFlag != "" {
|
||||
err := caddy.PIDFile(pidfileFlag)
|
||||
if err != nil {
|
||||
caddy.Log().Error("unable to write PID file",
|
||||
zap.String("pidfile", runCmdPidfileFlag),
|
||||
zap.String("pidfile", pidfileFlag),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
@@ -227,13 +236,13 @@ func cmdRun(fl Flags) (int, error) {
|
||||
|
||||
// if we are to report to another process the successful start
|
||||
// of the server, do so now by echoing back contents of stdin
|
||||
if runCmdPingbackFlag != "" {
|
||||
if pingbackFlag != "" {
|
||||
confirmationBytes, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("reading confirmation bytes from stdin: %v", err)
|
||||
}
|
||||
conn, err := net.Dial("tcp", runCmdPingbackFlag)
|
||||
conn, err := net.Dial("tcp", pingbackFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("dialing confirmation address: %v", err)
|
||||
@@ -242,14 +251,14 @@ func cmdRun(fl Flags) (int, error) {
|
||||
_, err = conn.Write(confirmationBytes)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("writing confirmation bytes to %s: %v", runCmdPingbackFlag, err)
|
||||
fmt.Errorf("writing confirmation bytes to %s: %v", pingbackFlag, err)
|
||||
}
|
||||
}
|
||||
|
||||
// if enabled, reload config file automatically on changes
|
||||
// (this better only be used in dev!)
|
||||
if runCmdWatchFlag {
|
||||
go watchConfigFile(configFile, runCmdConfigAdapterFlag)
|
||||
if watchFlag {
|
||||
go watchConfigFile(configFile, configAdapterFlag)
|
||||
}
|
||||
|
||||
// warn if the environment does not provide enough information about the disk
|
||||
@@ -275,11 +284,11 @@ func cmdRun(fl Flags) (int, error) {
|
||||
}
|
||||
|
||||
func cmdStop(fl Flags) (int, error) {
|
||||
addrFlag := fl.String("address")
|
||||
addressFlag := fl.String("address")
|
||||
configFlag := fl.String("config")
|
||||
configAdapterFlag := fl.String("adapter")
|
||||
|
||||
adminAddr, err := DetermineAdminAPIAddress(addrFlag, nil, configFlag, configAdapterFlag)
|
||||
adminAddr, err := DetermineAdminAPIAddress(addressFlag, nil, configFlag, configAdapterFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
|
||||
}
|
||||
@@ -297,7 +306,7 @@ func cmdStop(fl Flags) (int, error) {
|
||||
func cmdReload(fl Flags) (int, error) {
|
||||
configFlag := fl.String("config")
|
||||
configAdapterFlag := fl.String("adapter")
|
||||
addrFlag := fl.String("address")
|
||||
addressFlag := fl.String("address")
|
||||
forceFlag := fl.Bool("force")
|
||||
|
||||
// get the config in caddy's native format
|
||||
@@ -309,7 +318,7 @@ func cmdReload(fl Flags) (int, error) {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
|
||||
}
|
||||
|
||||
adminAddr, err := DetermineAdminAPIAddress(addrFlag, config, configFlag, configAdapterFlag)
|
||||
adminAddr, err := DetermineAdminAPIAddress(addressFlag, config, configFlag, configAdapterFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
|
||||
}
|
||||
@@ -411,60 +420,60 @@ func cmdListModules(fl Flags) (int, error) {
|
||||
return caddy.ExitCodeSuccess, nil
|
||||
}
|
||||
|
||||
func cmdEnviron(_ Flags) (int, error) {
|
||||
func cmdEnviron(fl Flags) (int, error) {
|
||||
// load all additional envs as soon as possible
|
||||
err := handleEnvFileFlag(fl)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
printEnvironment()
|
||||
return caddy.ExitCodeSuccess, nil
|
||||
}
|
||||
|
||||
func cmdAdaptConfig(fl Flags) (int, error) {
|
||||
adaptCmdInputFlag := fl.String("config")
|
||||
adaptCmdAdapterFlag := fl.String("adapter")
|
||||
adaptCmdPrettyFlag := fl.Bool("pretty")
|
||||
adaptCmdValidateFlag := fl.Bool("validate")
|
||||
inputFlag := fl.String("config")
|
||||
adapterFlag := fl.String("adapter")
|
||||
prettyFlag := fl.Bool("pretty")
|
||||
validateFlag := fl.Bool("validate")
|
||||
|
||||
// if no input file was specified, try a default
|
||||
// Caddyfile if the Caddyfile adapter is plugged in
|
||||
if adaptCmdInputFlag == "" && caddyconfig.GetAdapter("caddyfile") != nil {
|
||||
_, err := os.Stat("Caddyfile")
|
||||
if err == nil {
|
||||
// default Caddyfile exists
|
||||
adaptCmdInputFlag = "Caddyfile"
|
||||
caddy.Log().Info("using adjacent Caddyfile")
|
||||
} else if !os.IsNotExist(err) {
|
||||
// default Caddyfile exists, but error accessing it
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("accessing default Caddyfile: %v", err)
|
||||
}
|
||||
var err error
|
||||
inputFlag, err = configFileWithRespectToDefault(caddy.Log(), inputFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
if adaptCmdInputFlag == "" {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
|
||||
// load all additional envs as soon as possible
|
||||
err = handleEnvFileFlag(fl)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
if adaptCmdAdapterFlag == "" {
|
||||
|
||||
if adapterFlag == "" {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("adapter name is required (use --adapt flag or leave unspecified for default)")
|
||||
}
|
||||
|
||||
cfgAdapter := caddyconfig.GetAdapter(adaptCmdAdapterFlag)
|
||||
cfgAdapter := caddyconfig.GetAdapter(adapterFlag)
|
||||
if cfgAdapter == nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("unrecognized config adapter: %s", adaptCmdAdapterFlag)
|
||||
fmt.Errorf("unrecognized config adapter: %s", adapterFlag)
|
||||
}
|
||||
|
||||
input, err := os.ReadFile(adaptCmdInputFlag)
|
||||
input, err := os.ReadFile(inputFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("reading input file: %v", err)
|
||||
}
|
||||
|
||||
opts := map[string]any{"filename": adaptCmdInputFlag}
|
||||
opts := map[string]any{"filename": inputFlag}
|
||||
|
||||
adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
if adaptCmdPrettyFlag {
|
||||
if prettyFlag {
|
||||
var prettyBuf bytes.Buffer
|
||||
err = json.Indent(&prettyBuf, adaptedConfig, "", "\t")
|
||||
if err != nil {
|
||||
@@ -482,13 +491,13 @@ func cmdAdaptConfig(fl Flags) (int, error) {
|
||||
if warn.Directive != "" {
|
||||
msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
|
||||
}
|
||||
caddy.Log().Named(adaptCmdAdapterFlag).Warn(msg,
|
||||
caddy.Log().Named(adapterFlag).Warn(msg,
|
||||
zap.String("file", warn.File),
|
||||
zap.Int("line", warn.Line))
|
||||
}
|
||||
|
||||
// validate output if requested
|
||||
if adaptCmdValidateFlag {
|
||||
if validateFlag {
|
||||
var cfg *caddy.Config
|
||||
err = caddy.StrictUnmarshalJSON(adaptedConfig, &cfg)
|
||||
if err != nil {
|
||||
@@ -504,19 +513,26 @@ func cmdAdaptConfig(fl Flags) (int, error) {
|
||||
}
|
||||
|
||||
func cmdValidateConfig(fl Flags) (int, error) {
|
||||
validateCmdConfigFlag := fl.String("config")
|
||||
validateCmdAdapterFlag := fl.String("adapter")
|
||||
runCmdLoadEnvfileFlag := fl.String("envfile")
|
||||
configFlag := fl.String("config")
|
||||
adapterFlag := fl.String("adapter")
|
||||
|
||||
// load all additional envs as soon as possible
|
||||
if runCmdLoadEnvfileFlag != "" {
|
||||
if err := loadEnvFromFile(runCmdLoadEnvfileFlag); err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("loading additional environment variables: %v", err)
|
||||
}
|
||||
err := handleEnvFileFlag(fl)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
input, _, err := LoadConfig(validateCmdConfigFlag, validateCmdAdapterFlag)
|
||||
// use default config and ensure a config file is specified
|
||||
configFlag, err = configFileWithRespectToDefault(caddy.Log(), configFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
if configFlag == "" {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
|
||||
}
|
||||
|
||||
input, _, err := LoadConfig(configFlag, adapterFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
@@ -539,13 +555,13 @@ func cmdValidateConfig(fl Flags) (int, error) {
|
||||
}
|
||||
|
||||
func cmdFmt(fl Flags) (int, error) {
|
||||
formatCmdConfigFile := fl.Arg(0)
|
||||
if formatCmdConfigFile == "" {
|
||||
formatCmdConfigFile = "Caddyfile"
|
||||
configFile := fl.Arg(0)
|
||||
if configFile == "" {
|
||||
configFile = "Caddyfile"
|
||||
}
|
||||
|
||||
// as a special case, read from stdin if the file name is "-"
|
||||
if formatCmdConfigFile == "-" {
|
||||
if configFile == "-" {
|
||||
input, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
@@ -555,7 +571,7 @@ func cmdFmt(fl Flags) (int, error) {
|
||||
return caddy.ExitCodeSuccess, nil
|
||||
}
|
||||
|
||||
input, err := os.ReadFile(formatCmdConfigFile)
|
||||
input, err := os.ReadFile(configFile)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup,
|
||||
fmt.Errorf("reading input file: %v", err)
|
||||
@@ -564,7 +580,7 @@ func cmdFmt(fl Flags) (int, error) {
|
||||
output := caddyfile.Format(input)
|
||||
|
||||
if fl.Bool("overwrite") {
|
||||
if err := os.WriteFile(formatCmdConfigFile, output, 0600); err != nil {
|
||||
if err := os.WriteFile(configFile, output, 0o600); err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err)
|
||||
}
|
||||
return caddy.ExitCodeSuccess, nil
|
||||
@@ -588,7 +604,7 @@ func cmdFmt(fl Flags) (int, error) {
|
||||
fmt.Print(string(output))
|
||||
}
|
||||
|
||||
if warning, diff := caddyfile.FormattingDifference(formatCmdConfigFile, input); diff {
|
||||
if warning, diff := caddyfile.FormattingDifference(configFile, input); diff {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf(`%s:%d: Caddyfile input is not formatted; Tip: use '--overwrite' to update your Caddyfile in-place instead of previewing it. Consult '--help' for more options`,
|
||||
warning.File,
|
||||
warning.Line,
|
||||
@@ -598,6 +614,25 @@ func cmdFmt(fl Flags) (int, error) {
|
||||
return caddy.ExitCodeSuccess, nil
|
||||
}
|
||||
|
||||
// handleEnvFileFlag loads the environment variables from the given --envfile
|
||||
// flag if specified. This should be called as early in the command function.
|
||||
func handleEnvFileFlag(fl Flags) error {
|
||||
var err error
|
||||
var envfileFlag []string
|
||||
envfileFlag, err = fl.GetStringSlice("envfile")
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading envfile flag: %v", err)
|
||||
}
|
||||
|
||||
for _, envfile := range envfileFlag {
|
||||
if err := loadEnvFromFile(envfile); err != nil {
|
||||
return fmt.Errorf("loading additional environment variables: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AdminAPIRequest makes an API request according to the CLI flags given,
|
||||
// with the given HTTP method and request URI. If body is non-nil, it will
|
||||
// be assumed to be Content-Type application/json. The caller should close
|
||||
@@ -610,7 +645,17 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
|
||||
}
|
||||
origin := "http://" + parsedAddr.JoinHostPort(0)
|
||||
if parsedAddr.IsUnixNetwork() {
|
||||
origin = "http://unixsocket" // hack so that http.NewRequest() is happy
|
||||
origin = "http://127.0.0.1" // bogus host is a hack so that http.NewRequest() is happy
|
||||
|
||||
// the unix address at this point might still contain the optional
|
||||
// unix socket permissions, which are part of the address/host.
|
||||
// those need to be removed first, as they aren't part of the
|
||||
// resulting unix file path
|
||||
addr, _, err := internal.SplitUnixSocketPermissionsBits(parsedAddr.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parsedAddr.Host = addr
|
||||
}
|
||||
|
||||
// form the request
|
||||
@@ -619,20 +664,24 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
|
||||
return nil, fmt.Errorf("making request: %v", err)
|
||||
}
|
||||
if parsedAddr.IsUnixNetwork() {
|
||||
// When listening on a unix socket, the admin endpoint doesn't
|
||||
// accept any Host header because there is no host:port for
|
||||
// a unix socket's address. The server's host check is fairly
|
||||
// strict for security reasons, so we don't allow just any
|
||||
// Host header. For unix sockets, the Host header must be
|
||||
// empty. Unfortunately, Go makes it impossible to make HTTP
|
||||
// requests with an empty Host header... except with this one
|
||||
// weird trick. (Hopefully they don't fix it. It's already
|
||||
// hard enough to use HTTP over unix sockets.)
|
||||
// We used to conform to RFC 2616 Section 14.26 which requires
|
||||
// an empty host header when there is no host, as is the case
|
||||
// with unix sockets. However, Go required a Host value so we
|
||||
// used a hack of a space character as the host (it would see
|
||||
// the Host was non-empty, then trim the space later). As of
|
||||
// Go 1.20.6 (July 2023), this hack no longer works. See:
|
||||
// https://github.com/golang/go/issues/60374
|
||||
// See also the discussion here:
|
||||
// https://github.com/golang/go/issues/61431
|
||||
//
|
||||
// An equivalent curl command would be something like:
|
||||
// $ curl --unix-socket caddy.sock http:/:$REQUEST_URI
|
||||
req.URL.Host = " "
|
||||
req.Host = ""
|
||||
// After that, we now require a Host value of either 127.0.0.1
|
||||
// or ::1 if one is set. Above I choose to use 127.0.0.1. Even
|
||||
// though the value should be completely irrelevant (it could be
|
||||
// "srldkjfsd"), if for some reason the Host *is* used, at least
|
||||
// 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 {
|
||||
req.Header.Set("Origin", origin)
|
||||
}
|
||||
@@ -721,6 +770,31 @@ func DetermineAdminAPIAddress(address string, config []byte, configFile, configA
|
||||
return caddy.DefaultAdminListen, nil
|
||||
}
|
||||
|
||||
// configFileWithRespectToDefault returns the filename to use for loading the config, based
|
||||
// on whether a config file is already specified and a supported default config file exists.
|
||||
func configFileWithRespectToDefault(logger *zap.Logger, configFile string) (string, error) {
|
||||
const defaultCaddyfile = "Caddyfile"
|
||||
|
||||
// if no input file was specified, try a default Caddyfile if the Caddyfile adapter is plugged in
|
||||
if configFile == "" && caddyconfig.GetAdapter("caddyfile") != nil {
|
||||
_, err := os.Stat(defaultCaddyfile)
|
||||
if err == nil {
|
||||
// default Caddyfile exists
|
||||
if logger != nil {
|
||||
logger.Info("using adjacent Caddyfile")
|
||||
}
|
||||
return defaultCaddyfile, nil
|
||||
}
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
// problem checking
|
||||
return configFile, fmt.Errorf("checking if default Caddyfile exists: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// default config file does not exist or is irrelevant
|
||||
return configFile, nil
|
||||
}
|
||||
|
||||
type moduleInfo struct {
|
||||
caddyModuleID string
|
||||
goModule *debug.Module
|
||||
|
||||
+92
-30
@@ -21,9 +21,10 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/cobra/doc"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
// Command represents a subcommand. Name, Func,
|
||||
@@ -93,8 +94,8 @@ func init() {
|
||||
Starts the Caddy process, optionally bootstrapped with an initial config file.
|
||||
This command unblocks after the server starts running or fails to run.
|
||||
|
||||
If --envfile is specified, an environment file with environment variables in
|
||||
the KEY=VALUE format will be loaded into the Caddy process.
|
||||
If --envfile is specified, an environment file with environment variables
|
||||
in the KEY=VALUE format will be loaded into the Caddy process.
|
||||
|
||||
On Windows, the spawned child process will remain attached to the terminal, so
|
||||
closing the window will forcefully stop Caddy; to avoid forgetting this, try
|
||||
@@ -103,7 +104,7 @@ using 'caddy run' instead to keep it in the foreground.
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringP("config", "c", "", "Configuration file")
|
||||
cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply")
|
||||
cmd.Flags().StringP("envfile", "", "", "Environment file to load")
|
||||
cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
|
||||
cmd.Flags().BoolP("watch", "w", false, "Reload changed config file automatically")
|
||||
cmd.Flags().StringP("pidfile", "", "", "Path of file to which to write process ID")
|
||||
cmd.RunE = WrapCommandFuncForCobra(cmdStart)
|
||||
@@ -132,8 +133,8 @@ As a special case, if the current working directory has a file called
|
||||
that file will be loaded and used to configure Caddy, even without any command
|
||||
line flags.
|
||||
|
||||
If --envfile is specified, an environment file with environment variables in
|
||||
the KEY=VALUE format will be loaded into the Caddy process.
|
||||
If --envfile is specified, an environment file with environment variables
|
||||
in the KEY=VALUE format will be loaded into the Caddy process.
|
||||
|
||||
If --environ is specified, the environment as seen by the Caddy process will
|
||||
be printed before starting. This is the same as the environ command but does
|
||||
@@ -149,7 +150,7 @@ option in a local development environment.
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringP("config", "c", "", "Configuration file")
|
||||
cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply")
|
||||
cmd.Flags().StringP("envfile", "", "", "Environment file to load")
|
||||
cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
|
||||
cmd.Flags().BoolP("environ", "e", false, "Print environment")
|
||||
cmd.Flags().BoolP("resume", "r", false, "Use saved config, if any (and prefer over --config file)")
|
||||
cmd.Flags().BoolP("watch", "w", false, "Watch config file for changes and reload it automatically")
|
||||
@@ -239,6 +240,7 @@ documentation: https://go.dev/doc/modules/version-numbers
|
||||
|
||||
RegisterCommand(Command{
|
||||
Name: "environ",
|
||||
Usage: "[--envfile <path>]",
|
||||
Short: "Prints the environment",
|
||||
Long: `
|
||||
Prints the environment as seen by this Caddy process.
|
||||
@@ -248,6 +250,9 @@ configuration uses environment variables (e.g. "{env.VARIABLE}") then
|
||||
this command can be useful for verifying that the variables will have
|
||||
the values you expect in your config.
|
||||
|
||||
If --envfile is specified, an environment file with environment variables
|
||||
in the KEY=VALUE format will be loaded into the Caddy process.
|
||||
|
||||
Note that environments may be different depending on how you run Caddy.
|
||||
Environments for Caddy instances started by service managers such as
|
||||
systemd are often different than the environment inherited from your
|
||||
@@ -258,12 +263,15 @@ by adding the "--environ" flag.
|
||||
|
||||
Environments may contain sensitive data.
|
||||
`,
|
||||
Func: cmdEnviron,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
|
||||
cmd.RunE = WrapCommandFuncForCobra(cmdEnviron)
|
||||
},
|
||||
})
|
||||
|
||||
RegisterCommand(Command{
|
||||
Name: "adapt",
|
||||
Usage: "--config <path> [--adapter <name>] [--pretty] [--validate]",
|
||||
Usage: "--config <path> [--adapter <name>] [--pretty] [--validate] [--envfile <path>]",
|
||||
Short: "Adapts a configuration to Caddy's native JSON",
|
||||
Long: `
|
||||
Adapts a configuration to Caddy's native JSON format and writes the
|
||||
@@ -275,12 +283,16 @@ for human readability.
|
||||
If --validate is used, the adapted config will be checked for validity.
|
||||
If the config is invalid, an error will be printed to stderr and a non-
|
||||
zero exit status will be returned.
|
||||
|
||||
If --envfile is specified, an environment file with environment variables
|
||||
in the KEY=VALUE format will be loaded into the Caddy process.
|
||||
`,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringP("config", "c", "", "Configuration file to adapt (required)")
|
||||
cmd.Flags().StringP("adapter", "a", "caddyfile", "Name of config adapter")
|
||||
cmd.Flags().BoolP("pretty", "p", false, "Format the output for human readability")
|
||||
cmd.Flags().BoolP("validate", "", false, "Validate the output")
|
||||
cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
|
||||
cmd.RunE = WrapCommandFuncForCobra(cmdAdaptConfig)
|
||||
},
|
||||
})
|
||||
@@ -294,17 +306,67 @@ Loads and provisions the provided config, but does not start running it.
|
||||
This reveals any errors with the configuration through the loading and
|
||||
provisioning stages.
|
||||
|
||||
If --envfile is specified, an environment file with environment variables in
|
||||
the KEY=VALUE format will be loaded into the Caddy process.
|
||||
If --envfile is specified, an environment file with environment variables
|
||||
in the KEY=VALUE format will be loaded into the Caddy process.
|
||||
`,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringP("config", "c", "", "Input configuration file")
|
||||
cmd.Flags().StringP("adapter", "a", "", "Name of config adapter")
|
||||
cmd.Flags().StringP("envfile", "", "", "Environment file to load")
|
||||
cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
|
||||
cmd.RunE = WrapCommandFuncForCobra(cmdValidateConfig)
|
||||
},
|
||||
})
|
||||
|
||||
RegisterCommand(Command{
|
||||
Name: "storage",
|
||||
Short: "Commands for working with Caddy's storage (EXPERIMENTAL)",
|
||||
Long: `
|
||||
Allows exporting and importing Caddy's storage contents. The two commands can be
|
||||
combined in a pipeline to transfer directly from one storage to another:
|
||||
|
||||
$ caddy storage export --config Caddyfile.old --output - |
|
||||
> caddy storage import --config Caddyfile.new --input -
|
||||
|
||||
The - argument refers to stdout and stdin, respectively.
|
||||
|
||||
NOTE: When importing to or exporting from file_system storage (the default), the command
|
||||
should be run as the user that owns the associated root path.
|
||||
|
||||
EXPERIMENTAL: May be changed or removed.
|
||||
`,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
exportCmd := &cobra.Command{
|
||||
Use: "export --config <path> --output <path>",
|
||||
Short: "Exports storage assets as a tarball",
|
||||
Long: `
|
||||
The contents of the configured storage module (TLS certificates, etc)
|
||||
are exported via a tarball.
|
||||
|
||||
--output is required, - can be given for stdout.
|
||||
`,
|
||||
RunE: WrapCommandFuncForCobra(cmdExportStorage),
|
||||
}
|
||||
exportCmd.Flags().StringP("config", "c", "", "Input configuration file (required)")
|
||||
exportCmd.Flags().StringP("output", "o", "", "Output path")
|
||||
cmd.AddCommand(exportCmd)
|
||||
|
||||
importCmd := &cobra.Command{
|
||||
Use: "import --config <path> --input <path>",
|
||||
Short: "Imports storage assets from a tarball.",
|
||||
Long: `
|
||||
Imports storage assets to the configured storage module. The import file must be
|
||||
a tar archive.
|
||||
|
||||
--input is required, - can be given for stdin.
|
||||
`,
|
||||
RunE: WrapCommandFuncForCobra(cmdImportStorage),
|
||||
}
|
||||
importCmd.Flags().StringP("config", "c", "", "Configuration file to load (required)")
|
||||
importCmd.Flags().StringP("input", "i", "", "Tar of assets to load (required)")
|
||||
cmd.AddCommand(importCmd)
|
||||
},
|
||||
})
|
||||
|
||||
RegisterCommand(Command{
|
||||
Name: "fmt",
|
||||
Usage: "[--overwrite] [--diff] [<path>]",
|
||||
@@ -351,7 +413,7 @@ latest versions. EXPERIMENTAL: May be changed or removed.
|
||||
Short: "Adds Caddy packages (EXPERIMENTAL)",
|
||||
Long: `
|
||||
Downloads an updated Caddy binary with the specified packages (module/plugin)
|
||||
added. Retains existing packages. Returns an error if the any of packages are
|
||||
added. Retains existing packages. Returns an error if the any of packages are
|
||||
already included. EXPERIMENTAL: May be changed or removed.
|
||||
`,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
@@ -366,8 +428,8 @@ already included. EXPERIMENTAL: May be changed or removed.
|
||||
Usage: "<packages...>",
|
||||
Short: "Removes Caddy packages (EXPERIMENTAL)",
|
||||
Long: `
|
||||
Downloads an updated Caddy binaries without the specified packages (module/plugin).
|
||||
Returns an error if any of the packages are not included.
|
||||
Downloads an updated Caddy binaries without the specified packages (module/plugin).
|
||||
Returns an error if any of the packages are not included.
|
||||
EXPERIMENTAL: May be changed or removed.
|
||||
`,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
@@ -394,7 +456,7 @@ argument of --directory. If the directory does not exist, it will be created.
|
||||
if dir == "" {
|
||||
return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required")
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return caddy.ExitCodeFailedQuit, err
|
||||
}
|
||||
if err := doc.GenManTree(rootCmd, &doc.GenManHeader{
|
||||
@@ -413,40 +475,40 @@ argument of --directory. If the directory does not exist, it will be created.
|
||||
Use: "completion [bash|zsh|fish|powershell]",
|
||||
Short: "Generate completion script",
|
||||
Long: fmt.Sprintf(`To load completions:
|
||||
|
||||
|
||||
Bash:
|
||||
|
||||
|
||||
$ source <(%[1]s completion bash)
|
||||
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
# Linux:
|
||||
$ %[1]s completion bash > /etc/bash_completion.d/%[1]s
|
||||
# macOS:
|
||||
$ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
|
||||
|
||||
|
||||
Zsh:
|
||||
|
||||
|
||||
# If shell completion is not already enabled in your environment,
|
||||
# you will need to enable it. You can execute the following once:
|
||||
|
||||
|
||||
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
|
||||
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
$ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
|
||||
|
||||
|
||||
# You will need to start a new shell for this setup to take effect.
|
||||
|
||||
|
||||
fish:
|
||||
|
||||
|
||||
$ %[1]s completion fish | source
|
||||
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
|
||||
|
||||
|
||||
PowerShell:
|
||||
|
||||
|
||||
PS> %[1]s completion powershell | Out-String | Invoke-Expression
|
||||
|
||||
|
||||
# To load completions for every new session, run:
|
||||
PS> %[1]s completion powershell > %[1]s.ps1
|
||||
# and source this file from your PowerShell profile.
|
||||
|
||||
+49
-56
@@ -17,6 +17,7 @@ package caddycmd
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -30,11 +31,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/spf13/pflag"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -62,6 +64,10 @@ func Main() {
|
||||
}
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
var exitError *exitError
|
||||
if errors.As(err, &exitError) {
|
||||
os.Exit(exitError.ExitCode)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -89,6 +95,10 @@ func handlePingbackConn(conn net.Conn, expect []byte) error {
|
||||
// and returns the resulting JSON config bytes along with
|
||||
// the name of the loaded config file (if any).
|
||||
func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
|
||||
return loadConfigWithLogger(caddy.Log(), configFile, adapterName)
|
||||
}
|
||||
|
||||
func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([]byte, string, error) {
|
||||
// specifying an adapter without a config file is ambiguous
|
||||
if adapterName != "" && configFile == "" {
|
||||
return nil, "", fmt.Errorf("cannot adapt config without config file (use --config)")
|
||||
@@ -107,13 +117,14 @@ func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("reading config file: %v", err)
|
||||
}
|
||||
caddy.Log().Info("using provided configuration",
|
||||
zap.String("config_file", configFile),
|
||||
zap.String("config_adapter", adapterName))
|
||||
if logger != nil {
|
||||
logger.Info("using provided configuration",
|
||||
zap.String("config_file", configFile),
|
||||
zap.String("config_adapter", adapterName))
|
||||
}
|
||||
} else if adapterName == "" {
|
||||
// as a special case when no config file or adapter
|
||||
// is specified, see if the Caddyfile adapter is
|
||||
// plugged in, and if so, try using a default Caddyfile
|
||||
// if the Caddyfile adapter is plugged in, we can try using an
|
||||
// adjacent Caddyfile by default
|
||||
cfgAdapter = caddyconfig.GetAdapter("caddyfile")
|
||||
if cfgAdapter != nil {
|
||||
config, err = os.ReadFile("Caddyfile")
|
||||
@@ -126,7 +137,9 @@ func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
|
||||
} else {
|
||||
// success reading default Caddyfile
|
||||
configFile = "Caddyfile"
|
||||
caddy.Log().Info("using adjacent Caddyfile")
|
||||
if logger != nil {
|
||||
logger.Info("using adjacent Caddyfile")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,7 +174,9 @@ func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
|
||||
if warn.Directive != "" {
|
||||
msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
|
||||
}
|
||||
caddy.Log().Warn(msg, zap.String("adapter", adapterName), zap.String("file", warn.File), zap.Int("line", warn.Line))
|
||||
if logger != nil {
|
||||
logger.Warn(msg, zap.String("adapter", adapterName), zap.String("file", warn.File), zap.Int("line", warn.Line))
|
||||
}
|
||||
}
|
||||
config = adaptedConfig
|
||||
}
|
||||
@@ -174,6 +189,8 @@ func LoadConfig(configFile, adapterName string) ([]byte, string, error) {
|
||||
// blocks indefinitely; it only quits if the poller has errors for
|
||||
// long enough time. The filename passed in must be the actual
|
||||
// config file used, not one to be discovered.
|
||||
// Each second the config files is loaded and parsed into an object
|
||||
// and is compared to the last config object that was loaded
|
||||
func watchConfigFile(filename, adapterName string) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
@@ -189,64 +206,36 @@ func watchConfigFile(filename, adapterName string) {
|
||||
With(zap.String("config_file", filename))
|
||||
}
|
||||
|
||||
// get the initial timestamp on the config file
|
||||
info, err := os.Stat(filename)
|
||||
// get current config
|
||||
lastCfg, _, err := loadConfigWithLogger(nil, filename, adapterName)
|
||||
if err != nil {
|
||||
logger().Error("cannot watch config file", zap.Error(err))
|
||||
logger().Error("unable to load latest config", zap.Error(err))
|
||||
return
|
||||
}
|
||||
lastModified := info.ModTime()
|
||||
|
||||
logger().Info("watching config file for changes")
|
||||
|
||||
// if the file disappears or something, we can
|
||||
// stop polling if the error lasts long enough
|
||||
var lastErr time.Time
|
||||
finalError := func(err error) bool {
|
||||
if lastErr.IsZero() {
|
||||
lastErr = time.Now()
|
||||
return false
|
||||
}
|
||||
if time.Since(lastErr) > 30*time.Second {
|
||||
logger().Error("giving up watching config file; too many errors",
|
||||
zap.Error(err))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// begin poller
|
||||
//nolint:staticcheck
|
||||
for range time.Tick(1 * time.Second) {
|
||||
// get the file info
|
||||
info, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
if finalError(err) {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
lastErr = time.Time{} // no error, so clear any memory of one
|
||||
|
||||
// if it hasn't changed, nothing to do
|
||||
if !info.ModTime().After(lastModified) {
|
||||
continue
|
||||
}
|
||||
|
||||
logger().Info("config file changed; reloading")
|
||||
|
||||
// remember this timestamp
|
||||
lastModified = info.ModTime()
|
||||
|
||||
// load the contents of the file
|
||||
config, _, err := LoadConfig(filename, adapterName)
|
||||
// get current config
|
||||
newCfg, _, err := loadConfigWithLogger(nil, filename, adapterName)
|
||||
if err != nil {
|
||||
logger().Error("unable to load latest config", zap.Error(err))
|
||||
continue
|
||||
return
|
||||
}
|
||||
|
||||
// if it hasn't changed, nothing to do
|
||||
if bytes.Equal(lastCfg, newCfg) {
|
||||
continue
|
||||
}
|
||||
logger().Info("config file changed; reloading")
|
||||
|
||||
// remember the current config
|
||||
lastCfg = newCfg
|
||||
|
||||
// apply the updated config
|
||||
err = caddy.Load(config, false)
|
||||
err = caddy.Load(lastCfg, false)
|
||||
if err != nil {
|
||||
logger().Error("applying latest config", zap.Error(err))
|
||||
continue
|
||||
@@ -316,8 +305,12 @@ func loadEnvFromFile(envFile string) error {
|
||||
}
|
||||
|
||||
for k, v := range envMap {
|
||||
if err := os.Setenv(k, v); err != nil {
|
||||
return fmt.Errorf("setting environment variables: %v", err)
|
||||
// do not overwrite existing environment variables
|
||||
_, exists := os.LookupEnv(k)
|
||||
if !exists {
|
||||
if err := os.Setenv(k, v); err != nil {
|
||||
return fmt.Errorf("setting environment variables: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-1
@@ -22,13 +22,15 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func cmdUpgrade(fl Flags) (int, error) {
|
||||
@@ -102,6 +104,15 @@ func upgradeBuild(pluginPkgs map[string]struct{}, fl Flags) (int, error) {
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("retrieving current executable permission bits: %v", err)
|
||||
}
|
||||
if thisExecStat.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||
symSource := thisExecPath
|
||||
// we are a symlink; resolve it
|
||||
thisExecPath, err = filepath.EvalSymlinks(thisExecPath)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("resolving current executable symlink: %v", err)
|
||||
}
|
||||
l.Info("this executable is a symlink", zap.String("source", symSource), zap.String("target", thisExecPath))
|
||||
}
|
||||
l.Info("this executable will be replaced", zap.String("path", thisExecPath))
|
||||
|
||||
// build the request URL to download this custom build
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package caddycmd
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
type storVal struct {
|
||||
StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`
|
||||
}
|
||||
|
||||
// determineStorage returns the top-level storage module from the given config.
|
||||
// It may return nil even if no error.
|
||||
func determineStorage(configFile string, configAdapter string) (*storVal, error) {
|
||||
cfg, _, err := LoadConfig(configFile, configAdapter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// storage defaults to FileStorage if not explicitly
|
||||
// defined in the config, so the config can be valid
|
||||
// json but unmarshaling will fail.
|
||||
if !json.Valid(cfg) {
|
||||
return nil, &json.SyntaxError{}
|
||||
}
|
||||
var tmpStruct storVal
|
||||
err = json.Unmarshal(cfg, &tmpStruct)
|
||||
if err != nil {
|
||||
// default case, ignore the error
|
||||
var jsonError *json.SyntaxError
|
||||
if errors.As(err, &jsonError) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &tmpStruct, nil
|
||||
}
|
||||
|
||||
func cmdImportStorage(fl Flags) (int, error) {
|
||||
importStorageCmdConfigFlag := fl.String("config")
|
||||
importStorageCmdImportFile := fl.String("input")
|
||||
|
||||
if importStorageCmdConfigFlag == "" {
|
||||
return caddy.ExitCodeFailedStartup, errors.New("--config is required")
|
||||
}
|
||||
if importStorageCmdImportFile == "" {
|
||||
return caddy.ExitCodeFailedStartup, errors.New("--input is required")
|
||||
}
|
||||
|
||||
// extract storage from config if possible
|
||||
storageCfg, err := determineStorage(importStorageCmdConfigFlag, "")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
// load specified storage or fallback to default
|
||||
var stor certmagic.Storage
|
||||
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||
defer cancel()
|
||||
if storageCfg != nil && storageCfg.StorageRaw != nil {
|
||||
val, err := ctx.LoadModule(storageCfg, "StorageRaw")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
stor, err = val.(caddy.StorageConverter).CertMagicStorage()
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
} else {
|
||||
stor = caddy.DefaultStorage
|
||||
}
|
||||
|
||||
// setup input
|
||||
var f *os.File
|
||||
if importStorageCmdImportFile == "-" {
|
||||
f = os.Stdin
|
||||
} else {
|
||||
f, err = os.Open(importStorageCmdImportFile)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("opening input file: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
}
|
||||
|
||||
// store each archive element
|
||||
tr := tar.NewReader(f)
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedQuit, fmt.Errorf("reading archive: %v", err)
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(tr)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedQuit, fmt.Errorf("reading archive: %v", err)
|
||||
}
|
||||
|
||||
err = stor.Store(ctx, hdr.Name, b)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedQuit, fmt.Errorf("reading archive: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Successfully imported storage")
|
||||
return caddy.ExitCodeSuccess, nil
|
||||
}
|
||||
|
||||
func cmdExportStorage(fl Flags) (int, error) {
|
||||
exportStorageCmdConfigFlag := fl.String("config")
|
||||
exportStorageCmdOutputFlag := fl.String("output")
|
||||
|
||||
if exportStorageCmdConfigFlag == "" {
|
||||
return caddy.ExitCodeFailedStartup, errors.New("--config is required")
|
||||
}
|
||||
if exportStorageCmdOutputFlag == "" {
|
||||
return caddy.ExitCodeFailedStartup, errors.New("--output is required")
|
||||
}
|
||||
|
||||
// extract storage from config if possible
|
||||
storageCfg, err := determineStorage(exportStorageCmdConfigFlag, "")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
// load specified storage or fallback to default
|
||||
var stor certmagic.Storage
|
||||
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||
defer cancel()
|
||||
if storageCfg != nil && storageCfg.StorageRaw != nil {
|
||||
val, err := ctx.LoadModule(storageCfg, "StorageRaw")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
stor, err = val.(caddy.StorageConverter).CertMagicStorage()
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
} else {
|
||||
stor = caddy.DefaultStorage
|
||||
}
|
||||
|
||||
// enumerate all keys
|
||||
keys, err := stor.List(ctx, "", true)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
// setup output
|
||||
var f *os.File
|
||||
if exportStorageCmdOutputFlag == "-" {
|
||||
f = os.Stdout
|
||||
} else {
|
||||
f, err = os.Create(exportStorageCmdOutputFlag)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("opening output file: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
}
|
||||
|
||||
// `IsTerminal: true` keys hold the values we
|
||||
// care about, write them out
|
||||
tw := tar.NewWriter(f)
|
||||
for _, k := range keys {
|
||||
info, err := stor.Stat(ctx, k)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedQuit, err
|
||||
}
|
||||
|
||||
if info.IsTerminal {
|
||||
v, err := stor.Load(ctx, k)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedQuit, err
|
||||
}
|
||||
|
||||
hdr := &tar.Header{
|
||||
Name: k,
|
||||
Mode: 0o600,
|
||||
Size: int64(len(v)),
|
||||
ModTime: info.Modified,
|
||||
}
|
||||
|
||||
if err = tw.WriteHeader(hdr); err != nil {
|
||||
return caddy.ExitCodeFailedQuit, fmt.Errorf("writing archive: %v", err)
|
||||
}
|
||||
if _, err = tw.Write(v); err != nil {
|
||||
return caddy.ExitCodeFailedQuit, fmt.Errorf("writing archive: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err = tw.Close(); err != nil {
|
||||
return caddy.ExitCodeFailedQuit, fmt.Errorf("writing archive: %v", err)
|
||||
}
|
||||
|
||||
return caddy.ExitCodeSuccess, nil
|
||||
}
|
||||
+22
-11
@@ -410,6 +410,11 @@ func (ctx Context) loadModuleInline(moduleNameKey, moduleScope string, raw json.
|
||||
// called during the Provision/Validate phase to reference a
|
||||
// module's own host app (since the parent app module is still
|
||||
// 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) {
|
||||
if app, ok := ctx.cfg.apps[name]; ok {
|
||||
return app, nil
|
||||
@@ -428,18 +433,21 @@ func (ctx Context) App(name string) (any, error) {
|
||||
|
||||
// AppIfConfigured returns an app by its name if it has been
|
||||
// configured. Can be called instead of App() to avoid
|
||||
// instantiating an empty app when that's not desirable.
|
||||
func (ctx Context) AppIfConfigured(name string) (any, error) {
|
||||
app, ok := ctx.cfg.apps[name]
|
||||
if !ok || app == nil {
|
||||
return nil, nil
|
||||
// instantiating an empty app when that's not desirable. If
|
||||
// the app has not been loaded, nil is returned.
|
||||
//
|
||||
// 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) 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
|
||||
}
|
||||
|
||||
appModule, err := ctx.App(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return appModule, nil
|
||||
return ctx.cfg.apps[name]
|
||||
}
|
||||
|
||||
// Storage returns the configured Caddy storage implementation.
|
||||
@@ -480,6 +488,9 @@ func (ctx Context) Logger(module ...Module) *zap.Logger {
|
||||
if len(module) > 0 {
|
||||
mod = module[0]
|
||||
}
|
||||
if mod == nil {
|
||||
return Log()
|
||||
}
|
||||
return ctx.cfg.Logging.Logger(mod)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,65 +1,72 @@
|
||||
module github.com/caddyserver/caddy/v2
|
||||
|
||||
go 1.19
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.2.1
|
||||
github.com/BurntSushi/toml v1.3.2
|
||||
github.com/Masterminds/sprig/v3 v3.2.3
|
||||
github.com/alecthomas/chroma/v2 v2.5.0
|
||||
github.com/alecthomas/chroma/v2 v2.9.1
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
|
||||
github.com/caddyserver/certmagic v0.17.2
|
||||
github.com/caddyserver/certmagic v0.20.0
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/go-chi/chi v4.1.2+incompatible
|
||||
github.com/google/cel-go v0.13.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/klauspost/compress v1.16.0
|
||||
github.com/klauspost/cpuid/v2 v2.2.4
|
||||
github.com/mastercactapus/proxyprotocol v0.0.4
|
||||
github.com/mholt/acmez v1.1.0
|
||||
github.com/prometheus/client_golang v1.14.0
|
||||
github.com/quic-go/quic-go v0.33.0
|
||||
github.com/smallstep/certificates v0.23.2
|
||||
github.com/go-chi/chi/v5 v5.0.10
|
||||
github.com/google/cel-go v0.15.1
|
||||
github.com/google/uuid v1.3.1
|
||||
github.com/klauspost/compress v1.17.0
|
||||
github.com/klauspost/cpuid/v2 v2.2.5
|
||||
github.com/mholt/acmez v1.2.0
|
||||
github.com/prometheus/client_golang v1.15.1
|
||||
github.com/quic-go/quic-go v0.40.0
|
||||
github.com/smallstep/certificates v0.25.0
|
||||
github.com/smallstep/nosql v0.6.0
|
||||
github.com/smallstep/truststore v0.12.1
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/tailscale/tscert v0.0.0-20230124224810-c6dc1f4049b2
|
||||
github.com/yuin/goldmark v1.5.4
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.40.0
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.40.0
|
||||
go.opentelemetry.io/otel v1.14.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.14.0
|
||||
go.opentelemetry.io/otel/sdk v1.14.0
|
||||
go.uber.org/zap v1.24.0
|
||||
golang.org/x/crypto v0.7.0
|
||||
golang.org/x/net v0.8.0
|
||||
golang.org/x/sync v0.1.0
|
||||
golang.org/x/term v0.6.0
|
||||
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046
|
||||
github.com/yuin/goldmark v1.5.6
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.42.0
|
||||
go.opentelemetry.io/otel v1.21.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0
|
||||
go.opentelemetry.io/otel/sdk v1.21.0
|
||||
go.uber.org/zap v1.25.0
|
||||
golang.org/x/crypto v0.14.0
|
||||
golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0
|
||||
golang.org/x/net v0.17.0
|
||||
golang.org/x/sync v0.4.0
|
||||
golang.org/x/term v0.13.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/iam v1.1.2 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
||||
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
|
||||
github.com/golang/glog v1.0.0 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.2.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.5.0 // indirect
|
||||
github.com/golang/glog v1.1.2 // indirect
|
||||
github.com/google/certificate-transparency-go v1.1.6 // indirect
|
||||
github.com/google/go-tpm v0.9.0 // indirect
|
||||
github.com/google/go-tspi v0.3.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-19 v0.2.1 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.1.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.9.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
||||
github.com/smallstep/go-attestation v0.4.4-0.20230627102604-cf579e53cbd2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.15.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.15.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.15.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.15.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
|
||||
github.com/zeebo/blake3 v0.2.3 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/aws v1.17.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/ot v1.17.0 // indirect
|
||||
go.uber.org/mock v0.3.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -67,30 +74,29 @@ require (
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/dgraph-io/badger v1.6.2 // indirect
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/go-kit/kit v0.10.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.5.1 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-logr/logr v1.3.0 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/huandu/xstrings v1.3.3 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgconn v1.14.0 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
@@ -102,45 +108,44 @@ require (
|
||||
github.com/libdns/libdns v0.2.1 // indirect
|
||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-isatty v0.0.13 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/micromdm/scep/v2 v2.1.0 // indirect
|
||||
github.com/miekg/dns v1.1.50 // indirect
|
||||
github.com/miekg/dns v1.1.55 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/pires/go-proxyproto v0.7.0
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/rs/xid v1.4.0 // indirect
|
||||
github.com/prometheus/client_model v0.4.0 // indirect
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.9.0 // indirect
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/slackhq/nebula v1.6.1 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/stoewer/go-strcase v1.2.0 // indirect
|
||||
github.com/urfave/cli v1.22.12 // indirect
|
||||
github.com/urfave/cli v1.22.14 // indirect
|
||||
go.etcd.io/bbolt v1.3.7 // indirect
|
||||
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.14.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v0.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.14.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
|
||||
go.step.sm/cli-utils v0.7.5 // indirect
|
||||
go.step.sm/crypto v0.23.2
|
||||
go.step.sm/linkedca v0.19.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/mod v0.8.0 // indirect
|
||||
golang.org/x/sys v0.6.0
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
golang.org/x/tools v0.6.0 // indirect
|
||||
google.golang.org/grpc v1.53.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.21.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.21.0
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
||||
go.step.sm/cli-utils v0.8.0 // indirect
|
||||
go.step.sm/crypto v0.35.1
|
||||
go.step.sm/linkedca v0.20.1 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/mod v0.11.0 // indirect
|
||||
golang.org/x/sys v0.14.0
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/tools v0.10.0 // indirect
|
||||
google.golang.org/grpc v1.59.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
)
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SplitUnixSocketPermissionsBits takes a unix socket address in the
|
||||
// unusual "path|bits" format (e.g. /run/caddy.sock|0222) and tries
|
||||
// to split it into socket path (host) and permissions bits (port).
|
||||
// Colons (":") can't be used as separator, as socket paths on Windows
|
||||
// may include a drive letter (e.g. `unix/c:\absolute\path.sock`).
|
||||
// Permission bits will default to 0200 if none are specified.
|
||||
// Throws an error, if the first carrying bit does not
|
||||
// include write perms (e.g. `0422` or `022`).
|
||||
// Symbolic permission representation (e.g. `u=w,g=w,o=w`)
|
||||
// is not supported and will throw an error for now!
|
||||
func SplitUnixSocketPermissionsBits(addr string) (path string, fileMode fs.FileMode, err error) {
|
||||
addrSplit := strings.SplitN(addr, "|", 2)
|
||||
|
||||
if len(addrSplit) == 2 {
|
||||
// parse octal permission bit string as uint32
|
||||
fileModeUInt64, err := strconv.ParseUint(addrSplit[1], 8, 32)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("could not parse octal permission bits in %s: %v", addr, err)
|
||||
}
|
||||
fileMode = fs.FileMode(fileModeUInt64)
|
||||
|
||||
// FileMode.String() returns a string like `-rwxr-xr--` for `u=rwx,g=rx,o=r` (`0754`)
|
||||
if string(fileMode.String()[2]) != "w" {
|
||||
return "", 0, fmt.Errorf("owner of the socket requires '-w-' (write, octal: '2') permissions at least; got '%s' in %s", fileMode.String()[1:4], addr)
|
||||
}
|
||||
|
||||
return addrSplit[0], fileMode, nil
|
||||
}
|
||||
|
||||
// default to 0200 (symbolic: `u=w,g=,o=`)
|
||||
// if no permission bits are specified
|
||||
return addr, 0o200, nil
|
||||
}
|
||||
@@ -30,18 +30,34 @@ func reuseUnixSocket(network, addr string) (any, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (net.Listener, error) {
|
||||
sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
|
||||
ln, err := config.Listen(ctx, network, address)
|
||||
func listenReusable(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (any, error) {
|
||||
switch network {
|
||||
case "udp", "udp4", "udp6", "unixgram":
|
||||
sharedPc, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
|
||||
pc, err := config.ListenPacket(ctx, network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sharedPacketConn{PacketConn: pc, key: lnKey}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sharedListener{Listener: ln, key: lnKey}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return &fakeClosePacketConn{sharedPacketConn: sharedPc.(*sharedPacketConn)}, nil
|
||||
|
||||
default:
|
||||
sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
|
||||
ln, err := config.Listen(ctx, network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sharedListener{Listener: ln, key: lnKey}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: config.KeepAlive}, nil
|
||||
}
|
||||
return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: config.KeepAlive}, nil
|
||||
}
|
||||
|
||||
// fakeCloseListener is a private wrapper over a listener that
|
||||
@@ -98,7 +114,7 @@ func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
|
||||
// so that it's clear in the code that side-effects are shared with other
|
||||
// users of this listener, not just our own reference to it; we also don't
|
||||
// do anything with the error because all we could do is log it, but we
|
||||
// expliclty assign it to nothing so we don't forget it's there if needed
|
||||
// explicitly assign it to nothing so we don't forget it's there if needed
|
||||
_ = fcl.sharedListener.clearDeadline()
|
||||
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
@@ -172,3 +188,75 @@ func (sl *sharedListener) setDeadline() error {
|
||||
func (sl *sharedListener) Destruct() error {
|
||||
return sl.Listener.Close()
|
||||
}
|
||||
|
||||
// fakeClosePacketConn is like fakeCloseListener, but for PacketConns,
|
||||
// or more specifically, *net.UDPConn
|
||||
type fakeClosePacketConn struct {
|
||||
closed int32 // accessed atomically; belongs to this struct only
|
||||
*sharedPacketConn // embedded, so we also become a net.PacketConn; its key is used in Close
|
||||
}
|
||||
|
||||
func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
// if the listener is already "closed", return error
|
||||
if atomic.LoadInt32(&fcpc.closed) == 1 {
|
||||
return 0, nil, &net.OpError{
|
||||
Op: "readfrom",
|
||||
Net: fcpc.LocalAddr().Network(),
|
||||
Addr: fcpc.LocalAddr(),
|
||||
Err: errFakeClosed,
|
||||
}
|
||||
}
|
||||
|
||||
// call underlying readfrom
|
||||
n, addr, err = fcpc.sharedPacketConn.ReadFrom(p)
|
||||
if err != nil {
|
||||
// this server was stopped, so clear the deadline and let
|
||||
// any new server continue reading; but we will exit
|
||||
if atomic.LoadInt32(&fcpc.closed) == 1 {
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
if err = fcpc.SetReadDeadline(time.Time{}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Close won't close the underlying socket unless there is no more reference, then listenerPool will close it.
|
||||
func (fcpc *fakeClosePacketConn) Close() error {
|
||||
if atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) {
|
||||
_ = fcpc.SetReadDeadline(time.Now()) // unblock ReadFrom() calls to kick old servers out of their loops
|
||||
_, _ = listenerPool.Delete(fcpc.sharedPacketConn.key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fcpc *fakeClosePacketConn) Unwrap() net.PacketConn {
|
||||
return fcpc.sharedPacketConn.PacketConn
|
||||
}
|
||||
|
||||
// sharedPacketConn is like sharedListener, but for net.PacketConns.
|
||||
type sharedPacketConn struct {
|
||||
net.PacketConn
|
||||
key string
|
||||
}
|
||||
|
||||
// Destruct closes the underlying socket.
|
||||
func (spc *sharedPacketConn) Destruct() error {
|
||||
return spc.PacketConn.Close()
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying socket
|
||||
func (spc *sharedPacketConn) Unwrap() net.PacketConn {
|
||||
return spc.PacketConn
|
||||
}
|
||||
|
||||
// Interface guards (see https://github.com/caddyserver/caddy/issues/3998)
|
||||
var (
|
||||
_ (interface {
|
||||
Unwrap() net.PacketConn
|
||||
}) = (*fakeClosePacketConn)(nil)
|
||||
)
|
||||
|
||||
+102
-4
@@ -22,8 +22,10 @@ package caddy
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
@@ -87,7 +89,7 @@ func reuseUnixSocket(network, addr string) (any, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (net.Listener, error) {
|
||||
func listenReusable(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (any, error) {
|
||||
// wrap any Control function set by the user so we can also add our reusePort control without clobbering theirs
|
||||
oldControl := config.Control
|
||||
config.Control = func(network, address string, c syscall.RawConn) error {
|
||||
@@ -103,14 +105,44 @@ func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string,
|
||||
// we still put it in the listenerPool so we can count how many
|
||||
// configs are using this socket; necessary to ensure we can know
|
||||
// whether to enforce shutdown delays, for example (see #5393).
|
||||
ln, err := config.Listen(ctx, network, address)
|
||||
var ln io.Closer
|
||||
var err error
|
||||
switch network {
|
||||
case "udp", "udp4", "udp6", "unixgram":
|
||||
ln, err = config.ListenPacket(ctx, network, address)
|
||||
default:
|
||||
ln, err = config.Listen(ctx, network, address)
|
||||
}
|
||||
if err == nil {
|
||||
listenerPool.LoadOrStore(lnKey, nil)
|
||||
}
|
||||
|
||||
// if new listener is a unix socket, make sure we can reuse it later
|
||||
// (we do our own "unlink on close" -- not required, but more tidy)
|
||||
one := int32(1)
|
||||
if unix, ok := ln.(*net.UnixListener); ok {
|
||||
unix.SetUnlinkOnClose(false)
|
||||
ln = &unixListener{unix, lnKey, &one}
|
||||
unixSockets[lnKey] = ln.(*unixListener)
|
||||
}
|
||||
|
||||
// TODO: Not 100% sure this is necessary, but we do this for net.UnixListener in listen_unix.go, so...
|
||||
if unix, ok := ln.(*net.UnixConn); ok {
|
||||
ln = &unixConn{unix, address, lnKey, &one}
|
||||
unixSockets[lnKey] = ln.(*unixConn)
|
||||
}
|
||||
|
||||
// lightly wrap the listener so that when it is closed,
|
||||
// we can decrement the usage pool counter
|
||||
return deleteListener{ln, lnKey}, err
|
||||
switch specificLn := ln.(type) {
|
||||
case net.Listener:
|
||||
return deleteListener{specificLn, lnKey}, err
|
||||
case net.PacketConn:
|
||||
return deletePacketConn{specificLn, lnKey}, err
|
||||
}
|
||||
|
||||
// other types, I guess we just return them directly
|
||||
return ln, err
|
||||
}
|
||||
|
||||
// reusePort sets SO_REUSEPORT. Ineffective for unix sockets.
|
||||
@@ -119,7 +151,7 @@ func reusePort(network, address string, conn syscall.RawConn) error {
|
||||
return nil
|
||||
}
|
||||
return conn.Control(func(descriptor uintptr) {
|
||||
if err := unix.SetsockoptInt(int(descriptor), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
|
||||
if err := unix.SetsockoptInt(int(descriptor), unix.SOL_SOCKET, unixSOREUSEPORT, 1); err != nil {
|
||||
Log().Error("setting SO_REUSEPORT",
|
||||
zap.String("network", network),
|
||||
zap.String("address", address),
|
||||
@@ -129,6 +161,56 @@ func reusePort(network, address string, conn syscall.RawConn) error {
|
||||
})
|
||||
}
|
||||
|
||||
type unixListener struct {
|
||||
*net.UnixListener
|
||||
mapKey string
|
||||
count *int32 // accessed atomically
|
||||
}
|
||||
|
||||
func (uln *unixListener) Close() error {
|
||||
newCount := atomic.AddInt32(uln.count, -1)
|
||||
if newCount == 0 {
|
||||
defer func() {
|
||||
addr := uln.Addr().String()
|
||||
unixSocketsMu.Lock()
|
||||
delete(unixSockets, uln.mapKey)
|
||||
unixSocketsMu.Unlock()
|
||||
_ = syscall.Unlink(addr)
|
||||
}()
|
||||
}
|
||||
return uln.UnixListener.Close()
|
||||
}
|
||||
|
||||
type unixConn struct {
|
||||
*net.UnixConn
|
||||
filename string
|
||||
mapKey string
|
||||
count *int32 // accessed atomically
|
||||
}
|
||||
|
||||
func (uc *unixConn) Close() error {
|
||||
newCount := atomic.AddInt32(uc.count, -1)
|
||||
if newCount == 0 {
|
||||
defer func() {
|
||||
unixSocketsMu.Lock()
|
||||
delete(unixSockets, uc.mapKey)
|
||||
unixSocketsMu.Unlock()
|
||||
_ = syscall.Unlink(uc.filename)
|
||||
}()
|
||||
}
|
||||
return uc.UnixConn.Close()
|
||||
}
|
||||
|
||||
func (uc *unixConn) Unwrap() net.PacketConn {
|
||||
return uc.UnixConn
|
||||
}
|
||||
|
||||
// unixSockets keeps track of the currently-active unix sockets
|
||||
// so we can transfer their FDs gracefully during reloads.
|
||||
var unixSockets = make(map[string]interface {
|
||||
File() (*os.File, error)
|
||||
})
|
||||
|
||||
// deleteListener is a type that simply deletes itself
|
||||
// from the listenerPool when it closes. It is used
|
||||
// solely for the purpose of reference counting (i.e.
|
||||
@@ -142,3 +224,19 @@ func (dl deleteListener) Close() error {
|
||||
_, _ = listenerPool.Delete(dl.lnKey)
|
||||
return dl.Listener.Close()
|
||||
}
|
||||
|
||||
// deletePacketConn is like deleteListener, but
|
||||
// for net.PacketConns.
|
||||
type deletePacketConn struct {
|
||||
net.PacketConn
|
||||
lnKey string
|
||||
}
|
||||
|
||||
func (dl deletePacketConn) Close() error {
|
||||
_, _ = listenerPool.Delete(dl.lnKey)
|
||||
return dl.PacketConn.Close()
|
||||
}
|
||||
|
||||
func (dl deletePacketConn) Unwrap() net.PacketConn {
|
||||
return dl.PacketConn
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build unix && !freebsd
|
||||
|
||||
package caddy
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
const unixSOREUSEPORT = unix.SO_REUSEPORT
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build freebsd
|
||||
|
||||
package caddy
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
const unixSOREUSEPORT = unix.SO_REUSEPORT_LB
|
||||
+220
-171
@@ -20,6 +20,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
@@ -27,14 +28,21 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/internal"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterNamespace("caddy.listeners", []interface{}{
|
||||
(*ListenerWrapper)(nil),
|
||||
})
|
||||
}
|
||||
|
||||
// NetworkAddress represents one or more network addresses.
|
||||
// It contains the individual components for a parsed network
|
||||
// address of the form accepted by ParseNetworkAddress().
|
||||
@@ -146,38 +154,44 @@ func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net
|
||||
}
|
||||
|
||||
func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
|
||||
var ln any
|
||||
var err error
|
||||
var (
|
||||
ln any
|
||||
err error
|
||||
address string
|
||||
unixFileMode fs.FileMode
|
||||
isAbtractUnixSocket bool
|
||||
)
|
||||
|
||||
address := na.JoinHostPort(portOffset)
|
||||
// split unix socket addr early so lnKey
|
||||
// is independent of permissions bits
|
||||
if na.IsUnixNetwork() {
|
||||
var err error
|
||||
address, unixFileMode, err = internal.SplitUnixSocketPermissionsBits(na.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
isAbtractUnixSocket = strings.HasPrefix(address, "@")
|
||||
} else {
|
||||
address = na.JoinHostPort(portOffset)
|
||||
}
|
||||
|
||||
// if this is a unix socket, see if we already have it open
|
||||
// if this is a unix socket, see if we already have it open,
|
||||
// force socket permissions on it and return early
|
||||
if socket, err := reuseUnixSocket(na.Network, address); socket != nil || err != nil {
|
||||
if !isAbtractUnixSocket {
|
||||
if err := os.Chmod(address, unixFileMode); err != nil {
|
||||
return nil, fmt.Errorf("unable to set permissions (%s) on %s: %v", unixFileMode, address, err)
|
||||
}
|
||||
}
|
||||
return socket, err
|
||||
}
|
||||
|
||||
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") {
|
||||
ln, err = config.ListenPacket(ctx, na.Network, address)
|
||||
} else {
|
||||
ln, err = listenReusable(ctx, lnKey, na.Network, address, config)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -186,17 +200,12 @@ func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net
|
||||
return nil, fmt.Errorf("unsupported network type: %s", na.Network)
|
||||
}
|
||||
|
||||
// if new listener is a unix socket, make sure we can reuse it later
|
||||
// (we do our own "unlink on close" -- not required, but more tidy)
|
||||
one := int32(1)
|
||||
switch unix := ln.(type) {
|
||||
case *net.UnixListener:
|
||||
unix.SetUnlinkOnClose(false)
|
||||
ln = &unixListener{unix, lnKey, &one}
|
||||
unixSockets[lnKey] = ln.(*unixListener)
|
||||
case *net.UnixConn:
|
||||
ln = &unixConn{unix, address, lnKey, &one}
|
||||
unixSockets[lnKey] = ln.(*unixConn)
|
||||
if IsUnixNetwork(na.Network) {
|
||||
if !isAbtractUnixSocket {
|
||||
if err := os.Chmod(address, unixFileMode); err != nil {
|
||||
return nil, fmt.Errorf("unable to set permissions (%s) on %s: %v", unixFileMode, address, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ln, nil
|
||||
@@ -303,22 +312,32 @@ func IsUnixNetwork(netw string) bool {
|
||||
// Network addresses are distinct from URLs and do not
|
||||
// use URL syntax.
|
||||
func ParseNetworkAddress(addr string) (NetworkAddress, error) {
|
||||
return ParseNetworkAddressWithDefaults(addr, "tcp", 0)
|
||||
}
|
||||
|
||||
// ParseNetworkAddressWithDefaults is like ParseNetworkAddress but allows
|
||||
// the default network and port to be specified.
|
||||
func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort uint) (NetworkAddress, error) {
|
||||
var host, port string
|
||||
network, host, port, err := SplitNetworkAddress(addr)
|
||||
if err != nil {
|
||||
return NetworkAddress{}, err
|
||||
}
|
||||
if network == "" {
|
||||
network = "tcp"
|
||||
network = defaultNetwork
|
||||
}
|
||||
if IsUnixNetwork(network) {
|
||||
_, _, err := internal.SplitUnixSocketPermissionsBits(host)
|
||||
return NetworkAddress{
|
||||
Network: network,
|
||||
Host: host,
|
||||
}, nil
|
||||
}, err
|
||||
}
|
||||
var start, end uint64
|
||||
if port != "" {
|
||||
if port == "" {
|
||||
start = uint64(defaultPort)
|
||||
end = uint64(defaultPort)
|
||||
} else {
|
||||
before, after, found := strings.Cut(port, "-")
|
||||
if !found {
|
||||
after = before
|
||||
@@ -434,46 +453,93 @@ func ListenPacket(network, addr string) (net.PacketConn, error) {
|
||||
// unixgram will be used; otherwise, udp will be used).
|
||||
//
|
||||
// NOTE: This API is EXPERIMENTAL and may be changed or removed.
|
||||
//
|
||||
// TODO: See if we can find a more elegant solution closer to the new NetworkAddress.Listen API.
|
||||
func ListenQUIC(ln net.PacketConn, tlsConf *tls.Config, activeRequests *int64) (quic.EarlyListener, error) {
|
||||
lnKey := listenerKey("quic+"+ln.LocalAddr().Network(), ln.LocalAddr().String())
|
||||
func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config net.ListenConfig, tlsConf *tls.Config, activeRequests *int64) (http3.QUICEarlyListener, error) {
|
||||
lnKey := listenerKey("quic"+na.Network, na.JoinHostPort(portOffset))
|
||||
|
||||
sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
|
||||
earlyLn, err := quic.ListenEarly(ln, http3.ConfigureTLSConfig(tlsConf), &quic.Config{
|
||||
Allow0RTT: func(net.Addr) bool { return true },
|
||||
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 {
|
||||
var highLoad bool
|
||||
if activeRequests != nil {
|
||||
highLoad = atomic.LoadInt64(activeRequests) > 1000 // TODO: make tunable?
|
||||
}
|
||||
return highLoad
|
||||
// TODO: make tunable?
|
||||
return sqs.getActiveRequests() > 1000
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sharedQuicListener{EarlyListener: earlyLn, key: lnKey}, nil
|
||||
// using the original net.PacketConn to close them properly
|
||||
return &sharedQuicListener{EarlyListener: earlyLn, packetConn: ln, sqs: sqs, key: lnKey}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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
|
||||
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.
|
||||
func ListenQUIC(ln net.PacketConn, tlsConf *tls.Config, activeRequests *int64) (http3.QUICEarlyListener, error) {
|
||||
lnKey := listenerKey("quic+"+ln.LocalAddr().Network(), ln.LocalAddr().String())
|
||||
|
||||
sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
|
||||
sqs := newSharedQUICState(tlsConf, activeRequests)
|
||||
// http3.ConfigureTLSConfig only uses this field and tls App sets this field as well
|
||||
//nolint:gosec
|
||||
quicTlsConfig := &tls.Config{GetConfigForClient: sqs.getConfigForClient}
|
||||
earlyLn, err := quic.ListenEarly(ln, http3.ConfigureTLSConfig(quicTlsConfig), &quic.Config{
|
||||
Allow0RTT: true,
|
||||
RequireAddressValidation: func(clientAddr net.Addr) bool {
|
||||
// TODO: make tunable?
|
||||
return sqs.getActiveRequests() > 1000
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sharedQuicListener{EarlyListener: earlyLn, sqs: sqs, key: lnKey}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
sql := sharedEarlyListener.(*sharedQuicListener)
|
||||
// add current tls.Config and request counter to sqs, so GetConfigForClient will always return the latest tls.Config in case of context cancellation,
|
||||
// and the request counter will reflect current http server
|
||||
ctx, cancel := sql.sqs.addState(tlsConf, activeRequests)
|
||||
|
||||
return &fakeCloseQuicListener{
|
||||
sharedQuicListener: sharedEarlyListener.(*sharedQuicListener),
|
||||
uc: unix,
|
||||
sharedQuicListener: sql,
|
||||
context: ctx,
|
||||
contextCancel: cancel,
|
||||
}, nil
|
||||
@@ -485,26 +551,101 @@ func ListenerUsage(network, addr string) int {
|
||||
return count
|
||||
}
|
||||
|
||||
// contextAndCancelFunc groups context and its cancelFunc
|
||||
type contextAndCancelFunc struct {
|
||||
context.Context
|
||||
context.CancelFunc
|
||||
}
|
||||
|
||||
// sharedQUICState manages GetConfigForClient and current number of active requests
|
||||
// see issue: https://github.com/caddyserver/caddy/pull/4849
|
||||
type sharedQUICState struct {
|
||||
rmu sync.RWMutex
|
||||
tlsConfs map[*tls.Config]contextAndCancelFunc
|
||||
requestCounters map[*tls.Config]*int64
|
||||
activeTlsConf *tls.Config
|
||||
activeRequestsCounter *int64
|
||||
}
|
||||
|
||||
// newSharedQUICState creates a new sharedQUICState
|
||||
func newSharedQUICState(tlsConfig *tls.Config, activeRequests *int64) *sharedQUICState {
|
||||
sqtc := &sharedQUICState{
|
||||
tlsConfs: make(map[*tls.Config]contextAndCancelFunc),
|
||||
requestCounters: make(map[*tls.Config]*int64),
|
||||
activeTlsConf: tlsConfig,
|
||||
activeRequestsCounter: activeRequests,
|
||||
}
|
||||
sqtc.addState(tlsConfig, activeRequests)
|
||||
return sqtc
|
||||
}
|
||||
|
||||
// getConfigForClient is used as tls.Config's GetConfigForClient field
|
||||
func (sqs *sharedQUICState) getConfigForClient(ch *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
sqs.rmu.RLock()
|
||||
defer sqs.rmu.RUnlock()
|
||||
return sqs.activeTlsConf.GetConfigForClient(ch)
|
||||
}
|
||||
|
||||
// getActiveRequests returns the number of active requests
|
||||
func (sqs *sharedQUICState) getActiveRequests() int64 {
|
||||
// Prevent a race when a context is cancelled and active request counter is being changed
|
||||
sqs.rmu.RLock()
|
||||
defer sqs.rmu.RUnlock()
|
||||
return atomic.LoadInt64(sqs.activeRequestsCounter)
|
||||
}
|
||||
|
||||
// addState adds tls.Config and activeRequests to the map if not present and returns the corresponding context and its cancelFunc
|
||||
// so that when cancelled, the active tls.Config and request counter will change
|
||||
func (sqs *sharedQUICState) addState(tlsConfig *tls.Config, activeRequests *int64) (context.Context, context.CancelFunc) {
|
||||
sqs.rmu.Lock()
|
||||
defer sqs.rmu.Unlock()
|
||||
|
||||
if cacc, ok := sqs.tlsConfs[tlsConfig]; ok {
|
||||
return cacc.Context, cacc.CancelFunc
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
wrappedCancel := func() {
|
||||
cancel()
|
||||
|
||||
sqs.rmu.Lock()
|
||||
defer sqs.rmu.Unlock()
|
||||
|
||||
delete(sqs.tlsConfs, tlsConfig)
|
||||
delete(sqs.requestCounters, tlsConfig)
|
||||
if sqs.activeTlsConf == tlsConfig {
|
||||
// select another tls.Config and request counter, if there is none,
|
||||
// related sharedQuicListener will be destroyed anyway
|
||||
for tc, counter := range sqs.requestCounters {
|
||||
sqs.activeTlsConf = tc
|
||||
sqs.activeRequestsCounter = counter
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
sqs.tlsConfs[tlsConfig] = contextAndCancelFunc{ctx, wrappedCancel}
|
||||
sqs.requestCounters[tlsConfig] = activeRequests
|
||||
// there should be at most 2 tls.Configs
|
||||
if len(sqs.tlsConfs) > 2 {
|
||||
Log().Warn("quic listener tls configs are more than 2", zap.Int("number of configs", len(sqs.tlsConfs)))
|
||||
}
|
||||
return ctx, wrappedCancel
|
||||
}
|
||||
|
||||
// sharedQuicListener is like sharedListener, but for quic.EarlyListeners.
|
||||
type sharedQuicListener struct {
|
||||
quic.EarlyListener
|
||||
key string
|
||||
*quic.EarlyListener
|
||||
packetConn net.PacketConn // we have to hold these because quic-go won't close listeners it didn't create
|
||||
sqs *sharedQUICState
|
||||
key string
|
||||
}
|
||||
|
||||
// Destruct closes the underlying QUIC listener.
|
||||
// Destruct closes the underlying QUIC listener and its associated net.PacketConn.
|
||||
func (sql *sharedQuicListener) Destruct() error {
|
||||
return sql.EarlyListener.Close()
|
||||
}
|
||||
|
||||
// sharedPacketConn is like sharedListener, but for net.PacketConns.
|
||||
type sharedPacketConn struct {
|
||||
net.PacketConn
|
||||
key string
|
||||
}
|
||||
|
||||
// Destruct closes the underlying socket.
|
||||
func (spc *sharedPacketConn) Destruct() error {
|
||||
return spc.PacketConn.Close()
|
||||
// close EarlyListener first to stop any operations being done to the net.PacketConn
|
||||
_ = sql.EarlyListener.Close()
|
||||
// then close the net.PacketConn
|
||||
return sql.packetConn.Close()
|
||||
}
|
||||
|
||||
// fakeClosedErr returns an error value that is not temporary
|
||||
@@ -526,41 +667,9 @@ func fakeClosedErr(l interface{ Addr() net.Addr }) error {
|
||||
// socket is actually left open.
|
||||
var errFakeClosed = fmt.Errorf("listener 'closed' 😉")
|
||||
|
||||
// fakeClosePacketConn is like fakeCloseListener, but for PacketConns.
|
||||
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 {
|
||||
closed int32 // accessed atomically; belongs to this struct only
|
||||
*sharedQuicListener // embedded, so we also become a quic.EarlyListener
|
||||
uc *unixConn // underlying unix socket, if UDS
|
||||
closed int32 // accessed atomically; belongs to this struct only
|
||||
*sharedQuicListener // embedded, so we also become a quic.EarlyListener
|
||||
context context.Context
|
||||
contextCancel context.CancelFunc
|
||||
}
|
||||
@@ -587,11 +696,6 @@ func (fcql *fakeCloseQuicListener) Close() error {
|
||||
if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) {
|
||||
fcql.contextCancel()
|
||||
_, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
|
||||
if fcql.uc != nil {
|
||||
// unix sockets need to be closed ourselves because we dup() the file
|
||||
// descriptor when we reuse them, so this avoids a resource leak
|
||||
fcql.uc.Close()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -617,54 +721,7 @@ func RegisterNetwork(network string, getListener ListenerFunc) {
|
||||
networkTypes[network] = getListener
|
||||
}
|
||||
|
||||
type unixListener struct {
|
||||
*net.UnixListener
|
||||
mapKey string
|
||||
count *int32 // accessed atomically
|
||||
}
|
||||
|
||||
func (uln *unixListener) Close() error {
|
||||
newCount := atomic.AddInt32(uln.count, -1)
|
||||
if newCount == 0 {
|
||||
defer func() {
|
||||
addr := uln.Addr().String()
|
||||
unixSocketsMu.Lock()
|
||||
delete(unixSockets, uln.mapKey)
|
||||
unixSocketsMu.Unlock()
|
||||
_ = syscall.Unlink(addr)
|
||||
}()
|
||||
}
|
||||
return uln.UnixListener.Close()
|
||||
}
|
||||
|
||||
type unixConn struct {
|
||||
*net.UnixConn
|
||||
filename string
|
||||
mapKey string
|
||||
count *int32 // accessed atomically
|
||||
}
|
||||
|
||||
func (uc *unixConn) Close() error {
|
||||
newCount := atomic.AddInt32(uc.count, -1)
|
||||
if newCount == 0 {
|
||||
defer func() {
|
||||
unixSocketsMu.Lock()
|
||||
delete(unixSockets, uc.mapKey)
|
||||
unixSocketsMu.Unlock()
|
||||
_ = syscall.Unlink(uc.filename)
|
||||
}()
|
||||
}
|
||||
return uc.UnixConn.Close()
|
||||
}
|
||||
|
||||
// unixSockets keeps track of the currently-active unix sockets
|
||||
// so we can transfer their FDs gracefully during reloads.
|
||||
var (
|
||||
unixSockets = make(map[string]interface {
|
||||
File() (*os.File, error)
|
||||
})
|
||||
unixSocketsMu sync.Mutex
|
||||
)
|
||||
var unixSocketsMu sync.Mutex
|
||||
|
||||
// getListenerFromPlugin returns a listener on the given network and address
|
||||
// if a plugin has registered the network name. It may return (nil, nil) if
|
||||
@@ -708,11 +765,3 @@ type ListenerWrapper interface {
|
||||
var listenerPool = NewUsagePool()
|
||||
|
||||
const maxPortSpan = 65535
|
||||
|
||||
// Interface guards (see https://github.com/caddyserver/caddy/issues/3998)
|
||||
var (
|
||||
_ (interface{ SetReadBuffer(int) error }) = (*fakeClosePacketConn)(nil)
|
||||
_ (interface {
|
||||
SyscallConn() (syscall.RawConn, error)
|
||||
}) = (*fakeClosePacketConn)(nil)
|
||||
)
|
||||
|
||||
+261
-22
@@ -17,6 +17,8 @@ package caddy
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/internal"
|
||||
)
|
||||
|
||||
func TestSplitNetworkAddress(t *testing.T) {
|
||||
@@ -175,47 +177,57 @@ func TestJoinNetworkAddress(t *testing.T) {
|
||||
|
||||
func TestParseNetworkAddress(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
input string
|
||||
expectAddr NetworkAddress
|
||||
expectErr bool
|
||||
input string
|
||||
defaultNetwork string
|
||||
defaultPort uint
|
||||
expectAddr NetworkAddress
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
input: "",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
input: ":",
|
||||
input: ":",
|
||||
defaultNetwork: "udp",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "tcp",
|
||||
Network: "udp",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "[::]",
|
||||
input: "[::]",
|
||||
defaultNetwork: "udp",
|
||||
defaultPort: 53,
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "tcp",
|
||||
Host: "::",
|
||||
Network: "udp",
|
||||
Host: "::",
|
||||
StartPort: 53,
|
||||
EndPort: 53,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: ":1234",
|
||||
input: ":1234",
|
||||
defaultNetwork: "udp",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "tcp",
|
||||
Network: "udp",
|
||||
Host: "",
|
||||
StartPort: 1234,
|
||||
EndPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "tcp/:1234",
|
||||
input: "udp/:1234",
|
||||
defaultNetwork: "udp",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "tcp",
|
||||
Network: "udp",
|
||||
Host: "",
|
||||
StartPort: 1234,
|
||||
EndPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "tcp6/:1234",
|
||||
input: "tcp6/:1234",
|
||||
defaultNetwork: "tcp",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "tcp6",
|
||||
Host: "",
|
||||
@@ -224,7 +236,8 @@ func TestParseNetworkAddress(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "tcp4/localhost:1234",
|
||||
input: "tcp4/localhost:1234",
|
||||
defaultNetwork: "tcp",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "tcp4",
|
||||
Host: "localhost",
|
||||
@@ -233,14 +246,16 @@ func TestParseNetworkAddress(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "unix//foo/bar",
|
||||
input: "unix//foo/bar",
|
||||
defaultNetwork: "tcp",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "unix",
|
||||
Host: "/foo/bar",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "localhost:1234-1234",
|
||||
input: "localhost:1234-1234",
|
||||
defaultNetwork: "tcp",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "tcp",
|
||||
Host: "localhost",
|
||||
@@ -249,11 +264,13 @@ func TestParseNetworkAddress(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "localhost:2-1",
|
||||
expectErr: true,
|
||||
input: "localhost:2-1",
|
||||
defaultNetwork: "tcp",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
input: "localhost:0",
|
||||
input: "localhost:0",
|
||||
defaultNetwork: "tcp",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "tcp",
|
||||
Host: "localhost",
|
||||
@@ -262,11 +279,138 @@ func TestParseNetworkAddress(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "localhost:1-999999999999",
|
||||
expectErr: true,
|
||||
input: "localhost:1-999999999999",
|
||||
defaultNetwork: "tcp",
|
||||
expectErr: true,
|
||||
},
|
||||
} {
|
||||
actualAddr, err := ParseNetworkAddress(tc.input)
|
||||
actualAddr, err := ParseNetworkAddressWithDefaults(tc.input, tc.defaultNetwork, tc.defaultPort)
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("Test %d: Expected error but got: %v", i, err)
|
||||
}
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error but got: %v", i, err)
|
||||
}
|
||||
|
||||
if actualAddr.Network != tc.expectAddr.Network {
|
||||
t.Errorf("Test %d: Expected network '%v' but got '%v'", i, tc.expectAddr, actualAddr)
|
||||
}
|
||||
if !reflect.DeepEqual(tc.expectAddr, actualAddr) {
|
||||
t.Errorf("Test %d: Expected addresses %v but got %v", i, tc.expectAddr, actualAddr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNetworkAddressWithDefaults(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
input string
|
||||
defaultNetwork string
|
||||
defaultPort uint
|
||||
expectAddr NetworkAddress
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
input: "",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
input: ":",
|
||||
defaultNetwork: "udp",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "udp",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "[::]",
|
||||
defaultNetwork: "udp",
|
||||
defaultPort: 53,
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "udp",
|
||||
Host: "::",
|
||||
StartPort: 53,
|
||||
EndPort: 53,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: ":1234",
|
||||
defaultNetwork: "udp",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "udp",
|
||||
Host: "",
|
||||
StartPort: 1234,
|
||||
EndPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "udp/:1234",
|
||||
defaultNetwork: "udp",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "udp",
|
||||
Host: "",
|
||||
StartPort: 1234,
|
||||
EndPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "tcp6/:1234",
|
||||
defaultNetwork: "tcp",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "tcp6",
|
||||
Host: "",
|
||||
StartPort: 1234,
|
||||
EndPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "tcp4/localhost:1234",
|
||||
defaultNetwork: "tcp",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "tcp4",
|
||||
Host: "localhost",
|
||||
StartPort: 1234,
|
||||
EndPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "unix//foo/bar",
|
||||
defaultNetwork: "tcp",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "unix",
|
||||
Host: "/foo/bar",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "localhost:1234-1234",
|
||||
defaultNetwork: "tcp",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "tcp",
|
||||
Host: "localhost",
|
||||
StartPort: 1234,
|
||||
EndPort: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "localhost:2-1",
|
||||
defaultNetwork: "tcp",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
input: "localhost:0",
|
||||
defaultNetwork: "tcp",
|
||||
expectAddr: NetworkAddress{
|
||||
Network: "tcp",
|
||||
Host: "localhost",
|
||||
StartPort: 0,
|
||||
EndPort: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "localhost:1-999999999999",
|
||||
defaultNetwork: "tcp",
|
||||
expectErr: true,
|
||||
},
|
||||
} {
|
||||
actualAddr, err := ParseNetworkAddressWithDefaults(tc.input, tc.defaultNetwork, tc.defaultPort)
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("Test %d: Expected error but got: %v", i, err)
|
||||
}
|
||||
@@ -413,3 +557,98 @@ func TestExpand(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitUnixSocketPermissionsBits(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
input string
|
||||
expectNetwork string
|
||||
expectPath string
|
||||
expectFileMode string
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
input: "./foo.socket",
|
||||
expectPath: "./foo.socket",
|
||||
expectFileMode: "--w-------",
|
||||
},
|
||||
{
|
||||
input: `.\relative\path.socket`,
|
||||
expectPath: `.\relative\path.socket`,
|
||||
expectFileMode: "--w-------",
|
||||
},
|
||||
{
|
||||
// literal colon in resulting address
|
||||
// and defaulting to 0200 bits
|
||||
input: "./foo.socket:0666",
|
||||
expectPath: "./foo.socket:0666",
|
||||
expectFileMode: "--w-------",
|
||||
},
|
||||
{
|
||||
input: "./foo.socket|0220",
|
||||
expectPath: "./foo.socket",
|
||||
expectFileMode: "--w--w----",
|
||||
},
|
||||
{
|
||||
input: "/var/run/foo|222",
|
||||
expectPath: "/var/run/foo",
|
||||
expectFileMode: "--w--w--w-",
|
||||
},
|
||||
{
|
||||
input: "./foo.socket|0660",
|
||||
expectPath: "./foo.socket",
|
||||
expectFileMode: "-rw-rw----",
|
||||
},
|
||||
{
|
||||
input: "./foo.socket|0666",
|
||||
expectPath: "./foo.socket",
|
||||
expectFileMode: "-rw-rw-rw-",
|
||||
},
|
||||
{
|
||||
input: "/var/run/foo|666",
|
||||
expectPath: "/var/run/foo",
|
||||
expectFileMode: "-rw-rw-rw-",
|
||||
},
|
||||
{
|
||||
input: `c:\absolute\path.socket|220`,
|
||||
expectPath: `c:\absolute\path.socket`,
|
||||
expectFileMode: "--w--w----",
|
||||
},
|
||||
{
|
||||
// symbolic permission representation is not supported for now
|
||||
input: "./foo.socket|u=rw,g=rw,o=rw",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
// octal (base-8) permission representation has to be between
|
||||
// `0` for no read, no write, no exec (`---`) and
|
||||
// `7` for read (4), write (2), exec (1) (`rwx` => `4+2+1 = 7`)
|
||||
input: "./foo.socket|888",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
// too many colons in address
|
||||
input: "./foo.socket|123456|0660",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
// owner is missing write perms
|
||||
input: "./foo.socket|0522",
|
||||
expectErr: true,
|
||||
},
|
||||
} {
|
||||
actualPath, actualFileMode, err := internal.SplitUnixSocketPermissionsBits(tc.input)
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("Test %d: Expected error but got: %v", i, err)
|
||||
}
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("Test %d: Expected no error but got: %v", i, err)
|
||||
}
|
||||
if actualPath != tc.expectPath {
|
||||
t.Errorf("Test %d: Expected path '%s' but got '%s'", i, tc.expectPath, actualPath)
|
||||
}
|
||||
// fileMode.Perm().String() parses 0 to "----------"
|
||||
if !tc.expectErr && actualFileMode.Perm().String() != tc.expectFileMode {
|
||||
t.Errorf("Test %d: Expected perms '%s' but got '%s'", i, tc.expectFileMode, actualFileMode.Perm().String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,12 @@ func init() {
|
||||
RegisterModule(StdoutWriter{})
|
||||
RegisterModule(StderrWriter{})
|
||||
RegisterModule(DiscardWriter{})
|
||||
RegisterNamespace("caddy.logging.encoders", []interface{}{
|
||||
(*zapcore.Encoder)(nil),
|
||||
})
|
||||
RegisterNamespace("caddy.logging.writers", []interface{}{
|
||||
(*WriterOpener)(nil),
|
||||
})
|
||||
}
|
||||
|
||||
// Logging facilitates logging within Caddy. The default log is
|
||||
@@ -265,6 +271,8 @@ type BaseLog struct {
|
||||
WriterRaw json.RawMessage `json:"writer,omitempty" caddy:"namespace=caddy.logging.writers inline_key=output"`
|
||||
|
||||
// The encoder is how the log entries are formatted or encoded.
|
||||
// An `encoder` should implement the following interfaces:
|
||||
// - [zapcore.Encoder](https://pkg.go.dev/go.uber.org/zap/zapcore#Encoder)
|
||||
EncoderRaw json.RawMessage `json:"encoder,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"`
|
||||
|
||||
// Level is the minimum level to emit, and is inclusive.
|
||||
|
||||
+8
-1
@@ -3,10 +3,11 @@ package caddy
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/internal/metrics"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
|
||||
"github.com/caddyserver/caddy/v2/internal/metrics"
|
||||
)
|
||||
|
||||
// define and register the metrics used in this package.
|
||||
@@ -66,3 +67,9 @@ func (d *delegator) WriteHeader(code int) {
|
||||
d.status = code
|
||||
d.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying ResponseWriter, necessary for
|
||||
// http.ResponseController to work correctly.
|
||||
func (d *delegator) Unwrap() http.ResponseWriter {
|
||||
return d.ResponseWriter
|
||||
}
|
||||
|
||||
@@ -22,9 +22,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
+30
-20
@@ -20,16 +20,19 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"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/modules/caddyevents"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -70,7 +73,7 @@ func init() {
|
||||
// `{http.request.remote.host}` | The host (IP) part of the remote client's address
|
||||
// `{http.request.remote.port}` | The port part of the remote client's address
|
||||
// `{http.request.remote}` | The address of the remote client
|
||||
// `{http.request.scheme}` | The request scheme
|
||||
// `{http.request.scheme}` | The request scheme, typically `http` or `https`
|
||||
// `{http.request.tls.version}` | The TLS version name
|
||||
// `{http.request.tls.cipher_suite}` | The TLS cipher suite
|
||||
// `{http.request.tls.resumed}` | The TLS connection resumed a previous connection
|
||||
@@ -293,11 +296,19 @@ func (app *App) Provision(ctx caddy.Context) error {
|
||||
if srv.Errors != nil {
|
||||
err := srv.Errors.Routes.Provision(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("server %s: setting up server error handling routes: %v", srvName, err)
|
||||
return fmt.Errorf("server %s: setting up error handling routes: %v", srvName, err)
|
||||
}
|
||||
srv.errorHandlerChain = srv.Errors.Routes.Compile(errorEmptyHandler)
|
||||
}
|
||||
|
||||
// provision the named routes (they get compiled at runtime)
|
||||
for name, route := range srv.NamedRoutes {
|
||||
err := route.Provision(ctx, srv.Metrics)
|
||||
if err != nil {
|
||||
return fmt.Errorf("server %s: setting up named route '%s' handlers: %v", name, srvName, err)
|
||||
}
|
||||
}
|
||||
|
||||
// prepare the TLS connection policies
|
||||
err = srv.TLSConnPolicies.Provision(ctx)
|
||||
if err != nil {
|
||||
@@ -317,9 +328,15 @@ func (app *App) Provision(ctx caddy.Context) error {
|
||||
|
||||
// Validate ensures the app's configuration is valid.
|
||||
func (app *App) Validate() error {
|
||||
isGo120 := strings.Contains(runtime.Version(), "go1.20")
|
||||
|
||||
// each server must use distinct listener addresses
|
||||
lnAddrs := make(map[string]string)
|
||||
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 {
|
||||
listenAddr, err := caddy.ParseNetworkAddress(addr)
|
||||
if err != nil {
|
||||
@@ -361,11 +378,7 @@ func (app *App) Start() error {
|
||||
return context.WithValue(ctx, ConnCtxKey, c)
|
||||
},
|
||||
}
|
||||
h2server := &http2.Server{
|
||||
NewWriteScheduler: func() http2.WriteScheduler {
|
||||
return http2.NewPriorityWriteScheduler(nil)
|
||||
},
|
||||
}
|
||||
h2server := new(http2.Server)
|
||||
|
||||
// disable HTTP/2, which we enabled by default during provisioning
|
||||
if !srv.protocol("h2") {
|
||||
@@ -586,16 +599,13 @@ func (app *App) Stop() error {
|
||||
return
|
||||
}
|
||||
|
||||
// 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()))
|
||||
}
|
||||
}
|
||||
// First close h3server then close listeners unlike stdlib for several reasons:
|
||||
// 1, udp has only a single socket, once closed, no more data can be read and
|
||||
// written. In contrast, closing tcp listeners won't affect established connections.
|
||||
// This have something to do with graceful shutdown when upstream implements it.
|
||||
// 2, h3server will only close listeners it's registered (quic listeners). Closing
|
||||
// listener first and these listeners maybe unregistered thus won't be closed. caddy
|
||||
// distinguishes quic-listener and underlying datagram sockets.
|
||||
|
||||
// TODO: CloseGracefully, once implemented upstream (see https://github.com/quic-go/quic-go/issues/2103)
|
||||
if err := server.h3server.Close(); err != nil {
|
||||
|
||||
@@ -20,10 +20,11 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||
)
|
||||
|
||||
// AutoHTTPSConfig is used to disable automatic HTTPS
|
||||
@@ -83,6 +84,8 @@ func (ahc AutoHTTPSConfig) Skipped(name string, skipSlice []string) bool {
|
||||
// even servers to the app, which still need to be set up with the
|
||||
// rest of them during provisioning.
|
||||
func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) error {
|
||||
logger := app.logger.Named("auto_https")
|
||||
|
||||
// this map acts as a set to store the domain names
|
||||
// for which we will manage certificates automatically
|
||||
uniqueDomainsForCerts := make(map[string]struct{})
|
||||
@@ -114,13 +117,13 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
|
||||
srv.AutoHTTPS = new(AutoHTTPSConfig)
|
||||
}
|
||||
if srv.AutoHTTPS.Disabled {
|
||||
app.logger.Warn("automatic HTTPS is completely disabled for server", zap.String("server_name", srvName))
|
||||
logger.Warn("automatic HTTPS is completely disabled for server", zap.String("server_name", srvName))
|
||||
continue
|
||||
}
|
||||
|
||||
// skip if all listeners use the HTTP port
|
||||
if !srv.listenersUseAnyPortOtherThan(app.httpPort()) {
|
||||
app.logger.Warn("server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server",
|
||||
logger.Warn("server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server",
|
||||
zap.String("server_name", srvName),
|
||||
zap.Int("http_port", app.httpPort()),
|
||||
)
|
||||
@@ -134,7 +137,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
|
||||
// needing to specify one empty policy to enable it
|
||||
if srv.TLSConnPolicies == nil &&
|
||||
!srv.listenersUseAnyPortOtherThan(app.httpsPort()) {
|
||||
app.logger.Info("server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS",
|
||||
logger.Info("server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS",
|
||||
zap.String("server_name", srvName),
|
||||
zap.Int("https_port", app.httpsPort()),
|
||||
)
|
||||
@@ -186,22 +189,16 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
|
||||
// a deduplicated list of names for which to obtain certs
|
||||
// (only if cert management not disabled for this server)
|
||||
if srv.AutoHTTPS.DisableCerts {
|
||||
app.logger.Warn("skipping automated certificate management for server because it is disabled", zap.String("server_name", srvName))
|
||||
logger.Warn("skipping automated certificate management for server because it is disabled", zap.String("server_name", srvName))
|
||||
} else {
|
||||
for d := range serverDomainSet {
|
||||
// the implicit Tailscale manager module will get its own certs at run-time
|
||||
if isTailscaleDomain(d) {
|
||||
continue
|
||||
}
|
||||
|
||||
if certmagic.SubjectQualifiesForCert(d) &&
|
||||
!srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) {
|
||||
// if a certificate for this name is already loaded,
|
||||
// don't obtain another one for it, unless we are
|
||||
// supposed to ignore loaded certificates
|
||||
if !srv.AutoHTTPS.IgnoreLoadedCerts &&
|
||||
len(app.tlsApp.AllMatchingCertificates(d)) > 0 {
|
||||
app.logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded",
|
||||
if !srv.AutoHTTPS.IgnoreLoadedCerts && app.tlsApp.HasCertificateForSubject(d) {
|
||||
logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded",
|
||||
zap.String("domain", d),
|
||||
zap.String("server_name", srvName),
|
||||
)
|
||||
@@ -212,7 +209,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
|
||||
// can handle that, but as a courtesy, warn the user
|
||||
if strings.Contains(d, "*") &&
|
||||
strings.Count(strings.Trim(d, "."), ".") == 1 {
|
||||
app.logger.Warn("most clients do not trust second-level wildcard certificates (*.tld)",
|
||||
logger.Warn("most clients do not trust second-level wildcard certificates (*.tld)",
|
||||
zap.String("domain", d))
|
||||
}
|
||||
|
||||
@@ -228,11 +225,11 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
|
||||
|
||||
// nothing left to do if auto redirects are disabled
|
||||
if srv.AutoHTTPS.DisableRedir {
|
||||
app.logger.Warn("automatic HTTP->HTTPS redirects are disabled", zap.String("server_name", srvName))
|
||||
logger.Warn("automatic HTTP->HTTPS redirects are disabled", zap.String("server_name", srvName))
|
||||
continue
|
||||
}
|
||||
|
||||
app.logger.Info("enabling automatic HTTP->HTTPS redirects", zap.String("server_name", srvName))
|
||||
logger.Info("enabling automatic HTTP->HTTPS redirects", zap.String("server_name", srvName))
|
||||
|
||||
// create HTTP->HTTPS redirects
|
||||
for _, listenAddr := range srv.Listen {
|
||||
@@ -272,12 +269,15 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
|
||||
// we now have a list of all the unique names for which we need certs;
|
||||
// turn the set into a slice so that phase 2 can use it
|
||||
app.allCertDomains = make([]string, 0, len(uniqueDomainsForCerts))
|
||||
var internal []string
|
||||
var internal, tailscale []string
|
||||
uniqueDomainsLoop:
|
||||
for d := range uniqueDomainsForCerts {
|
||||
// whether or not there is already an automation policy for this
|
||||
// name, we should add it to the list to manage a cert for it
|
||||
app.allCertDomains = append(app.allCertDomains, d)
|
||||
if !isTailscaleDomain(d) {
|
||||
// whether or not there is already an automation policy for this
|
||||
// name, we should add it to the list to manage a cert for it,
|
||||
// unless it's a Tailscale domain, because we don't manage those
|
||||
app.allCertDomains = append(app.allCertDomains, d)
|
||||
}
|
||||
|
||||
// some names we've found might already have automation policies
|
||||
// explicitly specified for them; we should exclude those from
|
||||
@@ -295,13 +295,15 @@ uniqueDomainsLoop:
|
||||
|
||||
// if no automation policy exists for the name yet, we
|
||||
// will associate it with an implicit one
|
||||
if !certmagic.SubjectQualifiesForPublicCert(d) {
|
||||
if isTailscaleDomain(d) {
|
||||
tailscale = append(tailscale, d)
|
||||
} else if !certmagic.SubjectQualifiesForPublicCert(d) {
|
||||
internal = append(internal, d)
|
||||
}
|
||||
}
|
||||
|
||||
// ensure there is an automation policy to handle these certs
|
||||
err := app.createAutomationPolicies(ctx, internal)
|
||||
err := app.createAutomationPolicies(ctx, internal, tailscale)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -424,6 +426,10 @@ redirServersLoop:
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("adjusted config",
|
||||
zap.Reflect("tls", app.tlsApp),
|
||||
zap.Reflect("http", app))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -466,7 +472,7 @@ func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route {
|
||||
// automation policy exists, it will be shallow-copied and used as the
|
||||
// base for the new ones (this is important for preserving behavior the
|
||||
// user intends to be "defaults").
|
||||
func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []string) error {
|
||||
func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames, tailscaleNames []string) error {
|
||||
// before we begin, loop through the existing automation policies
|
||||
// and, for any ACMEIssuers we find, make sure they're filled in
|
||||
// with default values that might be specified in our HTTP app; also
|
||||
@@ -480,6 +486,22 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []stri
|
||||
app.tlsApp.Automation = new(caddytls.AutomationConfig)
|
||||
}
|
||||
for _, ap := range app.tlsApp.Automation.Policies {
|
||||
// on-demand policies can have the tailscale manager added implicitly
|
||||
// if there's no explicit manager configured -- for convenience
|
||||
if ap.OnDemand && len(ap.Managers) == 0 {
|
||||
var ts caddytls.Tailscale
|
||||
if err := ts.Provision(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
ap.Managers = []certmagic.Manager{ts}
|
||||
|
||||
// must reprovision the automation policy so that the underlying
|
||||
// CertMagic config knows about the updated Managers
|
||||
if err := ap.Provision(app.tlsApp); err != nil {
|
||||
return fmt.Errorf("re-provisioning automation policy: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// set up default issuer -- honestly, this is only
|
||||
// really necessary because the HTTP app is opinionated
|
||||
// and has settings which could be inferred as new
|
||||
@@ -501,22 +523,6 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []stri
|
||||
}
|
||||
}
|
||||
|
||||
// if no external managers were configured, enable
|
||||
// implicit Tailscale support for convenience
|
||||
if ap.Managers == nil {
|
||||
ts, err := implicitTailscale(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ap.Managers = []certmagic.Manager{ts}
|
||||
|
||||
// must reprovision the automation policy so that the underlying
|
||||
// CertMagic config knows about the updated Managers
|
||||
if err := ap.Provision(app.tlsApp); err != nil {
|
||||
return fmt.Errorf("re-provisioning automation policy: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// while we're here, is this the catch-all/base policy?
|
||||
if !foundBasePolicy && len(ap.SubjectsRaw) == 0 {
|
||||
basePolicy = ap
|
||||
@@ -529,15 +535,6 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []stri
|
||||
basePolicy = new(caddytls.AutomationPolicy)
|
||||
}
|
||||
|
||||
if basePolicy.Managers == nil {
|
||||
// add implicit Tailscale integration, for harmless convenience
|
||||
ts, err := implicitTailscale(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
basePolicy.Managers = []certmagic.Manager{ts}
|
||||
}
|
||||
|
||||
// if the basePolicy has an existing ACMEIssuer (particularly to
|
||||
// include any type that embeds/wraps an ACMEIssuer), let's use it
|
||||
// (I guess we just use the first one?), otherwise we'll make one
|
||||
@@ -642,6 +639,27 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames []stri
|
||||
}
|
||||
}
|
||||
|
||||
// tailscale names go in their own automation policies because
|
||||
// they require on-demand TLS to be enabled, which we obviously
|
||||
// can't enable for everything
|
||||
if len(tailscaleNames) > 0 {
|
||||
policyCopy := *basePolicy
|
||||
newPolicy := &policyCopy
|
||||
|
||||
var ts caddytls.Tailscale
|
||||
if err := ts.Provision(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newPolicy.SubjectsRaw = tailscaleNames
|
||||
newPolicy.Issuers = nil
|
||||
newPolicy.Managers = append(newPolicy.Managers, ts)
|
||||
err := app.tlsApp.AddAutomationPolicy(newPolicy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// we just changed a lot of stuff, so double-check that it's all good
|
||||
err := app.tlsApp.Validate()
|
||||
if err != nil {
|
||||
@@ -720,13 +738,6 @@ func (app *App) automaticHTTPSPhase2() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// implicitTailscale returns a new and provisioned Tailscale module configured to be optional.
|
||||
func implicitTailscale(ctx caddy.Context) (caddytls.Tailscale, error) {
|
||||
ts := caddytls.Tailscale{Optional: true}
|
||||
err := ts.Provision(ctx)
|
||||
return ts, err
|
||||
}
|
||||
|
||||
func isTailscaleDomain(name string) bool {
|
||||
return strings.HasSuffix(strings.ToLower(name), ".ts.net")
|
||||
}
|
||||
|
||||
@@ -23,16 +23,21 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/singleflight"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(HTTPBasicAuth{})
|
||||
|
||||
weakrand.Seed(time.Now().UnixNano())
|
||||
caddy.RegisterNamespace("http.authentication.hashes", []interface{}{
|
||||
(*Comparer)(nil),
|
||||
})
|
||||
caddy.RegisterNamespace("http.authentication.providers", []interface{}{
|
||||
(*Authenticator)(nil),
|
||||
})
|
||||
}
|
||||
|
||||
// HTTPBasicAuth facilitates HTTP basic authentication.
|
||||
|
||||
@@ -18,9 +18,10 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -22,10 +22,12 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
|
||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -18,9 +18,10 @@ import (
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/crypto/scrypt"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -17,7 +17,6 @@ package caddyhttp
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -165,7 +164,7 @@ func (ws *WeakString) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON marshals as a boolean if true or false,
|
||||
// MarshalJSON marshals was a boolean if true or false,
|
||||
// a number if an integer, or a string otherwise.
|
||||
func (ws WeakString) MarshalJSON() ([]byte, error) {
|
||||
if ws == "true" {
|
||||
@@ -205,82 +204,6 @@ func (ws WeakString) String() string {
|
||||
return string(ws)
|
||||
}
|
||||
|
||||
// Ratio is a type that unmarshals a valid numerical ratio string.
|
||||
// Valid formats are:
|
||||
// - a/b as a fraction (a / b)
|
||||
// - a:b as a ratio (a / a+b)
|
||||
// - a floating point number
|
||||
type Ratio float64
|
||||
|
||||
// UnmarshalJSON satisfies json.Unmarshaler according to
|
||||
// this type's documentation.
|
||||
func (r *Ratio) UnmarshalJSON(b []byte) error {
|
||||
if len(b) == 0 {
|
||||
return io.EOF
|
||||
}
|
||||
if b[0] == byte('"') && b[len(b)-1] == byte('"') {
|
||||
if !strings.Contains(string(b), "/") && !strings.Contains(string(b), ":") {
|
||||
return fmt.Errorf("ratio string '%s' did not contain a slash '/' or colon ':'", string(b[1:len(b)-1]))
|
||||
}
|
||||
if strings.Contains(string(b), "/") {
|
||||
left, right, _ := strings.Cut(string(b[1:len(b)-1]), "/")
|
||||
num, err := strconv.Atoi(left)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed parsing numerator as integer %s: %v", left, err)
|
||||
}
|
||||
denom, err := strconv.Atoi(right)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed parsing denominator as integer %s: %v", right, err)
|
||||
}
|
||||
*r = Ratio(float64(num) / float64(denom))
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(string(b), ":") {
|
||||
left, right, _ := strings.Cut(string(b[1:len(b)-1]), ":")
|
||||
num, err := strconv.Atoi(left)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed parsing numerator as integer %s: %v", left, err)
|
||||
}
|
||||
denom, err := strconv.Atoi(right)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed parsing denominator as integer %s: %v", right, err)
|
||||
}
|
||||
*r = Ratio(float64(num) / (float64(num) + float64(denom)))
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid ratio string '%s'", string(b[1:len(b)-1]))
|
||||
}
|
||||
if bytes.Equal(b, []byte("null")) {
|
||||
return nil
|
||||
}
|
||||
float, err := strconv.ParseFloat(string(b), 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed parsing ratio as float %s: %v", b, err)
|
||||
}
|
||||
*r = Ratio(float)
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseRatio(r string) (Ratio, error) {
|
||||
if strings.Contains(r, "/") {
|
||||
left, right, _ := strings.Cut(r, "/")
|
||||
num, err := strconv.Atoi(left)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed parsing numerator as integer %s: %v", left, err)
|
||||
}
|
||||
denom, err := strconv.Atoi(right)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed parsing denominator as integer %s: %v", right, err)
|
||||
}
|
||||
return Ratio(float64(num) / float64(denom)), nil
|
||||
}
|
||||
float, err := strconv.ParseFloat(r, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed parsing ratio as float %s: %v", r, err)
|
||||
}
|
||||
return Ratio(float), nil
|
||||
}
|
||||
|
||||
// StatusCodeMatches returns true if a real HTTP status code matches
|
||||
// the configured status code, which may be either a real HTTP status
|
||||
// code or an integer representing a class of codes (e.g. 4 for all
|
||||
@@ -384,5 +307,7 @@ const (
|
||||
const separator = string(filepath.Separator)
|
||||
|
||||
// Interface guard
|
||||
var _ caddy.ListenerWrapper = (*tlsPlaceholderWrapper)(nil)
|
||||
var _ caddyfile.Unmarshaler = (*tlsPlaceholderWrapper)(nil)
|
||||
var (
|
||||
_ caddy.ListenerWrapper = (*tlsPlaceholderWrapper)(nil)
|
||||
_ caddyfile.Unmarshaler = (*tlsPlaceholderWrapper)(nil)
|
||||
)
|
||||
|
||||
@@ -149,79 +149,3 @@ func TestCleanPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalRatio(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
input []byte
|
||||
expect float64
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
input: []byte("null"),
|
||||
expect: 0,
|
||||
},
|
||||
{
|
||||
input: []byte(`"1/3"`),
|
||||
expect: float64(1) / float64(3),
|
||||
},
|
||||
{
|
||||
input: []byte(`"1/100"`),
|
||||
expect: float64(1) / float64(100),
|
||||
},
|
||||
{
|
||||
input: []byte(`"3:2"`),
|
||||
expect: 0.6,
|
||||
},
|
||||
{
|
||||
input: []byte(`"99:1"`),
|
||||
expect: 0.99,
|
||||
},
|
||||
{
|
||||
input: []byte(`"1/100"`),
|
||||
expect: float64(1) / float64(100),
|
||||
},
|
||||
{
|
||||
input: []byte(`0.1`),
|
||||
expect: 0.1,
|
||||
},
|
||||
{
|
||||
input: []byte(`0.005`),
|
||||
expect: 0.005,
|
||||
},
|
||||
{
|
||||
input: []byte(`0`),
|
||||
expect: 0,
|
||||
},
|
||||
{
|
||||
input: []byte(`"0"`),
|
||||
errMsg: `ratio string '0' did not contain a slash '/' or colon ':'`,
|
||||
},
|
||||
{
|
||||
input: []byte(`a`),
|
||||
errMsg: `failed parsing ratio as float a: strconv.ParseFloat: parsing "a": invalid syntax`,
|
||||
},
|
||||
{
|
||||
input: []byte(`"a/1"`),
|
||||
errMsg: `failed parsing numerator as integer a: strconv.Atoi: parsing "a": invalid syntax`,
|
||||
},
|
||||
{
|
||||
input: []byte(`"1/a"`),
|
||||
errMsg: `failed parsing denominator as integer a: strconv.Atoi: parsing "a": invalid syntax`,
|
||||
},
|
||||
} {
|
||||
ratio := Ratio(0)
|
||||
err := ratio.UnmarshalJSON(tc.input)
|
||||
if err != nil {
|
||||
if tc.errMsg != "" {
|
||||
if tc.errMsg != err.Error() {
|
||||
t.Fatalf("Test %d: expected error: %v, got: %v", i, tc.errMsg, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
t.Fatalf("Test %d: invalid ratio: %v", i, err)
|
||||
}
|
||||
if ratio != Ratio(tc.expect) {
|
||||
t.Fatalf("Test %d: expected %v, got %v", i, tc.expect, ratio)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common"
|
||||
"github.com/google/cel-go/common/operators"
|
||||
@@ -39,6 +37,9 @@ import (
|
||||
"github.com/google/cel-go/parser"
|
||||
"go.uber.org/zap"
|
||||
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -234,9 +235,11 @@ func (cr celHTTPRequest) Parent() interpreter.Activation {
|
||||
func (cr celHTTPRequest) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
||||
return cr.Request, nil
|
||||
}
|
||||
|
||||
func (celHTTPRequest) ConvertToType(typeVal ref.Type) ref.Val {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (cr celHTTPRequest) Equal(other ref.Val) ref.Val {
|
||||
if o, ok := other.Value().(celHTTPRequest); ok {
|
||||
return types.Bool(o.Request == cr.Request)
|
||||
@@ -255,9 +258,14 @@ type celPkixName struct{ *pkix.Name }
|
||||
func (pn celPkixName) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
||||
return pn.Name, nil
|
||||
}
|
||||
func (celPkixName) ConvertToType(typeVal ref.Type) ref.Val {
|
||||
|
||||
func (pn celPkixName) ConvertToType(typeVal ref.Type) ref.Val {
|
||||
if typeVal.TypeName() == "string" {
|
||||
return types.String(pn.Name.String())
|
||||
}
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (pn celPkixName) Equal(other ref.Val) ref.Val {
|
||||
if o, ok := other.Value().(string); ok {
|
||||
return types.Bool(pn.Name.String() == o)
|
||||
@@ -493,7 +501,7 @@ func celMatcherStringMacroExpander(funcName string) parser.MacroExpander {
|
||||
}
|
||||
}
|
||||
|
||||
// celMatcherStringMacroExpander validates that the macro is called a single
|
||||
// celMatcherJSONMacroExpander validates that the macro is called a single
|
||||
// map literal argument.
|
||||
//
|
||||
// The following function call is returned: <funcName>(request, arg)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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()
|
||||
}
|
||||
@@ -37,6 +37,9 @@ import (
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(Encode{})
|
||||
caddy.RegisterNamespace("http.encoders", []interface{}{
|
||||
(*Encoding)(nil),
|
||||
})
|
||||
}
|
||||
|
||||
// Encode is a middleware which can encode responses.
|
||||
@@ -93,6 +96,7 @@ func (enc *Encode) Provision(ctx caddy.Context) error {
|
||||
"application/xhtml+xml*",
|
||||
"application/atom+xml*",
|
||||
"application/rss+xml*",
|
||||
"application/wasm*",
|
||||
"image/svg+xml*",
|
||||
},
|
||||
},
|
||||
@@ -167,10 +171,10 @@ func (enc *Encode) openResponseWriter(encodingName string, w http.ResponseWriter
|
||||
// initResponseWriter initializes the responseWriter instance
|
||||
// allocated in openResponseWriter, enabling mid-stack inlining.
|
||||
func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter) *responseWriter {
|
||||
if httpInterfaces, ok := wrappedRW.(caddyhttp.HTTPInterfaces); ok {
|
||||
rw.HTTPInterfaces = httpInterfaces
|
||||
if rww, ok := wrappedRW.(*caddyhttp.ResponseWriterWrapper); ok {
|
||||
rw.ResponseWriter = rww
|
||||
} else {
|
||||
rw.HTTPInterfaces = &caddyhttp.ResponseWriterWrapper{ResponseWriter: wrappedRW}
|
||||
rw.ResponseWriter = &caddyhttp.ResponseWriterWrapper{ResponseWriter: wrappedRW}
|
||||
}
|
||||
rw.encodingName = encodingName
|
||||
rw.config = enc
|
||||
@@ -182,7 +186,7 @@ func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, w
|
||||
// using the encoding represented by encodingName and
|
||||
// configured by config.
|
||||
type responseWriter struct {
|
||||
caddyhttp.HTTPInterfaces
|
||||
http.ResponseWriter
|
||||
encodingName string
|
||||
w Encoder
|
||||
config *Encode
|
||||
@@ -211,7 +215,8 @@ func (rw *responseWriter) Flush() {
|
||||
// to rw.Write (see bug in #4314)
|
||||
return
|
||||
}
|
||||
rw.HTTPInterfaces.Flush()
|
||||
//nolint:bodyclose
|
||||
http.NewResponseController(rw.ResponseWriter).Flush()
|
||||
}
|
||||
|
||||
// Hijack implements http.Hijacker. It will flush status code if set. We don't track actual hijacked
|
||||
@@ -219,11 +224,12 @@ func (rw *responseWriter) Flush() {
|
||||
func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if !rw.wroteHeader {
|
||||
if rw.statusCode != 0 {
|
||||
rw.HTTPInterfaces.WriteHeader(rw.statusCode)
|
||||
rw.ResponseWriter.WriteHeader(rw.statusCode)
|
||||
}
|
||||
rw.wroteHeader = true
|
||||
}
|
||||
return rw.HTTPInterfaces.Hijack()
|
||||
//nolint:bodyclose
|
||||
return http.NewResponseController(rw.ResponseWriter).Hijack()
|
||||
}
|
||||
|
||||
// Write writes to the response. If the response qualifies,
|
||||
@@ -260,7 +266,7 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
|
||||
// by the standard library
|
||||
if !rw.wroteHeader {
|
||||
if rw.statusCode != 0 {
|
||||
rw.HTTPInterfaces.WriteHeader(rw.statusCode)
|
||||
rw.ResponseWriter.WriteHeader(rw.statusCode)
|
||||
}
|
||||
rw.wroteHeader = true
|
||||
}
|
||||
@@ -268,7 +274,7 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
|
||||
if rw.w != nil {
|
||||
return rw.w.Write(p)
|
||||
} else {
|
||||
return rw.HTTPInterfaces.Write(p)
|
||||
return rw.ResponseWriter.Write(p)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,7 +290,7 @@ func (rw *responseWriter) Close() error {
|
||||
|
||||
// issue #5059, don't write status code if not set explicitly.
|
||||
if rw.statusCode != 0 {
|
||||
rw.HTTPInterfaces.WriteHeader(rw.statusCode)
|
||||
rw.ResponseWriter.WriteHeader(rw.statusCode)
|
||||
}
|
||||
rw.wroteHeader = true
|
||||
}
|
||||
@@ -299,13 +305,18 @@ func (rw *responseWriter) Close() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying ResponseWriter.
|
||||
func (rw *responseWriter) Unwrap() http.ResponseWriter {
|
||||
return rw.ResponseWriter
|
||||
}
|
||||
|
||||
// init should be called before we write a response, if rw.buf has contents.
|
||||
func (rw *responseWriter) init() {
|
||||
if rw.Header().Get("Content-Encoding") == "" && isEncodeAllowed(rw.Header()) &&
|
||||
rw.config.Match(rw) {
|
||||
|
||||
rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder)
|
||||
rw.w.Reset(rw.HTTPInterfaces)
|
||||
rw.w.Reset(rw.ResponseWriter)
|
||||
rw.Header().Del("Content-Length") // https://github.com/golang/go/issues/14975
|
||||
rw.Header().Set("Content-Encoding", rw.encodingName)
|
||||
rw.Header().Add("Vary", "Accept-Encoding")
|
||||
@@ -424,5 +435,4 @@ var (
|
||||
_ caddy.Provisioner = (*Encode)(nil)
|
||||
_ caddy.Validator = (*Encode)(nil)
|
||||
_ caddyhttp.MiddlewareHandler = (*Encode)(nil)
|
||||
_ caddyhttp.HTTPInterfaces = (*responseWriter)(nil)
|
||||
)
|
||||
|
||||
@@ -18,10 +18,11 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/klauspost/compress/gzip"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
||||
"github.com/klauspost/compress/gzip"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -15,10 +15,11 @@
|
||||
package caddyzstd
|
||||
|
||||
import (
|
||||
"github.com/klauspost/compress/zstd"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -15,27 +15,24 @@
|
||||
package caddyhttp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
weakrand "math/rand"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
weakrand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// Error is a convenient way for a Handler to populate the
|
||||
// essential fields of a HandlerError. If err is itself a
|
||||
// HandlerError, then any essential fields that are not
|
||||
// set will be populated.
|
||||
func Error(statusCode int, err error) HandlerError {
|
||||
const idLen = 9
|
||||
if he, ok := err.(HandlerError); ok {
|
||||
var he HandlerError
|
||||
if errors.As(err, &he) {
|
||||
if he.ID == "" {
|
||||
he.ID = randString(idLen, true)
|
||||
}
|
||||
|
||||
@@ -29,18 +29,25 @@ import (
|
||||
"sync"
|
||||
"text/template"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// BrowseTemplate is the default template document to use for
|
||||
// file listings. By default, its default value is an embedded
|
||||
// document. You can override this value at program start, or
|
||||
// if you are running Caddy via config, you can specify a
|
||||
// custom template_file in the browse configuration.
|
||||
//
|
||||
//go:embed browse.html
|
||||
var defaultBrowseTemplate string
|
||||
var BrowseTemplate string
|
||||
|
||||
// Browse configures directory browsing.
|
||||
type Browse struct {
|
||||
// Use this template file instead of the default browse template.
|
||||
// Filename of the template to use instead of the embedded browse template.
|
||||
TemplateFile string `json:"template_file,omitempty"`
|
||||
}
|
||||
|
||||
@@ -82,8 +89,8 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
|
||||
|
||||
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
||||
|
||||
// calling path.Clean here prevents weird breadcrumbs when URL paths are sketchy like /%2e%2e%2f
|
||||
listing, err := fsrv.loadDirectoryContents(r.Context(), dir.(fs.ReadDirFile), root, path.Clean(r.URL.Path), repl)
|
||||
// TODO: not entirely sure if path.Clean() is necessary here but seems like a safe plan (i.e. /%2e%2e%2f) - someone could verify this
|
||||
listing, err := fsrv.loadDirectoryContents(r.Context(), dir.(fs.ReadDirFile), root, path.Clean(r.URL.EscapedPath()), repl)
|
||||
switch {
|
||||
case os.IsPermission(err):
|
||||
return caddyhttp.Error(http.StatusForbidden, err)
|
||||
@@ -93,7 +100,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
|
||||
return caddyhttp.Error(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
fsrv.browseApplyQueryParams(w, r, &listing)
|
||||
fsrv.browseApplyQueryParams(w, r, listing)
|
||||
|
||||
buf := bufPool.Get().(*bytes.Buffer)
|
||||
buf.Reset()
|
||||
@@ -113,7 +120,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
|
||||
fs = http.Dir(repl.ReplaceAll(fsrv.Root, "."))
|
||||
}
|
||||
|
||||
var tplCtx = &templateContext{
|
||||
tplCtx := &templateContext{
|
||||
TemplateContext: templates.TemplateContext{
|
||||
Root: fs,
|
||||
Req: r,
|
||||
@@ -137,10 +144,10 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, dir fs.ReadDirFile, root, urlPath string, repl *caddy.Replacer) (browseTemplateContext, error) {
|
||||
func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, dir fs.ReadDirFile, root, urlPath string, repl *caddy.Replacer) (*browseTemplateContext, error) {
|
||||
files, err := dir.ReadDir(10000) // TODO: this limit should probably be configurable
|
||||
if err != nil && err != io.EOF {
|
||||
return browseTemplateContext{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// user can presumably browse "up" to parent folder if path is longer than "/"
|
||||
@@ -204,7 +211,7 @@ func (fsrv *FileServer) makeBrowseTemplate(tplCtx *templateContext) (*template.T
|
||||
}
|
||||
} else {
|
||||
tpl = tplCtx.NewTemplate("default_listing")
|
||||
tpl, err = tpl.Parse(defaultBrowseTemplate)
|
||||
tpl, err = tpl.Parse(BrowseTemplate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing default browse template: %v", err)
|
||||
}
|
||||
@@ -237,7 +244,7 @@ func isSymlink(f fs.FileInfo) bool {
|
||||
// features.
|
||||
type templateContext struct {
|
||||
templates.TemplateContext
|
||||
browseTemplateContext
|
||||
*browseTemplateContext
|
||||
}
|
||||
|
||||
// bufPool is used to increase the efficiency of file listings.
|
||||
|
||||
@@ -1,90 +1,296 @@
|
||||
{{- define "icon"}}
|
||||
{{- if .IsDir}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-folder-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M9 3a1 1 0 0 1 .608 .206l.1 .087l2.706 2.707h6.586a3 3 0 0 1 2.995 2.824l.005 .176v8a3 3 0 0 1 -2.824 2.995l-.176 .005h-14a3 3 0 0 1 -2.995 -2.824l-.005 -.176v-11a3 3 0 0 1 2.824 -2.995l.176 -.005h4z" stroke-width="0" fill="currentColor"></path>
|
||||
{{- if .IsSymlink}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-folder-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M9 3a1 1 0 0 1 .608 .206l.1 .087l2.706 2.707h6.586a3 3 0 0 1 2.995 2.824l.005 .176v8a3 3 0 0 1 -2.824 2.995l-.176 .005h-14a3 3 0 0 1 -2.995 -2.824l-.005 -.176v-11a3 3 0 0 1 2.824 -2.995l.176 -.005h4z" stroke-width="0" fill="currentColor"/>
|
||||
<path fill="#000" d="M2.795 17.306c0-2.374 1.792-4.314 4.078-4.538v-1.104a.38.38 0 0 1 .651-.272l2.45 2.492a.132.132 0 0 1 0 .188l-2.45 2.492a.381.381 0 0 1-.651-.272V15.24c-1.889.297-3.436 1.39-3.817 3.26a2.809 2.809 0 0 1-.261-1.193Z" style="stroke-width:.127478"/>
|
||||
</svg>
|
||||
{{- else}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-folder-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M9 3a1 1 0 0 1 .608 .206l.1 .087l2.706 2.707h6.586a3 3 0 0 1 2.995 2.824l.005 .176v8a3 3 0 0 1 -2.824 2.995l-.176 .005h-14a3 3 0 0 1 -2.995 -2.824l-.005 -.176v-11a3 3 0 0 1 2.824 -2.995l.176 -.005h4z" stroke-width="0" fill="currentColor"/>
|
||||
</svg>
|
||||
{{- end}}
|
||||
{{- else if or (eq .Name "LICENSE") (eq .Name "README")}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-license" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M15 21h-9a3 3 0 0 1 -3 -3v-1h10v2a2 2 0 0 0 4 0v-14a2 2 0 1 1 2 2h-2m2 -4h-11a3 3 0 0 0 -3 3v11"/>
|
||||
<path d="M9 7l4 0"/>
|
||||
<path d="M9 11l4 0"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp"}}
|
||||
<img src="{{html .Name}}">
|
||||
{{- else if .HasExt ".mp4" ".mov" ".mpeg" ".avi" ".ogg" ".webm"}}
|
||||
{{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp" ".tiff" ".bmp" ".heif" ".heic" ".svg"}}
|
||||
{{- if eq .Tpl.Layout "grid"}}
|
||||
<img loading="lazy" src="{{html .Name}}">
|
||||
{{- else}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-photo" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M15 8h.01"/>
|
||||
<path d="M3 6a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v12a3 3 0 0 1 -3 3h-12a3 3 0 0 1 -3 -3v-12z"/>
|
||||
<path d="M3 16l5 -5c.928 -.893 2.072 -.893 3 0l5 5"/>
|
||||
<path d="M14 14l1 -1c.928 -.893 2.072 -.893 3 0l3 3"/>
|
||||
</svg>
|
||||
{{- end}}
|
||||
{{- else if .HasExt ".mp4" ".mov" ".m4v" ".mpeg" ".mpg" ".avi" ".ogg" ".webm" ".mkv" ".vob" ".gifv" ".3gp"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-movie" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
|
||||
<path d="M8 4l0 16"></path>
|
||||
<path d="M16 4l0 16"></path>
|
||||
<path d="M4 8l4 0"></path>
|
||||
<path d="M4 16l4 0"></path>
|
||||
<path d="M4 12l16 0"></path>
|
||||
<path d="M16 8l4 0"></path>
|
||||
<path d="M16 16l4 0"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"/>
|
||||
<path d="M8 4l0 16"/>
|
||||
<path d="M16 4l0 16"/>
|
||||
<path d="M4 8l4 0"/>
|
||||
<path d="M4 16l4 0"/>
|
||||
<path d="M4 12l16 0"/>
|
||||
<path d="M16 8l4 0"/>
|
||||
<path d="M16 16l4 0"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".mp3" ".flac" ".wav" ".wma"}}
|
||||
{{- else if .HasExt ".mp3" ".m4a" ".aac" ".ogg" ".flac" ".wav" ".wma" ".midi" ".cda"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-music" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
|
||||
<path d="M16 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"></path>
|
||||
<path d="M9 17l0 -13l10 0l0 13"></path>
|
||||
<path d="M9 8l10 0"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"/>
|
||||
<path d="M16 17m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"/>
|
||||
<path d="M9 17l0 -13l10 0l0 13"/>
|
||||
<path d="M9 8l10 0"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".pdf"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-pdf" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M10 8v8h2a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2h-2z"></path>
|
||||
<path d="M3 12h2a2 2 0 1 0 0 -4h-2v8"></path>
|
||||
<path d="M17 12h3"></path>
|
||||
<path d="M21 8h-4v8"></path>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-type-pdf" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"/>
|
||||
<path d="M5 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6"/>
|
||||
<path d="M17 18h2"/>
|
||||
<path d="M20 15h-3v6"/>
|
||||
<path d="M11 15v6h1a2 2 0 0 0 2 -2v-2a2 2 0 0 0 -2 -2h-1z"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".txt"}}
|
||||
{{- else if .HasExt ".csv" ".tsv"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-type-csv" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"/>
|
||||
<path d="M7 16.5a1.5 1.5 0 0 0 -3 0v3a1.5 1.5 0 0 0 3 0"/>
|
||||
<path d="M10 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"/>
|
||||
<path d="M16 15l2 6l2 -6"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".txt" ".doc" ".docx" ".odt" ".fodt" ".rtf"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-text" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"></path>
|
||||
<path d="M9 9l1 0"></path>
|
||||
<path d="M9 13l6 0"></path>
|
||||
<path d="M9 17l6 0"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"/>
|
||||
<path d="M9 9l1 0"/>
|
||||
<path d="M9 13l6 0"/>
|
||||
<path d="M9 17l6 0"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".zip" ".gz" ".xz" ".tar"}}
|
||||
{{- else if .HasExt ".xls" ".xlsx" ".ods" ".fods"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-spreadsheet" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"/>
|
||||
<path d="M8 11h8v7h-8z"/>
|
||||
<path d="M8 15h8"/>
|
||||
<path d="M11 11v7"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".ppt" ".pptx" ".odp" ".fodp"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-presentation-analytics" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M9 12v-4"/>
|
||||
<path d="M15 12v-2"/>
|
||||
<path d="M12 12v-1"/>
|
||||
<path d="M3 4h18"/>
|
||||
<path d="M4 4v10a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-10"/>
|
||||
<path d="M12 16v4"/>
|
||||
<path d="M9 20h6"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".zip" ".gz" ".xz" ".tar" ".7z" ".rar" ".xz" ".zst"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-zip" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 20.735a2 2 0 0 1 -1 -1.735v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-1"></path>
|
||||
<path d="M11 17a2 2 0 0 1 2 2v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a2 2 0 0 1 2 -2z"></path>
|
||||
<path d="M11 5l-1 0"></path>
|
||||
<path d="M13 7l-1 0"></path>
|
||||
<path d="M11 9l-1 0"></path>
|
||||
<path d="M13 11l-1 0"></path>
|
||||
<path d="M11 13l-1 0"></path>
|
||||
<path d="M13 15l-1 0"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 20.735a2 2 0 0 1 -1 -1.735v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-1"/>
|
||||
<path d="M11 17a2 2 0 0 1 2 2v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a2 2 0 0 1 2 -2z"/>
|
||||
<path d="M11 5l-1 0"/>
|
||||
<path d="M13 7l-1 0"/>
|
||||
<path d="M11 9l-1 0"/>
|
||||
<path d="M13 11l-1 0"/>
|
||||
<path d="M11 13l-1 0"/>
|
||||
<path d="M13 15l-1 0"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".deb" ".dpkg"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-debian" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 17c-2.397 -.943 -4 -3.153 -4 -5.635c0 -2.19 1.039 -3.14 1.604 -3.595c2.646 -2.133 6.396 -.27 6.396 3.23c0 2.5 -2.905 2.121 -3.5 1.5c-.595 -.621 -1 -1.5 -.5 -2.5"/>
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".rpm" ".exe" ".flatpak" ".appimage" ".jar" ".msi" ".apk"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-package" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5"/>
|
||||
<path d="M12 12l8 -4.5"/>
|
||||
<path d="M12 12l0 9"/>
|
||||
<path d="M12 12l-8 -4.5"/>
|
||||
<path d="M16 5.25l-8 4.5"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".ps1"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-powershell" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M4.887 20h11.868c.893 0 1.664 -.665 1.847 -1.592l2.358 -12c.212 -1.081 -.442 -2.14 -1.462 -2.366a1.784 1.784 0 0 0 -.385 -.042h-11.868c-.893 0 -1.664 .665 -1.847 1.592l-2.358 12c-.212 1.081 .442 2.14 1.462 2.366c.127 .028 .256 .042 .385 .042z"/>
|
||||
<path d="M9 8l4 4l-6 4"/>
|
||||
<path d="M12 16h3"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".py" ".pyc" ".pyo"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-python" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 9h-7a2 2 0 0 0 -2 2v4a2 2 0 0 0 2 2h3"/>
|
||||
<path d="M12 15h7a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2h-3"/>
|
||||
<path d="M8 9v-4a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v5a2 2 0 0 1 -2 2h-4a2 2 0 0 0 -2 2v5a2 2 0 0 0 2 2h4a2 2 0 0 0 2 -2v-4"/>
|
||||
<path d="M11 6l0 .01"/>
|
||||
<path d="M13 18l0 .01"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".bash" ".sh" ".com" ".bat" ".dll" ".so"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-script" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M17 20h-11a3 3 0 0 1 0 -6h11a3 3 0 0 0 0 6h1a3 3 0 0 0 3 -3v-11a2 2 0 0 0 -2 -2h-10a2 2 0 0 0 -2 2v8"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".dmg"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-finder" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M3 4m0 1a1 1 0 0 1 1 -1h16a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1z"/>
|
||||
<path d="M7 8v1"/>
|
||||
<path d="M17 8v1"/>
|
||||
<path d="M12.5 4c-.654 1.486 -1.26 3.443 -1.5 9h2.5c-.19 2.867 .094 5.024 .5 7"/>
|
||||
<path d="M7 15.5c3.667 2 6.333 2 10 0"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".iso" ".img"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-disc" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"/>
|
||||
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"/>
|
||||
<path d="M7 12a5 5 0 0 1 5 -5"/>
|
||||
<path d="M12 17a5 5 0 0 0 5 -5"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".md" ".mdown" ".markdown"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-markdown" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M3 5m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z"></path>
|
||||
<path d="M7 15v-6l2 2l2 -2v6"></path>
|
||||
<path d="M14 13l2 2l2 -2m-2 2v-6"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M3 5m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z"/>
|
||||
<path d="M7 15v-6l2 2l2 -2v6"/>
|
||||
<path d="M14 13l2 2l2 -2m-2 2v-6"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".ttf" ".otf" ".woff" ".woff2" ".eof"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-typography" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"/>
|
||||
<path d="M11 18h2"/>
|
||||
<path d="M12 18v-7"/>
|
||||
<path d="M9 12v-1h6v1"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".go"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-golang" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M15.695 14.305c1.061 1.06 2.953 .888 4.226 -.384c1.272 -1.273 1.444 -3.165 .384 -4.226c-1.061 -1.06 -2.953 -.888 -4.226 .384c-1.272 1.273 -1.444 3.165 -.384 4.226z"></path>
|
||||
<path d="M12.68 9.233c-1.084 -.497 -2.545 -.191 -3.591 .846c-1.284 1.273 -1.457 3.165 -.388 4.226c1.07 1.06 2.978 .888 4.261 -.384a3.669 3.669 0 0 0 1.038 -1.921h-2.427"></path>
|
||||
<path d="M5.5 15h-1.5"></path>
|
||||
<path d="M6 9h-2"></path>
|
||||
<path d="M5 12h-3"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M15.695 14.305c1.061 1.06 2.953 .888 4.226 -.384c1.272 -1.273 1.444 -3.165 .384 -4.226c-1.061 -1.06 -2.953 -.888 -4.226 .384c-1.272 1.273 -1.444 3.165 -.384 4.226z"/>
|
||||
<path d="M12.68 9.233c-1.084 -.497 -2.545 -.191 -3.591 .846c-1.284 1.273 -1.457 3.165 -.388 4.226c1.07 1.06 2.978 .888 4.261 -.384a3.669 3.669 0 0 0 1.038 -1.921h-2.427"/>
|
||||
<path d="M5.5 15h-1.5"/>
|
||||
<path d="M6 9h-2"/>
|
||||
<path d="M5 12h-3"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".html" ".htm"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-type-html" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"/>
|
||||
<path d="M2 21v-6"/>
|
||||
<path d="M5 15v6"/>
|
||||
<path d="M2 18h3"/>
|
||||
<path d="M20 15v6h2"/>
|
||||
<path d="M13 21v-6l2 3l2 -3v6"/>
|
||||
<path d="M7.5 15h3"/>
|
||||
<path d="M9 15v6"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".js"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-type-js" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M3 15h3v4.5a1.5 1.5 0 0 1 -3 0"/>
|
||||
<path d="M9 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"/>
|
||||
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-1"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".css"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-type-css" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"/>
|
||||
<path d="M8 16.5a1.5 1.5 0 0 0 -3 0v3a1.5 1.5 0 0 0 3 0"/>
|
||||
<path d="M11 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"/>
|
||||
<path d="M17 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".json" ".json5" ".jsonc"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-json" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M20 16v-8l3 8v-8"/>
|
||||
<path d="M15 8a2 2 0 0 1 2 2v4a2 2 0 1 1 -4 0v-4a2 2 0 0 1 2 -2z"/>
|
||||
<path d="M1 8h3v6.5a1.5 1.5 0 0 1 -3 0v-.5"/>
|
||||
<path d="M7 15a1 1 0 0 0 1 1h1a1 1 0 0 0 1 -1v-2a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1h1a1 1 0 0 1 1 1"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".ts"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-type-ts" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-1"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M9 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"/>
|
||||
<path d="M3.5 15h3"/>
|
||||
<path d="M5 15v6"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".sql"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-type-sql" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M5 20.25c0 .414 .336 .75 .75 .75h1.25a1 1 0 0 0 1 -1v-1a1 1 0 0 0 -1 -1h-1a1 1 0 0 1 -1 -1v-1a1 1 0 0 1 1 -1h1.25a.75 .75 0 0 1 .75 .75"/>
|
||||
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"/>
|
||||
<path d="M18 15v6h2"/>
|
||||
<path d="M13 15a2 2 0 0 1 2 2v2a2 2 0 1 1 -4 0v-2a2 2 0 0 1 2 -2z"/>
|
||||
<path d="M14 20l1.5 1.5"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".db" ".sqlite" ".bak" ".mdb"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-database" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0"/>
|
||||
<path d="M4 6v6a8 3 0 0 0 16 0v-6"/>
|
||||
<path d="M4 12v6a8 3 0 0 0 16 0v-6"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".eml" ".email" ".mailbox" ".mbox" ".msg"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-mail" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10z"/>
|
||||
<path d="M3 7l9 6l9 -6"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".crt" ".pem" ".x509" ".cer" ".ca-bundle"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-certificate" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M15 15m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"/>
|
||||
<path d="M13 17.5v4.5l2 -1.5l2 1.5v-4.5"/>
|
||||
<path d="M10 19h-5a2 2 0 0 1 -2 -2v-10c0 -1.1 .9 -2 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -1 1.73"/>
|
||||
<path d="M6 9l12 0"/>
|
||||
<path d="M6 12l3 0"/>
|
||||
<path d="M6 15l2 0"/>
|
||||
</svg>
|
||||
{{- else if .HasExt ".key" ".keystore" ".jks" ".p12" ".pfx" ".pub"}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-key" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M16.555 3.843l3.602 3.602a2.877 2.877 0 0 1 0 4.069l-2.643 2.643a2.877 2.877 0 0 1 -4.069 0l-.301 -.301l-6.558 6.558a2 2 0 0 1 -1.239 .578l-.175 .008h-1.172a1 1 0 0 1 -.993 -.883l-.007 -.117v-1.172a2 2 0 0 1 .467 -1.284l.119 -.13l.414 -.414h2v-2h2v-2l2.144 -2.144l-.301 -.301a2.877 2.877 0 0 1 0 -4.069l2.643 -2.643a2.877 2.877 0 0 1 4.069 0z"/>
|
||||
<path d="M15 9h.01"/>
|
||||
</svg>
|
||||
{{- else}}
|
||||
{{- if .IsSymlink}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-symlink" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M4 21v-4a3 3 0 0 1 3 -3h5"></path>
|
||||
<path d="M9 17l3 -3l-3 -3"></path>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M5 11v-6a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-9.5"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M4 21v-4a3 3 0 0 1 3 -3h5"/>
|
||||
<path d="M9 17l3 -3l-3 -3"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M5 11v-6a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-9.5"/>
|
||||
</svg>
|
||||
{{- else}}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z"/>
|
||||
</svg>
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
@@ -93,6 +299,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>{{html .Name}}</title>
|
||||
<link rel="canonical" href="{{.Path}}/" />
|
||||
<meta charset="utf-8">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -104,6 +311,7 @@ body {
|
||||
font-size: 16px;
|
||||
text-rendering: optimizespeed;
|
||||
background-color: #f3f6f7;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
img,
|
||||
@@ -201,7 +409,7 @@ header {
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
font-family: Poppins;
|
||||
font-family: Poppins, system-ui, sans-serif;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
@@ -384,17 +592,14 @@ td .go-up {
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(16em, 1fr));
|
||||
gap: 2px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.grid .entry {
|
||||
position: relative;
|
||||
width: 20%;
|
||||
min-width: 250px;
|
||||
max-width: 400px;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.grid .entry a {
|
||||
@@ -458,8 +663,13 @@ footer {
|
||||
#filter {
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.grid .entry {
|
||||
max-width: initial;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: black; /* overscroll */
|
||||
@@ -484,6 +694,10 @@ footer {
|
||||
color: white;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: white;
|
||||
}
|
||||
|
||||
h1 a:hover {
|
||||
background: hsl(213deg 100% 73% / 20%);
|
||||
}
|
||||
@@ -551,15 +765,16 @@ footer {
|
||||
stroke: #ccc !important;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
{{- if eq .Layout "grid"}}
|
||||
<style>.wrapper { max-width: none; } main { margin-top: 1px; }</style>
|
||||
{{- end}}
|
||||
</head>
|
||||
<body onload="initPage()">
|
||||
<header>
|
||||
<div class="wrapper">
|
||||
<div class="breadcrumbs">Folder Path</div>
|
||||
</head>
|
||||
<body onload="initPage()">
|
||||
<header>
|
||||
<div class="wrapper">
|
||||
<div class="breadcrumbs">Folder Path</div>
|
||||
<h1>
|
||||
{{range $i, $crumb := .Breadcrumbs}}<a href="{{html $crumb.Link}}">{{html $crumb.Text}}</a>{{if ne $i 0}}/{{end}}{{end}}
|
||||
</h1>
|
||||
@@ -583,19 +798,19 @@ footer {
|
||||
</div>
|
||||
<a href="javascript:queryParam('layout', '')" id="layout-list" class='layout{{if eq $.Layout "list" ""}}current{{end}}'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-layout-list" width="16" height="16" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
|
||||
<path d="M4 14m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"/>
|
||||
<path d="M4 14m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"/>
|
||||
</svg>
|
||||
List
|
||||
</a>
|
||||
<a href="javascript:queryParam('layout', 'grid')" id="layout-grid" class='layout{{if eq $.Layout "grid"}}current{{end}}'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-layout-grid" width="16" height="16" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M4 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"></path>
|
||||
<path d="M14 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"></path>
|
||||
<path d="M4 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"></path>
|
||||
<path d="M14 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M4 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/>
|
||||
<path d="M14 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/>
|
||||
<path d="M4 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/>
|
||||
<path d="M14 14m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z"/>
|
||||
</svg>
|
||||
Grid
|
||||
</a>
|
||||
@@ -620,22 +835,22 @@ footer {
|
||||
{{- if and (eq .Sort "namedirfirst") (ne .Order "desc")}}
|
||||
<a href="?sort=namedirfirst&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}" class="icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M18 14l-6 -6l-6 6h12"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M18 14l-6 -6l-6 6h12"/>
|
||||
</svg>
|
||||
</a>
|
||||
{{- else if and (eq .Sort "namedirfirst") (ne .Order "asc")}}
|
||||
<a href="?sort=namedirfirst&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}" class="icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-down" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 10l6 6l6 -6h-12"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 10l6 6l6 -6h-12"/>
|
||||
</svg>
|
||||
</a>
|
||||
{{- else}}
|
||||
<a href="?sort=namedirfirst&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}" class="icon sort">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M18 14l-6 -6l-6 6h12"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M18 14l-6 -6l-6 6h12"/>
|
||||
</svg>
|
||||
</a>
|
||||
{{- end}}
|
||||
@@ -644,16 +859,16 @@ footer {
|
||||
<a href="?sort=name&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
|
||||
Name
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M18 14l-6 -6l-6 6h12"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M18 14l-6 -6l-6 6h12"/>
|
||||
</svg>
|
||||
</a>
|
||||
{{- else if and (eq .Sort "name") (ne .Order "asc")}}
|
||||
<a href="?sort=name&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
|
||||
Name
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-down" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 10l6 6l6 -6h-12"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 10l6 6l6 -6h-12"/>
|
||||
</svg>
|
||||
</a>
|
||||
{{- else}}
|
||||
@@ -664,11 +879,11 @@ footer {
|
||||
|
||||
<div class="filter-container">
|
||||
<svg id="search-icon" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-search" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0"></path>
|
||||
<path d="M21 21l-6 -6"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0"/>
|
||||
<path d="M21 21l-6 -6"/>
|
||||
</svg>
|
||||
<input type="text" placeholder="Search" id="filter" onkeyup='filter()'>
|
||||
<input type="search" placeholder="Search" id="filter" onkeyup='filter()'>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
@@ -676,16 +891,16 @@ footer {
|
||||
<a href="?sort=size&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
|
||||
Size
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M18 14l-6 -6l-6 6h12"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M18 14l-6 -6l-6 6h12"/>
|
||||
</svg>
|
||||
</a>
|
||||
{{- else if and (eq .Sort "size") (ne .Order "asc")}}
|
||||
<a href="?sort=size&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
|
||||
Size
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-down" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 10l6 6l6 -6h-12"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 10l6 6l6 -6h-12"/>
|
||||
</svg>
|
||||
</a>
|
||||
{{- else}}
|
||||
@@ -699,16 +914,16 @@ footer {
|
||||
<a href="?sort=time&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
|
||||
Modified
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M18 14l-6 -6l-6 6h12"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M18 14l-6 -6l-6 6h12"/>
|
||||
</svg>
|
||||
</a>
|
||||
{{- else if and (eq .Sort "time") (ne .Order "asc")}}
|
||||
<a href="?sort=time&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
|
||||
Modified
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-down" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M6 10l6 6l6 -6h-12"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 10l6 6l6 -6h-12"/>
|
||||
</svg>
|
||||
</a>
|
||||
{{- else}}
|
||||
@@ -727,8 +942,8 @@ footer {
|
||||
<td>
|
||||
<a href="..">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-corner-left-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M18 18h-6a3 3 0 0 1 -3 -3v-10l-4 4m8 0l-4 -4"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M18 18h-6a3 3 0 0 1 -3 -3v-10l-4 4m8 0l-4 -4"/>
|
||||
</svg>
|
||||
<span class="go-up">Up</span>
|
||||
</a>
|
||||
@@ -850,11 +1065,11 @@ footer {
|
||||
|
||||
<script>
|
||||
const filterEl = document.getElementById('filter');
|
||||
filterEl.focus({ preventScroll: true });
|
||||
filterEl?.focus({ preventScroll: true });
|
||||
|
||||
function initPage() {
|
||||
// populate and evaluate filter
|
||||
if (!filterEl.value) {
|
||||
if (!filterEl?.value) {
|
||||
const filterParam = new URL(window.location.href).searchParams.get('filter');
|
||||
if (filterParam) {
|
||||
filterEl.value = filterParam;
|
||||
@@ -869,11 +1084,15 @@ footer {
|
||||
});
|
||||
document.querySelectorAll('.size').forEach(el => {
|
||||
const size = Number(el.dataset.size);
|
||||
el.querySelector('.sizebar-bar').style.width = `${size/largest * 100}%`;
|
||||
const sizebar = el.querySelector('.sizebar-bar');
|
||||
if (sizebar) {
|
||||
sizebar.style.width = `${size/largest * 100}%`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function filter() {
|
||||
if (!filterEl) return;
|
||||
const q = filterEl.value.trim().toLowerCase();
|
||||
document.querySelectorAll('tr.file').forEach(function(el) {
|
||||
if (!q) {
|
||||
@@ -916,7 +1135,7 @@ footer {
|
||||
return;
|
||||
}
|
||||
}
|
||||
e.textContent = d.toLocaleString([], {day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit"});
|
||||
e.textContent = d.toLocaleString();
|
||||
}
|
||||
var timeList = Array.prototype.slice.call(document.getElementsByTagName("time"));
|
||||
timeList.forEach(localizeDatetime);
|
||||
|
||||
@@ -25,17 +25,23 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/dustin/go-humanize"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
)
|
||||
|
||||
func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEntry, canGoUp bool, root, urlPath string, repl *caddy.Replacer) browseTemplateContext {
|
||||
func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEntry, canGoUp bool, root, urlPath string, repl *caddy.Replacer) *browseTemplateContext {
|
||||
filesToHide := fsrv.transformHidePaths(repl)
|
||||
|
||||
var dirCount, fileCount int
|
||||
fileInfos := []fileInfo{}
|
||||
name, _ := url.PathUnescape(urlPath)
|
||||
|
||||
tplCtx := &browseTemplateContext{
|
||||
Name: path.Base(name),
|
||||
Path: urlPath,
|
||||
CanGoUp: canGoUp,
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if err := ctx.Err(); err != nil {
|
||||
@@ -61,9 +67,9 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEn
|
||||
// add the slash after the escape of path to avoid escaping the slash as well
|
||||
if isDir {
|
||||
name += "/"
|
||||
dirCount++
|
||||
tplCtx.NumDirs++
|
||||
} else {
|
||||
fileCount++
|
||||
tplCtx.NumFiles++
|
||||
}
|
||||
|
||||
size := info.Size()
|
||||
@@ -82,7 +88,7 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEn
|
||||
|
||||
u := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name
|
||||
|
||||
fileInfos = append(fileInfos, fileInfo{
|
||||
tplCtx.Items = append(tplCtx.Items, fileInfo{
|
||||
IsDir: isDir,
|
||||
IsSymlink: fileIsSymlink,
|
||||
Name: name,
|
||||
@@ -90,17 +96,11 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEn
|
||||
URL: u.String(),
|
||||
ModTime: info.ModTime().UTC(),
|
||||
Mode: info.Mode(),
|
||||
Tpl: tplCtx, // a reference up to the template context is useful
|
||||
})
|
||||
}
|
||||
name, _ := url.PathUnescape(urlPath)
|
||||
return browseTemplateContext{
|
||||
Name: path.Base(name),
|
||||
Path: urlPath,
|
||||
CanGoUp: canGoUp,
|
||||
Items: fileInfos,
|
||||
NumDirs: dirCount,
|
||||
NumFiles: fileCount,
|
||||
}
|
||||
|
||||
return tplCtx
|
||||
}
|
||||
|
||||
// browseTemplateContext provides the template context for directory listings.
|
||||
@@ -230,6 +230,9 @@ type fileInfo struct {
|
||||
Mode os.FileMode `json:"mode"`
|
||||
IsDir bool `json:"is_dir"`
|
||||
IsSymlink bool `json:"is_symlink"`
|
||||
|
||||
// a pointer to the template context is useful inside nested templates
|
||||
Tpl *browseTemplateContext `json:"-"`
|
||||
}
|
||||
|
||||
// HasExt returns true if the filename has any of the given suffixes, case-insensitive.
|
||||
|
||||
@@ -25,6 +25,45 @@ func TestBreadcrumbs(t *testing.T) {
|
||||
}{
|
||||
{"", []crumb{}},
|
||||
{"/", []crumb{{Text: "/"}}},
|
||||
{"/foo/", []crumb{
|
||||
{Link: "../", Text: "/"},
|
||||
{Link: "", Text: "foo"},
|
||||
}},
|
||||
{"/foo/bar/", []crumb{
|
||||
{Link: "../../", Text: "/"},
|
||||
{Link: "../", Text: "foo"},
|
||||
{Link: "", Text: "bar"},
|
||||
}},
|
||||
{"/foo bar/", []crumb{
|
||||
{Link: "../", Text: "/"},
|
||||
{Link: "", Text: "foo bar"},
|
||||
}},
|
||||
{"/foo bar/baz/", []crumb{
|
||||
{Link: "../../", Text: "/"},
|
||||
{Link: "../", Text: "foo bar"},
|
||||
{Link: "", Text: "baz"},
|
||||
}},
|
||||
{"/100%25 test coverage/is a lie/", []crumb{
|
||||
{Link: "../../", Text: "/"},
|
||||
{Link: "../", Text: "100% test coverage"},
|
||||
{Link: "", Text: "is a lie"},
|
||||
}},
|
||||
{"/AC%2FDC/", []crumb{
|
||||
{Link: "../", Text: "/"},
|
||||
{Link: "", Text: "AC/DC"},
|
||||
}},
|
||||
{"/foo/%2e%2e%2f/bar", []crumb{
|
||||
{Link: "../../../", Text: "/"},
|
||||
{Link: "../../", Text: "foo"},
|
||||
{Link: "../", Text: "../"},
|
||||
{Link: "", Text: "bar"},
|
||||
}},
|
||||
{"/foo/../bar", []crumb{
|
||||
{Link: "../../../", Text: "/"},
|
||||
{Link: "../../", Text: "foo"},
|
||||
{Link: "../", Text: ".."},
|
||||
{Link: "", Text: "bar"},
|
||||
}},
|
||||
{"foo/bar/baz", []crumb{
|
||||
{Link: "../../", Text: "foo"},
|
||||
{Link: "../", Text: "bar"},
|
||||
@@ -51,16 +90,16 @@ func TestBreadcrumbs(t *testing.T) {
|
||||
}},
|
||||
}
|
||||
|
||||
for _, d := range testdata {
|
||||
for testNum, d := range testdata {
|
||||
l := browseTemplateContext{Path: d.path}
|
||||
actual := l.Breadcrumbs()
|
||||
if len(actual) != len(d.expected) {
|
||||
t.Errorf("wrong size output, got %d elements but expected %d", len(actual), len(d.expected))
|
||||
t.Errorf("Test %d: Got %d components but expected %d; got: %+v", testNum, len(actual), len(d.expected), actual)
|
||||
continue
|
||||
}
|
||||
for i, c := range actual {
|
||||
if c != d.expected[i] {
|
||||
t.Errorf("got %#v but expected %#v at index %d", c, d.expected[i], i)
|
||||
t.Errorf("Test %d crumb %d: got %#v but expected %#v at index %d", testNum, i, c, d.expected[i], i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,24 +16,30 @@ package fileserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
caddytpl "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/zap"
|
||||
|
||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
||||
caddytpl "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddycmd.RegisterCommand(caddycmd.Command{
|
||||
Name: "file-server",
|
||||
Usage: "[--domain <example.com>] [--root <path>] [--listen <addr>] [--browse] [--access-log]",
|
||||
Usage: "[--domain <example.com>] [--root <path>] [--listen <addr>] [--browse] [--access-log] [--precompressed]",
|
||||
Short: "Spins up a production-ready file server",
|
||||
Long: `
|
||||
A simple but production-ready file server. Useful for quick deployments,
|
||||
@@ -46,17 +52,31 @@ will be changed to the HTTPS port and the server will use HTTPS. If using
|
||||
a public domain, ensure A/AAAA records are properly configured before
|
||||
using this option.
|
||||
|
||||
By default, Zstandard and Gzip compression are enabled. Use --no-compress
|
||||
to disable compression.
|
||||
|
||||
If --browse is enabled, requests for folders without an index file will
|
||||
respond with a file listing.`,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringP("domain", "d", "", "Domain name at which to serve the files")
|
||||
cmd.Flags().StringP("root", "r", "", "The path to the root of the site")
|
||||
cmd.Flags().StringP("listen", "", "", "The address to which to bind the listener")
|
||||
cmd.Flags().StringP("listen", "l", "", "The address to which to bind the listener")
|
||||
cmd.Flags().BoolP("browse", "b", false, "Enable directory browsing")
|
||||
cmd.Flags().BoolP("templates", "t", false, "Enable template rendering")
|
||||
cmd.Flags().BoolP("access-log", "", false, "Enable the access log")
|
||||
cmd.Flags().BoolP("access-log", "a", false, "Enable the access log")
|
||||
cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
|
||||
cmd.Flags().BoolP("no-compress", "", false, "Disable Zstandard and Gzip compression")
|
||||
cmd.Flags().StringSliceP("precompressed", "p", []string{}, "Specify precompression file extensions. Compression preference implied from flag order.")
|
||||
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdFileServer)
|
||||
cmd.AddCommand(&cobra.Command{
|
||||
Use: "export-template",
|
||||
Short: "Exports the default file browser template",
|
||||
Example: "caddy file-server export-template > browse.html",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
_, err := io.WriteString(os.Stdout, BrowseTemplate)
|
||||
return err
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -71,15 +91,64 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
|
||||
templates := fs.Bool("templates")
|
||||
accessLog := fs.Bool("access-log")
|
||||
debug := fs.Bool("debug")
|
||||
compress := !fs.Bool("no-compress")
|
||||
precompressed, err := fs.GetStringSlice("precompressed")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid precompressed flag: %v", err)
|
||||
}
|
||||
|
||||
var handlers []json.RawMessage
|
||||
|
||||
if compress {
|
||||
zstd, err := caddy.GetModule("http.encoders.zstd")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
gzip, err := caddy.GetModule("http.encoders.gzip")
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
handlers = append(handlers, caddyconfig.JSONModuleObject(encode.Encode{
|
||||
EncodingsRaw: caddy.ModuleMap{
|
||||
"zstd": caddyconfig.JSON(zstd.New(), nil),
|
||||
"gzip": caddyconfig.JSON(gzip.New(), nil),
|
||||
},
|
||||
Prefer: []string{"zstd", "gzip"},
|
||||
}, "handler", "encode", nil))
|
||||
}
|
||||
|
||||
if templates {
|
||||
handler := caddytpl.Templates{FileRoot: root}
|
||||
handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "templates", nil))
|
||||
}
|
||||
|
||||
handler := FileServer{Root: root}
|
||||
|
||||
if len(precompressed) != 0 {
|
||||
// logic mirrors modules/caddyhttp/fileserver/caddyfile.go case "precompressed"
|
||||
var order []string
|
||||
for _, compression := range precompressed {
|
||||
modID := "http.precompressed." + compression
|
||||
mod, err := caddy.GetModule(modID)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("getting module named '%s': %v", modID, err)
|
||||
}
|
||||
inst := mod.New()
|
||||
precompress, ok := inst.(encode.Precompressed)
|
||||
if !ok {
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("module %s is not a precompressor; is %T", modID, inst)
|
||||
}
|
||||
if handler.PrecompressedRaw == nil {
|
||||
handler.PrecompressedRaw = make(caddy.ModuleMap)
|
||||
}
|
||||
handler.PrecompressedRaw[compression] = caddyconfig.JSON(precompress, nil)
|
||||
order = append(order, compression)
|
||||
}
|
||||
handler.PrecompressedOrder = order
|
||||
}
|
||||
|
||||
if browse {
|
||||
handler.Browse = new(Browse)
|
||||
}
|
||||
@@ -141,7 +210,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
err := caddy.Run(cfg)
|
||||
err = caddy.Run(cfg)
|
||||
if err != nil {
|
||||
return caddy.ExitCodeFailedStartup, err
|
||||
}
|
||||
|
||||
@@ -26,9 +26,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common"
|
||||
"github.com/google/cel-go/common/operators"
|
||||
@@ -36,6 +33,10 @@ import (
|
||||
"github.com/google/cel-go/parser"
|
||||
"go.uber.org/zap"
|
||||
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -558,7 +559,7 @@ func indexFold(haystack, needle string) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
// isCELMapLiteral returns whether the expression resolves to a map literal containing
|
||||
// isCELTryFilesLiteral returns whether the expression resolves to a map literal containing
|
||||
// only string keys with or a placeholder call.
|
||||
func isCELTryFilesLiteral(e *exprpb.Expr) bool {
|
||||
switch e.GetExprKind().(type) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user