mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-26 08:42:31 -04:00
Compare commits
162 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 |
+3
-3
@@ -7,7 +7,7 @@ The Caddy project would like to make sure that it stays on top of all practicall
|
|||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | ------------------ |
|
| ------- | ------------------ |
|
||||||
| 2.x | :white_check_mark: |
|
| 2.x | ✔️ |
|
||||||
| 1.x | :x: |
|
| 1.x | :x: |
|
||||||
| < 1.x | :x: |
|
| < 1.x | :x: |
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ We do not accept reports if the steps imply or require a compromised system or t
|
|||||||
|
|
||||||
Client-side exploits are out of scope. In other words, it is not a bug in Caddy if the web browser does something unsafe, even if the downloaded content was served by Caddy. (Those kinds of exploits can generally be mitigated by proper configuration of HTTP headers.) As a general rule, the content served by Caddy is not considered in scope because content is configurable by the site owner or the associated web application.
|
Client-side exploits are out of scope. In other words, it is not a bug in Caddy if the web browser does something unsafe, even if the downloaded content was served by Caddy. (Those kinds of exploits can generally be mitigated by proper configuration of HTTP headers.) As a general rule, the content served by Caddy is not considered in scope because content is configurable by the site owner or the associated web application.
|
||||||
|
|
||||||
Security bugs in code dependencies 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
|
## Reporting a Vulnerability
|
||||||
@@ -42,7 +42,7 @@ We'll need enough information to verify the bug and make a patch. To speed thing
|
|||||||
- Specific minimal steps to reproduce the issue from scratch
|
- Specific minimal steps to reproduce the issue from scratch
|
||||||
- A working patch
|
- A working patch
|
||||||
|
|
||||||
Please DO NOT use containers, VMs, cloud instances or services, or any other complex infrastructure in your steps. Always prefer `curl` 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.
|
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
|
# Default is true, cancels jobs for other platforms in the matrix if one fails
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
os:
|
||||||
go: [ '1.19', '1.20' ]
|
- ubuntu-latest
|
||||||
|
- macos-latest
|
||||||
|
- windows-latest
|
||||||
|
go:
|
||||||
|
- '1.20'
|
||||||
|
- '1.21'
|
||||||
|
|
||||||
include:
|
include:
|
||||||
# Set the minimum Go patch version for the given Go minor
|
# Set the minimum Go patch version for the given Go minor
|
||||||
# Usable via ${{ matrix.GO_SEMVER }}
|
# Usable via ${{ matrix.GO_SEMVER }}
|
||||||
- go: '1.19'
|
|
||||||
GO_SEMVER: '~1.19.6'
|
|
||||||
|
|
||||||
- go: '1.20'
|
- 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 }}
|
# Set some variables per OS, usable via ${{ matrix.VAR }}
|
||||||
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
|
# CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
|
||||||
@@ -49,7 +54,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
@@ -68,6 +73,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Print Go version and environment
|
- name: Print Go version and environment
|
||||||
id: vars
|
id: vars
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
printf "Using go at: $(which go)\n"
|
printf "Using go at: $(which go)\n"
|
||||||
printf "Go version: $(go version)\n"
|
printf "Go version: $(go version)\n"
|
||||||
@@ -130,7 +136,7 @@ jobs:
|
|||||||
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
|
continue-on-error: true # August 2020: s390x VM is down due to weather and power issues
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
|
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
|
||||||
@@ -156,9 +162,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: goreleaser/goreleaser-action@v4
|
- uses: goreleaser/goreleaser-action@v5
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: check
|
args: check
|
||||||
|
|||||||
@@ -15,20 +15,33 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
goos: ['android', 'linux', 'solaris', 'illumos', 'dragonfly', 'freebsd', 'openbsd', 'plan9', 'windows', 'darwin', 'netbsd']
|
goos:
|
||||||
go: [ '1.20' ]
|
- 'aix'
|
||||||
|
- 'android'
|
||||||
|
- 'linux'
|
||||||
|
- 'solaris'
|
||||||
|
- 'illumos'
|
||||||
|
- 'dragonfly'
|
||||||
|
- 'freebsd'
|
||||||
|
- 'openbsd'
|
||||||
|
- 'plan9'
|
||||||
|
- 'windows'
|
||||||
|
- 'darwin'
|
||||||
|
- 'netbsd'
|
||||||
|
go:
|
||||||
|
- '1.21'
|
||||||
|
|
||||||
include:
|
include:
|
||||||
# Set the minimum Go patch version for the given Go minor
|
# Set the minimum Go patch version for the given Go minor
|
||||||
# Usable via ${{ matrix.GO_SEMVER }}
|
# Usable via ${{ matrix.GO_SEMVER }}
|
||||||
- go: '1.20'
|
- go: '1.21'
|
||||||
GO_SEMVER: '~1.20.1'
|
GO_SEMVER: '~1.21.0'
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
@@ -50,11 +63,12 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
GOOS: ${{ matrix.goos }}
|
GOOS: ${{ matrix.goos }}
|
||||||
|
GOARCH: ${{ matrix.goos == 'aix' && 'ppc64' || 'amd64' }}
|
||||||
shell: bash
|
shell: bash
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
working-directory: ./cmd/caddy
|
working-directory: ./cmd/caddy
|
||||||
run: |
|
run: |
|
||||||
GOOS=$GOOS 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
|
if [ $? -ne 0 ]; then
|
||||||
echo "::warning ::$GOOS Build Failed"
|
echo "::warning ::$GOOS Build Failed"
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -17,25 +17,45 @@ jobs:
|
|||||||
# From https://github.com/golangci/golangci-lint-action
|
# From https://github.com/golangci/golangci-lint-action
|
||||||
golangci:
|
golangci:
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # for actions/checkout to fetch code
|
contents: read # for actions/checkout to fetch code
|
||||||
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
|
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
|
||||||
name: lint
|
name: lint
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os:
|
||||||
|
- ubuntu-latest
|
||||||
|
- macos-latest
|
||||||
|
- windows-latest
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: '~1.19.6'
|
go-version: '~1.21.0'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
|
# Workaround for https://github.com/golangci/golangci-lint-action/issues/135
|
||||||
|
skip-pkg-cache: true
|
||||||
|
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v3
|
uses: golangci/golangci-lint-action@v3
|
||||||
with:
|
with:
|
||||||
version: v1.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.
|
# Windows times out frequently after about 5m50s if we don't set a longer timeout.
|
||||||
args: --timeout 10m
|
args: --timeout 10m
|
||||||
|
|
||||||
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
# Optional: show only new issues if it's a pull request. The default value is `false`.
|
||||||
# only-new-issues: true
|
# only-new-issues: true
|
||||||
|
|
||||||
|
govulncheck:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: govulncheck
|
||||||
|
uses: golang/govulncheck-action@v1
|
||||||
|
with:
|
||||||
|
go-version-input: '~1.21.0'
|
||||||
|
check-latest: true
|
||||||
|
|||||||
@@ -10,14 +10,16 @@ jobs:
|
|||||||
name: Release
|
name: Release
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-latest ]
|
os:
|
||||||
go: [ '1.20' ]
|
- ubuntu-latest
|
||||||
|
go:
|
||||||
|
- '1.21'
|
||||||
|
|
||||||
include:
|
include:
|
||||||
# Set the minimum Go patch version for the given Go minor
|
# Set the minimum Go patch version for the given Go minor
|
||||||
# Usable via ${{ matrix.GO_SEMVER }}
|
# Usable via ${{ matrix.GO_SEMVER }}
|
||||||
- go: '1.20'
|
- go: '1.21'
|
||||||
GO_SEMVER: '~1.20.1'
|
GO_SEMVER: '~1.21.0'
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
|
# https://github.com/sigstore/cosign/issues/1258#issuecomment-1002251233
|
||||||
@@ -30,7 +32,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -41,7 +43,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
# Force fetch upstream tags -- because 65 minutes
|
# Force fetch upstream tags -- because 65 minutes
|
||||||
# tl;dr: actions/checkout@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/
|
# git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/
|
||||||
# which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran:
|
# which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran:
|
||||||
# git fetch --prune --unshallow
|
# git fetch --prune --unshallow
|
||||||
@@ -104,10 +106,10 @@ jobs:
|
|||||||
run: syft version
|
run: syft version
|
||||||
# GoReleaser will take care of publishing those artifacts into the release
|
# GoReleaser will take care of publishing those artifacts into the release
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v4
|
uses: goreleaser/goreleaser-action@v5
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: release --rm-dist --timeout 60m
|
args: release --clean --timeout 60m
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAG: ${{ steps.vars.outputs.version_tag }}
|
TAG: ${{ steps.vars.outputs.version_tag }}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ jobs:
|
|||||||
name: Release Published
|
name: Release Published
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-latest ]
|
os:
|
||||||
|
- ubuntu-latest
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ Caddyfile.*
|
|||||||
# build artifacts and helpers
|
# build artifacts and helpers
|
||||||
cmd/caddy/caddy
|
cmd/caddy/caddy
|
||||||
cmd/caddy/caddy.exe
|
cmd/caddy/caddy.exe
|
||||||
|
cmd/caddy/tmp/*.exe
|
||||||
|
cmd/caddy/.env
|
||||||
|
|
||||||
# mac specific
|
# mac specific
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
+20
-7
@@ -2,14 +2,27 @@ linters-settings:
|
|||||||
errcheck:
|
errcheck:
|
||||||
ignore: fmt:.*,go.uber.org/zap/zapcore:^Add.*
|
ignore: fmt:.*,go.uber.org/zap/zapcore:^Add.*
|
||||||
ignoretests: true
|
ignoretests: true
|
||||||
|
gci:
|
||||||
|
sections:
|
||||||
|
- standard # Standard section: captures all standard packages.
|
||||||
|
- default # Default section: contains all imports that could not be matched to another section type.
|
||||||
|
- prefix(github.com/caddyserver/caddy/v2/cmd) # ensure that this is always at the top and always has a line break.
|
||||||
|
- prefix(github.com/caddyserver/caddy) # Custom section: groups all imports with the specified Prefix.
|
||||||
|
# Skip generated files.
|
||||||
|
# Default: true
|
||||||
|
skip-generated: true
|
||||||
|
# Enable custom order of sections.
|
||||||
|
# If `true`, make the section order the same as the order of `sections`.
|
||||||
|
# Default: false
|
||||||
|
custom-order: true
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
disable-all: true
|
disable-all: true
|
||||||
enable:
|
enable:
|
||||||
- bodyclose
|
- bodyclose
|
||||||
- errcheck
|
- errcheck
|
||||||
- gofmt
|
- gci
|
||||||
- goimports
|
- gofumpt
|
||||||
- gosec
|
- gosec
|
||||||
- gosimple
|
- gosimple
|
||||||
- govet
|
- govet
|
||||||
@@ -77,23 +90,23 @@ output:
|
|||||||
issues:
|
issues:
|
||||||
exclude-rules:
|
exclude-rules:
|
||||||
# we aren't calling unknown URL
|
# we aren't calling unknown URL
|
||||||
- text: "G107" # G107: Url provided to HTTP request as taint input
|
- text: 'G107' # G107: Url provided to HTTP request as taint input
|
||||||
linters:
|
linters:
|
||||||
- gosec
|
- gosec
|
||||||
# as a web server that's expected to handle any template, this is totally in the hands of the user.
|
# as a web server that's expected to handle any template, this is totally in the hands of the user.
|
||||||
- text: "G203" # G203: Use of unescaped data in HTML templates
|
- text: 'G203' # G203: Use of unescaped data in HTML templates
|
||||||
linters:
|
linters:
|
||||||
- gosec
|
- gosec
|
||||||
# we're shelling out to known commands, not relying on user-defined input.
|
# we're shelling out to known commands, not relying on user-defined input.
|
||||||
- text: "G204" # G204: Audit use of command execution
|
- text: 'G204' # G204: Audit use of command execution
|
||||||
linters:
|
linters:
|
||||||
- gosec
|
- gosec
|
||||||
# the choice of weakrand is deliberate, hence the named import "weakrand"
|
# the choice of weakrand is deliberate, hence the named import "weakrand"
|
||||||
- path: modules/caddyhttp/reverseproxy/selectionpolicies.go
|
- path: modules/caddyhttp/reverseproxy/selectionpolicies.go
|
||||||
text: "G404" # G404: Insecure random number source (rand)
|
text: 'G404' # G404: Insecure random number source (rand)
|
||||||
linters:
|
linters:
|
||||||
- gosec
|
- gosec
|
||||||
- path: modules/caddyhttp/reverseproxy/streaming.go
|
- path: modules/caddyhttp/reverseproxy/streaming.go
|
||||||
text: "G404" # G404: Insecure random number source (rand)
|
text: 'G404' # G404: Insecure random number source (rand)
|
||||||
linters:
|
linters:
|
||||||
- gosec
|
- gosec
|
||||||
|
|||||||
+8
-10
@@ -43,6 +43,7 @@ builds:
|
|||||||
- arm64
|
- arm64
|
||||||
- s390x
|
- s390x
|
||||||
- ppc64le
|
- ppc64le
|
||||||
|
- riscv64
|
||||||
goarm:
|
goarm:
|
||||||
- "5"
|
- "5"
|
||||||
- "6"
|
- "6"
|
||||||
@@ -54,14 +55,20 @@ builds:
|
|||||||
goarch: ppc64le
|
goarch: ppc64le
|
||||||
- goos: darwin
|
- goos: darwin
|
||||||
goarch: s390x
|
goarch: s390x
|
||||||
|
- goos: darwin
|
||||||
|
goarch: riscv64
|
||||||
- goos: windows
|
- goos: windows
|
||||||
goarch: ppc64le
|
goarch: ppc64le
|
||||||
- goos: windows
|
- goos: windows
|
||||||
goarch: s390x
|
goarch: s390x
|
||||||
|
- goos: windows
|
||||||
|
goarch: riscv64
|
||||||
- goos: freebsd
|
- goos: freebsd
|
||||||
goarch: ppc64le
|
goarch: ppc64le
|
||||||
- goos: freebsd
|
- goos: freebsd
|
||||||
goarch: s390x
|
goarch: s390x
|
||||||
|
- goos: freebsd
|
||||||
|
goarch: riscv64
|
||||||
- goos: freebsd
|
- goos: freebsd
|
||||||
goarch: arm
|
goarch: arm
|
||||||
goarm: "5"
|
goarm: "5"
|
||||||
@@ -106,11 +113,10 @@ archives:
|
|||||||
{{- with .Mips }}_{{ . }}{{ end }}
|
{{- with .Mips }}_{{ . }}{{ end }}
|
||||||
{{- if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ 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.
|
# allowing users to build the exact same set of files as ours.
|
||||||
- id: source
|
- id: source
|
||||||
meta: true
|
meta: true
|
||||||
rlcp: true
|
|
||||||
name_template: "{{ .ProjectName }}_{{ .Version }}_buildable-artifact"
|
name_template: "{{ .ProjectName }}_{{ .Version }}_buildable-artifact"
|
||||||
files:
|
files:
|
||||||
- src: LICENSE
|
- src: LICENSE
|
||||||
@@ -127,14 +133,6 @@ source:
|
|||||||
name_template: '{{ .ProjectName }}_{{ .Version }}_src'
|
name_template: '{{ .ProjectName }}_{{ .Version }}_src'
|
||||||
format: 'tar.gz'
|
format: 'tar.gz'
|
||||||
|
|
||||||
# This will make the destination paths be relative to the longest common
|
|
||||||
# path prefix between all the files matched and the source glob.
|
|
||||||
# Enabling this essentially mimic the behavior of nfpm's contents section.
|
|
||||||
# It will be the default by June 2023.
|
|
||||||
#
|
|
||||||
# Default: false
|
|
||||||
rlcp: true
|
|
||||||
|
|
||||||
# Additional files/template/globs you want to add to the source archive.
|
# Additional files/template/globs you want to add to the source archive.
|
||||||
#
|
#
|
||||||
# Default: empty.
|
# Default: empty.
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ See [our online documentation](https://caddyserver.com/docs/install) for other i
|
|||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
|
|
||||||
- [Go 1.19 or newer](https://golang.org/dl/)
|
- [Go 1.20 or newer](https://golang.org/dl/)
|
||||||
|
|
||||||
### For development
|
### For development
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ func init() {
|
|||||||
if env, exists := os.LookupEnv("CADDY_ADMIN"); exists {
|
if env, exists := os.LookupEnv("CADDY_ADMIN"); exists {
|
||||||
DefaultAdminListen = env
|
DefaultAdminListen = env
|
||||||
}
|
}
|
||||||
|
RegisterNamespace("caddy.config_loaders", []interface{}{(*ConfigLoader)(nil)})
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminConfig configures Caddy's API endpoint, which is used
|
// AdminConfig configures Caddy's API endpoint, which is used
|
||||||
@@ -318,7 +319,32 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
|
|||||||
// messages. If the requested URI does not include an Internet host
|
// messages. If the requested URI does not include an Internet host
|
||||||
// name for the service being requested, then the Host header field MUST
|
// name for the service being requested, then the Host header field MUST
|
||||||
// be given with an empty value."
|
// be given with an empty value."
|
||||||
|
//
|
||||||
|
// UPDATE July 2023: Go broke this by patching a minor security bug in 1.20.6.
|
||||||
|
// Understandable, but frustrating. See:
|
||||||
|
// https://github.com/golang/go/issues/60374
|
||||||
|
// See also the discussion here:
|
||||||
|
// https://github.com/golang/go/issues/61431
|
||||||
|
//
|
||||||
|
// We can no longer conform to RFC 2616 Section 14.26 from either Go or curl
|
||||||
|
// in purity. (Curl allowed no host between 7.40 and 7.50, but now requires a
|
||||||
|
// bogus host; see https://superuser.com/a/925610.) If we disable Host/Origin
|
||||||
|
// security checks, the infosec community assures me that it is secure to do
|
||||||
|
// so, because:
|
||||||
|
// 1) Browsers do not allow access to unix sockets
|
||||||
|
// 2) DNS is irrelevant to unix sockets
|
||||||
|
//
|
||||||
|
// I am not quite ready to trust either of those external factors, so instead
|
||||||
|
// of disabling Host/Origin checks, we now allow specific Host values when
|
||||||
|
// accessing the admin endpoint over unix sockets. I definitely don't trust
|
||||||
|
// DNS (e.g. I don't trust 'localhost' to always resolve to the local host),
|
||||||
|
// and IP shouldn't even be used, but if it is for some reason, I think we can
|
||||||
|
// at least be reasonably assured that 127.0.0.1 and ::1 route to the local
|
||||||
|
// machine, meaning that a hypothetical browser origin would have to be on the
|
||||||
|
// local machine as well.
|
||||||
uniqueOrigins[""] = struct{}{}
|
uniqueOrigins[""] = struct{}{}
|
||||||
|
uniqueOrigins["127.0.0.1"] = struct{}{}
|
||||||
|
uniqueOrigins["::1"] = struct{}{}
|
||||||
} else {
|
} else {
|
||||||
uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
|
uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
|
||||||
uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
|
uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
|
||||||
@@ -1016,9 +1042,9 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
|
|||||||
id := parts[2]
|
id := parts[2]
|
||||||
|
|
||||||
// map the ID to the expanded path
|
// map the ID to the expanded path
|
||||||
currentCtxMu.RLock()
|
rawCfgMu.RLock()
|
||||||
expanded, ok := rawCfgIndex[id]
|
expanded, ok := rawCfgIndex[id]
|
||||||
defer currentCtxMu.RUnlock()
|
rawCfgMu.RUnlock()
|
||||||
if !ok {
|
if !ok {
|
||||||
return APIError{
|
return APIError{
|
||||||
HTTPStatus: http.StatusNotFound,
|
HTTPStatus: http.StatusNotFound,
|
||||||
@@ -1171,15 +1197,27 @@ traverseLoop:
|
|||||||
}
|
}
|
||||||
case http.MethodPut:
|
case http.MethodPut:
|
||||||
if _, ok := v[part]; ok {
|
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
|
v[part] = val
|
||||||
case http.MethodPatch:
|
case http.MethodPatch:
|
||||||
if _, ok := v[part]; !ok {
|
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
|
v[part] = val
|
||||||
case http.MethodDelete:
|
case http.MethodDelete:
|
||||||
|
if _, ok := v[part]; !ok {
|
||||||
|
return APIError{
|
||||||
|
HTTPStatus: http.StatusNotFound,
|
||||||
|
Err: fmt.Errorf("[%s] key does not exist: %s", path, part),
|
||||||
|
}
|
||||||
|
}
|
||||||
delete(v, part)
|
delete(v, part)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unrecognized method %s", method)
|
return fmt.Errorf("unrecognized method %s", method)
|
||||||
@@ -1321,7 +1359,7 @@ var (
|
|||||||
// will get deleted before the process gracefully exits.
|
// will get deleted before the process gracefully exits.
|
||||||
func PIDFile(filename string) error {
|
func PIDFile(filename string) error {
|
||||||
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
|
pid := []byte(strconv.Itoa(os.Getpid()) + "\n")
|
||||||
err := os.WriteFile(filename, pid, 0600)
|
err := os.WriteFile(filename, pid, 0o600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,12 @@ func TestUnsyncedConfigAccess(t *testing.T) {
|
|||||||
path: "/bar/qq",
|
path: "/bar/qq",
|
||||||
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
|
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
path: "/bar/qq",
|
||||||
|
expect: `{"foo": "jet", "bar": {"aa": "bb"}, "list": ["a", "b", "c"]}`,
|
||||||
|
shouldErr: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
path: "/list",
|
path: "/list",
|
||||||
|
|||||||
@@ -34,12 +34,22 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/notify"
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/notify"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterNamespace("", []interface{}{
|
||||||
|
(*App)(nil),
|
||||||
|
})
|
||||||
|
RegisterNamespace("caddy.storage", []interface{}{
|
||||||
|
(*StorageConverter)(nil),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Config is the top (or beginning) of the Caddy configuration structure.
|
// Config is the top (or beginning) of the Caddy configuration structure.
|
||||||
// Caddy config is expressed natively as a JSON document. If you prefer
|
// Caddy config is expressed natively as a JSON document. If you prefer
|
||||||
// not to work with JSON directly, there are [many config adapters](/docs/config-adapters)
|
// not to work with JSON directly, there are [many config adapters](/docs/config-adapters)
|
||||||
@@ -71,11 +81,15 @@ type Config struct {
|
|||||||
// module is `caddy.storage.file_system` (the local file system),
|
// module is `caddy.storage.file_system` (the local file system),
|
||||||
// and the default path
|
// and the default path
|
||||||
// [depends on the OS and environment](/docs/conventions#data-directory).
|
// [depends on the OS and environment](/docs/conventions#data-directory).
|
||||||
|
// A storage `module` should implement the following interfaces:
|
||||||
|
// - [StorageConverter](https://pkg.go.dev/github.com/caddyserver/caddy/v2#StorageConverter)
|
||||||
StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`
|
StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`
|
||||||
|
|
||||||
// AppsRaw are the apps that Caddy will load and run. The
|
// AppsRaw are the apps that Caddy will load and run. The
|
||||||
// app module name is the key, and the app's config is the
|
// app module name is the key, and the app's config is the
|
||||||
// associated value.
|
// associated value.
|
||||||
|
// An `app` should implement the following interfaces:
|
||||||
|
// - [caddy.App](https://pkg.go.dev/github.com/caddyserver/caddy/v2?tab=doc#App)
|
||||||
AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="`
|
AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="`
|
||||||
|
|
||||||
apps map[string]App
|
apps map[string]App
|
||||||
@@ -156,8 +170,8 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
|
|||||||
return fmt.Errorf("method not allowed")
|
return fmt.Errorf("method not allowed")
|
||||||
}
|
}
|
||||||
|
|
||||||
currentCtxMu.Lock()
|
rawCfgMu.Lock()
|
||||||
defer currentCtxMu.Unlock()
|
defer rawCfgMu.Unlock()
|
||||||
|
|
||||||
if ifMatchHeader != "" {
|
if ifMatchHeader != "" {
|
||||||
// expect the first and last character to be quotes
|
// 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
|
// readConfig traverses the current config to path
|
||||||
// and writes its JSON encoding to out.
|
// and writes its JSON encoding to out.
|
||||||
func readConfig(path string, out io.Writer) error {
|
func readConfig(path string, out io.Writer) error {
|
||||||
currentCtxMu.RLock()
|
rawCfgMu.RLock()
|
||||||
defer currentCtxMu.RUnlock()
|
defer rawCfgMu.RUnlock()
|
||||||
return unsyncedConfigAccess(http.MethodGet, path, nil, out)
|
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 as the new config, replacing any other current config.
|
||||||
// It does NOT update the raw config state, as this is a
|
// It does NOT update the raw config state, as this is a
|
||||||
// lower-level function; most callers will want to use Load
|
// lower-level function; most callers will want to use Load
|
||||||
// instead. A write lock on currentCtxMu is required! If
|
// instead. A write lock on rawCfgMu is required! If
|
||||||
// allowPersist is false, it will not be persisted to disk,
|
// allowPersist is false, it will not be persisted to disk,
|
||||||
// even if it is configured to.
|
// even if it is configured to.
|
||||||
func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
||||||
@@ -340,8 +354,10 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// swap old context (including its config) with the new one
|
// swap old context (including its config) with the new one
|
||||||
|
currentCtxMu.Lock()
|
||||||
oldCtx := currentCtx
|
oldCtx := currentCtx
|
||||||
currentCtx = ctx
|
currentCtx = ctx
|
||||||
|
currentCtxMu.Unlock()
|
||||||
|
|
||||||
// Stop, Cleanup each old app
|
// Stop, Cleanup each old app
|
||||||
unsyncedStop(oldCtx)
|
unsyncedStop(oldCtx)
|
||||||
@@ -354,13 +370,13 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
|||||||
newCfg.Admin.Config.Persist == nil ||
|
newCfg.Admin.Config.Persist == nil ||
|
||||||
*newCfg.Admin.Config.Persist) {
|
*newCfg.Admin.Config.Persist) {
|
||||||
dir := filepath.Dir(ConfigAutosavePath)
|
dir := filepath.Dir(ConfigAutosavePath)
|
||||||
err := os.MkdirAll(dir, 0700)
|
err := os.MkdirAll(dir, 0o700)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Log().Error("unable to create folder for config autosave",
|
Log().Error("unable to create folder for config autosave",
|
||||||
zap.String("dir", dir),
|
zap.String("dir", dir),
|
||||||
zap.Error(err))
|
zap.Error(err))
|
||||||
} else {
|
} else {
|
||||||
err := os.WriteFile(ConfigAutosavePath, cfgJSON, 0600)
|
err := os.WriteFile(ConfigAutosavePath, cfgJSON, 0o600)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
Log().Info("autosaved config (load with --resume flag)", zap.String("file", ConfigAutosavePath))
|
Log().Info("autosaved config (load with --resume flag)", zap.String("file", ConfigAutosavePath))
|
||||||
} else {
|
} else {
|
||||||
@@ -627,22 +643,35 @@ type ConfigLoader interface {
|
|||||||
// stop the others. Stop should only be called
|
// stop the others. Stop should only be called
|
||||||
// if not replacing with a new config.
|
// if not replacing with a new config.
|
||||||
func Stop() error {
|
func Stop() error {
|
||||||
|
currentCtxMu.RLock()
|
||||||
|
ctx := currentCtx
|
||||||
|
currentCtxMu.RUnlock()
|
||||||
|
|
||||||
|
rawCfgMu.Lock()
|
||||||
|
unsyncedStop(ctx)
|
||||||
|
|
||||||
currentCtxMu.Lock()
|
currentCtxMu.Lock()
|
||||||
defer currentCtxMu.Unlock()
|
|
||||||
unsyncedStop(currentCtx)
|
|
||||||
currentCtx = Context{}
|
currentCtx = Context{}
|
||||||
|
currentCtxMu.Unlock()
|
||||||
|
|
||||||
rawCfgJSON = nil
|
rawCfgJSON = nil
|
||||||
rawCfgIndex = nil
|
rawCfgIndex = nil
|
||||||
rawCfg[rawConfigKey] = nil
|
rawCfg[rawConfigKey] = nil
|
||||||
|
rawCfgMu.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// unsyncedStop stops cfg from running, but has
|
// unsyncedStop stops ctx from running, but has
|
||||||
// no locking around cfg. It is a no-op if cfg is
|
// no locking around ctx. It is a no-op if ctx has a
|
||||||
// nil. If any app returns an error when stopping,
|
// nil cfg. If any app returns an error when stopping,
|
||||||
// it is logged and the function continues stopping
|
// it is logged and the function continues stopping
|
||||||
// the next app. This function assumes all apps in
|
// the next app. This function assumes all apps in
|
||||||
// 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) {
|
func unsyncedStop(ctx Context) {
|
||||||
if ctx.cfg == nil {
|
if ctx.cfg == nil {
|
||||||
return
|
return
|
||||||
@@ -809,14 +838,19 @@ func ParseDuration(s string) (time.Duration, error) {
|
|||||||
// regardless of storage configuration, since each instance is intended to
|
// regardless of storage configuration, since each instance is intended to
|
||||||
// have its own unique ID.
|
// have its own unique ID.
|
||||||
func InstanceID() (uuid.UUID, error) {
|
func InstanceID() (uuid.UUID, error) {
|
||||||
uuidFilePath := filepath.Join(AppDataDir(), "instance.uuid")
|
appDataDir := AppDataDir()
|
||||||
|
uuidFilePath := filepath.Join(appDataDir, "instance.uuid")
|
||||||
uuidFileBytes, err := os.ReadFile(uuidFilePath)
|
uuidFileBytes, err := os.ReadFile(uuidFilePath)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
uuid, err := uuid.NewRandom()
|
uuid, err := uuid.NewRandom()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return uuid, err
|
return uuid, err
|
||||||
}
|
}
|
||||||
err = os.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
|
return uuid, err
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return [16]byte{}, err
|
return [16]byte{}, err
|
||||||
@@ -969,14 +1003,12 @@ type CtxKey string
|
|||||||
|
|
||||||
// This group of variables pertains to the current configuration.
|
// This group of variables pertains to the current configuration.
|
||||||
var (
|
var (
|
||||||
// currentCtxMu protects everything in this var block.
|
|
||||||
currentCtxMu sync.RWMutex
|
|
||||||
|
|
||||||
// currentCtx is the root context for the currently-running
|
// currentCtx is the root context for the currently-running
|
||||||
// configuration, which can be accessed through this value.
|
// configuration, which can be accessed through this value.
|
||||||
// If the Config contained in this value is not nil, then
|
// If the Config contained in this value is not nil, then
|
||||||
// a config is currently active/running.
|
// a config is currently active/running.
|
||||||
currentCtx Context
|
currentCtx Context
|
||||||
|
currentCtxMu sync.RWMutex
|
||||||
|
|
||||||
// rawCfg is the current, generic-decoded configuration;
|
// rawCfg is the current, generic-decoded configuration;
|
||||||
// we initialize it as a map with one field ("config")
|
// we initialize it as a map with one field ("config")
|
||||||
@@ -994,6 +1026,10 @@ var (
|
|||||||
// rawCfgIndex is the map of user-assigned ID to expanded
|
// rawCfgIndex is the map of user-assigned ID to expanded
|
||||||
// path, for converting /id/ paths to /config/ paths.
|
// path, for converting /id/ paths to /config/ paths.
|
||||||
rawCfgIndex map[string]string
|
rawCfgIndex map[string]string
|
||||||
|
|
||||||
|
// rawCfgMu protects all the rawCfg fields and also
|
||||||
|
// essentially synchronizes config changes/reloads.
|
||||||
|
rawCfgMu sync.RWMutex
|
||||||
)
|
)
|
||||||
|
|
||||||
// errSameConfig is returned if the new config is the same
|
// errSameConfig is returned if the new config is the same
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Dispenser is a type that dispenses tokens, similarly to a lexer,
|
// Dispenser is a type that dispenses tokens, similarly to a lexer,
|
||||||
@@ -105,7 +106,7 @@ func (d *Dispenser) nextOnSameLine() bool {
|
|||||||
}
|
}
|
||||||
curr := d.tokens[d.cursor]
|
curr := d.tokens[d.cursor]
|
||||||
next := d.tokens[d.cursor+1]
|
next := d.tokens[d.cursor+1]
|
||||||
if curr.File == next.File && curr.Line+curr.NumLineBreaks() == next.Line {
|
if !isNextOnNewLine(curr, next) {
|
||||||
d.cursor++
|
d.cursor++
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -126,7 +127,7 @@ func (d *Dispenser) NextLine() bool {
|
|||||||
}
|
}
|
||||||
curr := d.tokens[d.cursor]
|
curr := d.tokens[d.cursor]
|
||||||
next := d.tokens[d.cursor+1]
|
next := d.tokens[d.cursor+1]
|
||||||
if curr.File != next.File || curr.Line+curr.NumLineBreaks() < next.Line {
|
if isNextOnNewLine(curr, next) {
|
||||||
d.cursor++
|
d.cursor++
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -390,22 +391,22 @@ func (d *Dispenser) Reset() {
|
|||||||
// an argument.
|
// an argument.
|
||||||
func (d *Dispenser) ArgErr() error {
|
func (d *Dispenser) ArgErr() error {
|
||||||
if d.Val() == "{" {
|
if d.Val() == "{" {
|
||||||
return d.Err("Unexpected token '{', expecting argument")
|
return d.Err("unexpected token '{', expecting argument")
|
||||||
}
|
}
|
||||||
return d.Errf("Wrong argument count or unexpected line ending after '%s'", d.Val())
|
return d.Errf("wrong argument count or unexpected line ending after '%s'", d.Val())
|
||||||
}
|
}
|
||||||
|
|
||||||
// SyntaxErr creates a generic syntax error which explains what was
|
// SyntaxErr creates a generic syntax error which explains what was
|
||||||
// found and what was expected.
|
// found and what was expected.
|
||||||
func (d *Dispenser) SyntaxErr(expected string) error {
|
func (d *Dispenser) SyntaxErr(expected string) error {
|
||||||
msg := fmt.Sprintf("%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)
|
return errors.New(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EOFErr returns an error indicating that the dispenser reached
|
// EOFErr returns an error indicating that the dispenser reached
|
||||||
// the end of the input when searching for the next token.
|
// the end of the input when searching for the next token.
|
||||||
func (d *Dispenser) EOFErr() error {
|
func (d *Dispenser) EOFErr() error {
|
||||||
return d.Errf("Unexpected EOF")
|
return d.Errf("unexpected EOF")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Err generates a custom parse-time error with a message of msg.
|
// Err generates a custom parse-time error with a message of msg.
|
||||||
@@ -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.
|
// WrapErr takes an existing error and adds the Caddyfile file and line number.
|
||||||
func (d *Dispenser) WrapErr(err error) error {
|
func (d *Dispenser) WrapErr(err error) error {
|
||||||
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
|
// 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]
|
prev := d.tokens[d.cursor-1]
|
||||||
curr := d.tokens[d.cursor]
|
curr := d.tokens[d.cursor]
|
||||||
|
return isNextOnNewLine(prev, curr)
|
||||||
// If the previous token is from a different file,
|
|
||||||
// we can assume it's from a different line
|
|
||||||
if prev.File != curr.File {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the previous token (incl line breaks) ends
|
|
||||||
// on a line earlier than the current token,
|
|
||||||
// then the current token is on a new line
|
|
||||||
return prev.Line+prev.NumLineBreaks() < curr.Line
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// isNextOnNewLine determines whether the current token is on a different
|
// isNextOnNewLine determines whether the current token is on a different
|
||||||
@@ -489,15 +483,5 @@ func (d *Dispenser) isNextOnNewLine() bool {
|
|||||||
|
|
||||||
curr := d.tokens[d.cursor]
|
curr := d.tokens[d.cursor]
|
||||||
next := d.tokens[d.cursor+1]
|
next := d.tokens[d.cursor+1]
|
||||||
|
return isNextOnNewLine(curr, next)
|
||||||
// If the next token is from a different file,
|
|
||||||
// we can assume it's from a different line
|
|
||||||
if curr.File != next.File {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the current token (incl line breaks) ends
|
|
||||||
// on a line earlier than the next token,
|
|
||||||
// then the next token is on a new line
|
|
||||||
return curr.Line+curr.NumLineBreaks() < next.Line
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// parseVariadic determines if the token is a variadic placeholder,
|
// parseVariadic determines if the token is a variadic placeholder,
|
||||||
@@ -39,7 +40,7 @@ func parseVariadic(token Token, argCount int) (bool, int, int) {
|
|||||||
if argRange == "" {
|
if argRange == "" {
|
||||||
caddy.Log().Named("caddyfile").Warn(
|
caddy.Log().Named("caddyfile").Warn(
|
||||||
"Placeholder "+token.Text+" cannot have an empty index",
|
"Placeholder "+token.Text+" cannot have an empty index",
|
||||||
zap.String("file", token.File+":"+strconv.Itoa(token.Line)))
|
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
|
||||||
return false, 0, 0
|
return false, 0, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +52,13 @@ func parseVariadic(token Token, argCount int) (bool, int, int) {
|
|||||||
return false, 0, 0
|
return false, 0, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A valid token may contain several placeholders, and
|
||||||
|
// they may be separated by ":". It's not variadic.
|
||||||
|
// https://github.com/caddyserver/caddy/issues/5716
|
||||||
|
if strings.Contains(start, "}") || strings.Contains(end, "{") {
|
||||||
|
return false, 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
startIndex = 0
|
startIndex = 0
|
||||||
endIndex = argCount
|
endIndex = argCount
|
||||||
@@ -61,7 +69,7 @@ func parseVariadic(token Token, argCount int) (bool, int, int) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
caddy.Log().Named("caddyfile").Warn(
|
caddy.Log().Named("caddyfile").Warn(
|
||||||
"Variadic placeholder "+token.Text+" has an invalid start index",
|
"Variadic placeholder "+token.Text+" has an invalid start index",
|
||||||
zap.String("file", token.File+":"+strconv.Itoa(token.Line)))
|
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
|
||||||
return false, 0, 0
|
return false, 0, 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,7 +78,7 @@ func parseVariadic(token Token, argCount int) (bool, int, int) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
caddy.Log().Named("caddyfile").Warn(
|
caddy.Log().Named("caddyfile").Warn(
|
||||||
"Variadic placeholder "+token.Text+" has an invalid end index",
|
"Variadic placeholder "+token.Text+" has an invalid end index",
|
||||||
zap.String("file", token.File+":"+strconv.Itoa(token.Line)))
|
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
|
||||||
return false, 0, 0
|
return false, 0, 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,7 +87,7 @@ func parseVariadic(token Token, argCount int) (bool, int, int) {
|
|||||||
if startIndex < 0 || startIndex > endIndex || endIndex > argCount {
|
if startIndex < 0 || startIndex > endIndex || endIndex > argCount {
|
||||||
caddy.Log().Named("caddyfile").Warn(
|
caddy.Log().Named("caddyfile").Warn(
|
||||||
"Variadic placeholder "+token.Text+" indices are out of bounds, only "+strconv.Itoa(argCount)+" argument(s) exist",
|
"Variadic placeholder "+token.Text+" indices are out of bounds, only "+strconv.Itoa(argCount)+" argument(s) exist",
|
||||||
zap.String("file", token.File+":"+strconv.Itoa(token.Line)))
|
zap.String("file", token.File+":"+strconv.Itoa(token.Line)), zap.Strings("import_chain", token.imports))
|
||||||
return false, 0, 0
|
return false, 0, 0
|
||||||
}
|
}
|
||||||
return true, startIndex, endIndex
|
return true, startIndex, endIndex
|
||||||
@@ -93,6 +101,11 @@ func makeArgsReplacer(args []string) *caddy.Replacer {
|
|||||||
// TODO: Remove the deprecated {args.*} placeholder
|
// TODO: Remove the deprecated {args.*} placeholder
|
||||||
// support at some point in the future
|
// support at some point in the future
|
||||||
if matches := argsRegexpIndexDeprecated.FindStringSubmatch(key); len(matches) > 0 {
|
if matches := argsRegexpIndexDeprecated.FindStringSubmatch(key); len(matches) > 0 {
|
||||||
|
// What's matched may be a substring of the key
|
||||||
|
if matches[0] != key {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
value, err := strconv.Atoi(matches[1])
|
value, err := strconv.Atoi(matches[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
caddy.Log().Named("caddyfile").Warn(
|
caddy.Log().Named("caddyfile").Warn(
|
||||||
@@ -111,6 +124,11 @@ func makeArgsReplacer(args []string) *caddy.Replacer {
|
|||||||
|
|
||||||
// Handle args[*] form
|
// Handle args[*] form
|
||||||
if matches := argsRegexpIndex.FindStringSubmatch(key); len(matches) > 0 {
|
if matches := argsRegexpIndex.FindStringSubmatch(key); len(matches) > 0 {
|
||||||
|
// What's matched may be a substring of the key
|
||||||
|
if matches[0] != key {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
if strings.Contains(matches[1], ":") {
|
if strings.Contains(matches[1], ":") {
|
||||||
caddy.Log().Named("caddyfile").Warn(
|
caddy.Log().Named("caddyfile").Warn(
|
||||||
"Variadic placeholder {args[" + matches[1] + "]} must be a token on its own")
|
"Variadic placeholder {args[" + matches[1] + "]} must be a token on its own")
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ func (i *importGraph) addNode(name string) {
|
|||||||
}
|
}
|
||||||
i.nodes[name] = true
|
i.nodes[name] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *importGraph) addNodes(names []string) {
|
func (i *importGraph) addNodes(names []string) {
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
i.addNode(name)
|
i.addNode(name)
|
||||||
@@ -43,6 +44,7 @@ func (i *importGraph) addNodes(names []string) {
|
|||||||
func (i *importGraph) removeNode(name string) {
|
func (i *importGraph) removeNode(name string) {
|
||||||
delete(i.nodes, name)
|
delete(i.nodes, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *importGraph) removeNodes(names []string) {
|
func (i *importGraph) removeNodes(names []string) {
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
i.removeNode(name)
|
i.removeNode(name)
|
||||||
@@ -73,6 +75,7 @@ func (i *importGraph) addEdge(from, to string) error {
|
|||||||
i.edges[from] = append(i.edges[from], to)
|
i.edges[from] = append(i.edges[from], to)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *importGraph) addEdges(from string, tos []string) error {
|
func (i *importGraph) addEdges(from string, tos []string) error {
|
||||||
for _, to := range tos {
|
for _, to := range tos {
|
||||||
err := i.addEdge(from, to)
|
err := i.addEdge(from, to)
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ type (
|
|||||||
// Token represents a single parsable unit.
|
// Token represents a single parsable unit.
|
||||||
Token struct {
|
Token struct {
|
||||||
File string
|
File string
|
||||||
origFile string
|
imports []string
|
||||||
Line int
|
Line int
|
||||||
Text string
|
Text string
|
||||||
wasQuoted rune // enclosing quote character, if any
|
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
|
// detect whether we have the start of a heredoc
|
||||||
if !inHeredoc && !heredocEscaped && len(val) > 1 && string(val[:2]) == "<<" {
|
if !(quoted || btQuoted) && !(inHeredoc || heredocEscaped) &&
|
||||||
if ch == '<' {
|
len(val) > 1 && string(val[:2]) == "<<" {
|
||||||
return false, fmt.Errorf("too many '<' for heredoc on line #%d; only use two, for example <<END", l.line)
|
// 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' {
|
if ch == '\r' {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// after hitting a newline, we know that the heredoc marker
|
// after hitting a newline, we know that the heredoc marker
|
||||||
// is the characters after the two << and the newline.
|
// is the characters after the two << and the newline.
|
||||||
// we reset the val because the heredoc is syntax we don't
|
// we reset the val because the heredoc is syntax we don't
|
||||||
// want to keep.
|
// want to keep.
|
||||||
if ch == '\n' {
|
if ch == '\n' {
|
||||||
|
if len(val) == 2 {
|
||||||
|
return false, fmt.Errorf("missing opening heredoc marker on line #%d; must contain only alpha-numeric characters, dashes and underscores; got empty string", l.line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if there's too many <
|
||||||
|
if string(val[:3]) == "<<<" {
|
||||||
|
return false, fmt.Errorf("too many '<' for heredoc on line #%d; only use two, for example <<END", l.line)
|
||||||
|
}
|
||||||
|
|
||||||
heredocMarker = string(val[2:])
|
heredocMarker = string(val[2:])
|
||||||
if !heredocMarkerRegexp.Match([]byte(heredocMarker)) {
|
if !heredocMarkerRegexp.Match([]byte(heredocMarker)) {
|
||||||
return false, fmt.Errorf("heredoc marker on line #%d must contain only alpha-numeric characters, dashes and underscores; got '%s'", l.line, heredocMarker)
|
return false, fmt.Errorf("heredoc marker on line #%d must contain only alpha-numeric characters, dashes and underscores; got '%s'", l.line, heredocMarker)
|
||||||
@@ -321,23 +335,6 @@ func (l *lexer) finalizeHeredoc(val []rune, marker string) ([]rune, error) {
|
|||||||
return []rune(out), nil
|
return []rune(out), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// originalFile gets original filename before import modification.
|
|
||||||
func (t Token) originalFile() string {
|
|
||||||
if t.origFile != "" {
|
|
||||||
return t.origFile
|
|
||||||
}
|
|
||||||
return t.File
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateFile updates the token's source filename for error display
|
|
||||||
// and remembers the original filename. Used during "import" processing.
|
|
||||||
func (t *Token) updateFile(file string) {
|
|
||||||
if t.origFile == "" {
|
|
||||||
t.origFile = t.File
|
|
||||||
}
|
|
||||||
t.File = file
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t Token) Quoted() bool {
|
func (t Token) Quoted() bool {
|
||||||
return t.wasQuoted > 0
|
return t.wasQuoted > 0
|
||||||
}
|
}
|
||||||
@@ -355,3 +352,28 @@ func (t Token) NumLineBreaks() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var heredocMarkerRegexp = regexp.MustCompile("^[A-Za-z0-9_-]+$")
|
var heredocMarkerRegexp = regexp.MustCompile("^[A-Za-z0-9_-]+$")
|
||||||
|
|
||||||
|
// isNextOnNewLine tests whether t2 is on a different line from t1
|
||||||
|
func isNextOnNewLine(t1, t2 Token) bool {
|
||||||
|
// If the second token is from a different file,
|
||||||
|
// we can assume it's from a different line
|
||||||
|
if t1.File != t2.File {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the second token is from a different import chain,
|
||||||
|
// we can assume it's from a different line
|
||||||
|
if len(t1.imports) != len(t2.imports) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for i, im := range t1.imports {
|
||||||
|
if im != t2.imports[i] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the first token (incl line breaks) ends
|
||||||
|
// on a line earlier than the next token,
|
||||||
|
// then the second token is on a new line
|
||||||
|
return t1.Line+t1.NumLineBreaks() < t2.Line
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
content
|
||||||
EOF same-line-arg
|
|
||||||
`),
|
`),
|
||||||
expected: []Token{
|
expected: []Token{
|
||||||
{Line: 1, Text: `heredoc`},
|
{Line: 1, Text: `not-a-heredoc`},
|
||||||
{Line: 1, Text: `<EOF`},
|
{Line: 1, Text: `<EOF`},
|
||||||
{Line: 2, Text: `content`},
|
{Line: 2, Text: `content`},
|
||||||
{Line: 3, Text: `EOF`},
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`not-a-heredoc <<<EOF content`),
|
||||||
|
expected: []Token{
|
||||||
|
{Line: 1, Text: `not-a-heredoc`},
|
||||||
|
{Line: 1, Text: `<<<EOF`},
|
||||||
|
{Line: 1, Text: `content`},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`not-a-heredoc "<<" ">>"`),
|
||||||
|
expected: []Token{
|
||||||
|
{Line: 1, Text: `not-a-heredoc`},
|
||||||
|
{Line: 1, Text: `<<`},
|
||||||
|
{Line: 1, Text: `>>`},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`not-a-heredoc << >>`),
|
||||||
|
expected: []Token{
|
||||||
|
{Line: 1, Text: `not-a-heredoc`},
|
||||||
|
{Line: 1, Text: `<<`},
|
||||||
|
{Line: 1, Text: `>>`},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: []byte(`not-a-heredoc <<HERE SAME LINE
|
||||||
|
content
|
||||||
|
HERE same-line-arg
|
||||||
|
`),
|
||||||
|
expected: []Token{
|
||||||
|
{Line: 1, Text: `not-a-heredoc`},
|
||||||
|
{Line: 1, Text: `<<HERE`},
|
||||||
|
{Line: 1, Text: `SAME`},
|
||||||
|
{Line: 1, Text: `LINE`},
|
||||||
|
{Line: 2, Text: `content`},
|
||||||
|
{Line: 3, Text: `HERE`},
|
||||||
{Line: 3, Text: `same-line-arg`},
|
{Line: 3, Text: `same-line-arg`},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -366,12 +410,9 @@ EOF same-line-arg
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: []byte(`heredoc <<HERE SAME LINE
|
input: []byte("not-a-heredoc <<\n"),
|
||||||
content
|
|
||||||
HERE same-line-arg
|
|
||||||
`),
|
|
||||||
expectErr: true,
|
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
|
input: []byte(`heredoc <<<EOF
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Parse parses the input just enough to group tokens, in
|
// Parse parses the input just enough to group tokens, in
|
||||||
@@ -214,7 +215,7 @@ func (p *parser) addresses() error {
|
|||||||
|
|
||||||
// special case: import directive replaces tokens during parse-time
|
// special case: import directive replaces tokens during parse-time
|
||||||
if tkn == "import" && p.isNewLine() {
|
if tkn == "import" && p.isNewLine() {
|
||||||
err := p.doImport()
|
err := p.doImport(0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -314,7 +315,7 @@ func (p *parser) directives() error {
|
|||||||
|
|
||||||
// special case: import directive replaces tokens during parse-time
|
// special case: import directive replaces tokens during parse-time
|
||||||
if p.Val() == "import" {
|
if p.Val() == "import" {
|
||||||
err := p.doImport()
|
err := p.doImport(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -340,7 +341,7 @@ func (p *parser) directives() error {
|
|||||||
// is on the token before where the import directive was. In
|
// is on the token before where the import directive was. In
|
||||||
// other words, call Next() to access the first token that was
|
// other words, call Next() to access the first token that was
|
||||||
// imported.
|
// imported.
|
||||||
func (p *parser) doImport() error {
|
func (p *parser) doImport(nesting int) error {
|
||||||
// syntax checks
|
// syntax checks
|
||||||
if !p.NextArg() {
|
if !p.NextArg() {
|
||||||
return p.ArgErr()
|
return p.ArgErr()
|
||||||
@@ -429,7 +430,7 @@ func (p *parser) doImport() error {
|
|||||||
nodes = matches
|
nodes = matches
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeName := p.Token().originalFile()
|
nodeName := p.File()
|
||||||
if p.Token().snippetName != "" {
|
if p.Token().snippetName != "" {
|
||||||
nodeName += fmt.Sprintf(":%s", p.Token().snippetName)
|
nodeName += fmt.Sprintf(":%s", p.Token().snippetName)
|
||||||
}
|
}
|
||||||
@@ -443,15 +444,55 @@ func (p *parser) doImport() error {
|
|||||||
// copy the tokens so we don't overwrite p.definedSnippets
|
// copy the tokens so we don't overwrite p.definedSnippets
|
||||||
tokensCopy := make([]Token, 0, len(importedTokens))
|
tokensCopy := make([]Token, 0, len(importedTokens))
|
||||||
|
|
||||||
|
var (
|
||||||
|
maybeSnippet bool
|
||||||
|
maybeSnippetId bool
|
||||||
|
index int
|
||||||
|
)
|
||||||
|
|
||||||
// run the argument replacer on the tokens
|
// run the argument replacer on the tokens
|
||||||
// golang for range slice return a copy of value
|
// golang for range slice return a copy of value
|
||||||
// similarly, append also copy value
|
// similarly, append also copy value
|
||||||
for _, token := range importedTokens {
|
for i, token := range importedTokens {
|
||||||
// set the token's file to refer to import directive line number and snippet name
|
// update the token's imports to refer to import directive filename, line number and snippet name if there is one
|
||||||
if token.snippetName != "" {
|
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 {
|
} 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))
|
foundVariadic, startIndex, endIndex := parseVariadic(token, len(args))
|
||||||
@@ -525,7 +566,6 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
|
|||||||
// are loaded into the current server block for later use
|
// are loaded into the current server block for later use
|
||||||
// by directive setup functions.
|
// by directive setup functions.
|
||||||
func (p *parser) directive() error {
|
func (p *parser) directive() error {
|
||||||
|
|
||||||
// a segment is a list of tokens associated with this directive
|
// a segment is a list of tokens associated with this directive
|
||||||
var segment Segment
|
var segment Segment
|
||||||
|
|
||||||
@@ -553,7 +593,7 @@ func (p *parser) directive() error {
|
|||||||
} else if p.Val() == "}" && p.nesting == 0 {
|
} else if p.Val() == "}" && p.nesting == 0 {
|
||||||
return p.Err("Unexpected '}' because no matching opening brace")
|
return p.Err("Unexpected '}' because no matching opening brace")
|
||||||
} else if p.Val() == "import" && p.isNewLine() {
|
} else if p.Val() == "import" && p.isNewLine() {
|
||||||
if err := p.doImport(); err != nil {
|
if err := p.doImport(1); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
p.cursor-- // cursor is advanced when we continue, so roll back one more
|
p.cursor-- // cursor is advanced when we continue, so roll back one more
|
||||||
|
|||||||
@@ -91,6 +91,10 @@ func TestParseVariadic(t *testing.T) {
|
|||||||
input: "{args[0:10]}",
|
input: "{args[0:10]}",
|
||||||
result: true,
|
result: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: "{args[0]}:{args[1]}:{args[2]}",
|
||||||
|
result: false,
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
token := Token{
|
token := Token{
|
||||||
File: "test",
|
File: "test",
|
||||||
@@ -718,6 +722,36 @@ func TestEnvironmentReplacement(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestImportReplacementInJSONWithBrace(t *testing.T) {
|
||||||
|
for i, test := range []struct {
|
||||||
|
args []string
|
||||||
|
input string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
args: []string{"123"},
|
||||||
|
input: "{args[0]}",
|
||||||
|
expect: "123",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{"123"},
|
||||||
|
input: `{"key":"{args[0]}"}`,
|
||||||
|
expect: `{"key":"123"}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{"123", "123"},
|
||||||
|
input: `{"key":[{args[0]},{args[1]}]}`,
|
||||||
|
expect: `{"key":[123,123]}`,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
repl := makeArgsReplacer(test.args)
|
||||||
|
actual := repl.ReplaceKnown(test.input, "")
|
||||||
|
if actual != test.expect {
|
||||||
|
t.Errorf("Test %d: Expected: '%s' but got '%s'", i, test.expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSnippets(t *testing.T) {
|
func TestSnippets(t *testing.T) {
|
||||||
p := testParser(`
|
p := testParser(`
|
||||||
(common) {
|
(common) {
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"github.com/caddyserver/certmagic"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// mapAddressToServerBlocks returns a map of listener address to list of server
|
// mapAddressToServerBlocks returns a map of listener address to list of server
|
||||||
@@ -77,7 +78,8 @@ import (
|
|||||||
// multiple addresses to the same lists of server blocks (a many:many mapping).
|
// multiple addresses to the same lists of server blocks (a many:many mapping).
|
||||||
// (Doing this is essentially a map-reduce technique.)
|
// (Doing this is essentially a map-reduce technique.)
|
||||||
func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock,
|
func (st *ServerType) mapAddressToServerBlocks(originalServerBlocks []serverBlock,
|
||||||
options map[string]any) (map[string][]serverBlock, error) {
|
options map[string]any,
|
||||||
|
) (map[string][]serverBlock, error) {
|
||||||
sbmap := make(map[string][]serverBlock)
|
sbmap := make(map[string][]serverBlock)
|
||||||
|
|
||||||
for i, sblock := range originalServerBlocks {
|
for i, sblock := range originalServerBlocks {
|
||||||
@@ -187,13 +189,25 @@ func (st *ServerType) consolidateAddrMappings(addrToServerBlocks map[string][]se
|
|||||||
// listenerAddrsForServerBlockKey essentially converts the Caddyfile
|
// listenerAddrsForServerBlockKey essentially converts the Caddyfile
|
||||||
// site addresses to Caddy listener addresses for each server block.
|
// site addresses to Caddy listener addresses for each server block.
|
||||||
func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string,
|
func (st *ServerType) listenerAddrsForServerBlockKey(sblock serverBlock, key string,
|
||||||
options map[string]any) ([]string, error) {
|
options map[string]any,
|
||||||
|
) ([]string, error) {
|
||||||
addr, err := ParseAddress(key)
|
addr, err := ParseAddress(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parsing key: %v", err)
|
return nil, fmt.Errorf("parsing key: %v", err)
|
||||||
}
|
}
|
||||||
addr = addr.Normalize()
|
addr = addr.Normalize()
|
||||||
|
|
||||||
|
switch addr.Scheme {
|
||||||
|
case "wss":
|
||||||
|
return nil, fmt.Errorf("the scheme wss:// is only supported in browsers; use https:// instead")
|
||||||
|
case "ws":
|
||||||
|
return nil, fmt.Errorf("the scheme ws:// is only supported in browsers; use http:// instead")
|
||||||
|
case "https", "http", "":
|
||||||
|
// Do nothing or handle the valid schemes
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported URL scheme %s://", addr.Scheme)
|
||||||
|
}
|
||||||
|
|
||||||
// figure out the HTTP and HTTPS ports; either
|
// figure out the HTTP and HTTPS ports; either
|
||||||
// use defaults, or override with user config
|
// use defaults, or override with user config
|
||||||
httpPort, httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPPort), strconv.Itoa(caddyhttp.DefaultHTTPSPort)
|
httpPort, httpsPort := strconv.Itoa(caddyhttp.DefaultHTTPPort), strconv.Itoa(caddyhttp.DefaultHTTPSPort)
|
||||||
|
|||||||
@@ -26,14 +26,15 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/mholt/acmez/acme"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
"github.com/caddyserver/certmagic"
|
|
||||||
"github.com/mholt/acmez/acme"
|
|
||||||
"go.uber.org/zap/zapcore"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -180,17 +181,17 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
case "protocols":
|
case "protocols":
|
||||||
args := h.RemainingArgs()
|
args := h.RemainingArgs()
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return nil, h.SyntaxErr("one or two protocols")
|
return nil, h.Errf("protocols requires one or two arguments")
|
||||||
}
|
}
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
if _, ok := caddytls.SupportedProtocols[args[0]]; !ok {
|
if _, ok := caddytls.SupportedProtocols[args[0]]; !ok {
|
||||||
return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[0])
|
return nil, h.Errf("wrong protocol name or protocol not supported: '%s'", args[0])
|
||||||
}
|
}
|
||||||
cp.ProtocolMin = args[0]
|
cp.ProtocolMin = args[0]
|
||||||
}
|
}
|
||||||
if len(args) > 1 {
|
if len(args) > 1 {
|
||||||
if _, ok := caddytls.SupportedProtocols[args[1]]; !ok {
|
if _, ok := caddytls.SupportedProtocols[args[1]]; !ok {
|
||||||
return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[1])
|
return nil, h.Errf("wrong protocol name or protocol not supported: '%s'", args[1])
|
||||||
}
|
}
|
||||||
cp.ProtocolMax = args[1]
|
cp.ProtocolMax = args[1]
|
||||||
}
|
}
|
||||||
@@ -198,7 +199,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
case "ciphers":
|
case "ciphers":
|
||||||
for h.NextArg() {
|
for h.NextArg() {
|
||||||
if !caddytls.CipherSuiteNameSupported(h.Val()) {
|
if !caddytls.CipherSuiteNameSupported(h.Val()) {
|
||||||
return nil, h.Errf("Wrong cipher suite name or cipher suite not supported: '%s'", h.Val())
|
return nil, h.Errf("wrong cipher suite name or cipher suite not supported: '%s'", h.Val())
|
||||||
}
|
}
|
||||||
cp.CipherSuites = append(cp.CipherSuites, h.Val())
|
cp.CipherSuites = append(cp.CipherSuites, h.Val())
|
||||||
}
|
}
|
||||||
@@ -788,7 +789,8 @@ func parseInvoke(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
|||||||
|
|
||||||
// parseLog parses the log directive. Syntax:
|
// parseLog parses the log directive. Syntax:
|
||||||
//
|
//
|
||||||
// log {
|
// log <logger_name> {
|
||||||
|
// hostnames <hostnames...>
|
||||||
// output <writer_module> ...
|
// output <writer_module> ...
|
||||||
// format <encoder_module> ...
|
// format <encoder_module> ...
|
||||||
// level <level>
|
// level <level>
|
||||||
@@ -809,11 +811,13 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
|
|||||||
var configValues []ConfigValue
|
var configValues []ConfigValue
|
||||||
for h.Next() {
|
for h.Next() {
|
||||||
// Logic below expects that a name is always present when a
|
// Logic below expects that a name is always present when a
|
||||||
// global option is being parsed.
|
// global option is being parsed; or an optional override
|
||||||
var globalLogName string
|
// is supported for access logs.
|
||||||
|
var logName string
|
||||||
|
|
||||||
if parseAsGlobalOption {
|
if parseAsGlobalOption {
|
||||||
if h.NextArg() {
|
if h.NextArg() {
|
||||||
globalLogName = h.Val()
|
logName = h.Val()
|
||||||
|
|
||||||
// Only a single argument is supported.
|
// Only a single argument is supported.
|
||||||
if h.NextArg() {
|
if h.NextArg() {
|
||||||
@@ -824,26 +828,47 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
|
|||||||
// reference the default logger. See the
|
// reference the default logger. See the
|
||||||
// setupNewDefault function in the logging
|
// setupNewDefault function in the logging
|
||||||
// package for where this is configured.
|
// package for where this is configured.
|
||||||
globalLogName = caddy.DefaultLoggerName
|
logName = caddy.DefaultLoggerName
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify this name is unused.
|
// Verify this name is unused.
|
||||||
_, used := globalLogNames[globalLogName]
|
_, used := globalLogNames[logName]
|
||||||
if used {
|
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 {
|
} 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() {
|
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)
|
cl := new(caddy.CustomLog)
|
||||||
|
|
||||||
|
// allow overriding the current site block's hostnames for this logger;
|
||||||
|
// this is useful for setting up loggers per subdomain in a site block
|
||||||
|
// with a wildcard domain
|
||||||
|
customHostnames := []string{}
|
||||||
|
|
||||||
for h.NextBlock(0) {
|
for h.NextBlock(0) {
|
||||||
switch h.Val() {
|
switch h.Val() {
|
||||||
|
case "hostnames":
|
||||||
|
if parseAsGlobalOption {
|
||||||
|
return nil, h.Err("hostnames is not allowed in the log global options")
|
||||||
|
}
|
||||||
|
args := h.RemainingArgs()
|
||||||
|
if len(args) == 0 {
|
||||||
|
return nil, h.ArgErr()
|
||||||
|
}
|
||||||
|
customHostnames = append(customHostnames, args...)
|
||||||
|
|
||||||
case "output":
|
case "output":
|
||||||
if !h.NextArg() {
|
if !h.NextArg() {
|
||||||
return nil, h.ArgErr()
|
return nil, h.ArgErr()
|
||||||
@@ -902,18 +927,16 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "include":
|
case "include":
|
||||||
// This configuration is only allowed in the global options
|
|
||||||
if !parseAsGlobalOption {
|
if !parseAsGlobalOption {
|
||||||
return nil, h.ArgErr()
|
return nil, h.Err("include is not allowed in the log directive")
|
||||||
}
|
}
|
||||||
for h.NextArg() {
|
for h.NextArg() {
|
||||||
cl.Include = append(cl.Include, h.Val())
|
cl.Include = append(cl.Include, h.Val())
|
||||||
}
|
}
|
||||||
|
|
||||||
case "exclude":
|
case "exclude":
|
||||||
// This configuration is only allowed in the global options
|
|
||||||
if !parseAsGlobalOption {
|
if !parseAsGlobalOption {
|
||||||
return nil, h.ArgErr()
|
return nil, h.Err("exclude is not allowed in the log directive")
|
||||||
}
|
}
|
||||||
for h.NextArg() {
|
for h.NextArg() {
|
||||||
cl.Exclude = append(cl.Exclude, h.Val())
|
cl.Exclude = append(cl.Exclude, h.Val())
|
||||||
@@ -925,24 +948,34 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
|
|||||||
}
|
}
|
||||||
|
|
||||||
var val namedCustomLog
|
var val namedCustomLog
|
||||||
|
val.hostnames = customHostnames
|
||||||
|
|
||||||
|
isEmptyConfig := reflect.DeepEqual(cl, new(caddy.CustomLog))
|
||||||
|
|
||||||
// Skip handling of empty logging configs
|
// Skip handling of empty logging configs
|
||||||
if !reflect.DeepEqual(cl, new(caddy.CustomLog)) {
|
|
||||||
if parseAsGlobalOption {
|
if parseAsGlobalOption {
|
||||||
// Use indicated name for global log options
|
// Use indicated name for global log options
|
||||||
val.name = globalLogName
|
val.name = logName
|
||||||
val.log = cl
|
} else {
|
||||||
} else {
|
if logName != "" {
|
||||||
|
val.name = logName
|
||||||
|
} else if !isEmptyConfig {
|
||||||
// Construct a log name for server log streams
|
// Construct a log name for server log streams
|
||||||
logCounter, ok := h.State["logCounter"].(int)
|
logCounter, ok := h.State["logCounter"].(int)
|
||||||
if !ok {
|
if !ok {
|
||||||
logCounter = 0
|
logCounter = 0
|
||||||
}
|
}
|
||||||
val.name = fmt.Sprintf("log%d", logCounter)
|
val.name = fmt.Sprintf("log%d", logCounter)
|
||||||
cl.Include = []string{"http.log.access." + val.name}
|
|
||||||
val.log = cl
|
|
||||||
logCounter++
|
logCounter++
|
||||||
h.State["logCounter"] = logCounter
|
h.State["logCounter"] = logCounter
|
||||||
}
|
}
|
||||||
|
if val.name != "" {
|
||||||
|
cl.Include = []string{"http.log.access." + val.name}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isEmptyConfig {
|
||||||
|
val.log = cl
|
||||||
}
|
}
|
||||||
configValues = append(configValues, ConfigValue{
|
configValues = append(configValues, ConfigValue{
|
||||||
Class: "custom_log",
|
Class: "custom_log",
|
||||||
|
|||||||
@@ -52,12 +52,13 @@ func TestLogDirectiveSyntax(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: `:8080 {
|
input: `:8080 {
|
||||||
log invalid {
|
log name-override {
|
||||||
output file foo.log
|
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
|
import t1 true
|
||||||
}`,
|
}`,
|
||||||
errorFunc: func(err error) bool {
|
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
|
import t1 true
|
||||||
}`,
|
}`,
|
||||||
errorFunc: func(err error) bool {
|
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
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ func (h Helper) Caddyfiles() []string {
|
|||||||
for file := range files {
|
for file := range files {
|
||||||
filesSlice = append(filesSlice, file)
|
filesSlice = append(filesSlice, file)
|
||||||
}
|
}
|
||||||
|
sort.Strings(filesSlice)
|
||||||
return filesSlice
|
return filesSlice
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +217,8 @@ func (h Helper) ExtractMatcherSet() (caddy.ModuleMap, error) {
|
|||||||
|
|
||||||
// NewRoute returns config values relevant to creating a new HTTP route.
|
// NewRoute returns config values relevant to creating a new HTTP route.
|
||||||
func (h Helper) NewRoute(matcherSet caddy.ModuleMap,
|
func (h Helper) NewRoute(matcherSet caddy.ModuleMap,
|
||||||
handler caddyhttp.MiddlewareHandler) []ConfigValue {
|
handler caddyhttp.MiddlewareHandler,
|
||||||
|
) []ConfigValue {
|
||||||
mod, err := caddy.GetModule(caddy.GetModuleID(handler))
|
mod, err := caddy.GetModule(caddy.GetModuleID(handler))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
*h.warnings = append(*h.warnings, caddyconfig.Warning{
|
*h.warnings = append(*h.warnings, caddyconfig.Warning{
|
||||||
|
|||||||
@@ -17,19 +17,21 @@ package httpcaddyfile
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddypki"
|
"github.com/caddyserver/caddy/v2/modules/caddypki"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -48,8 +50,7 @@ type App struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ServerType can set up a config from an HTTP Caddyfile.
|
// ServerType can set up a config from an HTTP Caddyfile.
|
||||||
type ServerType struct {
|
type ServerType struct{}
|
||||||
}
|
|
||||||
|
|
||||||
// Setup makes a config from the tokens.
|
// Setup makes a config from the tokens.
|
||||||
func (st ServerType) Setup(
|
func (st ServerType) Setup(
|
||||||
@@ -81,46 +82,18 @@ func (st ServerType) Setup(
|
|||||||
return nil, warnings, err
|
return nil, warnings, err
|
||||||
}
|
}
|
||||||
|
|
||||||
originalServerBlocks, err = st.extractNamedRoutes(originalServerBlocks, options, &warnings)
|
// this will replace both static and user-defined placeholder shorthands
|
||||||
|
// with actual identifiers used by Caddy
|
||||||
|
replacer := NewShorthandReplacer()
|
||||||
|
|
||||||
|
originalServerBlocks, err = st.extractNamedRoutes(originalServerBlocks, options, &warnings, replacer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, warnings, err
|
return nil, warnings, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// replace shorthand placeholders (which are convenient
|
|
||||||
// when writing a Caddyfile) with their actual placeholder
|
|
||||||
// identifiers or variable names
|
|
||||||
replacer := strings.NewReplacer(placeholderShorthands()...)
|
|
||||||
|
|
||||||
// these are placeholders that allow a user-defined final
|
|
||||||
// parameters, but we still want to provide a shorthand
|
|
||||||
// for those, so we use a regexp to replace
|
|
||||||
regexpReplacements := []struct {
|
|
||||||
search *regexp.Regexp
|
|
||||||
replace string
|
|
||||||
}{
|
|
||||||
{regexp.MustCompile(`{header\.([\w-]*)}`), "{http.request.header.$1}"},
|
|
||||||
{regexp.MustCompile(`{cookie\.([\w-]*)}`), "{http.request.cookie.$1}"},
|
|
||||||
{regexp.MustCompile(`{labels\.([\w-]*)}`), "{http.request.host.labels.$1}"},
|
|
||||||
{regexp.MustCompile(`{path\.([\w-]*)}`), "{http.request.uri.path.$1}"},
|
|
||||||
{regexp.MustCompile(`{file\.([\w-]*)}`), "{http.request.uri.path.file.$1}"},
|
|
||||||
{regexp.MustCompile(`{query\.([\w-]*)}`), "{http.request.uri.query.$1}"},
|
|
||||||
{regexp.MustCompile(`{re\.([\w-]*)\.([\w-]*)}`), "{http.regexp.$1.$2}"},
|
|
||||||
{regexp.MustCompile(`{vars\.([\w-]*)}`), "{http.vars.$1}"},
|
|
||||||
{regexp.MustCompile(`{rp\.([\w-\.]*)}`), "{http.reverse_proxy.$1}"},
|
|
||||||
{regexp.MustCompile(`{err\.([\w-\.]*)}`), "{http.error.$1}"},
|
|
||||||
{regexp.MustCompile(`{file_match\.([\w-]*)}`), "{http.matchers.file.$1}"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, sb := range originalServerBlocks {
|
for _, sb := range originalServerBlocks {
|
||||||
for _, segment := range sb.block.Segments {
|
for i := range sb.block.Segments {
|
||||||
for i := 0; i < len(segment); i++ {
|
replacer.ApplyToSegment(&sb.block.Segments[i])
|
||||||
// simple string replacements
|
|
||||||
segment[i].Text = replacer.Replace(segment[i].Text)
|
|
||||||
// complex regexp replacements
|
|
||||||
for _, r := range regexpReplacements {
|
|
||||||
segment[i].Text = r.search.ReplaceAllString(segment[i].Text, r.replace)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(sb.block.Keys) == 0 {
|
if len(sb.block.Keys) == 0 {
|
||||||
@@ -241,7 +214,7 @@ func (st ServerType) Setup(
|
|||||||
if ncl.name == caddy.DefaultLoggerName {
|
if ncl.name == caddy.DefaultLoggerName {
|
||||||
hasDefaultLog = true
|
hasDefaultLog = true
|
||||||
}
|
}
|
||||||
if _, ok := options["debug"]; ok && ncl.log.Level == "" {
|
if _, ok := options["debug"]; ok && ncl.log != nil && ncl.log.Level == "" {
|
||||||
ncl.log.Level = zap.DebugLevel.CapitalString()
|
ncl.log.Level = zap.DebugLevel.CapitalString()
|
||||||
}
|
}
|
||||||
customLogs = append(customLogs, ncl)
|
customLogs = append(customLogs, ncl)
|
||||||
@@ -324,7 +297,21 @@ func (st ServerType) Setup(
|
|||||||
Logs: make(map[string]*caddy.CustomLog),
|
Logs: make(map[string]*caddy.CustomLog),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the default log first if defined, so that it doesn't
|
||||||
|
// accidentally get re-created below due to the Exclude logic
|
||||||
for _, ncl := range customLogs {
|
for _, ncl := range customLogs {
|
||||||
|
if ncl.name == caddy.DefaultLoggerName && ncl.log != nil {
|
||||||
|
cfg.Logging.Logs[caddy.DefaultLoggerName] = ncl.log
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the rest of the custom logs
|
||||||
|
for _, ncl := range customLogs {
|
||||||
|
if ncl.log == nil || ncl.name == caddy.DefaultLoggerName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if ncl.name != "" {
|
if ncl.name != "" {
|
||||||
cfg.Logging.Logs[ncl.name] = ncl.log
|
cfg.Logging.Logs[ncl.name] = ncl.log
|
||||||
}
|
}
|
||||||
@@ -338,8 +325,16 @@ func (st ServerType) Setup(
|
|||||||
cfg.Logging.Logs[caddy.DefaultLoggerName] = defaultLog
|
cfg.Logging.Logs[caddy.DefaultLoggerName] = defaultLog
|
||||||
}
|
}
|
||||||
defaultLog.Exclude = append(defaultLog.Exclude, ncl.log.Include...)
|
defaultLog.Exclude = append(defaultLog.Exclude, ncl.log.Include...)
|
||||||
|
|
||||||
|
// avoid duplicates by sorting + compacting
|
||||||
|
sort.Strings(defaultLog.Exclude)
|
||||||
|
defaultLog.Exclude = slices.Compact[[]string, string](defaultLog.Exclude)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// we may have not actually added anything, so remove if empty
|
||||||
|
if len(cfg.Logging.Logs) == 0 {
|
||||||
|
cfg.Logging = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cfg, warnings, nil
|
return cfg, warnings, nil
|
||||||
@@ -429,6 +424,7 @@ func (ServerType) extractNamedRoutes(
|
|||||||
serverBlocks []serverBlock,
|
serverBlocks []serverBlock,
|
||||||
options map[string]any,
|
options map[string]any,
|
||||||
warnings *[]caddyconfig.Warning,
|
warnings *[]caddyconfig.Warning,
|
||||||
|
replacer ShorthandReplacer,
|
||||||
) ([]serverBlock, error) {
|
) ([]serverBlock, error) {
|
||||||
namedRoutes := map[string]*caddyhttp.Route{}
|
namedRoutes := map[string]*caddyhttp.Route{}
|
||||||
|
|
||||||
@@ -454,11 +450,14 @@ func (ServerType) extractNamedRoutes(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// zip up all the segments since ParseSegmentAsSubroute
|
|
||||||
// was designed to take a directive+
|
|
||||||
wholeSegment := caddyfile.Segment{}
|
wholeSegment := caddyfile.Segment{}
|
||||||
for _, segment := range sb.block.Segments {
|
for i := range sb.block.Segments {
|
||||||
wholeSegment = append(wholeSegment, segment...)
|
// 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{
|
h := Helper{
|
||||||
@@ -687,6 +686,7 @@ func (st *ServerType) serversFromPairings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(hosts) > 0 {
|
if len(hosts) > 0 {
|
||||||
|
slices.Sort(hosts) // for deterministic JSON output
|
||||||
cp.MatchersRaw = caddy.ModuleMap{
|
cp.MatchersRaw = caddy.ModuleMap{
|
||||||
"sni": caddyconfig.JSON(hosts, warnings), // make sure to match all hosts, not just auto-HTTPS-qualified ones
|
"sni": caddyconfig.JSON(hosts, warnings), // make sure to match all hosts, not just auto-HTTPS-qualified ones
|
||||||
}
|
}
|
||||||
@@ -718,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
|
// we'll need to remember if the address qualifies for auto-HTTPS, so we
|
||||||
// can add a TLS conn policy if necessary
|
// can add a TLS conn policy if necessary
|
||||||
if addr.Scheme == "https" ||
|
if addr.Scheme == "https" ||
|
||||||
(addr.Scheme != "http" && addr.Host != "" && addr.Port != httpPort) {
|
(addr.Scheme != "http" && addr.Port != httpPort && hasTLSEnabled) {
|
||||||
addressQualifiesForTLS = true
|
addressQualifiesForTLS = true
|
||||||
}
|
}
|
||||||
// predict whether auto-HTTPS will add the conn policy for us; if so, we
|
// predict whether auto-HTTPS will add the conn policy for us; if so, we
|
||||||
@@ -770,18 +780,31 @@ func (st *ServerType) serversFromPairings(
|
|||||||
sblockLogHosts := sblock.hostsFromKeys(true)
|
sblockLogHosts := sblock.hostsFromKeys(true)
|
||||||
for _, cval := range sblock.pile["custom_log"] {
|
for _, cval := range sblock.pile["custom_log"] {
|
||||||
ncl := cval.Value.(namedCustomLog)
|
ncl := cval.Value.(namedCustomLog)
|
||||||
if sblock.hasHostCatchAllKey() {
|
if sblock.hasHostCatchAllKey() && len(ncl.hostnames) == 0 {
|
||||||
// all requests for hosts not able to be listed should use
|
// all requests for hosts not able to be listed should use
|
||||||
// this log because it's a catch-all-hosts server block
|
// this log because it's a catch-all-hosts server block
|
||||||
srv.Logs.DefaultLoggerName = ncl.name
|
srv.Logs.DefaultLoggerName = ncl.name
|
||||||
} else {
|
} else if len(ncl.hostnames) > 0 {
|
||||||
// map each host to the user's desired logger name
|
// if the logger overrides the hostnames, map that to the logger name
|
||||||
for _, h := range sblockLogHosts {
|
for _, h := range ncl.hostnames {
|
||||||
if srv.Logs.LoggerNames == nil {
|
if srv.Logs.LoggerNames == nil {
|
||||||
srv.Logs.LoggerNames = make(map[string]string)
|
srv.Logs.LoggerNames = make(map[string]string)
|
||||||
}
|
}
|
||||||
srv.Logs.LoggerNames[h] = ncl.name
|
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 {
|
if srv.Logs != nil && len(sblock.pile["custom_log"]) == 0 {
|
||||||
@@ -1028,8 +1051,8 @@ func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
|
|||||||
subroute *caddyhttp.Subroute,
|
subroute *caddyhttp.Subroute,
|
||||||
matcherSetsEnc []caddy.ModuleMap,
|
matcherSetsEnc []caddy.ModuleMap,
|
||||||
p sbAddrAssociation,
|
p sbAddrAssociation,
|
||||||
warnings *[]caddyconfig.Warning) caddyhttp.RouteList {
|
warnings *[]caddyconfig.Warning,
|
||||||
|
) caddyhttp.RouteList {
|
||||||
// nothing to do if... there's nothing to do
|
// nothing to do if... there's nothing to do
|
||||||
if len(matcherSetsEnc) == 0 && len(subroute.Routes) == 0 && subroute.Errors == nil {
|
if len(matcherSetsEnc) == 0 && len(subroute.Routes) == 0 && subroute.Errors == nil {
|
||||||
return routeList
|
return routeList
|
||||||
@@ -1418,37 +1441,6 @@ func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (caddy.Modul
|
|||||||
return msEncoded, nil
|
return msEncoded, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// placeholderShorthands returns a slice of old-new string pairs,
|
|
||||||
// where the left of the pair is a placeholder shorthand that may
|
|
||||||
// be used in the Caddyfile, and the right is the replacement.
|
|
||||||
func placeholderShorthands() []string {
|
|
||||||
return []string{
|
|
||||||
"{dir}", "{http.request.uri.path.dir}",
|
|
||||||
"{file}", "{http.request.uri.path.file}",
|
|
||||||
"{host}", "{http.request.host}",
|
|
||||||
"{hostport}", "{http.request.hostport}",
|
|
||||||
"{port}", "{http.request.port}",
|
|
||||||
"{method}", "{http.request.method}",
|
|
||||||
"{path}", "{http.request.uri.path}",
|
|
||||||
"{query}", "{http.request.uri.query}",
|
|
||||||
"{remote}", "{http.request.remote}",
|
|
||||||
"{remote_host}", "{http.request.remote.host}",
|
|
||||||
"{remote_port}", "{http.request.remote.port}",
|
|
||||||
"{scheme}", "{http.request.scheme}",
|
|
||||||
"{uri}", "{http.request.uri}",
|
|
||||||
"{tls_cipher}", "{http.request.tls.cipher_suite}",
|
|
||||||
"{tls_version}", "{http.request.tls.version}",
|
|
||||||
"{tls_client_fingerprint}", "{http.request.tls.client.fingerprint}",
|
|
||||||
"{tls_client_issuer}", "{http.request.tls.client.issuer}",
|
|
||||||
"{tls_client_serial}", "{http.request.tls.client.serial}",
|
|
||||||
"{tls_client_subject}", "{http.request.tls.client.subject}",
|
|
||||||
"{tls_client_certificate_pem}", "{http.request.tls.client.certificate_pem}",
|
|
||||||
"{tls_client_certificate_der_base64}", "{http.request.tls.client.certificate_der_base64}",
|
|
||||||
"{upstream_hostport}", "{http.reverse_proxy.upstream.hostport}",
|
|
||||||
"{client_ip}", "{http.vars.client_ip}",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WasReplacedPlaceholderShorthand checks if a token string was
|
// WasReplacedPlaceholderShorthand checks if a token string was
|
||||||
// likely a replaced shorthand of the known Caddyfile placeholder
|
// likely a replaced shorthand of the known Caddyfile placeholder
|
||||||
// replacement outputs. Useful to prevent some user-defined map
|
// replacement outputs. Useful to prevent some user-defined map
|
||||||
@@ -1564,8 +1556,9 @@ func (c counter) nextGroup() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type namedCustomLog struct {
|
type namedCustomLog struct {
|
||||||
name string
|
name string
|
||||||
log *caddy.CustomLog
|
hostnames []string
|
||||||
|
log *caddy.CustomLog
|
||||||
}
|
}
|
||||||
|
|
||||||
// sbAddrAssociation is a mapping from a list of
|
// sbAddrAssociation is a mapping from a list of
|
||||||
@@ -1576,8 +1569,10 @@ type sbAddrAssociation struct {
|
|||||||
serverBlocks []serverBlock
|
serverBlocks []serverBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
const matcherPrefix = "@"
|
const (
|
||||||
const namedRouteKey = "named_route"
|
matcherPrefix = "@"
|
||||||
|
namedRouteKey = "named_route"
|
||||||
|
)
|
||||||
|
|
||||||
// Interface guard
|
// Interface guard
|
||||||
var _ caddyfile.ServerType = (*ServerType)(nil)
|
var _ caddyfile.ServerType = (*ServerType)(nil)
|
||||||
|
|||||||
@@ -17,12 +17,13 @@ package httpcaddyfile
|
|||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/mholt/acmez/acme"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
"github.com/caddyserver/certmagic"
|
|
||||||
"github.com/mholt/acmez/acme"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -174,7 +174,6 @@ func (st ServerType) buildPKIApp(
|
|||||||
options map[string]any,
|
options map[string]any,
|
||||||
warnings []caddyconfig.Warning,
|
warnings []caddyconfig.Warning,
|
||||||
) (*caddypki.PKI, []caddyconfig.Warning, error) {
|
) (*caddypki.PKI, []caddyconfig.Warning, error) {
|
||||||
|
|
||||||
skipInstallTrust := false
|
skipInstallTrust := false
|
||||||
if _, ok := options["skip_install_trust"]; ok {
|
if _, ok := options["skip_install_trust"]; ok {
|
||||||
skipInstallTrust = true
|
skipInstallTrust = true
|
||||||
|
|||||||
@@ -18,11 +18,12 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"github.com/dustin/go-humanize"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// serverOptions collects server config overrides parsed from Caddyfile global options
|
// serverOptions collects server config overrides parsed from Caddyfile global options
|
||||||
@@ -41,6 +42,7 @@ type serverOptions struct {
|
|||||||
IdleTimeout caddy.Duration
|
IdleTimeout caddy.Duration
|
||||||
KeepAliveInterval caddy.Duration
|
KeepAliveInterval caddy.Duration
|
||||||
MaxHeaderBytes int
|
MaxHeaderBytes int
|
||||||
|
EnableFullDuplex bool
|
||||||
Protocols []string
|
Protocols []string
|
||||||
StrictSNIHost *bool
|
StrictSNIHost *bool
|
||||||
TrustedProxiesRaw json.RawMessage
|
TrustedProxiesRaw json.RawMessage
|
||||||
@@ -157,6 +159,12 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
|
|||||||
}
|
}
|
||||||
serverOpts.MaxHeaderBytes = int(size)
|
serverOpts.MaxHeaderBytes = int(size)
|
||||||
|
|
||||||
|
case "enable_full_duplex":
|
||||||
|
if d.NextArg() {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
serverOpts.EnableFullDuplex = true
|
||||||
|
|
||||||
case "log_credentials":
|
case "log_credentials":
|
||||||
if d.NextArg() {
|
if d.NextArg() {
|
||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
@@ -327,6 +335,7 @@ func applyServerOptions(
|
|||||||
server.IdleTimeout = opts.IdleTimeout
|
server.IdleTimeout = opts.IdleTimeout
|
||||||
server.KeepAliveInterval = opts.KeepAliveInterval
|
server.KeepAliveInterval = opts.KeepAliveInterval
|
||||||
server.MaxHeaderBytes = opts.MaxHeaderBytes
|
server.MaxHeaderBytes = opts.MaxHeaderBytes
|
||||||
|
server.EnableFullDuplex = opts.EnableFullDuplex
|
||||||
server.Protocols = opts.Protocols
|
server.Protocols = opts.Protocols
|
||||||
server.StrictSNIHost = opts.StrictSNIHost
|
server.StrictSNIHost = opts.StrictSNIHost
|
||||||
server.TrustedProxiesRaw = opts.TrustedProxiesRaw
|
server.TrustedProxiesRaw = opts.TrustedProxiesRaw
|
||||||
|
|||||||
@@ -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"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/mholt/acmez/acme"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
"github.com/caddyserver/certmagic"
|
|
||||||
"github.com/mholt/acmez/acme"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (st ServerType) buildTLSApp(
|
func (st ServerType) buildTLSApp(
|
||||||
@@ -36,7 +37,6 @@ func (st ServerType) buildTLSApp(
|
|||||||
options map[string]any,
|
options map[string]any,
|
||||||
warnings []caddyconfig.Warning,
|
warnings []caddyconfig.Warning,
|
||||||
) (*caddytls.TLS, []caddyconfig.Warning, error) {
|
) (*caddytls.TLS, []caddyconfig.Warning, error) {
|
||||||
|
|
||||||
tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}
|
tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}
|
||||||
var certLoaders []caddytls.CertificateLoader
|
var certLoaders []caddytls.CertificateLoader
|
||||||
|
|
||||||
@@ -582,6 +582,7 @@ outer:
|
|||||||
// eaten up by the one with subjects; and if both have subjects, we
|
// eaten up by the one with subjects; and if both have subjects, we
|
||||||
// need to combine their lists
|
// need to combine their lists
|
||||||
if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) &&
|
if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) &&
|
||||||
|
reflect.DeepEqual(aps[i].ManagersRaw, aps[j].ManagersRaw) &&
|
||||||
bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
|
bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
|
||||||
aps[i].MustStaple == aps[j].MustStaple &&
|
aps[i].MustStaple == aps[j].MustStaple &&
|
||||||
aps[i].KeyType == aps[j].KeyType &&
|
aps[i].KeyType == aps[j].KeyType &&
|
||||||
|
|||||||
@@ -30,8 +30,14 @@ func init() {
|
|||||||
caddy.RegisterModule(HTTPLoader{})
|
caddy.RegisterModule(HTTPLoader{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPLoader can load Caddy configs over HTTP(S). It can adapt the config
|
// HTTPLoader can load Caddy configs over HTTP(S).
|
||||||
// based on the Content-Type header of the HTTP response.
|
//
|
||||||
|
// If the response is not a JSON config, a config adapter must be specified
|
||||||
|
// either in the loader config (`adapter`), or in the Content-Type HTTP header
|
||||||
|
// returned in the HTTP response from the server. The Content-Type header is
|
||||||
|
// read just like the admin API's `/load` endpoint. Uf you don't have control
|
||||||
|
// over the HTTP server (but can still trust its response), you can override
|
||||||
|
// the Content-Type header by setting the `adapter` property in this config.
|
||||||
type HTTPLoader struct {
|
type HTTPLoader struct {
|
||||||
// The method for the request. Default: GET
|
// The method for the request. Default: GET
|
||||||
Method string `json:"method,omitempty"`
|
Method string `json:"method,omitempty"`
|
||||||
@@ -45,6 +51,11 @@ type HTTPLoader struct {
|
|||||||
// Maximum time allowed for a complete connection and request.
|
// Maximum time allowed for a complete connection and request.
|
||||||
Timeout caddy.Duration `json:"timeout,omitempty"`
|
Timeout caddy.Duration `json:"timeout,omitempty"`
|
||||||
|
|
||||||
|
// The name of the config adapter to use, if any. Only needed
|
||||||
|
// if the HTTP response is not a JSON config and if the server's
|
||||||
|
// Content-Type header is missing or incorrect.
|
||||||
|
Adapter string `json:"adapter,omitempty"`
|
||||||
|
|
||||||
TLS *struct {
|
TLS *struct {
|
||||||
// Present this instance's managed remote identity credentials to the server.
|
// Present this instance's managed remote identity credentials to the server.
|
||||||
UseServerIdentity bool `json:"use_server_identity,omitempty"`
|
UseServerIdentity bool `json:"use_server_identity,omitempty"`
|
||||||
@@ -108,7 +119,12 @@ func (hl HTTPLoader) LoadConfig(ctx caddy.Context) ([]byte, error) {
|
|||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-18
@@ -22,9 +22,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aryann/difflib"
|
"github.com/aryann/difflib"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
|
||||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
// plug in Caddy modules here
|
// plug in Caddy modules here
|
||||||
_ "github.com/caddyserver/caddy/v2/modules/standard"
|
_ "github.com/caddyserver/caddy/v2/modules/standard"
|
||||||
)
|
)
|
||||||
@@ -63,7 +64,6 @@ type Tester struct {
|
|||||||
|
|
||||||
// NewTester will create a new testing client with an attached cookie jar
|
// NewTester will create a new testing client with an attached cookie jar
|
||||||
func NewTester(t *testing.T) *Tester {
|
func NewTester(t *testing.T) *Tester {
|
||||||
|
|
||||||
jar, err := cookiejar.New(nil)
|
jar, err := cookiejar.New(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create cookiejar: %s", err)
|
t.Fatalf("failed to create cookiejar: %s", err)
|
||||||
@@ -94,7 +94,6 @@ func timeElapsed(start time.Time, name string) {
|
|||||||
// InitServer this will configure the server with a configurion of a specific
|
// InitServer this will configure the server with a configurion of a specific
|
||||||
// type. The configType must be either "json" or the adapter type.
|
// type. The configType must be either "json" or the adapter type.
|
||||||
func (tc *Tester) InitServer(rawConfig string, configType string) {
|
func (tc *Tester) InitServer(rawConfig string, configType string) {
|
||||||
|
|
||||||
if err := tc.initServer(rawConfig, configType); err != nil {
|
if err := tc.initServer(rawConfig, configType); err != nil {
|
||||||
tc.t.Logf("failed to load config: %s", err)
|
tc.t.Logf("failed to load config: %s", err)
|
||||||
tc.t.Fail()
|
tc.t.Fail()
|
||||||
@@ -108,7 +107,6 @@ func (tc *Tester) InitServer(rawConfig string, configType string) {
|
|||||||
// InitServer this will configure the server with a configurion of a specific
|
// InitServer this will configure the server with a configurion of a specific
|
||||||
// type. The configType must be either "json" or the adapter type.
|
// type. The configType must be either "json" or the adapter type.
|
||||||
func (tc *Tester) initServer(rawConfig string, configType string) error {
|
func (tc *Tester) initServer(rawConfig string, configType string) error {
|
||||||
|
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
tc.t.SkipNow()
|
tc.t.SkipNow()
|
||||||
return nil
|
return nil
|
||||||
@@ -232,7 +230,6 @@ const initConfig = `{
|
|||||||
// validateTestPrerequisites ensures the certificates are available in the
|
// validateTestPrerequisites ensures the certificates are available in the
|
||||||
// designated path and Caddy sub-process is running.
|
// designated path and Caddy sub-process is running.
|
||||||
func validateTestPrerequisites(t *testing.T) error {
|
func validateTestPrerequisites(t *testing.T) error {
|
||||||
|
|
||||||
// check certificates are found
|
// check certificates are found
|
||||||
for _, certName := range Default.Certifcates {
|
for _, certName := range Default.Certifcates {
|
||||||
if _, err := os.Stat(getIntegrationDir() + certName); os.IsNotExist(err) {
|
if _, err := os.Stat(getIntegrationDir() + certName); os.IsNotExist(err) {
|
||||||
@@ -284,7 +281,6 @@ func isCaddyAdminRunning() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getIntegrationDir() string {
|
func getIntegrationDir() string {
|
||||||
|
|
||||||
_, filename, _, ok := runtime.Caller(1)
|
_, filename, _, ok := runtime.Caller(1)
|
||||||
if !ok {
|
if !ok {
|
||||||
panic("unable to determine the current file path")
|
panic("unable to determine the current file path")
|
||||||
@@ -304,7 +300,6 @@ func prependCaddyFilePath(rawConfig string) string {
|
|||||||
|
|
||||||
// CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally
|
// CreateTestingTransport creates a testing transport that forces call dialing connections to happen locally
|
||||||
func CreateTestingTransport() *http.Transport {
|
func CreateTestingTransport() *http.Transport {
|
||||||
|
|
||||||
dialer := net.Dialer{
|
dialer := net.Dialer{
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
KeepAlive: 5 * time.Second,
|
KeepAlive: 5 * time.Second,
|
||||||
@@ -332,7 +327,6 @@ func CreateTestingTransport() *http.Transport {
|
|||||||
|
|
||||||
// AssertLoadError will load a config and expect an error
|
// AssertLoadError will load a config and expect an error
|
||||||
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
|
func AssertLoadError(t *testing.T, rawConfig string, configType string, expectedError string) {
|
||||||
|
|
||||||
tc := NewTester(t)
|
tc := NewTester(t)
|
||||||
|
|
||||||
err := tc.initServer(rawConfig, configType)
|
err := tc.initServer(rawConfig, configType)
|
||||||
@@ -343,7 +337,6 @@ func AssertLoadError(t *testing.T, rawConfig string, configType string, expected
|
|||||||
|
|
||||||
// AssertRedirect makes a request and asserts the redirection happens
|
// AssertRedirect makes a request and asserts the redirection happens
|
||||||
func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
|
func (tc *Tester) AssertRedirect(requestURI string, expectedToLocation string, expectedStatusCode int) *http.Response {
|
||||||
|
|
||||||
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
|
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
|
||||||
return http.ErrUseLastResponse
|
return http.ErrUseLastResponse
|
||||||
}
|
}
|
||||||
@@ -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
|
// CompareAdapt adapts a config and then compares it against an expected result
|
||||||
func CompareAdapt(t *testing.T, filename, rawConfig string, adapterName string, expectedResponse string) bool {
|
func CompareAdapt(t *testing.T, filename, rawConfig string, adapterName string, expectedResponse string) bool {
|
||||||
|
|
||||||
cfgAdapter := caddyconfig.GetAdapter(adapterName)
|
cfgAdapter := caddyconfig.GetAdapter(adapterName)
|
||||||
if cfgAdapter == nil {
|
if cfgAdapter == nil {
|
||||||
t.Logf("unrecognized config adapter '%s'", adapterName)
|
t.Logf("unrecognized config adapter '%s'", adapterName)
|
||||||
@@ -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
|
// AssertResponseCode will execute the request and verify the status code, returns a response for additional assertions
|
||||||
func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
|
func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int) *http.Response {
|
||||||
|
|
||||||
resp, err := tc.Client.Do(req)
|
resp, err := tc.Client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Fatalf("failed to call server %s", err)
|
tc.t.Fatalf("failed to call server %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if expectedStatusCode != resp.StatusCode {
|
if expectedStatusCode != resp.StatusCode {
|
||||||
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.RequestURI, expectedStatusCode, resp.StatusCode)
|
tc.t.Errorf("requesting \"%s\" expected status code: %d but got %d", req.URL.RequestURI(), expectedStatusCode, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp
|
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
|
// AssertResponse request a URI and assert the status code and the body contains a string
|
||||||
func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
|
|
||||||
resp := tc.AssertResponseCode(req, expectedStatusCode)
|
resp := tc.AssertResponseCode(req, expectedStatusCode)
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
@@ -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
|
// AssertGetResponse GET a URI and expect a statusCode and body text
|
||||||
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", requestURI, nil)
|
req, err := http.NewRequest("GET", requestURI, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Fatalf("unable to create request %s", err)
|
tc.t.Fatalf("unable to create request %s", err)
|
||||||
@@ -517,7 +506,6 @@ func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, e
|
|||||||
|
|
||||||
// AssertDeleteResponse request a URI and expect a statusCode and body text
|
// AssertDeleteResponse request a URI and expect a statusCode and body text
|
||||||
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
|
|
||||||
req, err := http.NewRequest("DELETE", requestURI, nil)
|
req, err := http.NewRequest("DELETE", requestURI, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Fatalf("unable to create request %s", err)
|
tc.t.Fatalf("unable to create request %s", err)
|
||||||
@@ -528,7 +516,6 @@ func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int
|
|||||||
|
|
||||||
// AssertPostResponseBody POST to a URI and assert the response code and body
|
// AssertPostResponseBody POST to a URI and assert the response code and body
|
||||||
func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", requestURI, requestBody)
|
req, err := http.NewRequest("POST", requestURI, requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Errorf("failed to create request %s", err)
|
tc.t.Errorf("failed to create request %s", err)
|
||||||
@@ -542,7 +529,6 @@ func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []str
|
|||||||
|
|
||||||
// AssertPutResponseBody PUT to a URI and assert the response code and body
|
// AssertPutResponseBody PUT to a URI and assert the response code and body
|
||||||
func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
|
|
||||||
req, err := http.NewRequest("PUT", requestURI, requestBody)
|
req, err := http.NewRequest("PUT", requestURI, requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Errorf("failed to create request %s", err)
|
tc.t.Errorf("failed to create request %s", err)
|
||||||
@@ -556,7 +542,6 @@ func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []stri
|
|||||||
|
|
||||||
// AssertPatchResponseBody PATCH to a URI and assert the response code and body
|
// AssertPatchResponseBody PATCH to a URI and assert the response code and body
|
||||||
func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
|
||||||
|
|
||||||
req, err := http.NewRequest("PATCH", requestURI, requestBody)
|
req, err := http.NewRequest("PATCH", requestURI, requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tc.t.Errorf("failed to create request %s", err)
|
tc.t.Errorf("failed to create request %s", err)
|
||||||
|
|||||||
@@ -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/xhtml+xml*
|
||||||
header Content-Type application/atom+xml*
|
header Content-Type application/atom+xml*
|
||||||
header Content-Type application/rss+xml*
|
header Content-Type application/rss+xml*
|
||||||
|
header Content-Type application/wasm*
|
||||||
header Content-Type image/svg+xml*
|
header Content-Type image/svg+xml*
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,6 +48,7 @@ encode {
|
|||||||
"application/xhtml+xml*",
|
"application/xhtml+xml*",
|
||||||
"application/atom+xml*",
|
"application/atom+xml*",
|
||||||
"application/rss+xml*",
|
"application/rss+xml*",
|
||||||
|
"application/wasm*",
|
||||||
"image/svg+xml*"
|
"image/svg+xml*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -69,11 +69,11 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"on_demand": {
|
"on_demand": {
|
||||||
|
"ask": "https://example.com",
|
||||||
"rate_limit": {
|
"rate_limit": {
|
||||||
"interval": 30000000000,
|
"interval": 30000000000,
|
||||||
"burst": 20
|
"burst": 20
|
||||||
},
|
}
|
||||||
"ask": "https://example.com"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"disable_ocsp_stapling": true
|
"disable_ocsp_stapling": true
|
||||||
|
|||||||
@@ -78,11 +78,11 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"on_demand": {
|
"on_demand": {
|
||||||
|
"ask": "https://example.com",
|
||||||
"rate_limit": {
|
"rate_limit": {
|
||||||
"interval": 30000000000,
|
"interval": 30000000000,
|
||||||
"burst": 20
|
"burst": 20
|
||||||
},
|
}
|
||||||
"ask": "https://example.com"
|
|
||||||
},
|
},
|
||||||
"ocsp_interval": 172800000000000,
|
"ocsp_interval": 172800000000000,
|
||||||
"renew_interval": 86400000000000,
|
"renew_interval": 86400000000000,
|
||||||
|
|||||||
@@ -71,11 +71,11 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"on_demand": {
|
"on_demand": {
|
||||||
|
"ask": "https://example.com",
|
||||||
"rate_limit": {
|
"rate_limit": {
|
||||||
"interval": 30000000000,
|
"interval": 30000000000,
|
||||||
"burst": 20
|
"burst": 20
|
||||||
},
|
}
|
||||||
"ask": "https://example.com"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
idle 30s
|
idle 30s
|
||||||
}
|
}
|
||||||
max_header_size 100MB
|
max_header_size 100MB
|
||||||
|
enable_full_duplex
|
||||||
log_credentials
|
log_credentials
|
||||||
protocols h1 h2 h2c h3
|
protocols h1 h2 h2c h3
|
||||||
strict_sni_host
|
strict_sni_host
|
||||||
@@ -45,6 +46,7 @@ foo.com {
|
|||||||
"write_timeout": 30000000000,
|
"write_timeout": 30000000000,
|
||||||
"idle_timeout": 30000000000,
|
"idle_timeout": 30000000000,
|
||||||
"max_header_bytes": 100000000,
|
"max_header_bytes": 100000000,
|
||||||
|
"enable_full_duplex": true,
|
||||||
"routes": [
|
"routes": [
|
||||||
{
|
{
|
||||||
"match": [
|
"match": [
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
+Link "Bar"
|
+Link "Bar"
|
||||||
}
|
}
|
||||||
header >Set Defer
|
header >Set Defer
|
||||||
|
header >Replace Deferred Replacement
|
||||||
}
|
}
|
||||||
----------
|
----------
|
||||||
{
|
{
|
||||||
@@ -148,6 +149,20 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"response": {
|
||||||
|
"deferred": true,
|
||||||
|
"replace": {
|
||||||
|
"Replace": [
|
||||||
|
{
|
||||||
|
"replace": "Replacement",
|
||||||
|
"search_regexp": "Deferred"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ http://localhost:2020 {
|
|||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"logger_names": {
|
"logger_names": {
|
||||||
"localhost:2020": ""
|
"localhost": ""
|
||||||
},
|
},
|
||||||
"skip_unmapped_hosts": true
|
"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-Key 95ca39e3cbe7
|
||||||
X-Header-Keys VbG4NZwWnipo 335Q9/MhqcNU3s2TO
|
X-Header-Keys VbG4NZwWnipo 335Q9/MhqcNU3s2TO
|
||||||
X-Empty-Value
|
X-Empty-Value
|
||||||
|
Same-Key 1
|
||||||
|
Same-Key 2
|
||||||
|
X-System-Hostname {system.hostname}
|
||||||
}
|
}
|
||||||
health_uri /health
|
health_uri /health
|
||||||
}
|
}
|
||||||
@@ -29,6 +32,10 @@ reverse_proxy 127.0.0.1:65535 {
|
|||||||
"Host": [
|
"Host": [
|
||||||
"example.com"
|
"example.com"
|
||||||
],
|
],
|
||||||
|
"Same-Key": [
|
||||||
|
"1",
|
||||||
|
"2"
|
||||||
|
],
|
||||||
"X-Empty-Value": [
|
"X-Empty-Value": [
|
||||||
""
|
""
|
||||||
],
|
],
|
||||||
@@ -38,6 +45,9 @@ reverse_proxy 127.0.0.1:65535 {
|
|||||||
"X-Header-Keys": [
|
"X-Header-Keys": [
|
||||||
"VbG4NZwWnipo",
|
"VbG4NZwWnipo",
|
||||||
"335Q9/MhqcNU3s2TO"
|
"335Q9/MhqcNU3s2TO"
|
||||||
|
],
|
||||||
|
"X-System-Hostname": [
|
||||||
|
"{system.hostname}"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"uri": "/health"
|
"uri": "/health"
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
|
# 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
|
// act and assert
|
||||||
tester.AssertGetResponse("http://localhost:9080/", 200, "")
|
tester.AssertGetResponse("http://localhost:9080/", 200, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInvalidPrefix(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
config, expectedError string
|
||||||
|
}
|
||||||
|
|
||||||
|
failureCases := []testCase{
|
||||||
|
{
|
||||||
|
config: `wss://localhost`,
|
||||||
|
expectedError: `the scheme wss:// is only supported in browsers; use https:// instead`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: `ws://localhost`,
|
||||||
|
expectedError: `the scheme ws:// is only supported in browsers; use http:// instead`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: `someInvalidPrefix://localhost`,
|
||||||
|
expectedError: "unsupported URL scheme someinvalidprefix://",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: `h2c://localhost`,
|
||||||
|
expectedError: `unsupported URL scheme h2c://`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: `localhost, wss://localhost`,
|
||||||
|
expectedError: `the scheme wss:// is only supported in browsers; use https:// instead`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: `localhost {
|
||||||
|
reverse_proxy ws://localhost"
|
||||||
|
}`,
|
||||||
|
expectedError: `the scheme ws:// is only supported in browsers; use http:// instead`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config: `localhost {
|
||||||
|
reverse_proxy someInvalidPrefix://localhost"
|
||||||
|
}`,
|
||||||
|
expectedError: `unsupported URL scheme someinvalidprefix://`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, failureCase := range failureCases {
|
||||||
|
caddytest.AssertLoadError(t, failureCase.config, "caddyfile", failureCase.expectedError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidPrefix(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
rawConfig, expectedResponse string
|
||||||
|
}
|
||||||
|
|
||||||
|
successCases := []testCase{
|
||||||
|
{
|
||||||
|
"localhost",
|
||||||
|
`{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"https://localhost",
|
||||||
|
`{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"http://localhost",
|
||||||
|
`{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`localhost {
|
||||||
|
reverse_proxy http://localhost:3000
|
||||||
|
}`,
|
||||||
|
`{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "localhost:3000"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`localhost {
|
||||||
|
reverse_proxy https://localhost:3000
|
||||||
|
}`,
|
||||||
|
`{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"transport": {
|
||||||
|
"protocol": "http",
|
||||||
|
"tls": {}
|
||||||
|
},
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "localhost:3000"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`localhost {
|
||||||
|
reverse_proxy h2c://localhost:3000
|
||||||
|
}`,
|
||||||
|
`{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"transport": {
|
||||||
|
"protocol": "http",
|
||||||
|
"versions": [
|
||||||
|
"h2c",
|
||||||
|
"2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "localhost:3000"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`localhost {
|
||||||
|
reverse_proxy localhost:3000
|
||||||
|
}`,
|
||||||
|
`{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "localhost:3000"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, successCase := range successCases {
|
||||||
|
caddytest.AssertAdapt(t, successCase.rawConfig, "caddyfile", successCase.expectedResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.Header().Set("Cache-Control", "no-store")
|
||||||
w.WriteHeader(200)
|
w.WriteHeader(200)
|
||||||
if f, ok := w.(http.Flusher); ok {
|
http.NewResponseController(w).Flush()
|
||||||
f.Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, 4*1024)
|
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
|
package caddycmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
@@ -95,15 +99,22 @@ https://caddyserver.com/docs/running
|
|||||||
// kind of annoying to have all the help text printed out if
|
// kind of annoying to have all the help text printed out if
|
||||||
// caddy has an error provisioning its modules, for instance...
|
// caddy has an error provisioning its modules, for instance...
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
|
Version: onlyVersionText(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullDocsFooter = `Full documentation is available at:
|
const fullDocsFooter = `Full documentation is available at:
|
||||||
https://caddyserver.com/docs/command-line`
|
https://caddyserver.com/docs/command-line`
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
rootCmd.SetVersionTemplate("{{.Version}}\n")
|
||||||
rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n")
|
rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func onlyVersionText() string {
|
||||||
|
_, f := caddy.Version()
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
func caddyCmdToCobra(caddyCmd Command) *cobra.Command {
|
func caddyCmdToCobra(caddyCmd Command) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: caddyCmd.Name,
|
Use: caddyCmd.Name,
|
||||||
@@ -123,7 +134,24 @@ func caddyCmdToCobra(caddyCmd Command) *cobra.Command {
|
|||||||
// in a cobra command's RunE field.
|
// in a cobra command's RunE field.
|
||||||
func WrapCommandFuncForCobra(f CommandFunc) func(cmd *cobra.Command, _ []string) error {
|
func WrapCommandFuncForCobra(f CommandFunc) func(cmd *cobra.Command, _ []string) error {
|
||||||
return func(cmd *cobra.Command, _ []string) error {
|
return func(cmd *cobra.Command, _ []string) error {
|
||||||
_, 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
|
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"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -32,18 +33,27 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/aryann/difflib"
|
"github.com/aryann/difflib"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"go.uber.org/zap"
|
"github.com/caddyserver/caddy/v2/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
func cmdStart(fl Flags) (int, error) {
|
func cmdStart(fl Flags) (int, error) {
|
||||||
startCmdConfigFlag := fl.String("config")
|
configFlag := fl.String("config")
|
||||||
startCmdConfigAdapterFlag := fl.String("adapter")
|
configAdapterFlag := fl.String("adapter")
|
||||||
startCmdPidfileFlag := fl.String("pidfile")
|
pidfileFlag := fl.String("pidfile")
|
||||||
startCmdWatchFlag := fl.Bool("watch")
|
watchFlag := fl.Bool("watch")
|
||||||
startCmdEnvfileFlag := fl.String("envfile")
|
|
||||||
|
var err error
|
||||||
|
var envfileFlag []string
|
||||||
|
envfileFlag, err = fl.GetStringSlice("envfile")
|
||||||
|
if err != nil {
|
||||||
|
return caddy.ExitCodeFailedStartup,
|
||||||
|
fmt.Errorf("reading envfile flag: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// open a listener to which the child process will connect when
|
// open a listener to which the child process will connect when
|
||||||
// it is ready to confirm that it has successfully started
|
// it is ready to confirm that it has successfully started
|
||||||
@@ -64,22 +74,23 @@ func cmdStart(fl Flags) (int, error) {
|
|||||||
// sure by giving it some random bytes and having it echo
|
// sure by giving it some random bytes and having it echo
|
||||||
// them back to us)
|
// them back to us)
|
||||||
cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String())
|
cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String())
|
||||||
if startCmdConfigFlag != "" {
|
if configFlag != "" {
|
||||||
cmd.Args = append(cmd.Args, "--config", startCmdConfigFlag)
|
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 != "" {
|
if configAdapterFlag != "" {
|
||||||
cmd.Args = append(cmd.Args, "--adapter", startCmdConfigAdapterFlag)
|
cmd.Args = append(cmd.Args, "--adapter", configAdapterFlag)
|
||||||
}
|
}
|
||||||
if startCmdWatchFlag {
|
if watchFlag {
|
||||||
cmd.Args = append(cmd.Args, "--watch")
|
cmd.Args = append(cmd.Args, "--watch")
|
||||||
}
|
}
|
||||||
if startCmdPidfileFlag != "" {
|
if pidfileFlag != "" {
|
||||||
cmd.Args = append(cmd.Args, "--pidfile", startCmdPidfileFlag)
|
cmd.Args = append(cmd.Args, "--pidfile", pidfileFlag)
|
||||||
}
|
}
|
||||||
stdinpipe, err := cmd.StdinPipe()
|
stdinPipe, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup,
|
return caddy.ExitCodeFailedStartup,
|
||||||
fmt.Errorf("creating stdin pipe: %v", err)
|
fmt.Errorf("creating stdin pipe: %v", err)
|
||||||
@@ -91,7 +102,8 @@ func cmdStart(fl Flags) (int, error) {
|
|||||||
expect := make([]byte, 32)
|
expect := make([]byte, 32)
|
||||||
_, err = rand.Read(expect)
|
_, err = rand.Read(expect)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup, 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
|
// 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
|
// started yet, and writing synchronously would result
|
||||||
// in a deadlock
|
// in a deadlock
|
||||||
go func() {
|
go func() {
|
||||||
_, _ = stdinpipe.Write(expect)
|
_, _ = stdinPipe.Write(expect)
|
||||||
stdinpipe.Close()
|
stdinPipe.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// start the process
|
// start the process
|
||||||
err = cmd.Start()
|
err = cmd.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup, 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
|
// 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) {
|
func cmdRun(fl Flags) (int, error) {
|
||||||
caddy.TrapSignals()
|
caddy.TrapSignals()
|
||||||
|
|
||||||
runCmdConfigFlag := fl.String("config")
|
configFlag := fl.String("config")
|
||||||
runCmdConfigAdapterFlag := fl.String("adapter")
|
configAdapterFlag := fl.String("adapter")
|
||||||
runCmdResumeFlag := fl.Bool("resume")
|
resumeFlag := fl.Bool("resume")
|
||||||
runCmdLoadEnvfileFlag := fl.String("envfile")
|
printEnvFlag := fl.Bool("environ")
|
||||||
runCmdPrintEnvFlag := fl.Bool("environ")
|
watchFlag := fl.Bool("watch")
|
||||||
runCmdWatchFlag := fl.Bool("watch")
|
pidfileFlag := fl.String("pidfile")
|
||||||
runCmdPidfileFlag := fl.String("pidfile")
|
pingbackFlag := fl.String("pingback")
|
||||||
runCmdPingbackFlag := fl.String("pingback")
|
|
||||||
|
|
||||||
// load all additional envs as soon as possible
|
// load all additional envs as soon as possible
|
||||||
if runCmdLoadEnvfileFlag != "" {
|
err := handleEnvFileFlag(fl)
|
||||||
if err := loadEnvFromFile(runCmdLoadEnvfileFlag); err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup,
|
return caddy.ExitCodeFailedStartup, err
|
||||||
fmt.Errorf("loading additional environment variables: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we are supposed to print the environment, do that first
|
// if we are supposed to print the environment, do that first
|
||||||
if runCmdPrintEnvFlag {
|
if printEnvFlag {
|
||||||
printEnvironment()
|
printEnvironment()
|
||||||
}
|
}
|
||||||
|
|
||||||
// load the config, depending on flags
|
// load the config, depending on flags
|
||||||
var config []byte
|
var config []byte
|
||||||
var err error
|
if resumeFlag {
|
||||||
if runCmdResumeFlag {
|
|
||||||
config, err = os.ReadFile(caddy.ConfigAutosavePath)
|
config, err = os.ReadFile(caddy.ConfigAutosavePath)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
// not a bad error; just can't resume if autosave file doesn't exist
|
// not a bad error; just can't resume if autosave file doesn't exist
|
||||||
caddy.Log().Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath))
|
caddy.Log().Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath))
|
||||||
runCmdResumeFlag = false
|
resumeFlag = false
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup, err
|
return caddy.ExitCodeFailedStartup, err
|
||||||
} else {
|
} else {
|
||||||
if runCmdConfigFlag == "" {
|
if configFlag == "" {
|
||||||
caddy.Log().Info("resuming from last configuration",
|
caddy.Log().Info("resuming from last configuration",
|
||||||
zap.String("autosave_file", caddy.ConfigAutosavePath))
|
zap.String("autosave_file", caddy.ConfigAutosavePath))
|
||||||
} else {
|
} else {
|
||||||
@@ -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
|
// we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
|
||||||
var configFile string
|
var configFile string
|
||||||
if !runCmdResumeFlag {
|
if !resumeFlag {
|
||||||
config, configFile, err = LoadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
|
config, configFile, err = LoadConfig(configFlag, configAdapterFlag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup, err
|
return caddy.ExitCodeFailedStartup, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// create pidfile now, in case loading config takes a while (issue #5477)
|
// create pidfile now, in case loading config takes a while (issue #5477)
|
||||||
if runCmdPidfileFlag != "" {
|
if pidfileFlag != "" {
|
||||||
err := caddy.PIDFile(runCmdPidfileFlag)
|
err := caddy.PIDFile(pidfileFlag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
caddy.Log().Error("unable to write PID file",
|
caddy.Log().Error("unable to write PID file",
|
||||||
zap.String("pidfile", runCmdPidfileFlag),
|
zap.String("pidfile", pidfileFlag),
|
||||||
zap.Error(err))
|
zap.Error(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,13 +236,13 @@ func cmdRun(fl Flags) (int, error) {
|
|||||||
|
|
||||||
// if we are to report to another process the successful start
|
// if we are to report to another process the successful start
|
||||||
// of the server, do so now by echoing back contents of stdin
|
// of the server, do so now by echoing back contents of stdin
|
||||||
if runCmdPingbackFlag != "" {
|
if pingbackFlag != "" {
|
||||||
confirmationBytes, err := io.ReadAll(os.Stdin)
|
confirmationBytes, err := io.ReadAll(os.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup,
|
return caddy.ExitCodeFailedStartup,
|
||||||
fmt.Errorf("reading confirmation bytes from stdin: %v", err)
|
fmt.Errorf("reading confirmation bytes from stdin: %v", err)
|
||||||
}
|
}
|
||||||
conn, err := net.Dial("tcp", runCmdPingbackFlag)
|
conn, err := net.Dial("tcp", pingbackFlag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup,
|
return caddy.ExitCodeFailedStartup,
|
||||||
fmt.Errorf("dialing confirmation address: %v", err)
|
fmt.Errorf("dialing confirmation address: %v", err)
|
||||||
@@ -242,14 +251,14 @@ func cmdRun(fl Flags) (int, error) {
|
|||||||
_, err = conn.Write(confirmationBytes)
|
_, err = conn.Write(confirmationBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup,
|
return caddy.ExitCodeFailedStartup,
|
||||||
fmt.Errorf("writing confirmation bytes to %s: %v", runCmdPingbackFlag, err)
|
fmt.Errorf("writing confirmation bytes to %s: %v", pingbackFlag, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if enabled, reload config file automatically on changes
|
// if enabled, reload config file automatically on changes
|
||||||
// (this better only be used in dev!)
|
// (this better only be used in dev!)
|
||||||
if runCmdWatchFlag {
|
if watchFlag {
|
||||||
go watchConfigFile(configFile, runCmdConfigAdapterFlag)
|
go watchConfigFile(configFile, configAdapterFlag)
|
||||||
}
|
}
|
||||||
|
|
||||||
// warn if the environment does not provide enough information about the disk
|
// 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) {
|
func cmdStop(fl Flags) (int, error) {
|
||||||
addrFlag := fl.String("address")
|
addressFlag := fl.String("address")
|
||||||
configFlag := fl.String("config")
|
configFlag := fl.String("config")
|
||||||
configAdapterFlag := fl.String("adapter")
|
configAdapterFlag := fl.String("adapter")
|
||||||
|
|
||||||
adminAddr, err := DetermineAdminAPIAddress(addrFlag, nil, configFlag, configAdapterFlag)
|
adminAddr, err := DetermineAdminAPIAddress(addressFlag, nil, configFlag, configAdapterFlag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
|
||||||
}
|
}
|
||||||
@@ -297,7 +306,7 @@ func cmdStop(fl Flags) (int, error) {
|
|||||||
func cmdReload(fl Flags) (int, error) {
|
func cmdReload(fl Flags) (int, error) {
|
||||||
configFlag := fl.String("config")
|
configFlag := fl.String("config")
|
||||||
configAdapterFlag := fl.String("adapter")
|
configAdapterFlag := fl.String("adapter")
|
||||||
addrFlag := fl.String("address")
|
addressFlag := fl.String("address")
|
||||||
forceFlag := fl.Bool("force")
|
forceFlag := fl.Bool("force")
|
||||||
|
|
||||||
// get the config in caddy's native format
|
// get the config in caddy's native format
|
||||||
@@ -309,7 +318,7 @@ func cmdReload(fl Flags) (int, error) {
|
|||||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
|
||||||
}
|
}
|
||||||
|
|
||||||
adminAddr, err := DetermineAdminAPIAddress(addrFlag, config, configFlag, configAdapterFlag)
|
adminAddr, err := DetermineAdminAPIAddress(addressFlag, config, configFlag, configAdapterFlag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
|
||||||
}
|
}
|
||||||
@@ -411,60 +420,60 @@ func cmdListModules(fl Flags) (int, error) {
|
|||||||
return caddy.ExitCodeSuccess, nil
|
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()
|
printEnvironment()
|
||||||
return caddy.ExitCodeSuccess, nil
|
return caddy.ExitCodeSuccess, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmdAdaptConfig(fl Flags) (int, error) {
|
func cmdAdaptConfig(fl Flags) (int, error) {
|
||||||
adaptCmdInputFlag := fl.String("config")
|
inputFlag := fl.String("config")
|
||||||
adaptCmdAdapterFlag := fl.String("adapter")
|
adapterFlag := fl.String("adapter")
|
||||||
adaptCmdPrettyFlag := fl.Bool("pretty")
|
prettyFlag := fl.Bool("pretty")
|
||||||
adaptCmdValidateFlag := fl.Bool("validate")
|
validateFlag := fl.Bool("validate")
|
||||||
|
|
||||||
// if no input file was specified, try a default
|
var err error
|
||||||
// Caddyfile if the Caddyfile adapter is plugged in
|
inputFlag, err = configFileWithRespectToDefault(caddy.Log(), inputFlag)
|
||||||
if adaptCmdInputFlag == "" && caddyconfig.GetAdapter("caddyfile") != nil {
|
if err != nil {
|
||||||
_, err := os.Stat("Caddyfile")
|
return caddy.ExitCodeFailedStartup, err
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if adaptCmdInputFlag == "" {
|
// load all additional envs as soon as possible
|
||||||
return caddy.ExitCodeFailedStartup,
|
err = handleEnvFileFlag(fl)
|
||||||
fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
|
if err != nil {
|
||||||
|
return caddy.ExitCodeFailedStartup, err
|
||||||
}
|
}
|
||||||
if adaptCmdAdapterFlag == "" {
|
|
||||||
|
if adapterFlag == "" {
|
||||||
return caddy.ExitCodeFailedStartup,
|
return caddy.ExitCodeFailedStartup,
|
||||||
fmt.Errorf("adapter name is required (use --adapt flag or leave unspecified for default)")
|
fmt.Errorf("adapter name is required (use --adapt flag or leave unspecified for default)")
|
||||||
}
|
}
|
||||||
|
|
||||||
cfgAdapter := caddyconfig.GetAdapter(adaptCmdAdapterFlag)
|
cfgAdapter := caddyconfig.GetAdapter(adapterFlag)
|
||||||
if cfgAdapter == nil {
|
if cfgAdapter == nil {
|
||||||
return caddy.ExitCodeFailedStartup,
|
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 {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup,
|
return caddy.ExitCodeFailedStartup,
|
||||||
fmt.Errorf("reading input file: %v", err)
|
fmt.Errorf("reading input file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := map[string]any{"filename": adaptCmdInputFlag}
|
opts := map[string]any{"filename": inputFlag}
|
||||||
|
|
||||||
adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts)
|
adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup, err
|
return caddy.ExitCodeFailedStartup, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if adaptCmdPrettyFlag {
|
if prettyFlag {
|
||||||
var prettyBuf bytes.Buffer
|
var prettyBuf bytes.Buffer
|
||||||
err = json.Indent(&prettyBuf, adaptedConfig, "", "\t")
|
err = json.Indent(&prettyBuf, adaptedConfig, "", "\t")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -482,13 +491,13 @@ func cmdAdaptConfig(fl Flags) (int, error) {
|
|||||||
if warn.Directive != "" {
|
if warn.Directive != "" {
|
||||||
msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
|
msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
|
||||||
}
|
}
|
||||||
caddy.Log().Named(adaptCmdAdapterFlag).Warn(msg,
|
caddy.Log().Named(adapterFlag).Warn(msg,
|
||||||
zap.String("file", warn.File),
|
zap.String("file", warn.File),
|
||||||
zap.Int("line", warn.Line))
|
zap.Int("line", warn.Line))
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate output if requested
|
// validate output if requested
|
||||||
if adaptCmdValidateFlag {
|
if validateFlag {
|
||||||
var cfg *caddy.Config
|
var cfg *caddy.Config
|
||||||
err = caddy.StrictUnmarshalJSON(adaptedConfig, &cfg)
|
err = caddy.StrictUnmarshalJSON(adaptedConfig, &cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -504,19 +513,26 @@ func cmdAdaptConfig(fl Flags) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cmdValidateConfig(fl Flags) (int, error) {
|
func cmdValidateConfig(fl Flags) (int, error) {
|
||||||
validateCmdConfigFlag := fl.String("config")
|
configFlag := fl.String("config")
|
||||||
validateCmdAdapterFlag := fl.String("adapter")
|
adapterFlag := fl.String("adapter")
|
||||||
runCmdLoadEnvfileFlag := fl.String("envfile")
|
|
||||||
|
|
||||||
// load all additional envs as soon as possible
|
// load all additional envs as soon as possible
|
||||||
if runCmdLoadEnvfileFlag != "" {
|
err := handleEnvFileFlag(fl)
|
||||||
if err := loadEnvFromFile(runCmdLoadEnvfileFlag); err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup,
|
return caddy.ExitCodeFailedStartup, err
|
||||||
fmt.Errorf("loading additional environment variables: %v", 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 {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup, err
|
return caddy.ExitCodeFailedStartup, err
|
||||||
}
|
}
|
||||||
@@ -539,13 +555,13 @@ func cmdValidateConfig(fl Flags) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cmdFmt(fl Flags) (int, error) {
|
func cmdFmt(fl Flags) (int, error) {
|
||||||
formatCmdConfigFile := fl.Arg(0)
|
configFile := fl.Arg(0)
|
||||||
if formatCmdConfigFile == "" {
|
if configFile == "" {
|
||||||
formatCmdConfigFile = "Caddyfile"
|
configFile = "Caddyfile"
|
||||||
}
|
}
|
||||||
|
|
||||||
// as a special case, read from stdin if the file name is "-"
|
// as a special case, read from stdin if the file name is "-"
|
||||||
if formatCmdConfigFile == "-" {
|
if configFile == "-" {
|
||||||
input, err := io.ReadAll(os.Stdin)
|
input, err := io.ReadAll(os.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup,
|
return caddy.ExitCodeFailedStartup,
|
||||||
@@ -555,7 +571,7 @@ func cmdFmt(fl Flags) (int, error) {
|
|||||||
return caddy.ExitCodeSuccess, nil
|
return caddy.ExitCodeSuccess, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
input, err := os.ReadFile(formatCmdConfigFile)
|
input, err := os.ReadFile(configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup,
|
return caddy.ExitCodeFailedStartup,
|
||||||
fmt.Errorf("reading input file: %v", err)
|
fmt.Errorf("reading input file: %v", err)
|
||||||
@@ -564,7 +580,7 @@ func cmdFmt(fl Flags) (int, error) {
|
|||||||
output := caddyfile.Format(input)
|
output := caddyfile.Format(input)
|
||||||
|
|
||||||
if fl.Bool("overwrite") {
|
if fl.Bool("overwrite") {
|
||||||
if err := os.WriteFile(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.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err)
|
||||||
}
|
}
|
||||||
return caddy.ExitCodeSuccess, nil
|
return caddy.ExitCodeSuccess, nil
|
||||||
@@ -588,7 +604,7 @@ func cmdFmt(fl Flags) (int, error) {
|
|||||||
fmt.Print(string(output))
|
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`,
|
return caddy.ExitCodeFailedStartup, fmt.Errorf(`%s:%d: Caddyfile input is not formatted; Tip: use '--overwrite' to update your Caddyfile in-place instead of previewing it. Consult '--help' for more options`,
|
||||||
warning.File,
|
warning.File,
|
||||||
warning.Line,
|
warning.Line,
|
||||||
@@ -598,6 +614,25 @@ func cmdFmt(fl Flags) (int, error) {
|
|||||||
return caddy.ExitCodeSuccess, nil
|
return caddy.ExitCodeSuccess, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleEnvFileFlag loads the environment variables from the given --envfile
|
||||||
|
// flag if specified. This should be called as early in the command function.
|
||||||
|
func handleEnvFileFlag(fl Flags) error {
|
||||||
|
var err error
|
||||||
|
var envfileFlag []string
|
||||||
|
envfileFlag, err = fl.GetStringSlice("envfile")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading envfile flag: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, envfile := range envfileFlag {
|
||||||
|
if err := loadEnvFromFile(envfile); err != nil {
|
||||||
|
return fmt.Errorf("loading additional environment variables: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// AdminAPIRequest makes an API request according to the CLI flags given,
|
// AdminAPIRequest makes an API request according to the CLI flags given,
|
||||||
// with the given HTTP method and request URI. If body is non-nil, it will
|
// with the given HTTP method and request URI. If body is non-nil, it will
|
||||||
// be assumed to be Content-Type application/json. The caller should close
|
// be assumed to be Content-Type application/json. The caller should close
|
||||||
@@ -610,7 +645,17 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
|
|||||||
}
|
}
|
||||||
origin := "http://" + parsedAddr.JoinHostPort(0)
|
origin := "http://" + parsedAddr.JoinHostPort(0)
|
||||||
if parsedAddr.IsUnixNetwork() {
|
if parsedAddr.IsUnixNetwork() {
|
||||||
origin = "http://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
|
// 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)
|
return nil, fmt.Errorf("making request: %v", err)
|
||||||
}
|
}
|
||||||
if parsedAddr.IsUnixNetwork() {
|
if parsedAddr.IsUnixNetwork() {
|
||||||
// When listening on a unix socket, the admin endpoint doesn't
|
// We used to conform to RFC 2616 Section 14.26 which requires
|
||||||
// accept any Host header because there is no host:port for
|
// an empty host header when there is no host, as is the case
|
||||||
// a unix socket's address. The server's host check is fairly
|
// with unix sockets. However, Go required a Host value so we
|
||||||
// strict for security reasons, so we don't allow just any
|
// used a hack of a space character as the host (it would see
|
||||||
// Host header. For unix sockets, the Host header must be
|
// the Host was non-empty, then trim the space later). As of
|
||||||
// empty. Unfortunately, Go makes it impossible to make HTTP
|
// Go 1.20.6 (July 2023), this hack no longer works. See:
|
||||||
// requests with an empty Host header... except with this one
|
// https://github.com/golang/go/issues/60374
|
||||||
// weird trick. (Hopefully they don't fix it. It's already
|
// See also the discussion here:
|
||||||
// hard enough to use HTTP over unix sockets.)
|
// https://github.com/golang/go/issues/61431
|
||||||
//
|
//
|
||||||
// An equivalent curl command would be something like:
|
// After that, we now require a Host value of either 127.0.0.1
|
||||||
// $ curl --unix-socket caddy.sock http:/:$REQUEST_URI
|
// or ::1 if one is set. Above I choose to use 127.0.0.1. Even
|
||||||
req.URL.Host = " "
|
// though the value should be completely irrelevant (it could be
|
||||||
req.Host = ""
|
// "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 {
|
} else {
|
||||||
req.Header.Set("Origin", origin)
|
req.Header.Set("Origin", origin)
|
||||||
}
|
}
|
||||||
@@ -721,6 +770,31 @@ func DetermineAdminAPIAddress(address string, config []byte, configFile, configA
|
|||||||
return caddy.DefaultAdminListen, nil
|
return caddy.DefaultAdminListen, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// configFileWithRespectToDefault returns the filename to use for loading the config, based
|
||||||
|
// on whether a config file is already specified and a supported default config file exists.
|
||||||
|
func configFileWithRespectToDefault(logger *zap.Logger, configFile string) (string, error) {
|
||||||
|
const defaultCaddyfile = "Caddyfile"
|
||||||
|
|
||||||
|
// if no input file was specified, try a default Caddyfile if the Caddyfile adapter is plugged in
|
||||||
|
if configFile == "" && caddyconfig.GetAdapter("caddyfile") != nil {
|
||||||
|
_, err := os.Stat(defaultCaddyfile)
|
||||||
|
if err == nil {
|
||||||
|
// default Caddyfile exists
|
||||||
|
if logger != nil {
|
||||||
|
logger.Info("using adjacent Caddyfile")
|
||||||
|
}
|
||||||
|
return defaultCaddyfile, nil
|
||||||
|
}
|
||||||
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
// problem checking
|
||||||
|
return configFile, fmt.Errorf("checking if default Caddyfile exists: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// default config file does not exist or is irrelevant
|
||||||
|
return configFile, nil
|
||||||
|
}
|
||||||
|
|
||||||
type moduleInfo struct {
|
type moduleInfo struct {
|
||||||
caddyModuleID string
|
caddyModuleID string
|
||||||
goModule *debug.Module
|
goModule *debug.Module
|
||||||
|
|||||||
+92
-30
@@ -21,9 +21,10 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/cobra/doc"
|
"github.com/spf13/cobra/doc"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Command represents a subcommand. Name, Func,
|
// Command represents a subcommand. Name, Func,
|
||||||
@@ -93,8 +94,8 @@ func init() {
|
|||||||
Starts the Caddy process, optionally bootstrapped with an initial config file.
|
Starts the Caddy process, optionally bootstrapped with an initial config file.
|
||||||
This command unblocks after the server starts running or fails to run.
|
This command unblocks after the server starts running or fails to run.
|
||||||
|
|
||||||
If --envfile is specified, an environment file with environment variables in
|
If --envfile is specified, an environment file with environment variables
|
||||||
the KEY=VALUE format will be loaded into the Caddy process.
|
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
|
On Windows, the spawned child process will remain attached to the terminal, so
|
||||||
closing the window will forcefully stop Caddy; to avoid forgetting this, try
|
closing the window will forcefully stop Caddy; to avoid forgetting this, try
|
||||||
@@ -103,7 +104,7 @@ using 'caddy run' instead to keep it in the foreground.
|
|||||||
CobraFunc: func(cmd *cobra.Command) {
|
CobraFunc: func(cmd *cobra.Command) {
|
||||||
cmd.Flags().StringP("config", "c", "", "Configuration file")
|
cmd.Flags().StringP("config", "c", "", "Configuration file")
|
||||||
cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply")
|
cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply")
|
||||||
cmd.Flags().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().BoolP("watch", "w", false, "Reload changed config file automatically")
|
||||||
cmd.Flags().StringP("pidfile", "", "", "Path of file to which to write process ID")
|
cmd.Flags().StringP("pidfile", "", "", "Path of file to which to write process ID")
|
||||||
cmd.RunE = WrapCommandFuncForCobra(cmdStart)
|
cmd.RunE = WrapCommandFuncForCobra(cmdStart)
|
||||||
@@ -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
|
that file will be loaded and used to configure Caddy, even without any command
|
||||||
line flags.
|
line flags.
|
||||||
|
|
||||||
If --envfile is specified, an environment file with environment variables in
|
If --envfile is specified, an environment file with environment variables
|
||||||
the KEY=VALUE format will be loaded into the Caddy process.
|
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
|
If --environ is specified, the environment as seen by the Caddy process will
|
||||||
be printed before starting. This is the same as the environ command but does
|
be printed before starting. This is the same as the environ command but does
|
||||||
@@ -149,7 +150,7 @@ option in a local development environment.
|
|||||||
CobraFunc: func(cmd *cobra.Command) {
|
CobraFunc: func(cmd *cobra.Command) {
|
||||||
cmd.Flags().StringP("config", "c", "", "Configuration file")
|
cmd.Flags().StringP("config", "c", "", "Configuration file")
|
||||||
cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply")
|
cmd.Flags().StringP("adapter", "a", "", "Name of config adapter to apply")
|
||||||
cmd.Flags().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("environ", "e", false, "Print environment")
|
||||||
cmd.Flags().BoolP("resume", "r", false, "Use saved config, if any (and prefer over --config file)")
|
cmd.Flags().BoolP("resume", "r", false, "Use saved config, if any (and prefer over --config file)")
|
||||||
cmd.Flags().BoolP("watch", "w", false, "Watch config file for changes and reload it automatically")
|
cmd.Flags().BoolP("watch", "w", false, "Watch config file for changes and reload it automatically")
|
||||||
@@ -239,6 +240,7 @@ documentation: https://go.dev/doc/modules/version-numbers
|
|||||||
|
|
||||||
RegisterCommand(Command{
|
RegisterCommand(Command{
|
||||||
Name: "environ",
|
Name: "environ",
|
||||||
|
Usage: "[--envfile <path>]",
|
||||||
Short: "Prints the environment",
|
Short: "Prints the environment",
|
||||||
Long: `
|
Long: `
|
||||||
Prints the environment as seen by this Caddy process.
|
Prints the environment as seen by this Caddy process.
|
||||||
@@ -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
|
this command can be useful for verifying that the variables will have
|
||||||
the values you expect in your config.
|
the values you expect in your config.
|
||||||
|
|
||||||
|
If --envfile is specified, an environment file with environment variables
|
||||||
|
in the KEY=VALUE format will be loaded into the Caddy process.
|
||||||
|
|
||||||
Note that environments may be different depending on how you run Caddy.
|
Note that environments may be different depending on how you run Caddy.
|
||||||
Environments for Caddy instances started by service managers such as
|
Environments for Caddy instances started by service managers such as
|
||||||
systemd are often different than the environment inherited from your
|
systemd are often different than the environment inherited from your
|
||||||
@@ -258,12 +263,15 @@ by adding the "--environ" flag.
|
|||||||
|
|
||||||
Environments may contain sensitive data.
|
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{
|
RegisterCommand(Command{
|
||||||
Name: "adapt",
|
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",
|
Short: "Adapts a configuration to Caddy's native JSON",
|
||||||
Long: `
|
Long: `
|
||||||
Adapts a configuration to Caddy's native JSON format and writes the
|
Adapts a configuration to Caddy's native JSON format and writes the
|
||||||
@@ -275,12 +283,16 @@ for human readability.
|
|||||||
If --validate is used, the adapted config will be checked for validity.
|
If --validate is used, the adapted config will be checked for validity.
|
||||||
If the config is invalid, an error will be printed to stderr and a non-
|
If the config is invalid, an error will be printed to stderr and a non-
|
||||||
zero exit status will be returned.
|
zero exit status will be returned.
|
||||||
|
|
||||||
|
If --envfile is specified, an environment file with environment variables
|
||||||
|
in the KEY=VALUE format will be loaded into the Caddy process.
|
||||||
`,
|
`,
|
||||||
CobraFunc: func(cmd *cobra.Command) {
|
CobraFunc: func(cmd *cobra.Command) {
|
||||||
cmd.Flags().StringP("config", "c", "", "Configuration file to adapt (required)")
|
cmd.Flags().StringP("config", "c", "", "Configuration file to adapt (required)")
|
||||||
cmd.Flags().StringP("adapter", "a", "caddyfile", "Name of config adapter")
|
cmd.Flags().StringP("adapter", "a", "caddyfile", "Name of config adapter")
|
||||||
cmd.Flags().BoolP("pretty", "p", false, "Format the output for human readability")
|
cmd.Flags().BoolP("pretty", "p", false, "Format the output for human readability")
|
||||||
cmd.Flags().BoolP("validate", "", false, "Validate the output")
|
cmd.Flags().BoolP("validate", "", false, "Validate the output")
|
||||||
|
cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
|
||||||
cmd.RunE = WrapCommandFuncForCobra(cmdAdaptConfig)
|
cmd.RunE = WrapCommandFuncForCobra(cmdAdaptConfig)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -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
|
This reveals any errors with the configuration through the loading and
|
||||||
provisioning stages.
|
provisioning stages.
|
||||||
|
|
||||||
If --envfile is specified, an environment file with environment variables in
|
If --envfile is specified, an environment file with environment variables
|
||||||
the KEY=VALUE format will be loaded into the Caddy process.
|
in the KEY=VALUE format will be loaded into the Caddy process.
|
||||||
`,
|
`,
|
||||||
CobraFunc: func(cmd *cobra.Command) {
|
CobraFunc: func(cmd *cobra.Command) {
|
||||||
cmd.Flags().StringP("config", "c", "", "Input configuration file")
|
cmd.Flags().StringP("config", "c", "", "Input configuration file")
|
||||||
cmd.Flags().StringP("adapter", "a", "", "Name of config adapter")
|
cmd.Flags().StringP("adapter", "a", "", "Name of config adapter")
|
||||||
cmd.Flags().StringP("envfile", "", "", "Environment file to load")
|
cmd.Flags().StringSliceP("envfile", "", []string{}, "Environment file(s) to load")
|
||||||
cmd.RunE = WrapCommandFuncForCobra(cmdValidateConfig)
|
cmd.RunE = WrapCommandFuncForCobra(cmdValidateConfig)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
RegisterCommand(Command{
|
||||||
|
Name: "storage",
|
||||||
|
Short: "Commands for working with Caddy's storage (EXPERIMENTAL)",
|
||||||
|
Long: `
|
||||||
|
Allows exporting and importing Caddy's storage contents. The two commands can be
|
||||||
|
combined in a pipeline to transfer directly from one storage to another:
|
||||||
|
|
||||||
|
$ caddy storage export --config Caddyfile.old --output - |
|
||||||
|
> caddy storage import --config Caddyfile.new --input -
|
||||||
|
|
||||||
|
The - argument refers to stdout and stdin, respectively.
|
||||||
|
|
||||||
|
NOTE: When importing to or exporting from file_system storage (the default), the command
|
||||||
|
should be run as the user that owns the associated root path.
|
||||||
|
|
||||||
|
EXPERIMENTAL: May be changed or removed.
|
||||||
|
`,
|
||||||
|
CobraFunc: func(cmd *cobra.Command) {
|
||||||
|
exportCmd := &cobra.Command{
|
||||||
|
Use: "export --config <path> --output <path>",
|
||||||
|
Short: "Exports storage assets as a tarball",
|
||||||
|
Long: `
|
||||||
|
The contents of the configured storage module (TLS certificates, etc)
|
||||||
|
are exported via a tarball.
|
||||||
|
|
||||||
|
--output is required, - can be given for stdout.
|
||||||
|
`,
|
||||||
|
RunE: WrapCommandFuncForCobra(cmdExportStorage),
|
||||||
|
}
|
||||||
|
exportCmd.Flags().StringP("config", "c", "", "Input configuration file (required)")
|
||||||
|
exportCmd.Flags().StringP("output", "o", "", "Output path")
|
||||||
|
cmd.AddCommand(exportCmd)
|
||||||
|
|
||||||
|
importCmd := &cobra.Command{
|
||||||
|
Use: "import --config <path> --input <path>",
|
||||||
|
Short: "Imports storage assets from a tarball.",
|
||||||
|
Long: `
|
||||||
|
Imports storage assets to the configured storage module. The import file must be
|
||||||
|
a tar archive.
|
||||||
|
|
||||||
|
--input is required, - can be given for stdin.
|
||||||
|
`,
|
||||||
|
RunE: WrapCommandFuncForCobra(cmdImportStorage),
|
||||||
|
}
|
||||||
|
importCmd.Flags().StringP("config", "c", "", "Configuration file to load (required)")
|
||||||
|
importCmd.Flags().StringP("input", "i", "", "Tar of assets to load (required)")
|
||||||
|
cmd.AddCommand(importCmd)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
RegisterCommand(Command{
|
RegisterCommand(Command{
|
||||||
Name: "fmt",
|
Name: "fmt",
|
||||||
Usage: "[--overwrite] [--diff] [<path>]",
|
Usage: "[--overwrite] [--diff] [<path>]",
|
||||||
@@ -351,7 +413,7 @@ latest versions. EXPERIMENTAL: May be changed or removed.
|
|||||||
Short: "Adds Caddy packages (EXPERIMENTAL)",
|
Short: "Adds Caddy packages (EXPERIMENTAL)",
|
||||||
Long: `
|
Long: `
|
||||||
Downloads an updated Caddy binary with the specified packages (module/plugin)
|
Downloads an updated Caddy binary with the specified packages (module/plugin)
|
||||||
added. Retains existing packages. Returns an error if the any of packages are
|
added. Retains existing packages. Returns an error if the any of packages are
|
||||||
already included. EXPERIMENTAL: May be changed or removed.
|
already included. EXPERIMENTAL: May be changed or removed.
|
||||||
`,
|
`,
|
||||||
CobraFunc: func(cmd *cobra.Command) {
|
CobraFunc: func(cmd *cobra.Command) {
|
||||||
@@ -366,8 +428,8 @@ already included. EXPERIMENTAL: May be changed or removed.
|
|||||||
Usage: "<packages...>",
|
Usage: "<packages...>",
|
||||||
Short: "Removes Caddy packages (EXPERIMENTAL)",
|
Short: "Removes Caddy packages (EXPERIMENTAL)",
|
||||||
Long: `
|
Long: `
|
||||||
Downloads an updated Caddy binaries without the specified packages (module/plugin).
|
Downloads an updated Caddy binaries without the specified packages (module/plugin).
|
||||||
Returns an error if any of the packages are not included.
|
Returns an error if any of the packages are not included.
|
||||||
EXPERIMENTAL: May be changed or removed.
|
EXPERIMENTAL: May be changed or removed.
|
||||||
`,
|
`,
|
||||||
CobraFunc: func(cmd *cobra.Command) {
|
CobraFunc: func(cmd *cobra.Command) {
|
||||||
@@ -394,7 +456,7 @@ argument of --directory. If the directory does not exist, it will be created.
|
|||||||
if dir == "" {
|
if dir == "" {
|
||||||
return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required")
|
return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required")
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
return caddy.ExitCodeFailedQuit, err
|
return caddy.ExitCodeFailedQuit, err
|
||||||
}
|
}
|
||||||
if err := doc.GenManTree(rootCmd, &doc.GenManHeader{
|
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]",
|
Use: "completion [bash|zsh|fish|powershell]",
|
||||||
Short: "Generate completion script",
|
Short: "Generate completion script",
|
||||||
Long: fmt.Sprintf(`To load completions:
|
Long: fmt.Sprintf(`To load completions:
|
||||||
|
|
||||||
Bash:
|
Bash:
|
||||||
|
|
||||||
$ source <(%[1]s completion bash)
|
$ source <(%[1]s completion bash)
|
||||||
|
|
||||||
# To load completions for each session, execute once:
|
# To load completions for each session, execute once:
|
||||||
# Linux:
|
# Linux:
|
||||||
$ %[1]s completion bash > /etc/bash_completion.d/%[1]s
|
$ %[1]s completion bash > /etc/bash_completion.d/%[1]s
|
||||||
# macOS:
|
# macOS:
|
||||||
$ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
|
$ %[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
|
||||||
|
|
||||||
Zsh:
|
Zsh:
|
||||||
|
|
||||||
# If shell completion is not already enabled in your environment,
|
# If shell completion is not already enabled in your environment,
|
||||||
# you will need to enable it. You can execute the following once:
|
# you will need to enable it. You can execute the following once:
|
||||||
|
|
||||||
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
|
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
|
||||||
|
|
||||||
# To load completions for each session, execute once:
|
# To load completions for each session, execute once:
|
||||||
$ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
|
$ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
|
||||||
|
|
||||||
# You will need to start a new shell for this setup to take effect.
|
# You will need to start a new shell for this setup to take effect.
|
||||||
|
|
||||||
fish:
|
fish:
|
||||||
|
|
||||||
$ %[1]s completion fish | source
|
$ %[1]s completion fish | source
|
||||||
|
|
||||||
# To load completions for each session, execute once:
|
# To load completions for each session, execute once:
|
||||||
$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
|
$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
|
||||||
|
|
||||||
PowerShell:
|
PowerShell:
|
||||||
|
|
||||||
PS> %[1]s completion powershell | Out-String | Invoke-Expression
|
PS> %[1]s completion powershell | Out-String | Invoke-Expression
|
||||||
|
|
||||||
# To load completions for every new session, run:
|
# To load completions for every new session, run:
|
||||||
PS> %[1]s completion powershell > %[1]s.ps1
|
PS> %[1]s completion powershell > %[1]s.ps1
|
||||||
# and source this file from your PowerShell profile.
|
# and source this file from your PowerShell profile.
|
||||||
|
|||||||
+17
-8
@@ -17,6 +17,7 @@ package caddycmd
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -30,11 +31,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -62,6 +64,10 @@ func Main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
var exitError *exitError
|
||||||
|
if errors.As(err, &exitError) {
|
||||||
|
os.Exit(exitError.ExitCode)
|
||||||
|
}
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,9 +123,8 @@ func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([
|
|||||||
zap.String("config_adapter", adapterName))
|
zap.String("config_adapter", adapterName))
|
||||||
}
|
}
|
||||||
} else if adapterName == "" {
|
} else if adapterName == "" {
|
||||||
// as a special case when no config file or adapter
|
// if the Caddyfile adapter is plugged in, we can try using an
|
||||||
// is specified, see if the Caddyfile adapter is
|
// adjacent Caddyfile by default
|
||||||
// plugged in, and if so, try using a default Caddyfile
|
|
||||||
cfgAdapter = caddyconfig.GetAdapter("caddyfile")
|
cfgAdapter = caddyconfig.GetAdapter("caddyfile")
|
||||||
if cfgAdapter != nil {
|
if cfgAdapter != nil {
|
||||||
config, err = os.ReadFile("Caddyfile")
|
config, err = os.ReadFile("Caddyfile")
|
||||||
@@ -214,7 +219,7 @@ func watchConfigFile(filename, adapterName string) {
|
|||||||
//nolint:staticcheck
|
//nolint:staticcheck
|
||||||
for range time.Tick(1 * time.Second) {
|
for range time.Tick(1 * time.Second) {
|
||||||
// get current config
|
// get current config
|
||||||
newCfg, _, err := LoadConfig(filename, adapterName)
|
newCfg, _, err := loadConfigWithLogger(nil, filename, adapterName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Error("unable to load latest config", zap.Error(err))
|
logger().Error("unable to load latest config", zap.Error(err))
|
||||||
return
|
return
|
||||||
@@ -300,8 +305,12 @@ func loadEnvFromFile(envFile string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for k, v := range envMap {
|
for k, v := range envMap {
|
||||||
if err := os.Setenv(k, v); err != nil {
|
// do not overwrite existing environment variables
|
||||||
return fmt.Errorf("setting environment variables: %v", err)
|
_, 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"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func cmdUpgrade(fl Flags) (int, error) {
|
func cmdUpgrade(fl Flags) (int, error) {
|
||||||
@@ -102,6 +104,15 @@ func upgradeBuild(pluginPkgs map[string]struct{}, fl Flags) (int, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("retrieving current executable permission bits: %v", err)
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("retrieving current executable permission bits: %v", err)
|
||||||
}
|
}
|
||||||
|
if thisExecStat.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||||
|
symSource := thisExecPath
|
||||||
|
// we are a symlink; resolve it
|
||||||
|
thisExecPath, err = filepath.EvalSymlinks(thisExecPath)
|
||||||
|
if err != nil {
|
||||||
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("resolving current executable symlink: %v", err)
|
||||||
|
}
|
||||||
|
l.Info("this executable is a symlink", zap.String("source", symSource), zap.String("target", thisExecPath))
|
||||||
|
}
|
||||||
l.Info("this executable will be replaced", zap.String("path", thisExecPath))
|
l.Info("this executable will be replaced", zap.String("path", thisExecPath))
|
||||||
|
|
||||||
// build the request URL to download this custom build
|
// build the request URL to download this custom build
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+19
-11
@@ -410,6 +410,11 @@ func (ctx Context) loadModuleInline(moduleNameKey, moduleScope string, raw json.
|
|||||||
// called during the Provision/Validate phase to reference a
|
// called during the Provision/Validate phase to reference a
|
||||||
// module's own host app (since the parent app module is still
|
// module's own host app (since the parent app module is still
|
||||||
// in the process of being provisioned, it is not yet ready).
|
// in the process of being provisioned, it is not yet ready).
|
||||||
|
//
|
||||||
|
// We return any type instead of the App type because it is NOT
|
||||||
|
// intended for the caller of this method to be the one to start
|
||||||
|
// or stop App modules. The caller is expected to assert to the
|
||||||
|
// concrete type.
|
||||||
func (ctx Context) App(name string) (any, error) {
|
func (ctx Context) App(name string) (any, error) {
|
||||||
if app, ok := ctx.cfg.apps[name]; ok {
|
if app, ok := ctx.cfg.apps[name]; ok {
|
||||||
return app, nil
|
return app, nil
|
||||||
@@ -428,18 +433,21 @@ func (ctx Context) App(name string) (any, error) {
|
|||||||
|
|
||||||
// AppIfConfigured returns an app by its name if it has been
|
// AppIfConfigured returns an app by its name if it has been
|
||||||
// configured. Can be called instead of App() to avoid
|
// configured. Can be called instead of App() to avoid
|
||||||
// instantiating an empty app when that's not desirable.
|
// instantiating an empty app when that's not desirable. If
|
||||||
func (ctx Context) AppIfConfigured(name string) (any, error) {
|
// the app has not been loaded, nil is returned.
|
||||||
app, ok := ctx.cfg.apps[name]
|
//
|
||||||
if !ok || app == nil {
|
// We return any type instead of the App type because it is not
|
||||||
return nil, nil
|
// 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
|
||||||
}
|
}
|
||||||
|
return ctx.cfg.apps[name]
|
||||||
appModule, err := ctx.App(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return appModule, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Storage returns the configured Caddy storage implementation.
|
// Storage returns the configured Caddy storage implementation.
|
||||||
|
|||||||
@@ -1,71 +1,72 @@
|
|||||||
module github.com/caddyserver/caddy/v2
|
module github.com/caddyserver/caddy/v2
|
||||||
|
|
||||||
go 1.19
|
go 1.20
|
||||||
|
|
||||||
require (
|
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/Masterminds/sprig/v3 v3.2.3
|
||||||
github.com/alecthomas/chroma/v2 v2.7.0
|
github.com/alecthomas/chroma/v2 v2.9.1
|
||||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
|
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
|
||||||
github.com/caddyserver/certmagic v0.17.3-0.20230511183644-8728b186fa68
|
github.com/caddyserver/certmagic v0.20.0
|
||||||
github.com/dustin/go-humanize v1.0.1
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/go-chi/chi v4.1.2+incompatible
|
github.com/go-chi/chi/v5 v5.0.10
|
||||||
github.com/google/cel-go v0.15.1
|
github.com/google/cel-go v0.15.1
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.1
|
||||||
github.com/klauspost/compress v1.16.5
|
github.com/klauspost/compress v1.17.0
|
||||||
github.com/klauspost/cpuid/v2 v2.2.4
|
github.com/klauspost/cpuid/v2 v2.2.5
|
||||||
github.com/mastercactapus/proxyprotocol v0.0.4
|
github.com/mholt/acmez v1.2.0
|
||||||
github.com/mholt/acmez v1.1.1
|
github.com/prometheus/client_golang v1.15.1
|
||||||
github.com/prometheus/client_golang v1.14.0
|
github.com/quic-go/quic-go v0.40.0
|
||||||
github.com/quic-go/quic-go v0.34.0
|
github.com/smallstep/certificates v0.25.0
|
||||||
github.com/smallstep/certificates v0.24.2
|
|
||||||
github.com/smallstep/nosql v0.6.0
|
github.com/smallstep/nosql v0.6.0
|
||||||
github.com/smallstep/truststore v0.12.1
|
github.com/smallstep/truststore v0.12.1
|
||||||
github.com/spf13/cobra v1.7.0
|
github.com/spf13/cobra v1.7.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/stretchr/testify v1.8.2
|
github.com/stretchr/testify v1.8.4
|
||||||
github.com/tailscale/tscert v0.0.0-20230509043813-4e9cb4f2b4ad
|
github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046
|
||||||
github.com/yuin/goldmark v1.5.4
|
github.com/yuin/goldmark v1.5.6
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.40.0
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0
|
||||||
go.opentelemetry.io/contrib/propagators/autoprop v0.40.0
|
go.opentelemetry.io/contrib/propagators/autoprop v0.42.0
|
||||||
go.opentelemetry.io/otel v1.14.0
|
go.opentelemetry.io/otel v1.21.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.14.0
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0
|
||||||
go.opentelemetry.io/otel/sdk v1.14.0
|
go.opentelemetry.io/otel/sdk v1.21.0
|
||||||
go.uber.org/zap v1.24.0
|
go.uber.org/zap v1.25.0
|
||||||
golang.org/x/crypto v0.9.0
|
golang.org/x/crypto v0.14.0
|
||||||
golang.org/x/net v0.10.0
|
golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0
|
||||||
golang.org/x/sync v0.2.0
|
golang.org/x/net v0.17.0
|
||||||
golang.org/x/term v0.8.0
|
golang.org/x/sync v0.4.0
|
||||||
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1
|
golang.org/x/term v0.13.0
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
cloud.google.com/go/iam v1.1.2 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
github.com/Microsoft/go-winio v0.6.0 // indirect
|
||||||
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
|
github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.5.0 // indirect
|
||||||
github.com/golang/glog v1.1.0 // indirect
|
github.com/golang/glog v1.1.2 // indirect
|
||||||
github.com/golang/mock v1.6.0 // indirect
|
github.com/google/certificate-transparency-go v1.1.6 // indirect
|
||||||
github.com/google/certificate-transparency-go v1.1.4 // indirect
|
github.com/google/go-tpm v0.9.0 // indirect
|
||||||
github.com/google/go-tpm v0.3.3 // indirect
|
|
||||||
github.com/google/go-tspi v0.3.0 // indirect
|
github.com/google/go-tspi v0.3.0 // indirect
|
||||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
|
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect
|
||||||
github.com/onsi/ginkgo/v2 v2.2.0 // indirect
|
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/quic-go/qpack v0.4.0 // indirect
|
github.com/quic-go/qpack v0.4.0 // indirect
|
||||||
github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
|
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
||||||
github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
|
github.com/smallstep/go-attestation v0.4.4-0.20230627102604-cf579e53cbd2 // indirect
|
||||||
github.com/smallstep/go-attestation v0.4.4-0.20230509120429-e17291421738 // indirect
|
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
go.opentelemetry.io/contrib/propagators/aws v1.15.0 // indirect
|
github.com/zeebo/blake3 v0.2.3 // indirect
|
||||||
go.opentelemetry.io/contrib/propagators/b3 v1.15.0 // indirect
|
go.opentelemetry.io/contrib/propagators/aws v1.17.0 // indirect
|
||||||
go.opentelemetry.io/contrib/propagators/jaeger v1.15.0 // indirect
|
go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect
|
||||||
go.opentelemetry.io/contrib/propagators/ot v1.15.0 // indirect
|
go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 // 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 (
|
require (
|
||||||
@@ -74,7 +75,7 @@ require (
|
|||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // 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 v1.1.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/chzyer/readline v1.5.1 // indirect
|
github.com/chzyer/readline v1.5.1 // indirect
|
||||||
@@ -83,14 +84,14 @@ require (
|
|||||||
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
|
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
|
||||||
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
||||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
||||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||||
github.com/go-kit/kit v0.10.0 // indirect
|
github.com/go-kit/kit v0.10.0 // indirect
|
||||||
github.com/go-logfmt/logfmt v0.5.1 // indirect
|
github.com/go-logfmt/logfmt v0.5.1 // indirect
|
||||||
github.com/go-logr/logr v1.2.3 // indirect
|
github.com/go-logr/logr v1.3.0 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/huandu/xstrings v1.3.3 // indirect
|
github.com/huandu/xstrings v1.3.3 // indirect
|
||||||
@@ -108,44 +109,43 @@ require (
|
|||||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
github.com/micromdm/scep/v2 v2.1.0 // indirect
|
github.com/micromdm/scep/v2 v2.1.0 // indirect
|
||||||
github.com/miekg/dns v1.1.54 // indirect
|
github.com/miekg/dns v1.1.55 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
|
github.com/pires/go-proxyproto v0.7.0
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/prometheus/client_model v0.3.0 // indirect
|
github.com/prometheus/client_model v0.4.0 // indirect
|
||||||
github.com/prometheus/common v0.37.0 // indirect
|
github.com/prometheus/common v0.42.0 // indirect
|
||||||
github.com/prometheus/procfs v0.8.0 // indirect
|
github.com/prometheus/procfs v0.9.0 // indirect
|
||||||
github.com/rs/xid v1.5.0 // indirect
|
github.com/rs/xid v1.5.0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/shopspring/decimal v1.2.0 // indirect
|
github.com/shopspring/decimal v1.2.0 // indirect
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
github.com/slackhq/nebula v1.6.1 // indirect
|
github.com/slackhq/nebula v1.6.1 // indirect
|
||||||
github.com/spf13/cast v1.4.1 // indirect
|
github.com/spf13/cast v1.4.1 // indirect
|
||||||
github.com/stoewer/go-strcase v1.2.0 // indirect
|
github.com/stoewer/go-strcase v1.2.0 // indirect
|
||||||
github.com/urfave/cli v1.22.13 // indirect
|
github.com/urfave/cli v1.22.14 // indirect
|
||||||
go.etcd.io/bbolt v1.3.7 // indirect
|
go.etcd.io/bbolt v1.3.7 // indirect
|
||||||
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
|
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.14.0 // indirect
|
go.opentelemetry.io/otel/metric v1.21.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v0.37.0 // indirect
|
go.opentelemetry.io/otel/trace v1.21.0
|
||||||
go.opentelemetry.io/otel/trace v1.14.0
|
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
|
go.step.sm/cli-utils v0.8.0 // indirect
|
||||||
go.step.sm/cli-utils v0.7.6 // indirect
|
go.step.sm/crypto v0.35.1
|
||||||
go.step.sm/crypto v0.30.0
|
go.step.sm/linkedca v0.20.1 // indirect
|
||||||
go.step.sm/linkedca v0.19.1 // indirect
|
|
||||||
go.uber.org/atomic v1.11.0 // indirect
|
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/mod v0.10.0 // indirect
|
golang.org/x/mod v0.11.0 // indirect
|
||||||
golang.org/x/sys v0.8.0
|
golang.org/x/sys v0.14.0
|
||||||
golang.org/x/text v0.9.0 // indirect
|
golang.org/x/text v0.13.0 // indirect
|
||||||
golang.org/x/tools v0.8.0 // indirect
|
golang.org/x/tools v0.10.0 // indirect
|
||||||
google.golang.org/grpc v1.55.0 // indirect
|
google.golang.org/grpc v1.59.0 // indirect
|
||||||
google.golang.org/protobuf v1.30.0 // indirect
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||||
howett.net/plist v1.0.0 // indirect
|
howett.net/plist v1.0.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
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) {
|
||||||
sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
|
switch network {
|
||||||
ln, err := config.Listen(ctx, network, address)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &sharedListener{Listener: ln, key: lnKey}, nil
|
return &fakeClosePacketConn{sharedPacketConn: sharedPc.(*sharedPacketConn)}, nil
|
||||||
})
|
|
||||||
if err != nil {
|
default:
|
||||||
return nil, err
|
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
|
// 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
|
// so that it's clear in the code that side-effects are shared with other
|
||||||
// users of this listener, not just our own reference to it; we also don't
|
// users of this listener, not just our own reference to it; we also don't
|
||||||
// do anything with the error because all we could do is log it, but we
|
// do anything with the error because all we could do is log it, but we
|
||||||
// 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()
|
_ = fcl.sharedListener.clearDeadline()
|
||||||
|
|
||||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||||
@@ -172,3 +188,75 @@ func (sl *sharedListener) setDeadline() error {
|
|||||||
func (sl *sharedListener) Destruct() error {
|
func (sl *sharedListener) Destruct() error {
|
||||||
return sl.Listener.Close()
|
return sl.Listener.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fakeClosePacketConn is like fakeCloseListener, but for PacketConns,
|
||||||
|
// or more specifically, *net.UDPConn
|
||||||
|
type fakeClosePacketConn struct {
|
||||||
|
closed int32 // accessed atomically; belongs to this struct only
|
||||||
|
*sharedPacketConn // embedded, so we also become a net.PacketConn; its key is used in Close
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||||
|
// if the listener is already "closed", return error
|
||||||
|
if atomic.LoadInt32(&fcpc.closed) == 1 {
|
||||||
|
return 0, nil, &net.OpError{
|
||||||
|
Op: "readfrom",
|
||||||
|
Net: fcpc.LocalAddr().Network(),
|
||||||
|
Addr: fcpc.LocalAddr(),
|
||||||
|
Err: errFakeClosed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// call underlying readfrom
|
||||||
|
n, addr, err = fcpc.sharedPacketConn.ReadFrom(p)
|
||||||
|
if err != nil {
|
||||||
|
// this server was stopped, so clear the deadline and let
|
||||||
|
// any new server continue reading; but we will exit
|
||||||
|
if atomic.LoadInt32(&fcpc.closed) == 1 {
|
||||||
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||||
|
if err = fcpc.SetReadDeadline(time.Time{}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close won't close the underlying socket unless there is no more reference, then listenerPool will close it.
|
||||||
|
func (fcpc *fakeClosePacketConn) Close() error {
|
||||||
|
if atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) {
|
||||||
|
_ = fcpc.SetReadDeadline(time.Now()) // unblock ReadFrom() calls to kick old servers out of their loops
|
||||||
|
_, _ = listenerPool.Delete(fcpc.sharedPacketConn.key)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fcpc *fakeClosePacketConn) Unwrap() net.PacketConn {
|
||||||
|
return fcpc.sharedPacketConn.PacketConn
|
||||||
|
}
|
||||||
|
|
||||||
|
// sharedPacketConn is like sharedListener, but for net.PacketConns.
|
||||||
|
type sharedPacketConn struct {
|
||||||
|
net.PacketConn
|
||||||
|
key string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destruct closes the underlying socket.
|
||||||
|
func (spc *sharedPacketConn) Destruct() error {
|
||||||
|
return spc.PacketConn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap returns the underlying socket
|
||||||
|
func (spc *sharedPacketConn) Unwrap() net.PacketConn {
|
||||||
|
return spc.PacketConn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards (see https://github.com/caddyserver/caddy/issues/3998)
|
||||||
|
var (
|
||||||
|
_ (interface {
|
||||||
|
Unwrap() net.PacketConn
|
||||||
|
}) = (*fakeClosePacketConn)(nil)
|
||||||
|
)
|
||||||
|
|||||||
+102
-4
@@ -22,8 +22,10 @@ package caddy
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
@@ -87,7 +89,7 @@ func reuseUnixSocket(network, addr string) (any, error) {
|
|||||||
return nil, nil
|
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
|
// wrap any Control function set by the user so we can also add our reusePort control without clobbering theirs
|
||||||
oldControl := config.Control
|
oldControl := config.Control
|
||||||
config.Control = func(network, address string, c syscall.RawConn) error {
|
config.Control = func(network, address string, c syscall.RawConn) error {
|
||||||
@@ -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
|
// we still put it in the listenerPool so we can count how many
|
||||||
// configs are using this socket; necessary to ensure we can know
|
// configs are using this socket; necessary to ensure we can know
|
||||||
// whether to enforce shutdown delays, for example (see #5393).
|
// whether to enforce shutdown delays, for example (see #5393).
|
||||||
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 {
|
if err == nil {
|
||||||
listenerPool.LoadOrStore(lnKey, nil)
|
listenerPool.LoadOrStore(lnKey, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if new listener is a unix socket, make sure we can reuse it later
|
||||||
|
// (we do our own "unlink on close" -- not required, but more tidy)
|
||||||
|
one := int32(1)
|
||||||
|
if unix, ok := ln.(*net.UnixListener); ok {
|
||||||
|
unix.SetUnlinkOnClose(false)
|
||||||
|
ln = &unixListener{unix, lnKey, &one}
|
||||||
|
unixSockets[lnKey] = ln.(*unixListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Not 100% sure this is necessary, but we do this for net.UnixListener in listen_unix.go, so...
|
||||||
|
if unix, ok := ln.(*net.UnixConn); ok {
|
||||||
|
ln = &unixConn{unix, address, lnKey, &one}
|
||||||
|
unixSockets[lnKey] = ln.(*unixConn)
|
||||||
|
}
|
||||||
|
|
||||||
// lightly wrap the listener so that when it is closed,
|
// lightly wrap the listener so that when it is closed,
|
||||||
// we can decrement the usage pool counter
|
// we can decrement the usage pool counter
|
||||||
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.
|
// reusePort sets SO_REUSEPORT. Ineffective for unix sockets.
|
||||||
@@ -119,7 +151,7 @@ func reusePort(network, address string, conn syscall.RawConn) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return conn.Control(func(descriptor uintptr) {
|
return conn.Control(func(descriptor uintptr) {
|
||||||
if err := unix.SetsockoptInt(int(descriptor), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
|
if err := unix.SetsockoptInt(int(descriptor), unix.SOL_SOCKET, unixSOREUSEPORT, 1); err != nil {
|
||||||
Log().Error("setting SO_REUSEPORT",
|
Log().Error("setting SO_REUSEPORT",
|
||||||
zap.String("network", network),
|
zap.String("network", network),
|
||||||
zap.String("address", address),
|
zap.String("address", address),
|
||||||
@@ -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
|
// deleteListener is a type that simply deletes itself
|
||||||
// from the listenerPool when it closes. It is used
|
// from the listenerPool when it closes. It is used
|
||||||
// solely for the purpose of reference counting (i.e.
|
// solely for the purpose of reference counting (i.e.
|
||||||
@@ -142,3 +224,19 @@ func (dl deleteListener) Close() error {
|
|||||||
_, _ = listenerPool.Delete(dl.lnKey)
|
_, _ = listenerPool.Delete(dl.lnKey)
|
||||||
return dl.Listener.Close()
|
return dl.Listener.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deletePacketConn is like deleteListener, but
|
||||||
|
// for net.PacketConns.
|
||||||
|
type deletePacketConn struct {
|
||||||
|
net.PacketConn
|
||||||
|
lnKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dl deletePacketConn) Close() error {
|
||||||
|
_, _ = listenerPool.Delete(dl.lnKey)
|
||||||
|
return dl.PacketConn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dl deletePacketConn) Unwrap() net.PacketConn {
|
||||||
|
return dl.PacketConn
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
+171
-205
@@ -20,6 +20,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
@@ -27,14 +28,21 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/quic-go/quic-go"
|
"github.com/quic-go/quic-go"
|
||||||
"github.com/quic-go/quic-go/http3"
|
"github.com/quic-go/quic-go/http3"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterNamespace("caddy.listeners", []interface{}{
|
||||||
|
(*ListenerWrapper)(nil),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// NetworkAddress represents one or more network addresses.
|
// NetworkAddress represents one or more network addresses.
|
||||||
// It contains the individual components for a parsed network
|
// It contains the individual components for a parsed network
|
||||||
// address of the form accepted by ParseNetworkAddress().
|
// address of the form accepted by ParseNetworkAddress().
|
||||||
@@ -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) {
|
func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
|
||||||
var ln any
|
var (
|
||||||
var err error
|
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 socket, err := reuseUnixSocket(na.Network, address); socket != nil || err != nil {
|
||||||
|
if !isAbtractUnixSocket {
|
||||||
|
if err := os.Chmod(address, unixFileMode); err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to set permissions (%s) on %s: %v", unixFileMode, address, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
return socket, err
|
return socket, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lnKey := listenerKey(na.Network, address)
|
lnKey := listenerKey(na.Network, address)
|
||||||
|
|
||||||
switch na.Network {
|
|
||||||
case "tcp", "tcp4", "tcp6", "unix", "unixpacket":
|
|
||||||
ln, err = listenTCPOrUnix(ctx, lnKey, na.Network, address, config)
|
|
||||||
case "unixgram":
|
|
||||||
ln, err = config.ListenPacket(ctx, na.Network, address)
|
|
||||||
case "udp", "udp4", "udp6":
|
|
||||||
sharedPc, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
|
|
||||||
pc, err := config.ListenPacket(ctx, na.Network, address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &sharedPacketConn{PacketConn: pc, key: lnKey}, nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ln = &fakeClosePacketConn{sharedPacketConn: sharedPc.(*sharedPacketConn)}
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(na.Network, "ip") {
|
if strings.HasPrefix(na.Network, "ip") {
|
||||||
ln, err = config.ListenPacket(ctx, na.Network, address)
|
ln, err = config.ListenPacket(ctx, na.Network, address)
|
||||||
|
} else {
|
||||||
|
ln, err = listenReusable(ctx, lnKey, na.Network, address, config)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -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)
|
return nil, fmt.Errorf("unsupported network type: %s", na.Network)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if new listener is a unix socket, make sure we can reuse it later
|
if IsUnixNetwork(na.Network) {
|
||||||
// (we do our own "unlink on close" -- not required, but more tidy)
|
if !isAbtractUnixSocket {
|
||||||
one := int32(1)
|
if err := os.Chmod(address, unixFileMode); err != nil {
|
||||||
switch unix := ln.(type) {
|
return nil, fmt.Errorf("unable to set permissions (%s) on %s: %v", unixFileMode, address, err)
|
||||||
case *net.UnixListener:
|
}
|
||||||
unix.SetUnlinkOnClose(false)
|
}
|
||||||
ln = &unixListener{unix, lnKey, &one}
|
|
||||||
unixSockets[lnKey] = ln.(*unixListener)
|
|
||||||
case *net.UnixConn:
|
|
||||||
ln = &unixConn{unix, address, lnKey, &one}
|
|
||||||
unixSockets[lnKey] = ln.(*unixConn)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ln, nil
|
return ln, nil
|
||||||
@@ -318,10 +327,11 @@ func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort ui
|
|||||||
network = defaultNetwork
|
network = defaultNetwork
|
||||||
}
|
}
|
||||||
if IsUnixNetwork(network) {
|
if IsUnixNetwork(network) {
|
||||||
|
_, _, err := internal.SplitUnixSocketPermissionsBits(host)
|
||||||
return NetworkAddress{
|
return NetworkAddress{
|
||||||
Network: network,
|
Network: network,
|
||||||
Host: host,
|
Host: host,
|
||||||
}, nil
|
}, err
|
||||||
}
|
}
|
||||||
var start, end uint64
|
var start, end uint64
|
||||||
if port == "" {
|
if port == "" {
|
||||||
@@ -443,53 +453,93 @@ func ListenPacket(network, addr string) (net.PacketConn, error) {
|
|||||||
// unixgram will be used; otherwise, udp will be used).
|
// unixgram will be used; otherwise, udp will be used).
|
||||||
//
|
//
|
||||||
// NOTE: This API is EXPERIMENTAL and may be changed or removed.
|
// NOTE: This API is EXPERIMENTAL and may be changed or removed.
|
||||||
//
|
func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config net.ListenConfig, tlsConf *tls.Config, activeRequests *int64) (http3.QUICEarlyListener, error) {
|
||||||
// TODO: See if we can find a more elegant solution closer to the new NetworkAddress.Listen API.
|
lnKey := listenerKey("quic"+na.Network, na.JoinHostPort(portOffset))
|
||||||
func ListenQUIC(ln net.PacketConn, tlsConf *tls.Config, activeRequests *int64) (quic.EarlyListener, error) {
|
|
||||||
lnKey := listenerKey("quic+"+ln.LocalAddr().Network(), ln.LocalAddr().String())
|
|
||||||
|
|
||||||
sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
|
sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
|
||||||
sqtc := newSharedQUICTLSConfig(tlsConf)
|
lnAny, err := na.Listen(ctx, portOffset, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ln := lnAny.(net.PacketConn)
|
||||||
|
|
||||||
|
h3ln := ln
|
||||||
|
for {
|
||||||
|
// retrieve the underlying socket, so quic-go can optimize.
|
||||||
|
if unwrapper, ok := h3ln.(interface{ Unwrap() net.PacketConn }); ok {
|
||||||
|
h3ln = unwrapper.Unwrap()
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sqs := newSharedQUICState(tlsConf, activeRequests)
|
||||||
// http3.ConfigureTLSConfig only uses this field and tls App sets this field as well
|
// http3.ConfigureTLSConfig only uses this field and tls App sets this field as well
|
||||||
//nolint:gosec
|
//nolint:gosec
|
||||||
quicTlsConfig := &tls.Config{GetConfigForClient: sqtc.getConfigForClient}
|
quicTlsConfig := &tls.Config{GetConfigForClient: sqs.getConfigForClient}
|
||||||
earlyLn, err := quic.ListenEarly(ln, http3.ConfigureTLSConfig(quicTlsConfig), &quic.Config{
|
earlyLn, err := quic.ListenEarly(h3ln, http3.ConfigureTLSConfig(quicTlsConfig), &quic.Config{
|
||||||
Allow0RTT: func(net.Addr) bool { return true },
|
Allow0RTT: true,
|
||||||
RequireAddressValidation: func(clientAddr net.Addr) bool {
|
RequireAddressValidation: func(clientAddr net.Addr) bool {
|
||||||
var highLoad bool
|
// TODO: make tunable?
|
||||||
if activeRequests != nil {
|
return sqs.getActiveRequests() > 1000
|
||||||
highLoad = atomic.LoadInt64(activeRequests) > 1000 // TODO: make tunable?
|
|
||||||
}
|
|
||||||
return highLoad
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &sharedQuicListener{EarlyListener: earlyLn, sqtc: sqtc, key: lnKey}, nil
|
// using the original net.PacketConn to close them properly
|
||||||
|
return &sharedQuicListener{EarlyListener: earlyLn, packetConn: ln, sqs: sqs, key: lnKey}, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sql := sharedEarlyListener.(*sharedQuicListener)
|
sql := sharedEarlyListener.(*sharedQuicListener)
|
||||||
// add current tls.Config to sqtc, so GetConfigForClient will always return the latest tls.Config in case of context cancellation
|
// add current tls.Config to sqs, so GetConfigForClient will always return the latest tls.Config in case of context cancellation,
|
||||||
ctx, cancel := sql.sqtc.addTLSConfig(tlsConf)
|
// and the request counter will reflect current http server
|
||||||
|
ctx, cancel := sql.sqs.addState(tlsConf, activeRequests)
|
||||||
// TODO: to serve QUIC over a unix socket, currently we need to hold onto
|
|
||||||
// the underlying net.PacketConn (which we wrap as unixConn to keep count
|
return &fakeCloseQuicListener{
|
||||||
// of closes) because closing the quic.EarlyListener doesn't actually close
|
sharedQuicListener: sql,
|
||||||
// the underlying PacketConn, but we need to for unix sockets since we dup
|
context: ctx,
|
||||||
// the file descriptor and thus need to close the original; track issue:
|
contextCancel: cancel,
|
||||||
// https://github.com/quic-go/quic-go/issues/3560#issuecomment-1258959608
|
}, nil
|
||||||
var unix *unixConn
|
}
|
||||||
if uc, ok := ln.(*unixConn); ok {
|
|
||||||
unix = uc
|
// DEPRECATED: Use NetworkAddress.ListenQUIC instead. This function will likely be changed or removed in the future.
|
||||||
}
|
// TODO: See if we can find a more elegant solution closer to the new NetworkAddress.Listen API.
|
||||||
|
func ListenQUIC(ln net.PacketConn, tlsConf *tls.Config, activeRequests *int64) (http3.QUICEarlyListener, error) {
|
||||||
|
lnKey := listenerKey("quic+"+ln.LocalAddr().Network(), ln.LocalAddr().String())
|
||||||
|
|
||||||
|
sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
|
||||||
|
sqs := newSharedQUICState(tlsConf, activeRequests)
|
||||||
|
// http3.ConfigureTLSConfig only uses this field and tls App sets this field as well
|
||||||
|
//nolint:gosec
|
||||||
|
quicTlsConfig := &tls.Config{GetConfigForClient: sqs.getConfigForClient}
|
||||||
|
earlyLn, err := quic.ListenEarly(ln, http3.ConfigureTLSConfig(quicTlsConfig), &quic.Config{
|
||||||
|
Allow0RTT: true,
|
||||||
|
RequireAddressValidation: func(clientAddr net.Addr) bool {
|
||||||
|
// TODO: make tunable?
|
||||||
|
return sqs.getActiveRequests() > 1000
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &sharedQuicListener{EarlyListener: earlyLn, sqs: sqs, key: lnKey}, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sql := sharedEarlyListener.(*sharedQuicListener)
|
||||||
|
// add current tls.Config and request counter to sqs, so GetConfigForClient will always return the latest tls.Config in case of context cancellation,
|
||||||
|
// and the request counter will reflect current http server
|
||||||
|
ctx, cancel := sql.sqs.addState(tlsConf, activeRequests)
|
||||||
|
|
||||||
return &fakeCloseQuicListener{
|
return &fakeCloseQuicListener{
|
||||||
sharedQuicListener: sql,
|
sharedQuicListener: sql,
|
||||||
uc: unix,
|
|
||||||
context: ctx,
|
context: ctx,
|
||||||
contextCancel: cancel,
|
contextCancel: cancel,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -507,38 +557,50 @@ type contextAndCancelFunc struct {
|
|||||||
context.CancelFunc
|
context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// sharedQUICTLSConfig manages GetConfigForClient
|
// sharedQUICState manages GetConfigForClient and current number of active requests
|
||||||
// see issue: https://github.com/caddyserver/caddy/pull/4849
|
// see issue: https://github.com/caddyserver/caddy/pull/4849
|
||||||
type sharedQUICTLSConfig struct {
|
type sharedQUICState struct {
|
||||||
rmu sync.RWMutex
|
rmu sync.RWMutex
|
||||||
tlsConfs map[*tls.Config]contextAndCancelFunc
|
tlsConfs map[*tls.Config]contextAndCancelFunc
|
||||||
activeTlsConf *tls.Config
|
requestCounters map[*tls.Config]*int64
|
||||||
|
activeTlsConf *tls.Config
|
||||||
|
activeRequestsCounter *int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// newSharedQUICTLSConfig creates a new sharedQUICTLSConfig
|
// newSharedQUICState creates a new sharedQUICState
|
||||||
func newSharedQUICTLSConfig(tlsConfig *tls.Config) *sharedQUICTLSConfig {
|
func newSharedQUICState(tlsConfig *tls.Config, activeRequests *int64) *sharedQUICState {
|
||||||
sqtc := &sharedQUICTLSConfig{
|
sqtc := &sharedQUICState{
|
||||||
tlsConfs: make(map[*tls.Config]contextAndCancelFunc),
|
tlsConfs: make(map[*tls.Config]contextAndCancelFunc),
|
||||||
activeTlsConf: tlsConfig,
|
requestCounters: make(map[*tls.Config]*int64),
|
||||||
|
activeTlsConf: tlsConfig,
|
||||||
|
activeRequestsCounter: activeRequests,
|
||||||
}
|
}
|
||||||
sqtc.addTLSConfig(tlsConfig)
|
sqtc.addState(tlsConfig, activeRequests)
|
||||||
return sqtc
|
return sqtc
|
||||||
}
|
}
|
||||||
|
|
||||||
// getConfigForClient is used as tls.Config's GetConfigForClient field
|
// getConfigForClient is used as tls.Config's GetConfigForClient field
|
||||||
func (sqtc *sharedQUICTLSConfig) getConfigForClient(ch *tls.ClientHelloInfo) (*tls.Config, error) {
|
func (sqs *sharedQUICState) getConfigForClient(ch *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||||
sqtc.rmu.RLock()
|
sqs.rmu.RLock()
|
||||||
defer sqtc.rmu.RUnlock()
|
defer sqs.rmu.RUnlock()
|
||||||
return sqtc.activeTlsConf.GetConfigForClient(ch)
|
return sqs.activeTlsConf.GetConfigForClient(ch)
|
||||||
}
|
}
|
||||||
|
|
||||||
// addTLSConfig adds tls.Config to the map if not present and returns the corresponding context and its cancelFunc
|
// getActiveRequests returns the number of active requests
|
||||||
// so that when cancelled, the active tls.Config will change
|
func (sqs *sharedQUICState) getActiveRequests() int64 {
|
||||||
func (sqtc *sharedQUICTLSConfig) addTLSConfig(tlsConfig *tls.Config) (context.Context, context.CancelFunc) {
|
// Prevent a race when a context is cancelled and active request counter is being changed
|
||||||
sqtc.rmu.Lock()
|
sqs.rmu.RLock()
|
||||||
defer sqtc.rmu.Unlock()
|
defer sqs.rmu.RUnlock()
|
||||||
|
return atomic.LoadInt64(sqs.activeRequestsCounter)
|
||||||
|
}
|
||||||
|
|
||||||
if cacc, ok := sqtc.tlsConfs[tlsConfig]; ok {
|
// addState adds tls.Config and activeRequests to the map if not present and returns the corresponding context and its cancelFunc
|
||||||
|
// so that when cancelled, the active tls.Config and request counter will change
|
||||||
|
func (sqs *sharedQUICState) addState(tlsConfig *tls.Config, activeRequests *int64) (context.Context, context.CancelFunc) {
|
||||||
|
sqs.rmu.Lock()
|
||||||
|
defer sqs.rmu.Unlock()
|
||||||
|
|
||||||
|
if cacc, ok := sqs.tlsConfs[tlsConfig]; ok {
|
||||||
return cacc.Context, cacc.CancelFunc
|
return cacc.Context, cacc.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,48 +608,44 @@ func (sqtc *sharedQUICTLSConfig) addTLSConfig(tlsConfig *tls.Config) (context.Co
|
|||||||
wrappedCancel := func() {
|
wrappedCancel := func() {
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
sqtc.rmu.Lock()
|
sqs.rmu.Lock()
|
||||||
defer sqtc.rmu.Unlock()
|
defer sqs.rmu.Unlock()
|
||||||
|
|
||||||
delete(sqtc.tlsConfs, tlsConfig)
|
delete(sqs.tlsConfs, tlsConfig)
|
||||||
if sqtc.activeTlsConf == tlsConfig {
|
delete(sqs.requestCounters, tlsConfig)
|
||||||
// select another tls.Config, if there is none,
|
if sqs.activeTlsConf == tlsConfig {
|
||||||
|
// select another tls.Config and request counter, if there is none,
|
||||||
// related sharedQuicListener will be destroyed anyway
|
// related sharedQuicListener will be destroyed anyway
|
||||||
for tc := range sqtc.tlsConfs {
|
for tc, counter := range sqs.requestCounters {
|
||||||
sqtc.activeTlsConf = tc
|
sqs.activeTlsConf = tc
|
||||||
|
sqs.activeRequestsCounter = counter
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sqtc.tlsConfs[tlsConfig] = contextAndCancelFunc{ctx, wrappedCancel}
|
sqs.tlsConfs[tlsConfig] = contextAndCancelFunc{ctx, wrappedCancel}
|
||||||
|
sqs.requestCounters[tlsConfig] = activeRequests
|
||||||
// there should be at most 2 tls.Configs
|
// there should be at most 2 tls.Configs
|
||||||
if len(sqtc.tlsConfs) > 2 {
|
if len(sqs.tlsConfs) > 2 {
|
||||||
Log().Warn("quic listener tls configs are more than 2", zap.Int("number of configs", len(sqtc.tlsConfs)))
|
Log().Warn("quic listener tls configs are more than 2", zap.Int("number of configs", len(sqs.tlsConfs)))
|
||||||
}
|
}
|
||||||
return ctx, wrappedCancel
|
return ctx, wrappedCancel
|
||||||
}
|
}
|
||||||
|
|
||||||
// sharedQuicListener is like sharedListener, but for quic.EarlyListeners.
|
// sharedQuicListener is like sharedListener, but for quic.EarlyListeners.
|
||||||
type sharedQuicListener struct {
|
type sharedQuicListener struct {
|
||||||
quic.EarlyListener
|
*quic.EarlyListener
|
||||||
sqtc *sharedQUICTLSConfig
|
packetConn net.PacketConn // we have to hold these because quic-go won't close listeners it didn't create
|
||||||
key string
|
sqs *sharedQUICState
|
||||||
|
key string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Destruct closes the underlying QUIC listener.
|
// Destruct closes the underlying QUIC listener and its associated net.PacketConn.
|
||||||
func (sql *sharedQuicListener) Destruct() error {
|
func (sql *sharedQuicListener) Destruct() error {
|
||||||
return sql.EarlyListener.Close()
|
// close EarlyListener first to stop any operations being done to the net.PacketConn
|
||||||
}
|
_ = sql.EarlyListener.Close()
|
||||||
|
// then close the net.PacketConn
|
||||||
// sharedPacketConn is like sharedListener, but for net.PacketConns.
|
return sql.packetConn.Close()
|
||||||
type sharedPacketConn struct {
|
|
||||||
net.PacketConn
|
|
||||||
key string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destruct closes the underlying socket.
|
|
||||||
func (spc *sharedPacketConn) Destruct() error {
|
|
||||||
return spc.PacketConn.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fakeClosedErr returns an error value that is not temporary
|
// fakeClosedErr returns an error value that is not temporary
|
||||||
@@ -609,41 +667,9 @@ func fakeClosedErr(l interface{ Addr() net.Addr }) error {
|
|||||||
// socket is actually left open.
|
// socket is actually left open.
|
||||||
var errFakeClosed = fmt.Errorf("listener 'closed' 😉")
|
var errFakeClosed = fmt.Errorf("listener 'closed' 😉")
|
||||||
|
|
||||||
// fakeClosePacketConn is like fakeCloseListener, but for PacketConns.
|
|
||||||
type fakeClosePacketConn struct {
|
|
||||||
closed int32 // accessed atomically; belongs to this struct only
|
|
||||||
*sharedPacketConn // embedded, so we also become a net.PacketConn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fcpc *fakeClosePacketConn) Close() error {
|
|
||||||
if atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) {
|
|
||||||
_, _ = listenerPool.Delete(fcpc.sharedPacketConn.key)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Supports QUIC implementation: https://github.com/caddyserver/caddy/issues/3998
|
|
||||||
func (fcpc fakeClosePacketConn) SetReadBuffer(bytes int) error {
|
|
||||||
if conn, ok := fcpc.PacketConn.(interface{ SetReadBuffer(int) error }); ok {
|
|
||||||
return conn.SetReadBuffer(bytes)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("SetReadBuffer() not implemented for %T", fcpc.PacketConn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Supports QUIC implementation: https://github.com/caddyserver/caddy/issues/3998
|
|
||||||
func (fcpc fakeClosePacketConn) SyscallConn() (syscall.RawConn, error) {
|
|
||||||
if conn, ok := fcpc.PacketConn.(interface {
|
|
||||||
SyscallConn() (syscall.RawConn, error)
|
|
||||||
}); ok {
|
|
||||||
return conn.SyscallConn()
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("SyscallConn() not implemented for %T", fcpc.PacketConn)
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakeCloseQuicListener struct {
|
type fakeCloseQuicListener struct {
|
||||||
closed int32 // accessed atomically; belongs to this struct only
|
closed int32 // accessed atomically; belongs to this struct only
|
||||||
*sharedQuicListener // embedded, so we also become a quic.EarlyListener
|
*sharedQuicListener // embedded, so we also become a quic.EarlyListener
|
||||||
uc *unixConn // underlying unix socket, if UDS
|
|
||||||
context context.Context
|
context context.Context
|
||||||
contextCancel context.CancelFunc
|
contextCancel context.CancelFunc
|
||||||
}
|
}
|
||||||
@@ -670,11 +696,6 @@ func (fcql *fakeCloseQuicListener) Close() error {
|
|||||||
if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) {
|
if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) {
|
||||||
fcql.contextCancel()
|
fcql.contextCancel()
|
||||||
_, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
|
_, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
|
||||||
if fcql.uc != nil {
|
|
||||||
// unix sockets need to be closed ourselves because we dup() the file
|
|
||||||
// descriptor when we reuse them, so this avoids a resource leak
|
|
||||||
fcql.uc.Close()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -700,54 +721,7 @@ func RegisterNetwork(network string, getListener ListenerFunc) {
|
|||||||
networkTypes[network] = getListener
|
networkTypes[network] = getListener
|
||||||
}
|
}
|
||||||
|
|
||||||
type unixListener struct {
|
var unixSocketsMu sync.Mutex
|
||||||
*net.UnixListener
|
|
||||||
mapKey string
|
|
||||||
count *int32 // accessed atomically
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uln *unixListener) Close() error {
|
|
||||||
newCount := atomic.AddInt32(uln.count, -1)
|
|
||||||
if newCount == 0 {
|
|
||||||
defer func() {
|
|
||||||
addr := uln.Addr().String()
|
|
||||||
unixSocketsMu.Lock()
|
|
||||||
delete(unixSockets, uln.mapKey)
|
|
||||||
unixSocketsMu.Unlock()
|
|
||||||
_ = syscall.Unlink(addr)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
return uln.UnixListener.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
type unixConn struct {
|
|
||||||
*net.UnixConn
|
|
||||||
filename string
|
|
||||||
mapKey string
|
|
||||||
count *int32 // accessed atomically
|
|
||||||
}
|
|
||||||
|
|
||||||
func (uc *unixConn) Close() error {
|
|
||||||
newCount := atomic.AddInt32(uc.count, -1)
|
|
||||||
if newCount == 0 {
|
|
||||||
defer func() {
|
|
||||||
unixSocketsMu.Lock()
|
|
||||||
delete(unixSockets, uc.mapKey)
|
|
||||||
unixSocketsMu.Unlock()
|
|
||||||
_ = syscall.Unlink(uc.filename)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
return uc.UnixConn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// unixSockets keeps track of the currently-active unix sockets
|
|
||||||
// so we can transfer their FDs gracefully during reloads.
|
|
||||||
var (
|
|
||||||
unixSockets = make(map[string]interface {
|
|
||||||
File() (*os.File, error)
|
|
||||||
})
|
|
||||||
unixSocketsMu sync.Mutex
|
|
||||||
)
|
|
||||||
|
|
||||||
// getListenerFromPlugin returns a listener on the given network and address
|
// getListenerFromPlugin returns a listener on the given network and address
|
||||||
// if a plugin has registered the network name. It may return (nil, nil) if
|
// if a plugin has registered the network name. It may return (nil, nil) if
|
||||||
@@ -791,11 +765,3 @@ type ListenerWrapper interface {
|
|||||||
var listenerPool = NewUsagePool()
|
var listenerPool = NewUsagePool()
|
||||||
|
|
||||||
const maxPortSpan = 65535
|
const maxPortSpan = 65535
|
||||||
|
|
||||||
// Interface guards (see https://github.com/caddyserver/caddy/issues/3998)
|
|
||||||
var (
|
|
||||||
_ (interface{ SetReadBuffer(int) error }) = (*fakeClosePacketConn)(nil)
|
|
||||||
_ (interface {
|
|
||||||
SyscallConn() (syscall.RawConn, error)
|
|
||||||
}) = (*fakeClosePacketConn)(nil)
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ package caddy
|
|||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSplitNetworkAddress(t *testing.T) {
|
func TestSplitNetworkAddress(t *testing.T) {
|
||||||
@@ -555,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(StdoutWriter{})
|
||||||
RegisterModule(StderrWriter{})
|
RegisterModule(StderrWriter{})
|
||||||
RegisterModule(DiscardWriter{})
|
RegisterModule(DiscardWriter{})
|
||||||
|
RegisterNamespace("caddy.logging.encoders", []interface{}{
|
||||||
|
(*zapcore.Encoder)(nil),
|
||||||
|
})
|
||||||
|
RegisterNamespace("caddy.logging.writers", []interface{}{
|
||||||
|
(*WriterOpener)(nil),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logging facilitates logging within Caddy. The default log is
|
// Logging facilitates logging within Caddy. The default log is
|
||||||
@@ -265,6 +271,8 @@ type BaseLog struct {
|
|||||||
WriterRaw json.RawMessage `json:"writer,omitempty" caddy:"namespace=caddy.logging.writers inline_key=output"`
|
WriterRaw json.RawMessage `json:"writer,omitempty" caddy:"namespace=caddy.logging.writers inline_key=output"`
|
||||||
|
|
||||||
// The encoder is how the log entries are formatted or encoded.
|
// The encoder is how the log entries are formatted or encoded.
|
||||||
|
// An `encoder` should implement the following interfaces:
|
||||||
|
// - [zapcore.Encoder](https://pkg.go.dev/go.uber.org/zap/zapcore#Encoder)
|
||||||
EncoderRaw json.RawMessage `json:"encoder,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"`
|
EncoderRaw json.RawMessage `json:"encoder,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"`
|
||||||
|
|
||||||
// Level is the minimum level to emit, and is inclusive.
|
// Level is the minimum level to emit, and is inclusive.
|
||||||
|
|||||||
+2
-1
@@ -3,10 +3,11 @@ package caddy
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/internal/metrics"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/internal/metrics"
|
||||||
)
|
)
|
||||||
|
|
||||||
// define and register the metrics used in this package.
|
// define and register the metrics used in this package.
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
+21
-19
@@ -20,16 +20,19 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
"golang.org/x/net/http2/h2c"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyevents"
|
"github.com/caddyserver/caddy/v2/modules/caddyevents"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
"go.uber.org/zap"
|
|
||||||
"golang.org/x/net/http2"
|
|
||||||
"golang.org/x/net/http2/h2c"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -70,7 +73,7 @@ func init() {
|
|||||||
// `{http.request.remote.host}` | The host (IP) part of the remote client's address
|
// `{http.request.remote.host}` | The host (IP) part of the remote client's address
|
||||||
// `{http.request.remote.port}` | The port part of the remote client's address
|
// `{http.request.remote.port}` | The port part of the remote client's address
|
||||||
// `{http.request.remote}` | The address of the remote client
|
// `{http.request.remote}` | The address of the remote client
|
||||||
// `{http.request.scheme}` | The request scheme
|
// `{http.request.scheme}` | The request scheme, typically `http` or `https`
|
||||||
// `{http.request.tls.version}` | The TLS version name
|
// `{http.request.tls.version}` | The TLS version name
|
||||||
// `{http.request.tls.cipher_suite}` | The TLS cipher suite
|
// `{http.request.tls.cipher_suite}` | The TLS cipher suite
|
||||||
// `{http.request.tls.resumed}` | The TLS connection resumed a previous connection
|
// `{http.request.tls.resumed}` | The TLS connection resumed a previous connection
|
||||||
@@ -325,9 +328,15 @@ func (app *App) Provision(ctx caddy.Context) error {
|
|||||||
|
|
||||||
// Validate ensures the app's configuration is valid.
|
// Validate ensures the app's configuration is valid.
|
||||||
func (app *App) Validate() error {
|
func (app *App) Validate() error {
|
||||||
|
isGo120 := strings.Contains(runtime.Version(), "go1.20")
|
||||||
|
|
||||||
// each server must use distinct listener addresses
|
// each server must use distinct listener addresses
|
||||||
lnAddrs := make(map[string]string)
|
lnAddrs := make(map[string]string)
|
||||||
for srvName, srv := range app.Servers {
|
for srvName, srv := range app.Servers {
|
||||||
|
if isGo120 && srv.EnableFullDuplex {
|
||||||
|
app.logger.Warn("enable_full_duplex is not supported in Go 1.20, use a build made with Go 1.21 or later", zap.String("server", srvName))
|
||||||
|
}
|
||||||
|
|
||||||
for _, addr := range srv.Listen {
|
for _, addr := range srv.Listen {
|
||||||
listenAddr, err := caddy.ParseNetworkAddress(addr)
|
listenAddr, err := caddy.ParseNetworkAddress(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -369,11 +378,7 @@ func (app *App) Start() error {
|
|||||||
return context.WithValue(ctx, ConnCtxKey, c)
|
return context.WithValue(ctx, ConnCtxKey, c)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
h2server := &http2.Server{
|
h2server := new(http2.Server)
|
||||||
NewWriteScheduler: func() http2.WriteScheduler {
|
|
||||||
return http2.NewPriorityWriteScheduler(nil)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// disable HTTP/2, which we enabled by default during provisioning
|
// disable HTTP/2, which we enabled by default during provisioning
|
||||||
if !srv.protocol("h2") {
|
if !srv.protocol("h2") {
|
||||||
@@ -594,16 +599,13 @@ func (app *App) Stop() error {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: we have to manually close our listeners because quic-go won't
|
// First close h3server then close listeners unlike stdlib for several reasons:
|
||||||
// close listeners it didn't create along with the server itself...
|
// 1, udp has only a single socket, once closed, no more data can be read and
|
||||||
// see https://github.com/quic-go/quic-go/issues/3560
|
// written. In contrast, closing tcp listeners won't affect established connections.
|
||||||
for _, el := range server.h3listeners {
|
// This have something to do with graceful shutdown when upstream implements it.
|
||||||
if err := el.Close(); err != nil {
|
// 2, h3server will only close listeners it's registered (quic listeners). Closing
|
||||||
app.logger.Error("HTTP/3 listener close",
|
// listener first and these listeners maybe unregistered thus won't be closed. caddy
|
||||||
zap.Error(err),
|
// distinguishes quic-listener and underlying datagram sockets.
|
||||||
zap.String("address", el.LocalAddr().String()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: CloseGracefully, once implemented upstream (see https://github.com/quic-go/quic-go/issues/2103)
|
// TODO: CloseGracefully, once implemented upstream (see https://github.com/quic-go/quic-go/issues/2103)
|
||||||
if err := server.h3server.Close(); err != nil {
|
if err := server.h3server.Close(); err != nil {
|
||||||
|
|||||||
@@ -20,10 +20,11 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AutoHTTPSConfig is used to disable automatic HTTPS
|
// AutoHTTPSConfig is used to disable automatic HTTPS
|
||||||
@@ -196,8 +197,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
|
|||||||
// if a certificate for this name is already loaded,
|
// if a certificate for this name is already loaded,
|
||||||
// don't obtain another one for it, unless we are
|
// don't obtain another one for it, unless we are
|
||||||
// supposed to ignore loaded certificates
|
// supposed to ignore loaded certificates
|
||||||
if !srv.AutoHTTPS.IgnoreLoadedCerts &&
|
if !srv.AutoHTTPS.IgnoreLoadedCerts && app.tlsApp.HasCertificateForSubject(d) {
|
||||||
len(app.tlsApp.AllMatchingCertificates(d)) > 0 {
|
|
||||||
logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded",
|
logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded",
|
||||||
zap.String("domain", d),
|
zap.String("domain", d),
|
||||||
zap.String("server_name", srvName),
|
zap.String("server_name", srvName),
|
||||||
|
|||||||
@@ -23,16 +23,21 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"golang.org/x/sync/singleflight"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(HTTPBasicAuth{})
|
caddy.RegisterModule(HTTPBasicAuth{})
|
||||||
|
|
||||||
weakrand.Seed(time.Now().UnixNano())
|
caddy.RegisterNamespace("http.authentication.hashes", []interface{}{
|
||||||
|
(*Comparer)(nil),
|
||||||
|
})
|
||||||
|
caddy.RegisterNamespace("http.authentication.providers", []interface{}{
|
||||||
|
(*Authenticator)(nil),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPBasicAuth facilitates HTTP basic authentication.
|
// HTTPBasicAuth facilitates HTTP basic authentication.
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -22,10 +22,12 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
|
|
||||||
|
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ import (
|
|||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"golang.org/x/crypto/scrypt"
|
"golang.org/x/crypto/scrypt"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -307,5 +307,7 @@ const (
|
|||||||
const separator = string(filepath.Separator)
|
const separator = string(filepath.Separator)
|
||||||
|
|
||||||
// Interface guard
|
// Interface guard
|
||||||
var _ caddy.ListenerWrapper = (*tlsPlaceholderWrapper)(nil)
|
var (
|
||||||
var _ caddyfile.Unmarshaler = (*tlsPlaceholderWrapper)(nil)
|
_ caddy.ListenerWrapper = (*tlsPlaceholderWrapper)(nil)
|
||||||
|
_ caddyfile.Unmarshaler = (*tlsPlaceholderWrapper)(nil)
|
||||||
|
)
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
|
||||||
"github.com/google/cel-go/cel"
|
"github.com/google/cel-go/cel"
|
||||||
"github.com/google/cel-go/common"
|
"github.com/google/cel-go/common"
|
||||||
"github.com/google/cel-go/common/operators"
|
"github.com/google/cel-go/common/operators"
|
||||||
@@ -39,6 +37,9 @@ import (
|
|||||||
"github.com/google/cel-go/parser"
|
"github.com/google/cel-go/parser"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -234,9 +235,11 @@ func (cr celHTTPRequest) Parent() interpreter.Activation {
|
|||||||
func (cr celHTTPRequest) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
func (cr celHTTPRequest) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
||||||
return cr.Request, nil
|
return cr.Request, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (celHTTPRequest) ConvertToType(typeVal ref.Type) ref.Val {
|
func (celHTTPRequest) ConvertToType(typeVal ref.Type) ref.Val {
|
||||||
panic("not implemented")
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cr celHTTPRequest) Equal(other ref.Val) ref.Val {
|
func (cr celHTTPRequest) Equal(other ref.Val) ref.Val {
|
||||||
if o, ok := other.Value().(celHTTPRequest); ok {
|
if o, ok := other.Value().(celHTTPRequest); ok {
|
||||||
return types.Bool(o.Request == cr.Request)
|
return types.Bool(o.Request == cr.Request)
|
||||||
@@ -255,12 +258,14 @@ type celPkixName struct{ *pkix.Name }
|
|||||||
func (pn celPkixName) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
func (pn celPkixName) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
||||||
return pn.Name, nil
|
return pn.Name, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pn celPkixName) ConvertToType(typeVal ref.Type) ref.Val {
|
func (pn celPkixName) ConvertToType(typeVal ref.Type) ref.Val {
|
||||||
if typeVal.TypeName() == "string" {
|
if typeVal.TypeName() == "string" {
|
||||||
return types.String(pn.Name.String())
|
return types.String(pn.Name.String())
|
||||||
}
|
}
|
||||||
panic("not implemented")
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pn celPkixName) Equal(other ref.Val) ref.Val {
|
func (pn celPkixName) Equal(other ref.Val) ref.Val {
|
||||||
if o, ok := other.Value().(string); ok {
|
if o, ok := other.Value().(string); ok {
|
||||||
return types.Bool(pn.Name.String() == o)
|
return types.Bool(pn.Name.String() == o)
|
||||||
|
|||||||
@@ -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() {
|
func init() {
|
||||||
caddy.RegisterModule(Encode{})
|
caddy.RegisterModule(Encode{})
|
||||||
|
caddy.RegisterNamespace("http.encoders", []interface{}{
|
||||||
|
(*Encoding)(nil),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode is a middleware which can encode responses.
|
// Encode is a middleware which can encode responses.
|
||||||
@@ -93,6 +96,7 @@ func (enc *Encode) Provision(ctx caddy.Context) error {
|
|||||||
"application/xhtml+xml*",
|
"application/xhtml+xml*",
|
||||||
"application/atom+xml*",
|
"application/atom+xml*",
|
||||||
"application/rss+xml*",
|
"application/rss+xml*",
|
||||||
|
"application/wasm*",
|
||||||
"image/svg+xml*",
|
"image/svg+xml*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -167,10 +171,10 @@ func (enc *Encode) openResponseWriter(encodingName string, w http.ResponseWriter
|
|||||||
// initResponseWriter initializes the responseWriter instance
|
// initResponseWriter initializes the responseWriter instance
|
||||||
// allocated in openResponseWriter, enabling mid-stack inlining.
|
// allocated in openResponseWriter, enabling mid-stack inlining.
|
||||||
func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter) *responseWriter {
|
func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter) *responseWriter {
|
||||||
if httpInterfaces, ok := wrappedRW.(caddyhttp.HTTPInterfaces); ok {
|
if rww, ok := wrappedRW.(*caddyhttp.ResponseWriterWrapper); ok {
|
||||||
rw.HTTPInterfaces = httpInterfaces
|
rw.ResponseWriter = rww
|
||||||
} else {
|
} else {
|
||||||
rw.HTTPInterfaces = &caddyhttp.ResponseWriterWrapper{ResponseWriter: wrappedRW}
|
rw.ResponseWriter = &caddyhttp.ResponseWriterWrapper{ResponseWriter: wrappedRW}
|
||||||
}
|
}
|
||||||
rw.encodingName = encodingName
|
rw.encodingName = encodingName
|
||||||
rw.config = enc
|
rw.config = enc
|
||||||
@@ -182,7 +186,7 @@ func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, w
|
|||||||
// using the encoding represented by encodingName and
|
// using the encoding represented by encodingName and
|
||||||
// configured by config.
|
// configured by config.
|
||||||
type responseWriter struct {
|
type responseWriter struct {
|
||||||
caddyhttp.HTTPInterfaces
|
http.ResponseWriter
|
||||||
encodingName string
|
encodingName string
|
||||||
w Encoder
|
w Encoder
|
||||||
config *Encode
|
config *Encode
|
||||||
@@ -211,7 +215,8 @@ func (rw *responseWriter) Flush() {
|
|||||||
// to rw.Write (see bug in #4314)
|
// to rw.Write (see bug in #4314)
|
||||||
return
|
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
|
// 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) {
|
func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
if !rw.wroteHeader {
|
if !rw.wroteHeader {
|
||||||
if rw.statusCode != 0 {
|
if rw.statusCode != 0 {
|
||||||
rw.HTTPInterfaces.WriteHeader(rw.statusCode)
|
rw.ResponseWriter.WriteHeader(rw.statusCode)
|
||||||
}
|
}
|
||||||
rw.wroteHeader = true
|
rw.wroteHeader = true
|
||||||
}
|
}
|
||||||
return rw.HTTPInterfaces.Hijack()
|
//nolint:bodyclose
|
||||||
|
return http.NewResponseController(rw.ResponseWriter).Hijack()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write writes to the response. If the response qualifies,
|
// 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
|
// by the standard library
|
||||||
if !rw.wroteHeader {
|
if !rw.wroteHeader {
|
||||||
if rw.statusCode != 0 {
|
if rw.statusCode != 0 {
|
||||||
rw.HTTPInterfaces.WriteHeader(rw.statusCode)
|
rw.ResponseWriter.WriteHeader(rw.statusCode)
|
||||||
}
|
}
|
||||||
rw.wroteHeader = true
|
rw.wroteHeader = true
|
||||||
}
|
}
|
||||||
@@ -268,7 +274,7 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
|
|||||||
if rw.w != nil {
|
if rw.w != nil {
|
||||||
return rw.w.Write(p)
|
return rw.w.Write(p)
|
||||||
} else {
|
} else {
|
||||||
return rw.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.
|
// issue #5059, don't write status code if not set explicitly.
|
||||||
if rw.statusCode != 0 {
|
if rw.statusCode != 0 {
|
||||||
rw.HTTPInterfaces.WriteHeader(rw.statusCode)
|
rw.ResponseWriter.WriteHeader(rw.statusCode)
|
||||||
}
|
}
|
||||||
rw.wroteHeader = true
|
rw.wroteHeader = true
|
||||||
}
|
}
|
||||||
@@ -301,7 +307,7 @@ func (rw *responseWriter) Close() error {
|
|||||||
|
|
||||||
// Unwrap returns the underlying ResponseWriter.
|
// Unwrap returns the underlying ResponseWriter.
|
||||||
func (rw *responseWriter) Unwrap() http.ResponseWriter {
|
func (rw *responseWriter) Unwrap() http.ResponseWriter {
|
||||||
return rw.HTTPInterfaces
|
return rw.ResponseWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
// init should be called before we write a response, if rw.buf has contents.
|
// init should be called before we write a response, if rw.buf has contents.
|
||||||
@@ -310,7 +316,7 @@ func (rw *responseWriter) init() {
|
|||||||
rw.config.Match(rw) {
|
rw.config.Match(rw) {
|
||||||
|
|
||||||
rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder)
|
rw.w = rw.config.writerPools[rw.encodingName].Get().(Encoder)
|
||||||
rw.w.Reset(rw.HTTPInterfaces)
|
rw.w.Reset(rw.ResponseWriter)
|
||||||
rw.Header().Del("Content-Length") // https://github.com/golang/go/issues/14975
|
rw.Header().Del("Content-Length") // https://github.com/golang/go/issues/14975
|
||||||
rw.Header().Set("Content-Encoding", rw.encodingName)
|
rw.Header().Set("Content-Encoding", rw.encodingName)
|
||||||
rw.Header().Add("Vary", "Accept-Encoding")
|
rw.Header().Add("Vary", "Accept-Encoding")
|
||||||
@@ -429,5 +435,4 @@ var (
|
|||||||
_ caddy.Provisioner = (*Encode)(nil)
|
_ caddy.Provisioner = (*Encode)(nil)
|
||||||
_ caddy.Validator = (*Encode)(nil)
|
_ caddy.Validator = (*Encode)(nil)
|
||||||
_ caddyhttp.MiddlewareHandler = (*Encode)(nil)
|
_ caddyhttp.MiddlewareHandler = (*Encode)(nil)
|
||||||
_ caddyhttp.HTTPInterfaces = (*responseWriter)(nil)
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,10 +18,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/klauspost/compress/gzip"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
||||||
"github.com/klauspost/compress/gzip"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -15,10 +15,11 @@
|
|||||||
package caddyzstd
|
package caddyzstd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/klauspost/compress/zstd"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
||||||
"github.com/klauspost/compress/zstd"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -15,27 +15,24 @@
|
|||||||
package caddyhttp
|
package caddyhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
weakrand "math/rand"
|
weakrand "math/rand"
|
||||||
"path"
|
"path"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
|
||||||
weakrand.Seed(time.Now().UnixNano())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error is a convenient way for a Handler to populate the
|
// Error is a convenient way for a Handler to populate the
|
||||||
// essential fields of a HandlerError. If err is itself a
|
// essential fields of a HandlerError. If err is itself a
|
||||||
// HandlerError, then any essential fields that are not
|
// HandlerError, then any essential fields that are not
|
||||||
// set will be populated.
|
// set will be populated.
|
||||||
func Error(statusCode int, err error) HandlerError {
|
func Error(statusCode int, err error) HandlerError {
|
||||||
const idLen = 9
|
const idLen = 9
|
||||||
if he, ok := err.(HandlerError); ok {
|
var he HandlerError
|
||||||
|
if errors.As(err, &he) {
|
||||||
if he.ID == "" {
|
if he.ID == "" {
|
||||||
he.ID = randString(idLen, true)
|
he.ID = randString(idLen, true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,18 +29,25 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// BrowseTemplate is the default template document to use for
|
||||||
|
// file listings. By default, its default value is an embedded
|
||||||
|
// document. You can override this value at program start, or
|
||||||
|
// if you are running Caddy via config, you can specify a
|
||||||
|
// custom template_file in the browse configuration.
|
||||||
|
//
|
||||||
//go:embed browse.html
|
//go:embed browse.html
|
||||||
var defaultBrowseTemplate string
|
var BrowseTemplate string
|
||||||
|
|
||||||
// Browse configures directory browsing.
|
// Browse configures directory browsing.
|
||||||
type Browse struct {
|
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"`
|
TemplateFile string `json:"template_file,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +100,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
|
|||||||
return caddyhttp.Error(http.StatusInternalServerError, err)
|
return caddyhttp.Error(http.StatusInternalServerError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fsrv.browseApplyQueryParams(w, r, &listing)
|
fsrv.browseApplyQueryParams(w, r, listing)
|
||||||
|
|
||||||
buf := bufPool.Get().(*bytes.Buffer)
|
buf := bufPool.Get().(*bytes.Buffer)
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
@@ -113,7 +120,7 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
|
|||||||
fs = http.Dir(repl.ReplaceAll(fsrv.Root, "."))
|
fs = http.Dir(repl.ReplaceAll(fsrv.Root, "."))
|
||||||
}
|
}
|
||||||
|
|
||||||
var tplCtx = &templateContext{
|
tplCtx := &templateContext{
|
||||||
TemplateContext: templates.TemplateContext{
|
TemplateContext: templates.TemplateContext{
|
||||||
Root: fs,
|
Root: fs,
|
||||||
Req: r,
|
Req: r,
|
||||||
@@ -137,10 +144,10 @@ func (fsrv *FileServer) serveBrowse(root, dirPath string, w http.ResponseWriter,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, dir fs.ReadDirFile, root, urlPath string, repl *caddy.Replacer) (browseTemplateContext, error) {
|
func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, dir fs.ReadDirFile, root, urlPath string, repl *caddy.Replacer) (*browseTemplateContext, error) {
|
||||||
files, err := dir.ReadDir(10000) // TODO: this limit should probably be configurable
|
files, err := dir.ReadDir(10000) // TODO: this limit should probably be configurable
|
||||||
if err != nil && err != io.EOF {
|
if err != nil && err != io.EOF {
|
||||||
return browseTemplateContext{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// user can presumably browse "up" to parent folder if path is longer than "/"
|
// user can presumably browse "up" to parent folder if path is longer than "/"
|
||||||
@@ -204,7 +211,7 @@ func (fsrv *FileServer) makeBrowseTemplate(tplCtx *templateContext) (*template.T
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tpl = tplCtx.NewTemplate("default_listing")
|
tpl = tplCtx.NewTemplate("default_listing")
|
||||||
tpl, err = tpl.Parse(defaultBrowseTemplate)
|
tpl, err = tpl.Parse(BrowseTemplate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parsing default browse template: %v", err)
|
return nil, fmt.Errorf("parsing default browse template: %v", err)
|
||||||
}
|
}
|
||||||
@@ -237,7 +244,7 @@ func isSymlink(f fs.FileInfo) bool {
|
|||||||
// features.
|
// features.
|
||||||
type templateContext struct {
|
type templateContext struct {
|
||||||
templates.TemplateContext
|
templates.TemplateContext
|
||||||
browseTemplateContext
|
*browseTemplateContext
|
||||||
}
|
}
|
||||||
|
|
||||||
// bufPool is used to increase the efficiency of file listings.
|
// bufPool is used to increase the efficiency of file listings.
|
||||||
|
|||||||
@@ -1,90 +1,296 @@
|
|||||||
{{- define "icon"}}
|
{{- define "icon"}}
|
||||||
{{- if .IsDir}}
|
{{- 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">
|
{{- if .IsSymlink}}
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<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 d="M9 3a1 1 0 0 1 .608 .206l.1 .087l2.706 2.707h6.586a3 3 0 0 1 2.995 2.824l.005 .176v8a3 3 0 0 1 -2.824 2.995l-.176 .005h-14a3 3 0 0 1 -2.995 -2.824l-.005 -.176v-11a3 3 0 0 1 2.824 -2.995l.176 -.005h4z" stroke-width="0" fill="currentColor"></path>
|
<path 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>
|
</svg>
|
||||||
{{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp"}}
|
{{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp" ".tiff" ".bmp" ".heif" ".heic" ".svg"}}
|
||||||
<img src="{{html .Name}}">
|
{{- if eq .Tpl.Layout "grid"}}
|
||||||
{{- else if .HasExt ".mp4" ".mov" ".mpeg" ".avi" ".ogg" ".webm"}}
|
<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">
|
<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 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>
|
<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>
|
<path d="M8 4l0 16"/>
|
||||||
<path d="M16 4l0 16"></path>
|
<path d="M16 4l0 16"/>
|
||||||
<path d="M4 8l4 0"></path>
|
<path d="M4 8l4 0"/>
|
||||||
<path d="M4 16l4 0"></path>
|
<path d="M4 16l4 0"/>
|
||||||
<path d="M4 12l16 0"></path>
|
<path d="M4 12l16 0"/>
|
||||||
<path d="M16 8l4 0"></path>
|
<path d="M16 8l4 0"/>
|
||||||
<path d="M16 16l4 0"></path>
|
<path d="M16 16l4 0"/>
|
||||||
</svg>
|
</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">
|
<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 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>
|
<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>
|
<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>
|
<path d="M9 17l0 -13l10 0l0 13"/>
|
||||||
<path d="M9 8l10 0"></path>
|
<path d="M9 8l10 0"/>
|
||||||
</svg>
|
</svg>
|
||||||
{{- else if .HasExt ".pdf"}}
|
{{- 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">
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-type-pdf" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
<path d="M10 8v8h2a2 2 0 0 0 2 -2v-4a2 2 0 0 0 -2 -2h-2z"></path>
|
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
|
||||||
<path d="M3 12h2a2 2 0 1 0 0 -4h-2v8"></path>
|
<path d="M5 12v-7a2 2 0 0 1 2 -2h7l5 5v4"/>
|
||||||
<path d="M17 12h3"></path>
|
<path d="M5 18h1.5a1.5 1.5 0 0 0 0 -3h-1.5v6"/>
|
||||||
<path d="M21 8h-4v8"></path>
|
<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>
|
</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">
|
<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 stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
<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>
|
<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>
|
<path d="M9 9l1 0"/>
|
||||||
<path d="M9 13l6 0"></path>
|
<path d="M9 13l6 0"/>
|
||||||
<path d="M9 17l6 0"></path>
|
<path d="M9 17l6 0"/>
|
||||||
</svg>
|
</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">
|
<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 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>
|
<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>
|
<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>
|
<path d="M11 5l-1 0"/>
|
||||||
<path d="M13 7l-1 0"></path>
|
<path d="M13 7l-1 0"/>
|
||||||
<path d="M11 9l-1 0"></path>
|
<path d="M11 9l-1 0"/>
|
||||||
<path d="M13 11l-1 0"></path>
|
<path d="M13 11l-1 0"/>
|
||||||
<path d="M11 13l-1 0"></path>
|
<path d="M11 13l-1 0"/>
|
||||||
<path d="M13 15l-1 0"></path>
|
<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>
|
</svg>
|
||||||
{{- else if .HasExt ".md" ".mdown" ".markdown"}}
|
{{- else if .HasExt ".md" ".mdown" ".markdown"}}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-markdown" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-markdown" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<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>
|
<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>
|
<path d="M7 15v-6l2 2l2 -2v6"/>
|
||||||
<path d="M14 13l2 2l2 -2m-2 2v-6"></path>
|
<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>
|
</svg>
|
||||||
{{- else if .HasExt ".go"}}
|
{{- else if .HasExt ".go"}}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-golang" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-golang" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<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>
|
<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>
|
<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>
|
<path d="M5.5 15h-1.5"/>
|
||||||
<path d="M6 9h-2"></path>
|
<path d="M6 9h-2"/>
|
||||||
<path d="M5 12h-3"></path>
|
<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>
|
</svg>
|
||||||
{{- else}}
|
{{- else}}
|
||||||
{{- if .IsSymlink}}
|
{{- if .IsSymlink}}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-symlink" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file-symlink" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
<path d="M4 21v-4a3 3 0 0 1 3 -3h5"></path>
|
<path d="M4 21v-4a3 3 0 0 1 3 -3h5"/>
|
||||||
<path d="M9 17l3 -3l-3 -3"></path>
|
<path d="M9 17l3 -3l-3 -3"/>
|
||||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
<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"></path>
|
<path d="M5 11v-6a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2h-9.5"/>
|
||||||
</svg>
|
</svg>
|
||||||
{{- else}}
|
{{- else}}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
<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>
|
<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>
|
</svg>
|
||||||
{{- end}}
|
{{- end}}
|
||||||
{{- end}}
|
{{- end}}
|
||||||
@@ -93,6 +299,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>{{html .Name}}</title>
|
<title>{{html .Name}}</title>
|
||||||
|
<link rel="canonical" href="{{.Path}}/" />
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="color-scheme" content="light dark">
|
<meta name="color-scheme" content="light dark">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
@@ -104,6 +311,7 @@ body {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
text-rendering: optimizespeed;
|
text-rendering: optimizespeed;
|
||||||
background-color: #f3f6f7;
|
background-color: #f3f6f7;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
img,
|
img,
|
||||||
@@ -201,7 +409,7 @@ header {
|
|||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-family: Poppins;
|
font-family: Poppins, system-ui, sans-serif;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
@@ -384,17 +592,14 @@ td .go-up {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(16em, 1fr));
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid .entry {
|
.grid .entry {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 20%;
|
width: 100%;
|
||||||
min-width: 250px;
|
|
||||||
max-width: 400px;
|
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid .entry a {
|
.grid .entry a {
|
||||||
@@ -458,8 +663,13 @@ footer {
|
|||||||
#filter {
|
#filter {
|
||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grid .entry {
|
||||||
|
max-width: initial;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
html {
|
html {
|
||||||
background: black; /* overscroll */
|
background: black; /* overscroll */
|
||||||
@@ -484,6 +694,10 @@ footer {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
h1 a:hover {
|
h1 a:hover {
|
||||||
background: hsl(213deg 100% 73% / 20%);
|
background: hsl(213deg 100% 73% / 20%);
|
||||||
}
|
}
|
||||||
@@ -551,15 +765,16 @@ footer {
|
|||||||
stroke: #ccc !important;
|
stroke: #ccc !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
{{- if eq .Layout "grid"}}
|
{{- if eq .Layout "grid"}}
|
||||||
<style>.wrapper { max-width: none; } main { margin-top: 1px; }</style>
|
<style>.wrapper { max-width: none; } main { margin-top: 1px; }</style>
|
||||||
{{- end}}
|
{{- end}}
|
||||||
</head>
|
</head>
|
||||||
<body onload="initPage()">
|
<body onload="initPage()">
|
||||||
<header>
|
<header>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div class="breadcrumbs">Folder Path</div>
|
<div class="breadcrumbs">Folder Path</div>
|
||||||
<h1>
|
<h1>
|
||||||
{{range $i, $crumb := .Breadcrumbs}}<a href="{{html $crumb.Link}}">{{html $crumb.Text}}</a>{{if ne $i 0}}/{{end}}{{end}}
|
{{range $i, $crumb := .Breadcrumbs}}<a href="{{html $crumb.Link}}">{{html $crumb.Text}}</a>{{if ne $i 0}}/{{end}}{{end}}
|
||||||
</h1>
|
</h1>
|
||||||
@@ -583,19 +798,19 @@ footer {
|
|||||||
</div>
|
</div>
|
||||||
<a href="javascript:queryParam('layout', '')" id="layout-list" class='layout{{if eq $.Layout "list" ""}}current{{end}}'>
|
<a href="javascript:queryParam('layout', '')" id="layout-list" class='layout{{if eq $.Layout "list" ""}}current{{end}}'>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-layout-list" width="16" height="16" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-layout-list" width="16" height="16" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<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>
|
<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"></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"/>
|
||||||
</svg>
|
</svg>
|
||||||
List
|
List
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript:queryParam('layout', 'grid')" id="layout-grid" class='layout{{if eq $.Layout "grid"}}current{{end}}'>
|
<a href="javascript:queryParam('layout', 'grid')" id="layout-grid" class='layout{{if eq $.Layout "grid"}}current{{end}}'>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-layout-grid" width="16" height="16" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-layout-grid" width="16" height="16" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<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>
|
<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>
|
<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>
|
<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"></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"/>
|
||||||
</svg>
|
</svg>
|
||||||
Grid
|
Grid
|
||||||
</a>
|
</a>
|
||||||
@@ -620,22 +835,22 @@ footer {
|
|||||||
{{- if and (eq .Sort "namedirfirst") (ne .Order "desc")}}
|
{{- if and (eq .Sort "namedirfirst") (ne .Order "desc")}}
|
||||||
<a href="?sort=namedirfirst&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}" class="icon">
|
<a href="?sort=namedirfirst&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}" class="icon">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
<path d="M18 14l-6 -6l-6 6h12"></path>
|
<path d="M18 14l-6 -6l-6 6h12"/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
{{- else if and (eq .Sort "namedirfirst") (ne .Order "asc")}}
|
{{- else if and (eq .Sort "namedirfirst") (ne .Order "asc")}}
|
||||||
<a href="?sort=namedirfirst&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}" class="icon">
|
<a href="?sort=namedirfirst&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}" class="icon">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-down" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-down" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
<path d="M6 10l6 6l6 -6h-12"></path>
|
<path d="M6 10l6 6l6 -6h-12"/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
{{- else}}
|
{{- else}}
|
||||||
<a href="?sort=namedirfirst&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}" class="icon sort">
|
<a href="?sort=namedirfirst&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}" class="icon sort">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
<path d="M18 14l-6 -6l-6 6h12"></path>
|
<path d="M18 14l-6 -6l-6 6h12"/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
{{- end}}
|
{{- 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}}">
|
<a href="?sort=name&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
|
||||||
Name
|
Name
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
<path d="M18 14l-6 -6l-6 6h12"></path>
|
<path d="M18 14l-6 -6l-6 6h12"/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
{{- else if and (eq .Sort "name") (ne .Order "asc")}}
|
{{- else if and (eq .Sort "name") (ne .Order "asc")}}
|
||||||
<a href="?sort=name&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
|
<a href="?sort=name&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
|
||||||
Name
|
Name
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-down" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-down" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
<path d="M6 10l6 6l6 -6h-12"></path>
|
<path d="M6 10l6 6l6 -6h-12"/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
{{- else}}
|
{{- else}}
|
||||||
@@ -664,11 +879,11 @@ footer {
|
|||||||
|
|
||||||
<div class="filter-container">
|
<div class="filter-container">
|
||||||
<svg id="search-icon" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-search" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg id="search-icon" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-search" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<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>
|
<path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0"/>
|
||||||
<path d="M21 21l-6 -6"></path>
|
<path d="M21 21l-6 -6"/>
|
||||||
</svg>
|
</svg>
|
||||||
<input type="text" placeholder="Search" id="filter" onkeyup='filter()'>
|
<input type="search" placeholder="Search" id="filter" onkeyup='filter()'>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<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}}">
|
<a href="?sort=size&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
|
||||||
Size
|
Size
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
<path d="M18 14l-6 -6l-6 6h12"></path>
|
<path d="M18 14l-6 -6l-6 6h12"/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
{{- else if and (eq .Sort "size") (ne .Order "asc")}}
|
{{- else if and (eq .Sort "size") (ne .Order "asc")}}
|
||||||
<a href="?sort=size&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
|
<a href="?sort=size&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
|
||||||
Size
|
Size
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-down" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-down" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
<path d="M6 10l6 6l6 -6h-12"></path>
|
<path d="M6 10l6 6l6 -6h-12"/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
{{- else}}
|
{{- 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}}">
|
<a href="?sort=time&order=desc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
|
||||||
Modified
|
Modified
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
<path d="M18 14l-6 -6l-6 6h12"></path>
|
<path d="M18 14l-6 -6l-6 6h12"/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
{{- else if and (eq .Sort "time") (ne .Order "asc")}}
|
{{- else if and (eq .Sort "time") (ne .Order "asc")}}
|
||||||
<a href="?sort=time&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
|
<a href="?sort=time&order=asc{{if ne 0 .Limit}}&limit={{.Limit}}{{end}}{{if ne 0 .Offset}}&offset={{.Offset}}{{end}}">
|
||||||
Modified
|
Modified
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-down" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-caret-down" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
<path d="M6 10l6 6l6 -6h-12"></path>
|
<path d="M6 10l6 6l6 -6h-12"/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
{{- else}}
|
{{- else}}
|
||||||
@@ -727,8 +942,8 @@ footer {
|
|||||||
<td>
|
<td>
|
||||||
<a href="..">
|
<a href="..">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-corner-left-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-corner-left-up" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<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"></path>
|
<path d="M18 18h-6a3 3 0 0 1 -3 -3v-10l-4 4m8 0l-4 -4"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="go-up">Up</span>
|
<span class="go-up">Up</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -869,7 +1084,10 @@ footer {
|
|||||||
});
|
});
|
||||||
document.querySelectorAll('.size').forEach(el => {
|
document.querySelectorAll('.size').forEach(el => {
|
||||||
const size = Number(el.dataset.size);
|
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}%`;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -917,7 +1135,7 @@ footer {
|
|||||||
return;
|
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"));
|
var timeList = Array.prototype.slice.call(document.getElementsByTagName("time"));
|
||||||
timeList.forEach(localizeDatetime);
|
timeList.forEach(localizeDatetime);
|
||||||
|
|||||||
@@ -25,17 +25,23 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
"go.uber.org/zap"
|
"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)
|
filesToHide := fsrv.transformHidePaths(repl)
|
||||||
|
|
||||||
var dirCount, fileCount int
|
name, _ := url.PathUnescape(urlPath)
|
||||||
fileInfos := []fileInfo{}
|
|
||||||
|
tplCtx := &browseTemplateContext{
|
||||||
|
Name: path.Base(name),
|
||||||
|
Path: urlPath,
|
||||||
|
CanGoUp: canGoUp,
|
||||||
|
}
|
||||||
|
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
@@ -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
|
// add the slash after the escape of path to avoid escaping the slash as well
|
||||||
if isDir {
|
if isDir {
|
||||||
name += "/"
|
name += "/"
|
||||||
dirCount++
|
tplCtx.NumDirs++
|
||||||
} else {
|
} else {
|
||||||
fileCount++
|
tplCtx.NumFiles++
|
||||||
}
|
}
|
||||||
|
|
||||||
size := info.Size()
|
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
|
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,
|
IsDir: isDir,
|
||||||
IsSymlink: fileIsSymlink,
|
IsSymlink: fileIsSymlink,
|
||||||
Name: name,
|
Name: name,
|
||||||
@@ -90,17 +96,11 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, entries []fs.DirEn
|
|||||||
URL: u.String(),
|
URL: u.String(),
|
||||||
ModTime: info.ModTime().UTC(),
|
ModTime: info.ModTime().UTC(),
|
||||||
Mode: info.Mode(),
|
Mode: info.Mode(),
|
||||||
|
Tpl: tplCtx, // a reference up to the template context is useful
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
name, _ := url.PathUnescape(urlPath)
|
|
||||||
return browseTemplateContext{
|
return tplCtx
|
||||||
Name: path.Base(name),
|
|
||||||
Path: urlPath,
|
|
||||||
CanGoUp: canGoUp,
|
|
||||||
Items: fileInfos,
|
|
||||||
NumDirs: dirCount,
|
|
||||||
NumFiles: fileCount,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// browseTemplateContext provides the template context for directory listings.
|
// browseTemplateContext provides the template context for directory listings.
|
||||||
@@ -230,6 +230,9 @@ type fileInfo struct {
|
|||||||
Mode os.FileMode `json:"mode"`
|
Mode os.FileMode `json:"mode"`
|
||||||
IsDir bool `json:"is_dir"`
|
IsDir bool `json:"is_dir"`
|
||||||
IsSymlink bool `json:"is_symlink"`
|
IsSymlink bool `json:"is_symlink"`
|
||||||
|
|
||||||
|
// a pointer to the template context is useful inside nested templates
|
||||||
|
Tpl *browseTemplateContext `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasExt returns true if the filename has any of the given suffixes, case-insensitive.
|
// HasExt returns true if the filename has any of the given suffixes, case-insensitive.
|
||||||
|
|||||||
@@ -16,24 +16,30 @@ package fileserver
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
|
||||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
|
||||||
caddytpl "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
||||||
|
caddytpl "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddycmd.RegisterCommand(caddycmd.Command{
|
caddycmd.RegisterCommand(caddycmd.Command{
|
||||||
Name: "file-server",
|
Name: "file-server",
|
||||||
Usage: "[--domain <example.com>] [--root <path>] [--listen <addr>] [--browse] [--access-log]",
|
Usage: "[--domain <example.com>] [--root <path>] [--listen <addr>] [--browse] [--access-log] [--precompressed]",
|
||||||
Short: "Spins up a production-ready file server",
|
Short: "Spins up a production-ready file server",
|
||||||
Long: `
|
Long: `
|
||||||
A simple but production-ready file server. Useful for quick deployments,
|
A simple but production-ready file server. Useful for quick deployments,
|
||||||
@@ -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
|
a public domain, ensure A/AAAA records are properly configured before
|
||||||
using this option.
|
using this option.
|
||||||
|
|
||||||
|
By default, Zstandard and Gzip compression are enabled. Use --no-compress
|
||||||
|
to disable compression.
|
||||||
|
|
||||||
If --browse is enabled, requests for folders without an index file will
|
If --browse is enabled, requests for folders without an index file will
|
||||||
respond with a file listing.`,
|
respond with a file listing.`,
|
||||||
CobraFunc: func(cmd *cobra.Command) {
|
CobraFunc: func(cmd *cobra.Command) {
|
||||||
cmd.Flags().StringP("domain", "d", "", "Domain name at which to serve the files")
|
cmd.Flags().StringP("domain", "d", "", "Domain name at which to serve the files")
|
||||||
cmd.Flags().StringP("root", "r", "", "The path to the root of the site")
|
cmd.Flags().StringP("root", "r", "", "The path to the root of the site")
|
||||||
cmd.Flags().StringP("listen", "", "", "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("browse", "b", false, "Enable directory browsing")
|
||||||
cmd.Flags().BoolP("templates", "t", false, "Enable template rendering")
|
cmd.Flags().BoolP("templates", "t", false, "Enable template rendering")
|
||||||
cmd.Flags().BoolP("access-log", "", 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("debug", "v", false, "Enable verbose debug logs")
|
||||||
|
cmd.Flags().BoolP("no-compress", "", false, "Disable Zstandard and Gzip compression")
|
||||||
|
cmd.Flags().StringSliceP("precompressed", "p", []string{}, "Specify precompression file extensions. Compression preference implied from flag order.")
|
||||||
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdFileServer)
|
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdFileServer)
|
||||||
|
cmd.AddCommand(&cobra.Command{
|
||||||
|
Use: "export-template",
|
||||||
|
Short: "Exports the default file browser template",
|
||||||
|
Example: "caddy file-server export-template > browse.html",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
_, err := io.WriteString(os.Stdout, BrowseTemplate)
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -71,15 +91,64 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
|
|||||||
templates := fs.Bool("templates")
|
templates := fs.Bool("templates")
|
||||||
accessLog := fs.Bool("access-log")
|
accessLog := fs.Bool("access-log")
|
||||||
debug := fs.Bool("debug")
|
debug := fs.Bool("debug")
|
||||||
|
compress := !fs.Bool("no-compress")
|
||||||
|
precompressed, err := fs.GetStringSlice("precompressed")
|
||||||
|
if err != nil {
|
||||||
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid precompressed flag: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
var handlers []json.RawMessage
|
var handlers []json.RawMessage
|
||||||
|
|
||||||
|
if compress {
|
||||||
|
zstd, err := caddy.GetModule("http.encoders.zstd")
|
||||||
|
if err != nil {
|
||||||
|
return caddy.ExitCodeFailedStartup, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gzip, err := caddy.GetModule("http.encoders.gzip")
|
||||||
|
if err != nil {
|
||||||
|
return caddy.ExitCodeFailedStartup, err
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers = append(handlers, caddyconfig.JSONModuleObject(encode.Encode{
|
||||||
|
EncodingsRaw: caddy.ModuleMap{
|
||||||
|
"zstd": caddyconfig.JSON(zstd.New(), nil),
|
||||||
|
"gzip": caddyconfig.JSON(gzip.New(), nil),
|
||||||
|
},
|
||||||
|
Prefer: []string{"zstd", "gzip"},
|
||||||
|
}, "handler", "encode", nil))
|
||||||
|
}
|
||||||
|
|
||||||
if templates {
|
if templates {
|
||||||
handler := caddytpl.Templates{FileRoot: root}
|
handler := caddytpl.Templates{FileRoot: root}
|
||||||
handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "templates", nil))
|
handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "templates", nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
handler := FileServer{Root: root}
|
handler := FileServer{Root: root}
|
||||||
|
|
||||||
|
if len(precompressed) != 0 {
|
||||||
|
// logic mirrors modules/caddyhttp/fileserver/caddyfile.go case "precompressed"
|
||||||
|
var order []string
|
||||||
|
for _, compression := range precompressed {
|
||||||
|
modID := "http.precompressed." + compression
|
||||||
|
mod, err := caddy.GetModule(modID)
|
||||||
|
if err != nil {
|
||||||
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("getting module named '%s': %v", modID, err)
|
||||||
|
}
|
||||||
|
inst := mod.New()
|
||||||
|
precompress, ok := inst.(encode.Precompressed)
|
||||||
|
if !ok {
|
||||||
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("module %s is not a precompressor; is %T", modID, inst)
|
||||||
|
}
|
||||||
|
if handler.PrecompressedRaw == nil {
|
||||||
|
handler.PrecompressedRaw = make(caddy.ModuleMap)
|
||||||
|
}
|
||||||
|
handler.PrecompressedRaw[compression] = caddyconfig.JSON(precompress, nil)
|
||||||
|
order = append(order, compression)
|
||||||
|
}
|
||||||
|
handler.PrecompressedOrder = order
|
||||||
|
}
|
||||||
|
|
||||||
if browse {
|
if browse {
|
||||||
handler.Browse = new(Browse)
|
handler.Browse = new(Browse)
|
||||||
}
|
}
|
||||||
@@ -141,7 +210,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := caddy.Run(cfg)
|
err = caddy.Run(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup, err
|
return caddy.ExitCodeFailedStartup, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,9 +26,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
|
||||||
"github.com/google/cel-go/cel"
|
"github.com/google/cel-go/cel"
|
||||||
"github.com/google/cel-go/common"
|
"github.com/google/cel-go/common"
|
||||||
"github.com/google/cel-go/common/operators"
|
"github.com/google/cel-go/common/operators"
|
||||||
@@ -36,6 +33,10 @@ import (
|
|||||||
"github.com/google/cel-go/parser"
|
"github.com/google/cel-go/parser"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -29,16 +29,18 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
weakrand.Seed(time.Now().UnixNano())
|
caddy.RegisterNamespace("http.precompressed", []interface{}{
|
||||||
|
(*encode.Precompressed)(nil),
|
||||||
|
})
|
||||||
|
|
||||||
caddy.RegisterModule(FileServer{})
|
caddy.RegisterModule(FileServer{})
|
||||||
}
|
}
|
||||||
@@ -62,7 +64,23 @@ func init() {
|
|||||||
// requested directory does not have an index file, Caddy writes a
|
// requested directory does not have an index file, Caddy writes a
|
||||||
// 404 response. Alternatively, file browsing can be enabled with
|
// 404 response. Alternatively, file browsing can be enabled with
|
||||||
// the "browse" parameter which shows a list of files when directories
|
// the "browse" parameter which shows a list of files when directories
|
||||||
// are requested if no index file is present.
|
// are requested if no index file is present. If "browse" is enabled,
|
||||||
|
// Caddy may serve a JSON array of the dirctory listing when the `Accept`
|
||||||
|
// header mentions `application/json` with the following structure:
|
||||||
|
//
|
||||||
|
// [{
|
||||||
|
// "name": "",
|
||||||
|
// "size": 0,
|
||||||
|
// "url": "",
|
||||||
|
// "mod_time": "",
|
||||||
|
// "mode": 0,
|
||||||
|
// "is_dir": false,
|
||||||
|
// "is_symlink": false
|
||||||
|
// }]
|
||||||
|
//
|
||||||
|
// with the `url` being relative to the request path and `mod_time` in the RFC 3339 format
|
||||||
|
// with sub-second precision. For any other value for the `Accept` header, the
|
||||||
|
// respective browse template is executed with `Content-Type: text/html`.
|
||||||
//
|
//
|
||||||
// By default, this handler will canonicalize URIs so that requests to
|
// By default, this handler will canonicalize URIs so that requests to
|
||||||
// directories end with a slash, but requests to regular files do not.
|
// directories end with a slash, but requests to regular files do not.
|
||||||
@@ -356,7 +374,9 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
|
|||||||
}
|
}
|
||||||
|
|
||||||
var file fs.File
|
var file fs.File
|
||||||
var etag string
|
|
||||||
|
// etag is usually unset, but if the user knows what they're doing, let them override it
|
||||||
|
etag := w.Header().Get("Etag")
|
||||||
|
|
||||||
// check for precompressed files
|
// check for precompressed files
|
||||||
for _, ae := range encode.AcceptedEncodings(r, fsrv.PrecompressedOrder) {
|
for _, ae := range encode.AcceptedEncodings(r, fsrv.PrecompressedOrder) {
|
||||||
@@ -388,7 +408,9 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
|
|||||||
// don't assign info = compressedInfo because sidecars are kind
|
// don't assign info = compressedInfo because sidecars are kind
|
||||||
// of transparent; however we do need to set the Etag:
|
// of transparent; however we do need to set the Etag:
|
||||||
// https://caddy.community/t/gzipped-sidecar-file-wrong-same-etag/16793
|
// https://caddy.community/t/gzipped-sidecar-file-wrong-same-etag/16793
|
||||||
etag = calculateEtag(compressedInfo)
|
if etag == "" {
|
||||||
|
etag = calculateEtag(compressedInfo)
|
||||||
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -408,20 +430,29 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
|
|||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
etag = calculateEtag(info)
|
if etag == "" {
|
||||||
|
etag = calculateEtag(info)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// at this point, we're serving a file; Go std lib supports only
|
// at this point, we're serving a file; Go std lib supports only
|
||||||
// GET and HEAD, which is sensible for a static file server - reject
|
// GET and HEAD, which is sensible for a static file server - reject
|
||||||
// any other methods (see issue #5166)
|
// any other methods (see issue #5166)
|
||||||
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||||
w.Header().Add("Allow", "GET, HEAD")
|
// if we're in an error context, then it doesn't make sense
|
||||||
return caddyhttp.Error(http.StatusMethodNotAllowed, nil)
|
// to repeat the error; just continue because we're probably
|
||||||
|
// trying to write an error page response (see issue #5703)
|
||||||
|
if _, ok := r.Context().Value(caddyhttp.ErrorCtxKey).(error); !ok {
|
||||||
|
w.Header().Add("Allow", "GET, HEAD")
|
||||||
|
return caddyhttp.Error(http.StatusMethodNotAllowed, nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the Etag - note that a conditional If-None-Match request is handled
|
// set the Etag - note that a conditional If-None-Match request is handled
|
||||||
// by http.ServeContent below, which checks against this Etag value
|
// by http.ServeContent below, which checks against this Etag value
|
||||||
w.Header().Set("Etag", etag)
|
if etag != "" {
|
||||||
|
w.Header().Set("Etag", etag)
|
||||||
|
}
|
||||||
|
|
||||||
if w.Header().Get("Content-Type") == "" {
|
if w.Header().Get("Content-Type") == "" {
|
||||||
mtyp := mime.TypeByExtension(filepath.Ext(filename))
|
mtyp := mime.TypeByExtension(filepath.Ext(filename))
|
||||||
@@ -608,7 +639,11 @@ func (fsrv *FileServer) notFound(w http.ResponseWriter, r *http.Request, next ca
|
|||||||
// Prefix the etag with "W/" to convert it into a weak etag.
|
// Prefix the etag with "W/" to convert it into a weak etag.
|
||||||
// See: https://tools.ietf.org/html/rfc7232#section-2.3
|
// See: https://tools.ietf.org/html/rfc7232#section-2.3
|
||||||
func calculateEtag(d os.FileInfo) string {
|
func calculateEtag(d os.FileInfo) string {
|
||||||
t := strconv.FormatInt(d.ModTime().Unix(), 36)
|
mtime := d.ModTime().Unix()
|
||||||
|
if mtime == 0 || mtime == 1 {
|
||||||
|
return "" // not useful anyway; see issue #5548
|
||||||
|
}
|
||||||
|
t := strconv.FormatInt(mtime, 36)
|
||||||
s := strconv.FormatInt(d.Size(), 36)
|
s := strconv.FormatInt(d.Size(), 36)
|
||||||
return `"` + t + s + `"`
|
return `"` + t + s + `"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -247,10 +247,14 @@ func applyHeaderOp(ops *HeaderOps, respHeaderOps *RespHeaderOps, field, value, r
|
|||||||
respHeaderOps.Set.Set(field, value)
|
respHeaderOps.Set.Set(field, value)
|
||||||
|
|
||||||
case replacement != "": // replace
|
case replacement != "": // replace
|
||||||
|
// allow defer shortcut for replace syntax
|
||||||
|
if strings.HasPrefix(field, ">") && respHeaderOps != nil {
|
||||||
|
respHeaderOps.Deferred = true
|
||||||
|
}
|
||||||
if ops.Replace == nil {
|
if ops.Replace == nil {
|
||||||
ops.Replace = make(map[string][]Replacement)
|
ops.Replace = make(map[string][]Replacement)
|
||||||
}
|
}
|
||||||
field = strings.TrimLeft(field, "+-?")
|
field = strings.TrimLeft(field, "+-?>")
|
||||||
ops.Replace[field] = append(
|
ops.Replace[field] = append(
|
||||||
ops.Replace[field],
|
ops.Replace[field],
|
||||||
Replacement{
|
Replacement{
|
||||||
|
|||||||
@@ -371,5 +371,5 @@ func (rww *responseWriterWrapper) Write(d []byte) (int, error) {
|
|||||||
var (
|
var (
|
||||||
_ caddy.Provisioner = (*Handler)(nil)
|
_ caddy.Provisioner = (*Handler)(nil)
|
||||||
_ caddyhttp.MiddlewareHandler = (*Handler)(nil)
|
_ caddyhttp.MiddlewareHandler = (*Handler)(nil)
|
||||||
_ caddyhttp.HTTPInterfaces = (*responseWriterWrapper)(nil)
|
_ http.ResponseWriter = (*responseWriterWrapper)(nil)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ package caddyhttp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
@@ -42,7 +43,11 @@ func init() {
|
|||||||
//
|
//
|
||||||
// This listener wrapper must be placed BEFORE the "tls" listener
|
// This listener wrapper must be placed BEFORE the "tls" listener
|
||||||
// wrapper, for it to work properly.
|
// wrapper, for it to work properly.
|
||||||
type HTTPRedirectListenerWrapper struct{}
|
type HTTPRedirectListenerWrapper struct {
|
||||||
|
// MaxHeaderBytes is the maximum size to parse from a client's
|
||||||
|
// HTTP request headers. Default: 1 MB
|
||||||
|
MaxHeaderBytes int64 `json:"max_header_bytes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
func (HTTPRedirectListenerWrapper) CaddyModule() caddy.ModuleInfo {
|
func (HTTPRedirectListenerWrapper) CaddyModule() caddy.ModuleInfo {
|
||||||
return caddy.ModuleInfo{
|
return caddy.ModuleInfo{
|
||||||
@@ -56,7 +61,7 @@ func (h *HTTPRedirectListenerWrapper) UnmarshalCaddyfile(d *caddyfile.Dispenser)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *HTTPRedirectListenerWrapper) WrapListener(l net.Listener) net.Listener {
|
func (h *HTTPRedirectListenerWrapper) WrapListener(l net.Listener) net.Listener {
|
||||||
return &httpRedirectListener{l}
|
return &httpRedirectListener{l, h.MaxHeaderBytes}
|
||||||
}
|
}
|
||||||
|
|
||||||
// httpRedirectListener is listener that checks the first few bytes
|
// httpRedirectListener is listener that checks the first few bytes
|
||||||
@@ -64,6 +69,7 @@ func (h *HTTPRedirectListenerWrapper) WrapListener(l net.Listener) net.Listener
|
|||||||
// to respond to an HTTP request with a redirect.
|
// to respond to an HTTP request with a redirect.
|
||||||
type httpRedirectListener struct {
|
type httpRedirectListener struct {
|
||||||
net.Listener
|
net.Listener
|
||||||
|
maxHeaderBytes int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accept waits for and returns the next connection to the listener,
|
// Accept waits for and returns the next connection to the listener,
|
||||||
@@ -74,16 +80,23 @@ func (l *httpRedirectListener) Accept() (net.Conn, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
maxHeaderBytes := l.maxHeaderBytes
|
||||||
|
if maxHeaderBytes == 0 {
|
||||||
|
maxHeaderBytes = 1024 * 1024
|
||||||
|
}
|
||||||
|
|
||||||
return &httpRedirectConn{
|
return &httpRedirectConn{
|
||||||
Conn: c,
|
Conn: c,
|
||||||
r: bufio.NewReader(c),
|
limit: maxHeaderBytes,
|
||||||
|
r: bufio.NewReader(c),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type httpRedirectConn struct {
|
type httpRedirectConn struct {
|
||||||
net.Conn
|
net.Conn
|
||||||
once sync.Once
|
once bool
|
||||||
r *bufio.Reader
|
limit int64
|
||||||
|
r *bufio.Reader
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read tries to peek at the first few bytes of the request, and if we get
|
// Read tries to peek at the first few bytes of the request, and if we get
|
||||||
@@ -91,53 +104,58 @@ type httpRedirectConn struct {
|
|||||||
// like an HTTP request, then we perform a HTTP->HTTPS redirect on the same
|
// like an HTTP request, then we perform a HTTP->HTTPS redirect on the same
|
||||||
// port as the original connection.
|
// port as the original connection.
|
||||||
func (c *httpRedirectConn) Read(p []byte) (int, error) {
|
func (c *httpRedirectConn) Read(p []byte) (int, error) {
|
||||||
var errReturn error
|
if c.once {
|
||||||
c.once.Do(func() {
|
return c.r.Read(p)
|
||||||
firstBytes, err := c.r.Peek(5)
|
}
|
||||||
if err != nil {
|
// no need to use sync.Once - net.Conn is not read from concurrently.
|
||||||
return
|
c.once = true
|
||||||
}
|
|
||||||
|
|
||||||
// If the request doesn't look like HTTP, then it's probably
|
firstBytes, err := c.r.Peek(5)
|
||||||
// TLS bytes and we don't need to do anything.
|
if err != nil {
|
||||||
if !firstBytesLookLikeHTTP(firstBytes) {
|
return 0, err
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the HTTP request, so we can get the Host and URL to redirect to.
|
|
||||||
req, err := http.ReadRequest(c.r)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the redirect response, using the same Host and URL,
|
|
||||||
// but replacing the scheme with https.
|
|
||||||
headers := make(http.Header)
|
|
||||||
headers.Add("Location", "https://"+req.Host+req.URL.String())
|
|
||||||
resp := &http.Response{
|
|
||||||
Proto: "HTTP/1.0",
|
|
||||||
Status: "308 Permanent Redirect",
|
|
||||||
StatusCode: 308,
|
|
||||||
ProtoMajor: 1,
|
|
||||||
ProtoMinor: 0,
|
|
||||||
Header: headers,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = resp.Write(c.Conn)
|
|
||||||
if err != nil {
|
|
||||||
errReturn = fmt.Errorf("couldn't write HTTP->HTTPS redirect")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
errReturn = fmt.Errorf("redirected HTTP request on HTTPS port")
|
|
||||||
c.Conn.Close()
|
|
||||||
})
|
|
||||||
|
|
||||||
if errReturn != nil {
|
|
||||||
return 0, errReturn
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.r.Read(p)
|
// If the request doesn't look like HTTP, then it's probably
|
||||||
|
// TLS bytes, and we don't need to do anything.
|
||||||
|
if !firstBytesLookLikeHTTP(firstBytes) {
|
||||||
|
return c.r.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// From now on, we can be almost certain the request is HTTP.
|
||||||
|
// The returned error will be non nil and caller are expected to
|
||||||
|
// close the connection.
|
||||||
|
|
||||||
|
// Set the read limit, io.MultiReader is needed because
|
||||||
|
// when resetting, *bufio.Reader discards buffered data.
|
||||||
|
buffered, _ := c.r.Peek(c.r.Buffered())
|
||||||
|
mr := io.MultiReader(bytes.NewReader(buffered), c.Conn)
|
||||||
|
c.r.Reset(io.LimitReader(mr, c.limit))
|
||||||
|
|
||||||
|
// Parse the HTTP request, so we can get the Host and URL to redirect to.
|
||||||
|
req, err := http.ReadRequest(c.r)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("couldn't read HTTP request")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the redirect response, using the same Host and URL,
|
||||||
|
// but replacing the scheme with https.
|
||||||
|
headers := make(http.Header)
|
||||||
|
headers.Add("Location", "https://"+req.Host+req.URL.String())
|
||||||
|
resp := &http.Response{
|
||||||
|
Proto: "HTTP/1.0",
|
||||||
|
Status: "308 Permanent Redirect",
|
||||||
|
StatusCode: 308,
|
||||||
|
ProtoMajor: 1,
|
||||||
|
ProtoMinor: 0,
|
||||||
|
Header: headers,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = resp.Write(c.Conn)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("couldn't write HTTP->HTTPS redirect")
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, fmt.Errorf("redirected HTTP request on HTTPS port")
|
||||||
}
|
}
|
||||||
|
|
||||||
// firstBytesLookLikeHTTP reports whether a TLS record header
|
// firstBytesLookLikeHTTP reports whether a TLS record header
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user