mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-26 08:42:31 -04:00
Compare commits
121 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d0a3cf0a0a | |||
| 77e9ce7404 | |||
| cc58caa109 | |||
| d80774cb3f | |||
| a4a38c3e88 | |||
| 761347aa63 | |||
| 4ba16fe82c | |||
| 0fab9f0f7d | |||
| 5e76b5ee43 | |||
| 9c78b97f9e | |||
| fb324331f4 | |||
| 0780d4489c | |||
| d2172bea61 | |||
| c7c9f3108a | |||
| 7e77eec0ae | |||
| ef496e58ef | |||
| 18ab0f955f | |||
| 6a64bb2ce5 | |||
| 4d6945769d | |||
| 2d33271482 | |||
| c653e7d61a | |||
| c1918ff1ad | |||
| fdbef2a6ef | |||
| 2a3ed96f8c | |||
| eeb13f1ca8 | |||
| 97f5fe0079 | |||
| 558ec222db | |||
| e3b1bf80f4 | |||
| 1b8d60c459 | |||
| 733aaba102 | |||
| ed44e4d3f6 | |||
| f970f397e2 | |||
| 6ba6cf5d13 | |||
| ccc76ac1f6 | |||
| cee04ab28e | |||
| e7055d85a4 | |||
| b9b12025c6 | |||
| 7ef9ecd48a | |||
| 307dfd0431 | |||
| daea7788ad | |||
| b68e9bfdd4 | |||
| 355c178213 | |||
| f6ee80be1b | |||
| 48c08e3890 | |||
| cf42f61566 | |||
| 41aee97386 | |||
| 441d5eb062 | |||
| aed1af5976 | |||
| 4430756d5c | |||
| af89c5ab02 | |||
| bd9f145321 | |||
| 24bebd0a07 | |||
| 7dedd1486c | |||
| 7586e68e27 | |||
| 0c7c91a447 | |||
| 1a3e900b35 | |||
| 0722cf6fd8 | |||
| 8e2dd5079c | |||
| 5f44ea0748 | |||
| c8e4ac2c8c | |||
| 7dcc041eec | |||
| ca0ca67fbd | |||
| 92b62004eb | |||
| 6c23ec2f3c | |||
| 5de1565ff6 | |||
| d7834676aa | |||
| 4f50458866 | |||
| ea4ee3ae5d | |||
| 30b80bece8 | |||
| 7a630f2910 | |||
| 62e9c05264 | |||
| 6f6771aa1d | |||
| acf8d6a1ae | |||
| e98ed6232d | |||
| c35ba5588d | |||
| 5d189aff40 | |||
| df65455b1f | |||
| 8499e34e10 | |||
| 1fbb28720b | |||
| ffb6ab0644 | |||
| 9371ee67c6 | |||
| 5d20adc7a9 | |||
| 6e5e08cf58 | |||
| fbfb8fc517 | |||
| e06dfcf6ed | |||
| 566e710991 | |||
| a5e7c6e232 | |||
| db2986028f | |||
| 7e83775e3a | |||
| 2dbcdefbbe | |||
| dc36082859 | |||
| 88616e86e6 | |||
| 7b34e3107e | |||
| a6acb3902c | |||
| 45cf61b127 | |||
| d935a6956c | |||
| 2dd3852416 | |||
| 11b56c6cfc | |||
| f283062d37 | |||
| 2ab043b890 | |||
| f145bce553 | |||
| 174fa2ddb9 | |||
| cd9e1660aa | |||
| 06a05e383c | |||
| ce203aa9e1 | |||
| eac02ee98f | |||
| 72eaf2583a | |||
| 9798f6964d | |||
| 9873752978 | |||
| 294dfff443 | |||
| 76b198f586 | |||
| 7ffb640a4d | |||
| d7b21c6104 | |||
| 6610e2f1bd | |||
| 03243e42fe | |||
| cb436f0a0e | |||
| a1081194bf | |||
| eec32a0bb5 | |||
| a2825c5dd9 | |||
| db256b53e5 | |||
| 6772ffb805 |
+10
-7
@@ -1,15 +1,14 @@
|
|||||||
# Security Policy
|
# Security Policy
|
||||||
|
|
||||||
The Caddy project would like to make sure that it stays on top of all practically-exploitable vulnerabilities.
|
The Caddy project would like to make sure that it stays on top of all relevant and practically-exploitable vulnerabilities.
|
||||||
|
|
||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| -------- | ----------|
|
| ----------- | ----------|
|
||||||
| 2.latest | ✔️ |
|
| 2.latest | ✔️ |
|
||||||
| 1.x | :x: |
|
| < 2.latest | :x: |
|
||||||
| < 1.x | :x: |
|
|
||||||
|
|
||||||
|
|
||||||
## Acceptable Scope
|
## Acceptable Scope
|
||||||
@@ -26,6 +25,10 @@ Client-side exploits are out of scope. In other words, it is not a bug in Caddy
|
|||||||
|
|
||||||
Security bugs in code dependencies (including Go's standard library) are out of scope. Instead, if a dependency has patched a relevant security bug, please feel free to open a public issue or pull request to update that dependency in our code.
|
Security bugs in code dependencies (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.
|
||||||
|
|
||||||
|
Many reports are not security bugs and can be addressed by updating the documentation.
|
||||||
|
|
||||||
|
We accept security reports and patches, but do not assign CVEs, for code that has not been released with a non-prerelease tag.
|
||||||
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
@@ -33,7 +36,7 @@ We get a lot of difficult reports that turn out to be invalid. Clear, obvious re
|
|||||||
|
|
||||||
First please ensure your report falls within the accepted scope of security bugs (above).
|
First please ensure your report falls within the accepted scope of security bugs (above).
|
||||||
|
|
||||||
**YOU MUST DISCLOSE THE USE OF LLMs ("AI") INVOLVED IN ANY WAY.** Whether you are using AI for discovery, as part of writing the report or its replies, and/or testing or validating proofs and changes, we require you to mention the extent of it. **FAILURE TO INCLUDE A DISCLOSURE MAY LEAD TO IMMEDIATE DISMISSAL OF YOUR REPORT AND POTENTIAL BLOCKLISTING.**
|
:warning: **YOU MUST DISCLOSE WHETHER YOU USED LLMs ("AI") IN ANY WAY.** Whether you are using AI for discovery, as part of writing the report or its replies, and/or testing or validating proofs and changes, we require you to mention the extent of it. **FAILURE TO INCLUDE A DISCLOSURE EVEN IF YOU DO NOT USE AI MAY LEAD TO IMMEDIATE DISMISSAL OF YOUR REPORT AND POTENTIAL BLOCKLISTING.** We will not waste our time chatting with bots. But if you're a human, pull up a chair and we'll drink some chocolate milk.
|
||||||
|
|
||||||
We'll need enough information to verify the bug and make a patch. To speed things up, please include:
|
We'll need enough information to verify the bug and make a patch. To speed things up, please include:
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ jobs:
|
|||||||
models: read
|
models: read
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||||
- uses: github/ai-moderator@6bcdb2a79c2e564db8d76d7d4439d91a044c4eb6
|
- uses: github/ai-moderator@81159c370785e295c97461ade67d7c33576e9319
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
spam-label: 'spam'
|
spam-label: 'spam'
|
||||||
|
|||||||
+14
-12
@@ -65,15 +65,15 @@ jobs:
|
|||||||
actions: write # to allow uploading artifacts and cache
|
actions: write # to allow uploading artifacts and cache
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.GO_SEMVER }}
|
go-version: ${{ matrix.GO_SEMVER }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -120,7 +120,7 @@ jobs:
|
|||||||
./caddy stop
|
./caddy stop
|
||||||
|
|
||||||
- name: Publish Build Artifact
|
- name: Publish Build Artifact
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: caddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }}
|
name: caddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }}
|
||||||
path: ${{ matrix.CADDY_BIN_PATH }}
|
path: ${{ matrix.CADDY_BIN_PATH }}
|
||||||
@@ -132,6 +132,8 @@ jobs:
|
|||||||
- name: Run tests
|
- name: Run tests
|
||||||
# id: step_test
|
# id: step_test
|
||||||
# continue-on-error: true
|
# continue-on-error: true
|
||||||
|
env:
|
||||||
|
GODEBUG: http2xconnect=1
|
||||||
run: |
|
run: |
|
||||||
# (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
|
# (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
|
||||||
go test -v -coverprofile="cover-profile.out" -short -race ./...
|
go test -v -coverprofile="cover-profile.out" -short -race ./...
|
||||||
@@ -162,13 +164,13 @@ 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: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
allowed-endpoints: ci-s390x.caddyserver.com:22
|
allowed-endpoints: ci-s390x.caddyserver.com:22
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: |
|
run: |
|
||||||
set +e
|
set +e
|
||||||
@@ -191,7 +193,7 @@ jobs:
|
|||||||
retries=3
|
retries=3
|
||||||
exit_code=0
|
exit_code=0
|
||||||
while ((retries > 0)); do
|
while ((retries > 0)); do
|
||||||
CGO_ENABLED=0 go test -p 1 -v ./...
|
GODEBUG=http2xconnect=1 CGO_ENABLED=0 go test -p 1 -v ./...
|
||||||
exit_code=$?
|
exit_code=$?
|
||||||
if ((exit_code == 0)); then
|
if ((exit_code == 0)); then
|
||||||
break
|
break
|
||||||
@@ -221,19 +223,19 @@ jobs:
|
|||||||
if: github.event.pull_request.head.repo.full_name == 'caddyserver/caddy' && github.actor != 'dependabot[bot]'
|
if: github.event.pull_request.head.repo.full_name == 'caddyserver/caddy' && github.actor != 'dependabot[bot]'
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
|
- uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: check
|
args: check
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version: "~1.26"
|
go-version: "~1.26"
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -241,7 +243,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
|
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
|
||||||
xcaddy version
|
xcaddy version
|
||||||
- uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
|
- uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: build --single-target --snapshot
|
args: build --single-target --snapshot
|
||||||
|
|||||||
@@ -51,15 +51,15 @@ jobs:
|
|||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.GO_SEMVER }}
|
go-version: ${{ matrix.GO_SEMVER }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|||||||
@@ -45,18 +45,18 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version: '~1.26'
|
go-version: '~1.26'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
|
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -90,14 +90,14 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: 'Checkout Repository'
|
- name: 'Checkout Repository'
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: 'Dependency Review'
|
- name: 'Dependency Review'
|
||||||
uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0
|
uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3
|
||||||
with:
|
with:
|
||||||
comment-summary-in-pr: on-failure
|
comment-summary-in-pr: on-failure
|
||||||
# https://github.com/actions/dependency-review-action/issues/430#issuecomment-1468975566
|
# https://github.com/actions/dependency-review-action/issues/430#issuecomment-1468975566
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
# Force fetch upstream tags -- because 65 minutes
|
# Force fetch upstream tags -- because 65 minutes
|
||||||
@@ -355,23 +355,23 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.GO_SEMVER }}
|
go-version: ${{ matrix.GO_SEMVER }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
# Force fetch upstream tags -- because 65 minutes
|
# Force fetch upstream tags -- because 65 minutes
|
||||||
# tl;dr: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.2.2 runs this line:
|
# tl;dr: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 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
|
||||||
@@ -419,7 +419,7 @@ jobs:
|
|||||||
- name: Cosign version
|
- name: Cosign version
|
||||||
run: cosign version
|
run: cosign version
|
||||||
- name: Install Syft
|
- name: Install Syft
|
||||||
uses: anchore/sbom-action/download-syft@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # main
|
uses: anchore/sbom-action/download-syft@17ae1740179002c89186b61233e0f892c3118b11 # main
|
||||||
- name: Syft version
|
- name: Syft version
|
||||||
run: syft version
|
run: syft version
|
||||||
- name: Install xcaddy
|
- name: Install xcaddy
|
||||||
@@ -428,7 +428,7 @@ jobs:
|
|||||||
xcaddy version
|
xcaddy 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@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
|
uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: release --clean --timeout 60m
|
args: release --clean --timeout 60m
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ jobs:
|
|||||||
|
|
||||||
# See https://github.com/peter-evans/repository-dispatch
|
# See https://github.com/peter-evans/repository-dispatch
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Trigger event on caddyserver/dist
|
- name: Trigger event on caddyserver/dist
|
||||||
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
|
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||||
repository: caddyserver/dist
|
repository: caddyserver/dist
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
|
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
|
||||||
|
|
||||||
- name: Trigger event on caddyserver/caddy-docker
|
- name: Trigger event on caddyserver/caddy-docker
|
||||||
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
|
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||||
repository: caddyserver/caddy-docker
|
repository: caddyserver/caddy-docker
|
||||||
|
|||||||
@@ -37,12 +37,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ jobs:
|
|||||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||||
# format to the repository Actions tab.
|
# format to the repository Actions tab.
|
||||||
- name: "Upload artifact"
|
- name: "Upload artifact"
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: SARIF file
|
name: SARIF file
|
||||||
path: results.sarif
|
path: results.sarif
|
||||||
@@ -81,6 +81,6 @@ jobs:
|
|||||||
# Upload the results to GitHub's code scanning dashboard (optional).
|
# Upload the results to GitHub's code scanning dashboard (optional).
|
||||||
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
|
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
|
||||||
- name: "Upload to code-scanning"
|
- name: "Upload to code-scanning"
|
||||||
uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.29.5
|
uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v3.29.5
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ linters:
|
|||||||
- importas
|
- importas
|
||||||
- ineffassign
|
- ineffassign
|
||||||
- misspell
|
- misspell
|
||||||
|
- modernize
|
||||||
- prealloc
|
- prealloc
|
||||||
- promlinter
|
- promlinter
|
||||||
- sloglint
|
- sloglint
|
||||||
|
|||||||
+3
-1
@@ -13,7 +13,7 @@ before:
|
|||||||
- cp cmd/caddy/main.go caddy-build/main.go
|
- cp cmd/caddy/main.go caddy-build/main.go
|
||||||
- /bin/sh -c 'cd ./caddy-build && go mod init caddy'
|
- /bin/sh -c 'cd ./caddy-build && go mod init caddy'
|
||||||
# prepare syso files for windows embedding
|
# prepare syso files for windows embedding
|
||||||
- /bin/sh -c 'for a in amd64 arm arm64; do XCADDY_SKIP_BUILD=1 GOOS=windows GOARCH=$a xcaddy build {{.Env.TAG}}; done'
|
- /bin/sh -c 'for a in amd64 arm64; do XCADDY_SKIP_BUILD=1 GOOS=windows GOARCH=$a xcaddy build {{.Env.TAG}}; done'
|
||||||
- /bin/sh -c 'mv /tmp/buildenv_*/*.syso caddy-build'
|
- /bin/sh -c 'mv /tmp/buildenv_*/*.syso caddy-build'
|
||||||
# GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env
|
# GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env
|
||||||
# so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate
|
# so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate
|
||||||
@@ -67,6 +67,8 @@ builds:
|
|||||||
goarch: s390x
|
goarch: s390x
|
||||||
- goos: windows
|
- goos: windows
|
||||||
goarch: riscv64
|
goarch: riscv64
|
||||||
|
- goos: windows
|
||||||
|
goarch: arm
|
||||||
- goos: freebsd
|
- goos: freebsd
|
||||||
goarch: ppc64le
|
goarch: ppc64le
|
||||||
- goos: freebsd
|
- goos: freebsd
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
# Caddy Project Guidelines
|
||||||
|
|
||||||
|
## Mission
|
||||||
|
|
||||||
|
**Every site on HTTPS.** Caddy is a security-first, modular, extensible server platform.
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
### Go Idioms
|
||||||
|
|
||||||
|
Follow [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments):
|
||||||
|
|
||||||
|
- **Error flow**: Early return, indent error handling—not else blocks
|
||||||
|
```go
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// normal code
|
||||||
|
```
|
||||||
|
- **Naming**: initialisms (`URL`, `HTTP`, `ID`—not `Url`, `Http`, `Id`)
|
||||||
|
- **Receiver names**: 1–2 letters reflecting type (`c` for `Client`, `h` for `Handler`)
|
||||||
|
- **Error strings**: Lowercase, no trailing punctuation (`"something failed"` not `"Something failed."`)
|
||||||
|
- **Doc comments**: Full sentences starting with the name being documented
|
||||||
|
```go
|
||||||
|
// Handler serves HTTP requests for the file server.
|
||||||
|
type Handler struct { ... }
|
||||||
|
```
|
||||||
|
- **Empty slices**: `var t []string` (nil slice), not `t := []string{}` (non-nil zero-length)
|
||||||
|
- **Don't panic**: Use error returns for normal error handling
|
||||||
|
|
||||||
|
### Caddy Patterns
|
||||||
|
|
||||||
|
**Module registration**:
|
||||||
|
```go
|
||||||
|
func init() {
|
||||||
|
caddy.RegisterModule(MyModule{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (MyModule) CaddyModule() caddy.ModuleInfo {
|
||||||
|
return caddy.ModuleInfo{
|
||||||
|
ID: "namespace.category.name",
|
||||||
|
New: func() caddy.Module { return new(MyModule) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Module lifecycle**: `New()` → JSON unmarshal → `Provision()` → `Validate()` → use → `Cleanup()`
|
||||||
|
|
||||||
|
**Interface guards** — compile-time verification that modules implement required interfaces:
|
||||||
|
```go
|
||||||
|
var (
|
||||||
|
_ caddy.Provisioner = (*MyModule)(nil)
|
||||||
|
_ caddy.Validator = (*MyModule)(nil)
|
||||||
|
_ caddyfile.Unmarshaler = (*MyModule)(nil)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Structured logging** — use the module-scoped logger from context:
|
||||||
|
```go
|
||||||
|
func (m *MyModule) Provision(ctx caddy.Context) error {
|
||||||
|
m.logger = ctx.Logger()
|
||||||
|
m.logger.Debug("provisioning", zap.String("field", m.Field))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Caddyfile support** — implement `UnmarshalCaddyfile(*caddyfile.Dispenser)` using the `Dispenser` API:
|
||||||
|
```go
|
||||||
|
// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
|
||||||
|
//
|
||||||
|
// directive [arg1] [arg2] {
|
||||||
|
// subdir value
|
||||||
|
// }
|
||||||
|
func (m *MyModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
|
d.Next() // consume directive name
|
||||||
|
for d.NextArg() {
|
||||||
|
// handle inline arguments
|
||||||
|
}
|
||||||
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
|
switch d.Val() {
|
||||||
|
case "subdir":
|
||||||
|
if !d.NextArg() {
|
||||||
|
return d.ArgErr()
|
||||||
|
}
|
||||||
|
m.Field = d.Val()
|
||||||
|
default:
|
||||||
|
return d.Errf("unrecognized subdirective: %s", d.Val())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Admin API**: Implement `caddy.AdminRouter` for custom endpoints.
|
||||||
|
|
||||||
|
**Context**: Use `caddy.Context` for accessing other apps/modules and logging—don't store contexts in structs.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Caddy is built around a **module system** where everything is a module registered via `caddy.RegisterModule()`:
|
||||||
|
|
||||||
|
- **Apps** (`caddy.App`): Top-level modules like `http`, `tls`, `pki` that Caddy loads and runs
|
||||||
|
- **Modules** (`caddy.Module`): Extensible components with namespaced IDs (e.g., `http.handlers.file_server`)
|
||||||
|
- **Configuration**: Native JSON with adapters (Caddyfile → JSON via `caddyconfig/httpcaddyfile`)
|
||||||
|
|
||||||
|
| Directory | Purpose |
|
||||||
|
|-----------|---------|
|
||||||
|
| `modules/` | All standard modules (HTTP, TLS, PKI, etc.) |
|
||||||
|
| `modules/standard/imports.go` | Standard module registry |
|
||||||
|
| `caddyconfig/httpcaddyfile/` | Caddyfile → JSON adapter for HTTP |
|
||||||
|
| `caddytest/` | Test utilities and integration tests |
|
||||||
|
| `cmd/caddy/` | CLI entry point with module imports |
|
||||||
|
|
||||||
|
### Critical Packages
|
||||||
|
|
||||||
|
`caddyhttp` and `caddytls` require **extra scrutiny** in code review—these are security-critical.
|
||||||
|
|
||||||
|
## Quality Gates
|
||||||
|
|
||||||
|
|
||||||
|
**All required before PR is merge-ready:**
|
||||||
|
|
||||||
|
| Gate | Command | Notes |
|
||||||
|
|------|---------|-------|
|
||||||
|
| Tests pass | `go test -race -short ./...` | Race detection enabled |
|
||||||
|
| Lint clean | `golangci-lint run --timeout 10m` | No warnings in changed files |
|
||||||
|
| Builds | `go build ./...` | Must compile |
|
||||||
|
| Benchmarks | `go test -bench=. -benchmem` | Required for optimizations |
|
||||||
|
|
||||||
|
CI runs tests on **Linux, macOS, and Windows**—ensure cross-platform compatibility.
|
||||||
|
|
||||||
|
### Build & Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
cd cmd/caddy && go build
|
||||||
|
|
||||||
|
# Tests with race detection (matches CI)
|
||||||
|
go test -race -short ./...
|
||||||
|
|
||||||
|
# Integration tests
|
||||||
|
go test ./caddytest/integration/...
|
||||||
|
|
||||||
|
# Lint (matches CI)
|
||||||
|
golangci-lint run --timeout 10m
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Conventions
|
||||||
|
|
||||||
|
**Table-driven tests** (preferred pattern):
|
||||||
|
```go
|
||||||
|
func TestFeature(t *testing.T) {
|
||||||
|
for i, tc := range []struct {
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{input: "valid", expected: "result", wantErr: false},
|
||||||
|
{input: "invalid", expected: "", wantErr: true},
|
||||||
|
} {
|
||||||
|
actual, err := Function(tc.input)
|
||||||
|
if tc.wantErr && err == nil {
|
||||||
|
t.Errorf("Test %d: expected error but got none", i)
|
||||||
|
}
|
||||||
|
if !tc.wantErr && err != nil {
|
||||||
|
t.Errorf("Test %d: unexpected error: %v", i, err)
|
||||||
|
}
|
||||||
|
if actual != tc.expected {
|
||||||
|
t.Errorf("Test %d: expected %q, got %q", i, tc.expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Integration tests** use `caddytest.Tester`:
|
||||||
|
```go
|
||||||
|
func TestHTTPFeature(t *testing.T) {
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(`
|
||||||
|
{
|
||||||
|
admin localhost:2999
|
||||||
|
http_port 9080
|
||||||
|
}
|
||||||
|
localhost:9080 {
|
||||||
|
respond "hello"
|
||||||
|
}`, "caddyfile")
|
||||||
|
|
||||||
|
tester.AssertGetResponse("http://localhost:9080/", 200, "hello")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use non-standard ports (9080, 9443, 2999) to avoid conflicts with running servers.
|
||||||
|
|
||||||
|
## AI Contribution Policy
|
||||||
|
|
||||||
|
Per [CONTRIBUTING.md](.github/CONTRIBUTING.md), AI-assisted code **MUST** be:
|
||||||
|
|
||||||
|
1. **Disclosed** — Tell reviewers when code was AI-generated or AI-assisted, mentioning which agent/model is used
|
||||||
|
2. **Fully comprehended** — You must be able to explain every line
|
||||||
|
3. **Tested** — Automated tests when feasible, thorough manual tests otherwise
|
||||||
|
4. **Licensed** — Verify AI output doesn't include plagiarized or incompatibly-licensed code
|
||||||
|
5. **Contributor License Agreement (CLA)** — The CLA must be signed by the human user
|
||||||
|
|
||||||
|
**Do NOT submit code you cannot fully explain.** Contributors are responsible for their submissions.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **Avoid new dependencies** — Justify any additions; tiny deps can be inlined
|
||||||
|
- **No exported dependency types** — Caddy must not export types defined by external packages
|
||||||
|
- Use Go modules; check with `go mod tidy`
|
||||||
|
|
||||||
|
## Further Reading
|
||||||
|
|
||||||
|
- [CONTRIBUTING.md](.github/CONTRIBUTING.md) — Full PR process and expectations
|
||||||
|
- [Extending Caddy](https://caddyserver.com/docs/extending-caddy) — Module development guide
|
||||||
|
- [JSON Config](https://caddyserver.com/docs/json/) — Native configuration reference
|
||||||
|
- [Caddyfile](https://caddyserver.com/docs/caddyfile/concepts) — Caddyfile syntax guide
|
||||||
@@ -45,6 +45,8 @@ import (
|
|||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
// testCertMagicStorageOverride is a package-level test hook. Tests may set
|
// testCertMagicStorageOverride is a package-level test hook. Tests may set
|
||||||
@@ -118,10 +120,6 @@ type AdminConfig struct {
|
|||||||
//
|
//
|
||||||
// EXPERIMENTAL: This feature is subject to change.
|
// EXPERIMENTAL: This feature is subject to change.
|
||||||
Remote *RemoteAdmin `json:"remote,omitempty"`
|
Remote *RemoteAdmin `json:"remote,omitempty"`
|
||||||
|
|
||||||
// Holds onto the routers so that we can later provision them
|
|
||||||
// if they require provisioning.
|
|
||||||
routers []AdminRouter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigSettings configures the management of configuration.
|
// ConfigSettings configures the management of configuration.
|
||||||
@@ -210,8 +208,8 @@ type AdminAccess struct {
|
|||||||
// AdminPermissions specifies what kinds of requests are allowed
|
// AdminPermissions specifies what kinds of requests are allowed
|
||||||
// to be made to the admin endpoint.
|
// to be made to the admin endpoint.
|
||||||
type AdminPermissions struct {
|
type AdminPermissions struct {
|
||||||
// The API paths allowed. Paths are simple prefix matches.
|
// The API paths allowed. A request path must either equal an
|
||||||
// Any subpath of the specified paths will be allowed.
|
// allowed path or be a subpath with a path-segment boundary.
|
||||||
Paths []string `json:"paths,omitempty"`
|
Paths []string `json:"paths,omitempty"`
|
||||||
|
|
||||||
// The HTTP methods allowed for the given paths.
|
// The HTTP methods allowed for the given paths.
|
||||||
@@ -220,7 +218,7 @@ type AdminPermissions struct {
|
|||||||
|
|
||||||
// newAdminHandler reads admin's config and returns an http.Handler suitable
|
// newAdminHandler reads admin's config and returns an http.Handler suitable
|
||||||
// for use in an admin endpoint server, which will be listening on listenAddr.
|
// for use in an admin endpoint server, which will be listening on listenAddr.
|
||||||
func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Context) adminHandler {
|
func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, ctx Context) (adminHandler, error) {
|
||||||
muxWrap := adminHandler{mux: http.NewServeMux()}
|
muxWrap := adminHandler{mux: http.NewServeMux()}
|
||||||
|
|
||||||
// secure the local or remote endpoint respectively
|
// secure the local or remote endpoint respectively
|
||||||
@@ -277,34 +275,21 @@ func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Co
|
|||||||
// register third-party module endpoints
|
// register third-party module endpoints
|
||||||
for _, m := range GetModules("admin.api") {
|
for _, m := range GetModules("admin.api") {
|
||||||
router := m.New().(AdminRouter)
|
router := m.New().(AdminRouter)
|
||||||
|
|
||||||
|
// provision the router before registering its routes, so
|
||||||
|
// handlers have access to all provisioned state
|
||||||
|
if provisioner, ok := router.(Provisioner); ok {
|
||||||
|
if err := provisioner.Provision(ctx); err != nil {
|
||||||
|
return adminHandler{}, fmt.Errorf("provisioning admin router module %s: %v", m.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, route := range router.Routes() {
|
for _, route := range router.Routes() {
|
||||||
addRoute(route.Pattern, handlerLabel, route.Handler)
|
addRoute(route.Pattern, handlerLabel, route.Handler)
|
||||||
}
|
}
|
||||||
admin.routers = append(admin.routers, router)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return muxWrap
|
return muxWrap, nil
|
||||||
}
|
|
||||||
|
|
||||||
// provisionAdminRouters provisions all the router modules
|
|
||||||
// in the admin.api namespace that need provisioning.
|
|
||||||
func (admin *AdminConfig) provisionAdminRouters(ctx Context) error {
|
|
||||||
for _, router := range admin.routers {
|
|
||||||
provisioner, ok := router.(Provisioner)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err := provisioner.Provision(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We no longer need the routers once provisioned, allow for GC
|
|
||||||
admin.routers = nil
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// allowedOrigins returns a list of origins that are allowed.
|
// allowedOrigins returns a list of origins that are allowed.
|
||||||
@@ -428,11 +413,7 @@ func replaceLocalAdminServer(cfg *Config, ctx Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
handler := cfg.Admin.newAdminHandler(addr, false, ctx)
|
handler, err := cfg.Admin.newAdminHandler(addr, false, ctx)
|
||||||
|
|
||||||
// run the provisioners for loaded modules to make sure local
|
|
||||||
// state is properly re-initialized in the new admin server
|
|
||||||
err = cfg.Admin.provisionAdminRouters(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -556,11 +537,7 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
|
|||||||
|
|
||||||
// make the HTTP handler but disable Host/Origin enforcement
|
// make the HTTP handler but disable Host/Origin enforcement
|
||||||
// because we are using TLS authentication instead
|
// because we are using TLS authentication instead
|
||||||
handler := cfg.Admin.newAdminHandler(addr, true, ctx)
|
handler, err := cfg.Admin.newAdminHandler(addr, true, ctx)
|
||||||
|
|
||||||
// run the provisioners for loaded modules to make sure local
|
|
||||||
// state is properly re-initialized in the new admin server
|
|
||||||
err = cfg.Admin.provisionAdminRouters(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -716,7 +693,7 @@ func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error {
|
|||||||
// verify path
|
// verify path
|
||||||
pathFound := accessPerm.Paths == nil
|
pathFound := accessPerm.Paths == nil
|
||||||
for _, allowedPath := range accessPerm.Paths {
|
for _, allowedPath := range accessPerm.Paths {
|
||||||
if strings.HasPrefix(r.URL.Path, allowedPath) {
|
if adminPathAllowed(r.URL.Path, allowedPath) {
|
||||||
pathFound = true
|
pathFound = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -745,14 +722,31 @@ func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func adminPathAllowed(reqPath, allowedPath string) bool {
|
||||||
|
if allowedPath == "" || allowedPath == "/" {
|
||||||
|
return strings.HasPrefix(reqPath, allowedPath)
|
||||||
|
}
|
||||||
|
if reqPath == allowedPath {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(allowedPath, "/") {
|
||||||
|
return strings.HasPrefix(reqPath, allowedPath)
|
||||||
|
}
|
||||||
|
return strings.HasPrefix(reqPath, allowedPath+"/")
|
||||||
|
}
|
||||||
|
|
||||||
func stopAdminServer(srv *http.Server) error {
|
func stopAdminServer(srv *http.Server) error {
|
||||||
if srv == nil {
|
if srv == nil {
|
||||||
return fmt.Errorf("no admin server")
|
return fmt.Errorf("no admin server")
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
timeout := 10 * time.Second
|
||||||
|
ctx, cancel := context.WithTimeoutCause(context.Background(), timeout, fmt.Errorf("stopping admin server: %ds timeout", int(timeout.Seconds())))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
err := srv.Shutdown(ctx)
|
err := srv.Shutdown(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if cause := context.Cause(ctx); cause != nil && errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
err = cause
|
||||||
|
}
|
||||||
return fmt.Errorf("shutting down admin server: %v", err)
|
return fmt.Errorf("shutting down admin server: %v", err)
|
||||||
}
|
}
|
||||||
Log().Named("admin").Info("stopped previous server", zap.String("address", srv.Addr))
|
Log().Named("admin").Info("stopped previous server", zap.String("address", srv.Addr))
|
||||||
@@ -796,7 +790,7 @@ func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
zap.String("uri", r.RequestURI),
|
zap.String("uri", r.RequestURI),
|
||||||
zap.String("remote_ip", ip),
|
zap.String("remote_ip", ip),
|
||||||
zap.String("remote_port", port),
|
zap.String("remote_port", port),
|
||||||
zap.Reflect("headers", r.Header),
|
zap.Object("headers", internal.LoggableHTTPHeader{Header: r.Header}),
|
||||||
)
|
)
|
||||||
if r.TLS != nil {
|
if r.TLS != nil {
|
||||||
log = log.With(
|
log = log.With(
|
||||||
@@ -855,6 +849,7 @@ func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
Err: errors.New("invalid origin 'null'"),
|
Err: errors.New("invalid origin 'null'"),
|
||||||
Message: "Buggy browser is sending null Origin header.",
|
Message: "Buggy browser is sending null Origin header.",
|
||||||
})
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.enforceHost {
|
if h.enforceHost {
|
||||||
@@ -1056,6 +1051,9 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
|
|||||||
buf.Reset()
|
buf.Reset()
|
||||||
defer bufPool.Put(buf)
|
defer bufPool.Put(buf)
|
||||||
|
|
||||||
|
const maxConfigSize = 100 * 1024 * 1024 // 100 MB
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxConfigSize)
|
||||||
|
|
||||||
_, err := io.Copy(buf, r.Body)
|
_, err := io.Copy(buf, r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return APIError{
|
return APIError{
|
||||||
@@ -1138,6 +1136,20 @@ func handleStop(w http.ResponseWriter, r *http.Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseCanonicalArrayIndex(idx string) (int, error) {
|
||||||
|
if idx == "" {
|
||||||
|
return 0, fmt.Errorf("empty index")
|
||||||
|
}
|
||||||
|
i, err := strconv.Atoi(idx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if strconv.Itoa(i) != idx {
|
||||||
|
return 0, fmt.Errorf("non-canonical array index")
|
||||||
|
}
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
// unsyncedConfigAccess traverses into the current config and performs
|
// unsyncedConfigAccess traverses into the current config and performs
|
||||||
// the operation at path according to method, using body and out as
|
// the operation at path according to method, using body and out as
|
||||||
// needed. This is a low-level, unsynchronized function; most callers
|
// needed. This is a low-level, unsynchronized function; most callers
|
||||||
@@ -1199,11 +1211,12 @@ traverseLoop:
|
|||||||
var idx int
|
var idx int
|
||||||
if method != http.MethodPost {
|
if method != http.MethodPost {
|
||||||
idxStr := parts[len(parts)-1]
|
idxStr := parts[len(parts)-1]
|
||||||
idx, err = strconv.Atoi(idxStr)
|
idx, err = parseCanonicalArrayIndex(idxStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[%s] invalid array index '%s': %v",
|
return fmt.Errorf("[%s] invalid array index '%s': %v",
|
||||||
path, idxStr, err)
|
path, idxStr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if idx < 0 || (method != http.MethodPut && idx >= len(arr)) || idx > len(arr) {
|
if idx < 0 || (method != http.MethodPut && idx >= len(arr)) || idx > len(arr) {
|
||||||
return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr)
|
return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr)
|
||||||
}
|
}
|
||||||
@@ -1303,7 +1316,7 @@ traverseLoop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
case []any:
|
case []any:
|
||||||
partInt, err := strconv.Atoi(part)
|
partInt, err := parseCanonicalArrayIndex(part)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("[/%s] invalid array index '%s': %v",
|
return fmt.Errorf("[/%s] invalid array index '%s': %v",
|
||||||
strings.Join(parts[:i+1], "/"), part, err)
|
strings.Join(parts[:i+1], "/"), part, err)
|
||||||
|
|||||||
+204
-15
@@ -15,9 +15,13 @@
|
|||||||
package caddy
|
package caddy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -31,6 +35,8 @@ import (
|
|||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
dto "github.com/prometheus/client_model/go"
|
dto "github.com/prometheus/client_model/go"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zaptest/observer"
|
||||||
)
|
)
|
||||||
|
|
||||||
var testCfg = []byte(`{
|
var testCfg = []byte(`{
|
||||||
@@ -51,6 +57,13 @@ var testCfg = []byte(`{
|
|||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
type testAdminPublicKey string
|
||||||
|
|
||||||
|
func (k testAdminPublicKey) Equal(x crypto.PublicKey) bool {
|
||||||
|
other, ok := x.(testAdminPublicKey)
|
||||||
|
return ok && k == other
|
||||||
|
}
|
||||||
|
|
||||||
func TestUnsyncedConfigAccess(t *testing.T) {
|
func TestUnsyncedConfigAccess(t *testing.T) {
|
||||||
// each test is performed in sequence, so
|
// each test is performed in sequence, so
|
||||||
// each change builds on the previous ones;
|
// each change builds on the previous ones;
|
||||||
@@ -242,6 +255,51 @@ func TestAdminHandlerErrorHandling(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAdminHandlerServeHTTPRedactsSensitiveHeadersInLogs(t *testing.T) {
|
||||||
|
core, logs := observer.New(zap.InfoLevel)
|
||||||
|
|
||||||
|
defaultLoggerMu.Lock()
|
||||||
|
origLogger := defaultLogger.logger
|
||||||
|
defaultLogger.logger = zap.New(core)
|
||||||
|
defaultLoggerMu.Unlock()
|
||||||
|
t.Cleanup(func() {
|
||||||
|
defaultLoggerMu.Lock()
|
||||||
|
defaultLogger.logger = origLogger
|
||||||
|
defaultLoggerMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
handler := adminHandler{
|
||||||
|
mux: http.NewServeMux(),
|
||||||
|
}
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer secret")
|
||||||
|
req.Header.Set("Cookie", "session=secret")
|
||||||
|
req.Header.Set("X-Test", "ok")
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if logs.Len() == 0 {
|
||||||
|
t.Fatal("expected request log entry")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := logs.All()[0].ContextMap()
|
||||||
|
headers, ok := ctx["headers"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected headers field in log context, got %T", ctx["headers"])
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := headers["Authorization"]; !reflect.DeepEqual(got, []any{"REDACTED"}) {
|
||||||
|
t.Fatalf("expected redacted Authorization header, got %#v", got)
|
||||||
|
}
|
||||||
|
if got := headers["Cookie"]; !reflect.DeepEqual(got, []any{"REDACTED"}) {
|
||||||
|
t.Fatalf("expected redacted Cookie header, got %#v", got)
|
||||||
|
}
|
||||||
|
if got := headers["X-Test"]; !reflect.DeepEqual(got, []any{"ok"}) {
|
||||||
|
t.Fatalf("expected X-Test header to remain visible, got %#v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func initAdminMetrics() {
|
func initAdminMetrics() {
|
||||||
if adminMetrics.requestErrors != nil {
|
if adminMetrics.requestErrors != nil {
|
||||||
prometheus.Unregister(adminMetrics.requestErrors)
|
prometheus.Unregister(adminMetrics.requestErrors)
|
||||||
@@ -282,7 +340,10 @@ func TestAdminHandlerBuiltinRouteErrors(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to parse address: %v", err)
|
t.Fatalf("Failed to parse address: %v", err)
|
||||||
}
|
}
|
||||||
handler := cfg.Admin.newAdminHandler(addr, false, Context{})
|
handler, err := cfg.Admin.newAdminHandler(addr, false, Context{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create admin handler: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -403,7 +464,10 @@ func TestNewAdminHandlerRouterRegistration(t *testing.T) {
|
|||||||
admin := &AdminConfig{
|
admin := &AdminConfig{
|
||||||
EnforceOrigin: false,
|
EnforceOrigin: false,
|
||||||
}
|
}
|
||||||
handler := admin.newAdminHandler(addr, false, Context{})
|
handler, err := admin.newAdminHandler(addr, false, Context{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create admin handler: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/mock", nil)
|
req := httptest.NewRequest("GET", "/mock", nil)
|
||||||
req.Host = "localhost:2019"
|
req.Host = "localhost:2019"
|
||||||
@@ -415,10 +479,6 @@ func TestNewAdminHandlerRouterRegistration(t *testing.T) {
|
|||||||
t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code)
|
t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code)
|
||||||
t.Logf("Response body: %s", rr.Body.String())
|
t.Logf("Response body: %s", rr.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(admin.routers) != 1 {
|
|
||||||
t.Errorf("Expected 1 router to be stored, got %d", len(admin.routers))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockProvisionableRouter struct {
|
type mockProvisionableRouter struct {
|
||||||
@@ -456,19 +516,16 @@ func TestAdminRouterProvisioning(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
provisionErr error
|
provisionErr error
|
||||||
wantErr bool
|
wantErr bool
|
||||||
routersAfter int // expected number of routers after provisioning
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "successful provisioning",
|
name: "successful provisioning",
|
||||||
provisionErr: nil,
|
provisionErr: nil,
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
routersAfter: 0,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "provisioning error",
|
name: "provisioning error",
|
||||||
provisionErr: fmt.Errorf("provision failed"),
|
provisionErr: fmt.Errorf("provision failed"),
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
routersAfter: 1,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,8 +561,7 @@ func TestAdminRouterProvisioning(t *testing.T) {
|
|||||||
t.Fatalf("Failed to parse address: %v", err)
|
t.Fatalf("Failed to parse address: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = admin.newAdminHandler(addr, false, Context{})
|
_, err = admin.newAdminHandler(addr, false, Context{})
|
||||||
err = admin.provisionAdminRouters(Context{})
|
|
||||||
|
|
||||||
if test.wantErr {
|
if test.wantErr {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -516,10 +572,6 @@ func TestAdminRouterProvisioning(t *testing.T) {
|
|||||||
t.Errorf("Expected no error but got: %v", err)
|
t.Errorf("Expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(admin.routers) != test.routersAfter {
|
|
||||||
t.Errorf("Expected %d routers after provisioning, got %d", test.routersAfter, len(admin.routers))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -604,6 +656,99 @@ func TestAllowedOriginsUnixSocket(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRemoteAdminAccessControlPathSegmentMatching(t *testing.T) {
|
||||||
|
const authorizedKey testAdminPublicKey = "authorized"
|
||||||
|
peerCert := &x509.Certificate{PublicKey: authorizedKey}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
allowedPath string
|
||||||
|
requestPath string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "exact path",
|
||||||
|
allowedPath: "/pki/ca/prod",
|
||||||
|
requestPath: "/pki/ca/prod",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "subpath",
|
||||||
|
allowedPath: "/pki/ca/prod",
|
||||||
|
requestPath: "/pki/ca/prod/certificates",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "trailing slash subpath",
|
||||||
|
allowedPath: "/pki/ca/prod/",
|
||||||
|
requestPath: "/pki/ca/prod/certificates",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sibling with shared prefix",
|
||||||
|
allowedPath: "/pki/ca/prod",
|
||||||
|
requestPath: "/pki/ca/prod-backup",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same segment plus digit",
|
||||||
|
allowedPath: "/pki/ca/prod",
|
||||||
|
requestPath: "/pki/ca/prod1",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "root path",
|
||||||
|
allowedPath: "/",
|
||||||
|
requestPath: "/pki/ca/prod",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
remote := RemoteAdmin{
|
||||||
|
AccessControl: []*AdminAccess{
|
||||||
|
{
|
||||||
|
Permissions: []AdminPermissions{
|
||||||
|
{
|
||||||
|
Methods: []string{http.MethodGet},
|
||||||
|
Paths: []string{test.allowedPath},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
publicKeys: []crypto.PublicKey{authorizedKey},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "https://localhost:2021"+test.requestPath, nil)
|
||||||
|
req.TLS = &tls.ConnectionState{
|
||||||
|
VerifiedChains: [][]*x509.Certificate{{peerCert}},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := remote.enforceAccessControls(req)
|
||||||
|
if test.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("test %d (%s): allowed path %q, request path %q: expected forbidden error, got nil", i, test.name, test.allowedPath, test.requestPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var apiErr APIError
|
||||||
|
if !errors.As(err, &apiErr) {
|
||||||
|
t.Errorf("test %d (%s): allowed path %q, request path %q: expected APIError with HTTP status %d, got %T: %v", i, test.name, test.allowedPath, test.requestPath, http.StatusForbidden, err, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if apiErr.HTTPStatus != http.StatusForbidden {
|
||||||
|
t.Errorf("test %d (%s): allowed path %q, request path %q: expected HTTP status %d, got %d", i, test.name, test.allowedPath, test.requestPath, http.StatusForbidden, apiErr.HTTPStatus)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("test %d (%s): allowed path %q, request path %q: expected no error, got %v", i, test.name, test.allowedPath, test.requestPath, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestReplaceRemoteAdminServer(t *testing.T) {
|
func TestReplaceRemoteAdminServer(t *testing.T) {
|
||||||
const testCert = `MIIDCTCCAfGgAwIBAgIUXsqJ1mY8pKlHQtI3HJ23x2eZPqwwDQYJKoZIhvcNAQEL
|
const testCert = `MIIDCTCCAfGgAwIBAgIUXsqJ1mY8pKlHQtI3HJ23x2eZPqwwDQYJKoZIhvcNAQEL
|
||||||
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwMTAwMDAwMFoXDTI0MDEw
|
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwMTAwMDAwMFoXDTI0MDEw
|
||||||
@@ -956,3 +1101,47 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnsyncedConfigAccessCanonicalArrayIndices(t *testing.T) {
|
||||||
|
rawCfg = map[string]any{
|
||||||
|
rawConfigKey: map[string]any{
|
||||||
|
"list": []any{"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
wantOutput string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{name: "allow zero", path: "/" + rawConfigKey + "/list/0", wantOutput: "\"zero\"\n"},
|
||||||
|
{name: "allow one", path: "/" + rawConfigKey + "/list/1", wantOutput: "\"one\"\n"},
|
||||||
|
{name: "allow ten", path: "/" + rawConfigKey + "/list/10", wantOutput: "\"ten\"\n"},
|
||||||
|
{name: "reject leading zero", path: "/" + rawConfigKey + "/list/01", wantErr: true},
|
||||||
|
{name: "reject multiple leading zeros", path: "/" + rawConfigKey + "/list/002", wantErr: true},
|
||||||
|
{name: "reject plus sign", path: "/" + rawConfigKey + "/list/+1", wantErr: true},
|
||||||
|
{name: "reject negative zero", path: "/" + rawConfigKey + "/list/-0", wantErr: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var gotOutput bytes.Buffer
|
||||||
|
err := unsyncedConfigAccess(http.MethodGet, tc.path, nil, &gotOutput)
|
||||||
|
|
||||||
|
if tc.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("test %d (%s): input path %q: expected error, got nil with output %q", i, tc.name, tc.path, gotOutput.String())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("test %d (%s): input path %q: expected no error with output %q, got error %v with output %q", i, tc.name, tc.path, tc.wantOutput, err, gotOutput.String())
|
||||||
|
}
|
||||||
|
if gotOutput.String() != tc.wantOutput {
|
||||||
|
t.Errorf("test %d (%s): input path %q: expected output %q, got %q", i, tc.name, tc.path, tc.wantOutput, gotOutput.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ type Config struct {
|
|||||||
storage certmagic.Storage
|
storage certmagic.Storage
|
||||||
eventEmitter eventEmitter
|
eventEmitter eventEmitter
|
||||||
|
|
||||||
cancelFunc context.CancelFunc
|
cancelFunc context.CancelCauseFunc
|
||||||
|
|
||||||
// fileSystems is a dict of fileSystems that will later be loaded from and added to.
|
// fileSystems is a dict of fileSystems that will later be loaded from and added to.
|
||||||
fileSystems FileSystems
|
fileSystems FileSystems
|
||||||
@@ -127,10 +127,9 @@ func Load(cfgJSON []byte, forceReload bool) error {
|
|||||||
zap.Error(notifyErr),
|
zap.Error(notifyErr),
|
||||||
zap.String("reload_err", err.Error()))
|
zap.String("reload_err", err.Error()))
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if err := notify.Ready(); err != nil {
|
if notifyErr := notify.Ready(); notifyErr != nil {
|
||||||
Log().Error("unable to notify to service manager of ready state", zap.Error(err))
|
Log().Error("unable to notify to service manager of ready state", zap.Error(notifyErr))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -227,8 +226,18 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
|
|||||||
idx := make(map[string]string)
|
idx := make(map[string]string)
|
||||||
err = indexConfigObjects(rawCfg[rawConfigKey], "/"+rawConfigKey, idx)
|
err = indexConfigObjects(rawCfg[rawConfigKey], "/"+rawConfigKey, idx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if len(rawCfgJSON) > 0 {
|
||||||
|
var oldCfg any
|
||||||
|
err2 := json.Unmarshal(rawCfgJSON, &oldCfg)
|
||||||
|
if err2 != nil {
|
||||||
|
err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
|
||||||
|
}
|
||||||
|
rawCfg[rawConfigKey] = oldCfg
|
||||||
|
} else {
|
||||||
|
rawCfg[rawConfigKey] = nil
|
||||||
|
}
|
||||||
return APIError{
|
return APIError{
|
||||||
HTTPStatus: http.StatusInternalServerError,
|
HTTPStatus: http.StatusBadRequest,
|
||||||
Err: fmt.Errorf("indexing config: %v", err),
|
Err: fmt.Errorf("indexing config: %v", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,6 +257,8 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
|
|||||||
err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
|
err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
|
||||||
}
|
}
|
||||||
rawCfg[rawConfigKey] = oldCfg
|
rawCfg[rawConfigKey] = oldCfg
|
||||||
|
} else {
|
||||||
|
rawCfg[rawConfigKey] = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("loading new config: %v", err)
|
return fmt.Errorf("loading new config: %v", err)
|
||||||
@@ -281,14 +292,19 @@ func indexConfigObjects(ptr any, configPath string, index map[string]string) err
|
|||||||
case map[string]any:
|
case map[string]any:
|
||||||
for k, v := range val {
|
for k, v := range val {
|
||||||
if k == idKey {
|
if k == idKey {
|
||||||
|
var idStr string
|
||||||
switch idVal := v.(type) {
|
switch idVal := v.(type) {
|
||||||
case string:
|
case string:
|
||||||
index[idVal] = configPath
|
idStr = idVal
|
||||||
case float64: // all JSON numbers decode as float64
|
case float64: // all JSON numbers decode as float64
|
||||||
index[fmt.Sprintf("%v", idVal)] = configPath
|
idStr = fmt.Sprintf("%v", idVal)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("%s: %s field must be a string or number", configPath, idKey)
|
return fmt.Errorf("%s: %s field must be a string or number", configPath, idKey)
|
||||||
}
|
}
|
||||||
|
if existingPath, ok := index[idStr]; ok {
|
||||||
|
return fmt.Errorf("duplicate ID '%s' found at %s and %s", idStr, existingPath, configPath)
|
||||||
|
}
|
||||||
|
index[idStr] = configPath
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// traverse this object property recursively
|
// traverse this object property recursively
|
||||||
@@ -416,7 +432,7 @@ func run(newCfg *Config, start bool) (Context, error) {
|
|||||||
// partially copied from provisionContext
|
// partially copied from provisionContext
|
||||||
if err != nil {
|
if err != nil {
|
||||||
globalMetrics.configSuccess.Set(0)
|
globalMetrics.configSuccess.Set(0)
|
||||||
ctx.cfg.cancelFunc()
|
ctx.cfg.cancelFunc(fmt.Errorf("configuration start error: %w", err))
|
||||||
|
|
||||||
if currentCtx.cfg != nil {
|
if currentCtx.cfg != nil {
|
||||||
certmagic.Default.Storage = currentCtx.cfg.storage
|
certmagic.Default.Storage = currentCtx.cfg.storage
|
||||||
@@ -424,13 +440,6 @@ func run(newCfg *Config, start bool) (Context, error) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Provision any admin routers which may need to access
|
|
||||||
// some of the other apps at runtime
|
|
||||||
err = ctx.cfg.Admin.provisionAdminRouters(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return ctx, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start
|
// Start
|
||||||
err = func() error {
|
err = func() error {
|
||||||
started := make([]string, 0, len(ctx.cfg.apps))
|
started := make([]string, 0, len(ctx.cfg.apps))
|
||||||
@@ -492,7 +501,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
|
|||||||
// cleanup occurs when we return if there
|
// cleanup occurs when we return if there
|
||||||
// was an error; if no error, it will get
|
// was an error; if no error, it will get
|
||||||
// cleaned up on next config cycle
|
// cleaned up on next config cycle
|
||||||
ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg})
|
ctx, cancelCause := NewContextWithCause(Context{Context: context.Background(), cfg: newCfg})
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
globalMetrics.configSuccess.Set(0)
|
globalMetrics.configSuccess.Set(0)
|
||||||
@@ -501,7 +510,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
|
|||||||
// since the associated config won't be used;
|
// since the associated config won't be used;
|
||||||
// this will cause all modules that were newly
|
// this will cause all modules that were newly
|
||||||
// provisioned to clean themselves up
|
// provisioned to clean themselves up
|
||||||
cancel()
|
cancelCause(fmt.Errorf("configuration error: %w", err))
|
||||||
|
|
||||||
// also undo any other state changes we made
|
// also undo any other state changes we made
|
||||||
if currentCtx.cfg != nil {
|
if currentCtx.cfg != nil {
|
||||||
@@ -509,7 +518,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
newCfg.cancelFunc = cancel // clean up later
|
newCfg.cancelFunc = cancelCause // clean up later
|
||||||
|
|
||||||
// set up logging before anything bad happens
|
// set up logging before anything bad happens
|
||||||
if newCfg.Logging == nil {
|
if newCfg.Logging == nil {
|
||||||
@@ -729,7 +738,7 @@ func unsyncedStop(ctx Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// clean up all modules
|
// clean up all modules
|
||||||
ctx.cfg.cancelFunc()
|
ctx.cfg.cancelFunc(fmt.Errorf("stopping apps"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate loads, provisions, and validates
|
// Validate loads, provisions, and validates
|
||||||
@@ -737,7 +746,7 @@ func unsyncedStop(ctx Context) {
|
|||||||
func Validate(cfg *Config) error {
|
func Validate(cfg *Config) error {
|
||||||
_, err := run(cfg, false)
|
_, err := run(cfg, false)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
cfg.cancelFunc() // call Cleanup on all modules
|
cfg.cancelFunc(fmt.Errorf("validation complete")) // call Cleanup on all modules
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -750,7 +759,7 @@ func Validate(cfg *Config) error {
|
|||||||
// code is emitted.
|
// code is emitted.
|
||||||
func exitProcess(ctx context.Context, logger *zap.Logger) {
|
func exitProcess(ctx context.Context, logger *zap.Logger) {
|
||||||
// let the rest of the program know we're quitting; only do it once
|
// let the rest of the program know we're quitting; only do it once
|
||||||
if !atomic.CompareAndSwapInt32(exiting, 0, 1) {
|
if !exiting.CompareAndSwap(false, true) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -829,11 +838,11 @@ func exitProcess(ctx context.Context, logger *zap.Logger) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
var exiting = new(int32) // accessed atomically
|
var exiting atomic.Bool
|
||||||
|
|
||||||
// Exiting returns true if the process is exiting.
|
// Exiting returns true if the process is exiting.
|
||||||
// EXPERIMENTAL API: subject to change or removal.
|
// EXPERIMENTAL API: subject to change or removal.
|
||||||
func Exiting() bool { return atomic.LoadInt32(exiting) == 1 }
|
func Exiting() bool { return exiting.Load() }
|
||||||
|
|
||||||
// OnExit registers a callback to invoke during process exit.
|
// OnExit registers a callback to invoke during process exit.
|
||||||
// This registration is PROCESS-GLOBAL, meaning that each
|
// This registration is PROCESS-GLOBAL, meaning that each
|
||||||
@@ -945,6 +954,34 @@ func InstanceID() (uuid.UUID, error) {
|
|||||||
// for example.
|
// for example.
|
||||||
var CustomVersion string
|
var CustomVersion string
|
||||||
|
|
||||||
|
// CustomBinaryName is an optional string that overrides the root
|
||||||
|
// command name from the default of "caddy". This is useful for
|
||||||
|
// downstream projects that embed Caddy but use a different binary
|
||||||
|
// name. Shell completions and help text will use this name instead
|
||||||
|
// of "caddy".
|
||||||
|
//
|
||||||
|
// Set this variable during `go build` with `-ldflags`:
|
||||||
|
//
|
||||||
|
// -ldflags '-X github.com/caddyserver/caddy/v2.CustomBinaryName=my_custom_caddy'
|
||||||
|
//
|
||||||
|
// for example.
|
||||||
|
var CustomBinaryName string
|
||||||
|
|
||||||
|
// CustomLongDescription is an optional string that overrides the
|
||||||
|
// long description of the root Cobra command. This is useful for
|
||||||
|
// downstream projects that embed Caddy but want different help
|
||||||
|
// output.
|
||||||
|
//
|
||||||
|
// Set this variable in an init() function of a package that is
|
||||||
|
// imported by your main:
|
||||||
|
//
|
||||||
|
// func init() {
|
||||||
|
// caddy.CustomLongDescription = "My custom server based on Caddy..."
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// for example.
|
||||||
|
var CustomLongDescription string
|
||||||
|
|
||||||
// Version returns the Caddy version in a simple/short form, and
|
// Version returns the Caddy version in a simple/short form, and
|
||||||
// a full version string. The short form will not have spaces and
|
// a full version string. The short form will not have spaces and
|
||||||
// is intended for User-Agent strings and similar, but may be
|
// is intended for User-Agent strings and similar, but may be
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ func (d *Dispenser) File() string {
|
|||||||
// targets are left unchanged. If all the targets are filled,
|
// targets are left unchanged. If all the targets are filled,
|
||||||
// then true is returned.
|
// then true is returned.
|
||||||
func (d *Dispenser) Args(targets ...*string) bool {
|
func (d *Dispenser) Args(targets ...*string) bool {
|
||||||
for i := 0; i < len(targets); i++ {
|
for i := range targets {
|
||||||
if !d.NextArg() {
|
if !d.NextArg() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,8 +63,33 @@ func Format(input []byte) []byte {
|
|||||||
heredocClosingMarker []rune
|
heredocClosingMarker []rune
|
||||||
|
|
||||||
nesting int // indentation level
|
nesting int // indentation level
|
||||||
|
|
||||||
|
currentToken strings.Builder
|
||||||
|
currentLineFirstToken string
|
||||||
|
previousLineWasTopLevelImport bool
|
||||||
|
openBraceOwnLine bool
|
||||||
)
|
)
|
||||||
|
|
||||||
|
finishToken := func() {
|
||||||
|
if currentToken.Len() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if currentLineFirstToken == "" {
|
||||||
|
currentLineFirstToken = currentToken.String()
|
||||||
|
}
|
||||||
|
currentToken.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
finishLine := func() {
|
||||||
|
finishToken()
|
||||||
|
if currentLineFirstToken != "" {
|
||||||
|
previousLineWasTopLevelImport = nesting == 0 && currentLineFirstToken == "import"
|
||||||
|
} else if !openBrace || !openBraceOwnLine || openBraceWritten {
|
||||||
|
previousLineWasTopLevelImport = false
|
||||||
|
}
|
||||||
|
currentLineFirstToken = ""
|
||||||
|
}
|
||||||
|
|
||||||
write := func(ch rune) {
|
write := func(ch rune) {
|
||||||
out.WriteRune(ch)
|
out.WriteRune(ch)
|
||||||
last = ch
|
last = ch
|
||||||
@@ -220,9 +245,11 @@ func Format(input []byte) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if unicode.IsSpace(ch) {
|
if unicode.IsSpace(ch) {
|
||||||
|
finishToken()
|
||||||
space = true
|
space = true
|
||||||
heredocEscaped = false
|
heredocEscaped = false
|
||||||
if ch == '\n' {
|
if ch == '\n' {
|
||||||
|
finishLine()
|
||||||
newLines++
|
newLines++
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -249,13 +276,19 @@ func Format(input []byte) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openBrace = false
|
openBrace = false
|
||||||
if beginningOfLine {
|
if openBraceOwnLine && previousLineWasTopLevelImport {
|
||||||
|
if last != '\n' {
|
||||||
|
nextLine()
|
||||||
|
}
|
||||||
|
indent()
|
||||||
|
} else if beginningOfLine {
|
||||||
indent()
|
indent()
|
||||||
} else if !openBraceSpace || !unicode.IsSpace(last) {
|
} else if !openBraceSpace || !unicode.IsSpace(last) {
|
||||||
write(' ')
|
write(' ')
|
||||||
}
|
}
|
||||||
write('{')
|
write('{')
|
||||||
openBraceWritten = true
|
openBraceWritten = true
|
||||||
|
openBraceOwnLine = false
|
||||||
nextLine()
|
nextLine()
|
||||||
newLines = 0
|
newLines = 0
|
||||||
// prevent infinite nesting from ridiculous inputs (issue #4169)
|
// prevent infinite nesting from ridiculous inputs (issue #4169)
|
||||||
@@ -266,8 +299,10 @@ func Format(input []byte) []byte {
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case ch == '{':
|
case ch == '{':
|
||||||
|
finishToken()
|
||||||
openBrace = true
|
openBrace = true
|
||||||
openBraceSpace = spacePrior && !beginningOfLine
|
openBraceSpace = spacePrior && !beginningOfLine
|
||||||
|
openBraceOwnLine = newLines > 0
|
||||||
if openBraceSpace && newLines == 0 {
|
if openBraceSpace && newLines == 0 {
|
||||||
write(' ')
|
write(' ')
|
||||||
}
|
}
|
||||||
@@ -275,11 +310,13 @@ func Format(input []byte) []byte {
|
|||||||
if quotes == "`" {
|
if quotes == "`" {
|
||||||
write('{')
|
write('{')
|
||||||
openBraceWritten = true
|
openBraceWritten = true
|
||||||
|
openBraceOwnLine = false
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
|
||||||
case ch == '}' && (spacePrior || !openBrace):
|
case ch == '}' && (spacePrior || !openBrace):
|
||||||
|
finishToken()
|
||||||
if quotes == "`" {
|
if quotes == "`" {
|
||||||
write('}')
|
write('}')
|
||||||
continue
|
continue
|
||||||
@@ -324,6 +361,7 @@ func Format(input []byte) []byte {
|
|||||||
space = true
|
space = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentToken.WriteRune(ch)
|
||||||
write(ch)
|
write(ch)
|
||||||
|
|
||||||
beginningOfLine = false
|
beginningOfLine = false
|
||||||
|
|||||||
@@ -475,6 +475,21 @@ Hope this helps.` + "`" + `
|
|||||||
}`,
|
}`,
|
||||||
expect: "https://localhost:8953 {\n\trespond `Here are some random numbers:\n\n{{randNumeric 16}}\n\nHope this helps.`\n}",
|
expect: "https://localhost:8953 {\n\trespond `Here are some random numbers:\n\n{{randNumeric 16}}\n\nHope this helps.`\n}",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: "imports before global options block keep standalone brace",
|
||||||
|
input: `import ./conf.d/matcher_my_subnet.caddy
|
||||||
|
import ./conf.d/matcher_not_my_subnet.caddy
|
||||||
|
{
|
||||||
|
order crowdsec first
|
||||||
|
order appsec after crowdsec
|
||||||
|
}`,
|
||||||
|
expect: `import ./conf.d/matcher_my_subnet.caddy
|
||||||
|
import ./conf.d/matcher_not_my_subnet.caddy
|
||||||
|
{
|
||||||
|
order crowdsec first
|
||||||
|
order appsec after crowdsec
|
||||||
|
}`,
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
// the formatter should output a trailing newline,
|
// the formatter should output a trailing newline,
|
||||||
// even if the tests aren't written to expect that
|
// even if the tests aren't written to expect that
|
||||||
|
|||||||
@@ -507,7 +507,7 @@ func (p *parser) doImport(nesting int) error {
|
|||||||
// format, won't check for nesting correctness or any other error, that's what parser does.
|
// format, won't check for nesting correctness or any other error, that's what parser does.
|
||||||
if !maybeSnippet && nesting == 0 {
|
if !maybeSnippet && nesting == 0 {
|
||||||
// first of the line
|
// first of the line
|
||||||
if i == 0 || isNextOnNewLine(tokensCopy[i-1], token) {
|
if i == 0 || isNextOnNewLine(tokensCopy[len(tokensCopy)-1], token) {
|
||||||
index = 0
|
index = 0
|
||||||
} else {
|
} else {
|
||||||
index++
|
index++
|
||||||
@@ -550,7 +550,11 @@ func (p *parser) doImport(nesting int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if foundBlockDirective {
|
if foundBlockDirective {
|
||||||
tokensCopy = append(tokensCopy, tokensToAdd...)
|
if maybeSnippet {
|
||||||
|
tokensCopy = append(tokensCopy, token)
|
||||||
|
} else {
|
||||||
|
tokensCopy = append(tokensCopy, tokensToAdd...)
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,7 +620,7 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, p.Errf("Failed to get absolute path of file: %s: %v", importFile, err)
|
return nil, p.Errf("Failed to get absolute path of file: %s: %v", importFile, err)
|
||||||
}
|
}
|
||||||
for i := 0; i < len(importedTokens); i++ {
|
for i := range importedTokens {
|
||||||
importedTokens[i].File = filename
|
importedTokens[i].File = filename
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,11 +686,28 @@ func (p *parser) directive() error {
|
|||||||
// a opening curly brace. It does NOT advance the token.
|
// a opening curly brace. It does NOT advance the token.
|
||||||
func (p *parser) openCurlyBrace() error {
|
func (p *parser) openCurlyBrace() error {
|
||||||
if p.Val() != "{" {
|
if p.Val() != "{" {
|
||||||
|
if p.valLooksLikeGlobalOptionsAfterImportedSnippets() {
|
||||||
|
return p.Err("global options block must appear before import directives; move the global options block to the top of the Caddyfile")
|
||||||
|
}
|
||||||
return p.SyntaxErr("{")
|
return p.SyntaxErr("{")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *parser) valLooksLikeGlobalOptionsAfterImportedSnippets() bool {
|
||||||
|
if p.Val() != "import" || len(p.block.Keys) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range p.block.Keys {
|
||||||
|
if !strings.HasPrefix(key.Text, "(") || !strings.HasSuffix(key.Text, ")") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// closeCurlyBrace expects the current token to be
|
// closeCurlyBrace expects the current token to be
|
||||||
// a closing curly brace. This acts like an assertion
|
// a closing curly brace. This acts like an assertion
|
||||||
// because it returns an error if the token is not
|
// because it returns an error if the token is not
|
||||||
|
|||||||
@@ -930,6 +930,107 @@ func TestAcceptSiteImportWithBraces(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGlobalOptionsAfterImportedSnippetsGivesHelpfulError(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
importFile1 := filepath.Join(tempDir, "matcher_snippet_1.caddy")
|
||||||
|
importFile2 := filepath.Join(tempDir, "matcher_snippet_2.caddy")
|
||||||
|
|
||||||
|
err := os.WriteFile(importFile1, []byte(`(matcher1)`), 0o644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("writing first import file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(importFile2, []byte(`(matcher2)`), 0o644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("writing second import file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = Parse("Testfile", []byte(`import `+importFile1+`
|
||||||
|
import `+importFile2+`
|
||||||
|
{
|
||||||
|
debug
|
||||||
|
}`))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expected an error, but got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := "global options block must appear before import directives; move the global options block to the top of the Caddyfile"
|
||||||
|
if !strings.HasPrefix(err.Error(), expected) {
|
||||||
|
t.Errorf("Expected error to start with '%s' but got '%v'", expected, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImportedSnippetDefinitionRetainsBlockPlaceholder(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
importFile := filepath.Join(tempDir, "snippets.caddy")
|
||||||
|
|
||||||
|
err := os.WriteFile(importFile, []byte(`
|
||||||
|
(site) {
|
||||||
|
http://{args[0]} {
|
||||||
|
respond "before"
|
||||||
|
{block}
|
||||||
|
respond "after"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`), 0o644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("writing imported snippet file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expectedDirectives []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "with nested block",
|
||||||
|
input: `
|
||||||
|
import ` + importFile + `
|
||||||
|
|
||||||
|
import site example.com {
|
||||||
|
redir https://example.net
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
expectedDirectives: []string{"respond", "redir", "respond"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "without nested block",
|
||||||
|
input: `
|
||||||
|
import ` + importFile + `
|
||||||
|
|
||||||
|
import site example.com
|
||||||
|
`,
|
||||||
|
expectedDirectives: []string{"respond", "respond"},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
p := testParser(tc.input)
|
||||||
|
blocks, err := p.parseAll()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseAll: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(blocks) != 1 {
|
||||||
|
t.Fatalf("expected exactly one server block, got %d", len(blocks))
|
||||||
|
}
|
||||||
|
|
||||||
|
if actual := blocks[0].GetKeysText(); len(actual) != 1 || actual[0] != "http://example.com" {
|
||||||
|
t.Fatalf("expected server block key http://example.com, got %v", actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(blocks[0].Segments) != len(tc.expectedDirectives) {
|
||||||
|
t.Fatalf("expected %d segments, got %d", len(tc.expectedDirectives), len(blocks[0].Segments))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, directive := range tc.expectedDirectives {
|
||||||
|
if actual := blocks[0].Segments[i].Directive(); actual != directive {
|
||||||
|
t.Fatalf("segment %d: expected directive %q, got %q", i, directive, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testParser(input string) parser {
|
func testParser(input string) parser {
|
||||||
return parser{Dispenser: NewTestDispenser(input)}
|
return parser{Dispenser: NewTestDispenser(input)}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -550,26 +550,11 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case acmeIssuer != nil:
|
case acmeIssuer != nil:
|
||||||
// implicit ACME issuers (from various subdirectives) - use defaults; there might be more than one
|
// implicit ACME issuers (from various subdirectives) should inherit from
|
||||||
defaultIssuers := caddytls.DefaultIssuers(acmeIssuer.Email)
|
// any globally-configured ACME issuer templates, then apply the local
|
||||||
|
// shortcut settings as overrides.
|
||||||
// if an ACME CA endpoint was set, the user expects to use that specific one,
|
defaultIssuers := implicitACMEIssuers(h, acmeIssuer)
|
||||||
// not any others that may be defaults, so replace all defaults with that ACME CA
|
|
||||||
if acmeIssuer.CA != "" {
|
|
||||||
defaultIssuers = []certmagic.Issuer{acmeIssuer}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, issuer := range defaultIssuers {
|
for _, issuer := range defaultIssuers {
|
||||||
// apply settings from the implicitly-configured ACMEIssuer to any
|
|
||||||
// default ACMEIssuers, but preserve each default issuer's CA endpoint,
|
|
||||||
// because, for example, if you configure the DNS challenge, it should
|
|
||||||
// apply to any of the default ACMEIssuers, but you don't want to trample
|
|
||||||
// out their unique CA endpoints
|
|
||||||
if iss, ok := issuer.(*caddytls.ACMEIssuer); ok && iss != nil {
|
|
||||||
acmeCopy := *acmeIssuer
|
|
||||||
acmeCopy.CA = iss.CA
|
|
||||||
issuer = &acmeCopy
|
|
||||||
}
|
|
||||||
configVals = append(configVals, ConfigValue{
|
configVals = append(configVals, ConfigValue{
|
||||||
Class: "tls.cert_issuer",
|
Class: "tls.cert_issuer",
|
||||||
Value: issuer,
|
Value: issuer,
|
||||||
@@ -668,6 +653,8 @@ func parseRoot(h Helper) ([]ConfigValue, error) {
|
|||||||
if !h.NextArg() {
|
if !h.NextArg() {
|
||||||
return nil, h.ArgErr()
|
return nil, h.ArgErr()
|
||||||
}
|
}
|
||||||
|
// store the unmatched root in block state so sibling directives can access it
|
||||||
|
h.BlockState["root"] = h.Val()
|
||||||
return h.NewRoute(nil, caddyhttp.VarsMiddleware{"root": h.Val()}), nil
|
return h.NewRoute(nil, caddyhttp.VarsMiddleware{"root": h.Val()}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,6 +669,10 @@ func parseRoot(h Helper) ([]ConfigValue, error) {
|
|||||||
if !h.NextArg() {
|
if !h.NextArg() {
|
||||||
return nil, h.ArgErr()
|
return nil, h.ArgErr()
|
||||||
}
|
}
|
||||||
|
// store the unmatched root in state so sibling/child directives can access it
|
||||||
|
if userMatcherSet == nil {
|
||||||
|
h.BlockState["root"] = h.Val()
|
||||||
|
}
|
||||||
// make the route with the matcher
|
// make the route with the matcher
|
||||||
return h.NewRoute(userMatcherSet, caddyhttp.VarsMiddleware{"root": h.Val()}), nil
|
return h.NewRoute(userMatcherSet, caddyhttp.VarsMiddleware{"root": h.Val()}), nil
|
||||||
}
|
}
|
||||||
@@ -1062,7 +1053,7 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
|
|||||||
if !d.NextArg() {
|
if !d.NextArg() {
|
||||||
return nil, d.ArgErr()
|
return nil, d.ArgErr()
|
||||||
}
|
}
|
||||||
interval, err := time.ParseDuration(d.Val() + "ns")
|
interval, err := caddy.ParseDuration(d.Val())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, d.Errf("failed to parse interval: %v", err)
|
return nil, d.Errf("failed to parse interval: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,14 +66,14 @@ func TestLogDirectiveSyntax(t *testing.T) {
|
|||||||
input: `:8080 {
|
input: `:8080 {
|
||||||
log {
|
log {
|
||||||
sampling {
|
sampling {
|
||||||
interval 2
|
interval 2s
|
||||||
first 3
|
first 3
|
||||||
thereafter 4
|
thereafter 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"sampling":{"interval":2,"first":3,"thereafter":4},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
|
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"sampling":{"interval":2000000000,"first":3,"thereafter":4},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
|
|||||||
@@ -202,7 +202,10 @@ func RegisterGlobalOption(opt string, setupFunc UnmarshalGlobalFunc) {
|
|||||||
type Helper struct {
|
type Helper struct {
|
||||||
*caddyfile.Dispenser
|
*caddyfile.Dispenser
|
||||||
// State stores intermediate variables during caddyfile adaptation.
|
// State stores intermediate variables during caddyfile adaptation.
|
||||||
State map[string]any
|
State map[string]any
|
||||||
|
// BlockState stores intermediate variables scoped to the current block.
|
||||||
|
// It propagates down, but unlike state not back up from child to parent.
|
||||||
|
BlockState map[string]any
|
||||||
options map[string]any
|
options map[string]any
|
||||||
warnings *[]caddyconfig.Warning
|
warnings *[]caddyconfig.Warning
|
||||||
matcherDefs map[string]caddy.ModuleMap
|
matcherDefs map[string]caddy.ModuleMap
|
||||||
@@ -385,6 +388,11 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clone BlockState once for the entire block so sibling directives
|
||||||
|
// can share state, but changes don't leak to the parent scope
|
||||||
|
subBlockState := make(map[string]any, len(h.BlockState))
|
||||||
|
maps.Copy(subBlockState, h.BlockState)
|
||||||
|
|
||||||
// with matchers ready to go, evaluate each directive's segment
|
// with matchers ready to go, evaluate each directive's segment
|
||||||
for _, seg := range segments {
|
for _, seg := range segments {
|
||||||
dir := seg.Directive()
|
dir := seg.Directive()
|
||||||
@@ -396,6 +404,7 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
|
|||||||
subHelper := h
|
subHelper := h
|
||||||
subHelper.Dispenser = caddyfile.NewDispenser(seg)
|
subHelper.Dispenser = caddyfile.NewDispenser(seg)
|
||||||
subHelper.matcherDefs = matcherDefs
|
subHelper.matcherDefs = matcherDefs
|
||||||
|
subHelper.BlockState = subBlockState
|
||||||
|
|
||||||
results, err := dirFunc(subHelper)
|
results, err := dirFunc(subHelper)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ func (st ServerType) Setup(
|
|||||||
parentBlock: sb.block,
|
parentBlock: sb.block,
|
||||||
groupCounter: gc,
|
groupCounter: gc,
|
||||||
State: state,
|
State: state,
|
||||||
|
BlockState: state,
|
||||||
}
|
}
|
||||||
|
|
||||||
results, err := dirFunc(h)
|
results, err := dirFunc(h)
|
||||||
@@ -504,6 +505,7 @@ func (ServerType) extractNamedRoutes(
|
|||||||
parentBlock: sb.block,
|
parentBlock: sb.block,
|
||||||
groupCounter: gc,
|
groupCounter: gc,
|
||||||
State: state,
|
State: state,
|
||||||
|
BlockState: state,
|
||||||
}
|
}
|
||||||
|
|
||||||
handler, err := ParseSegmentAsSubroute(h)
|
handler, err := ParseSegmentAsSubroute(h)
|
||||||
@@ -822,7 +824,7 @@ func (st *ServerType) serversFromPairings(
|
|||||||
// https://caddy.community/t/making-sense-of-auto-https-and-why-disabling-it-still-serves-https-instead-of-http/9761
|
// 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"]
|
createdTLSConnPolicies, ok := sblock.pile["tls.connection_policy"]
|
||||||
hasTLSEnabled := (ok && len(createdTLSConnPolicies) > 0) ||
|
hasTLSEnabled := (ok && len(createdTLSConnPolicies) > 0) ||
|
||||||
(addr.Host != "" && srv.AutoHTTPS != nil && !slices.Contains(srv.AutoHTTPS.Skip, addr.Host))
|
(addr.Host != "" && (srv.AutoHTTPS == nil || !slices.Contains(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
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package httpcaddyfile
|
package httpcaddyfile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMatcherSyntax(t *testing.T) {
|
func TestMatcherSyntax(t *testing.T) {
|
||||||
@@ -209,3 +211,53 @@ func TestGlobalOptions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDefaultSNIWithoutHTTPS(t *testing.T) {
|
||||||
|
caddyfileStr := `{
|
||||||
|
default_sni my-sni.com
|
||||||
|
}
|
||||||
|
example.com {
|
||||||
|
}`
|
||||||
|
|
||||||
|
adapter := caddyfile.Adapter{
|
||||||
|
ServerType: ServerType{},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, _, err := adapter.Adapt([]byte(caddyfileStr), nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to adapt Caddyfile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var config struct {
|
||||||
|
Apps struct {
|
||||||
|
HTTP struct {
|
||||||
|
Servers map[string]*caddyhttp.Server `json:"servers"`
|
||||||
|
} `json:"http"`
|
||||||
|
} `json:"apps"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(result, &config); err != nil {
|
||||||
|
t.Fatalf("Failed to unmarshal JSON config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server, ok := config.Apps.HTTP.Servers["srv0"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected server 'srv0' to be created")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(server.TLSConnPolicies) == 0 {
|
||||||
|
t.Fatalf("Expected TLS connection policies to be generated, got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, policy := range server.TLSConnPolicies {
|
||||||
|
if policy.DefaultSNI == "my-sni.com" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
t.Errorf("Expected default_sni 'my-sni.com' in TLS connection policies, but it was missing. Generated JSON: %s", string(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ func init() {
|
|||||||
RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
|
RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
|
||||||
RegisterGlobalOption("persist_config", parseOptPersistConfig)
|
RegisterGlobalOption("persist_config", parseOptPersistConfig)
|
||||||
RegisterGlobalOption("dns", parseOptDNS)
|
RegisterGlobalOption("dns", parseOptDNS)
|
||||||
|
RegisterGlobalOption("tls_resolvers", parseOptTLSResolvers)
|
||||||
RegisterGlobalOption("ech", parseOptECH)
|
RegisterGlobalOption("ech", parseOptECH)
|
||||||
RegisterGlobalOption("renewal_window_ratio", parseOptRenewalWindowRatio)
|
RegisterGlobalOption("renewal_window_ratio", parseOptRenewalWindowRatio)
|
||||||
}
|
}
|
||||||
@@ -306,6 +307,15 @@ func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) {
|
|||||||
return val, nil
|
return val, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseOptTLSResolvers(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
|
d.Next() // consume option name
|
||||||
|
resolvers := d.RemainingArgs()
|
||||||
|
if len(resolvers) == 0 {
|
||||||
|
return nil, d.ArgErr()
|
||||||
|
}
|
||||||
|
return resolvers, nil
|
||||||
|
}
|
||||||
|
|
||||||
func parseOptDefaultBind(d *caddyfile.Dispenser, _ any) (any, error) {
|
func parseOptDefaultBind(d *caddyfile.Dispenser, _ any) (any, error) {
|
||||||
d.Next() // consume option name
|
d.Next() // consume option name
|
||||||
|
|
||||||
@@ -474,6 +484,8 @@ func unmarshalCaddyfileMetricsOptions(d *caddyfile.Dispenser) (any, error) {
|
|||||||
metrics.PerHost = true
|
metrics.PerHost = true
|
||||||
case "observe_catchall_hosts":
|
case "observe_catchall_hosts":
|
||||||
metrics.ObserveCatchallHosts = true
|
metrics.ObserveCatchallHosts = true
|
||||||
|
case "otlp":
|
||||||
|
metrics.OTLP = true
|
||||||
default:
|
default:
|
||||||
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
|
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
package httpcaddyfile
|
package httpcaddyfile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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/caddytls"
|
||||||
_ "github.com/caddyserver/caddy/v2/modules/logging"
|
_ "github.com/caddyserver/caddy/v2/modules/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -62,3 +66,228 @@ func TestGlobalLogOptionSyntax(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGlobalResolversOption(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expectResolvers []string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "single resolver",
|
||||||
|
input: `{
|
||||||
|
tls_resolvers 1.1.1.1
|
||||||
|
}
|
||||||
|
example.com {
|
||||||
|
}`,
|
||||||
|
expectResolvers: []string{"1.1.1.1"},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "two resolvers",
|
||||||
|
input: `{
|
||||||
|
tls_resolvers 1.1.1.1 8.8.8.8
|
||||||
|
}
|
||||||
|
example.com {
|
||||||
|
}`,
|
||||||
|
expectResolvers: []string{"1.1.1.1", "8.8.8.8"},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple resolvers",
|
||||||
|
input: `{
|
||||||
|
tls_resolvers 1.1.1.1 8.8.8.8 9.9.9.9
|
||||||
|
}
|
||||||
|
example.com {
|
||||||
|
}`,
|
||||||
|
expectResolvers: []string{"1.1.1.1", "8.8.8.8", "9.9.9.9"},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no resolvers specified",
|
||||||
|
input: `{
|
||||||
|
}
|
||||||
|
example.com {
|
||||||
|
}`,
|
||||||
|
expectResolvers: nil,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
adapter := caddyfile.Adapter{
|
||||||
|
ServerType: ServerType{},
|
||||||
|
}
|
||||||
|
|
||||||
|
out, _, err := adapter.Adapt([]byte(tc.input), nil)
|
||||||
|
|
||||||
|
if (err != nil) != tc.expectError {
|
||||||
|
t.Errorf("error expectation failed. Expected error: %v, got: %v", tc.expectError, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectError {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the output JSON to check resolvers
|
||||||
|
var config struct {
|
||||||
|
Apps struct {
|
||||||
|
TLS *caddytls.TLS `json:"tls"`
|
||||||
|
} `json:"apps"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(out, &config); err != nil {
|
||||||
|
t.Errorf("failed to unmarshal output: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if resolvers match expected
|
||||||
|
if config.Apps.TLS == nil {
|
||||||
|
if tc.expectResolvers != nil {
|
||||||
|
t.Errorf("Expected TLS config with resolvers %v, but TLS config is nil", tc.expectResolvers)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actualResolvers := config.Apps.TLS.Resolvers
|
||||||
|
if len(tc.expectResolvers) == 0 && len(actualResolvers) == 0 {
|
||||||
|
return // Both empty, ok
|
||||||
|
}
|
||||||
|
if len(actualResolvers) != len(tc.expectResolvers) {
|
||||||
|
t.Errorf("Expected %d resolvers, got %d. Expected: %v, got: %v", len(tc.expectResolvers), len(actualResolvers), tc.expectResolvers, actualResolvers)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for j, expected := range tc.expectResolvers {
|
||||||
|
if actualResolvers[j] != expected {
|
||||||
|
t.Errorf("Resolver %d mismatch. Expected: %s, got: %s", j, expected, actualResolvers[j])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGlobalCertIssuerAppliesToImplicitACMEIssuer(t *testing.T) {
|
||||||
|
adapter := caddyfile.Adapter{
|
||||||
|
ServerType: ServerType{},
|
||||||
|
}
|
||||||
|
|
||||||
|
input := `{
|
||||||
|
cert_issuer acme {
|
||||||
|
disable_tlsalpn_challenge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
report.company.intern {
|
||||||
|
tls {
|
||||||
|
ca https://deglacme01.company.intern/acme/acme/directory
|
||||||
|
ca_root /etc/certs/company_root2.crt
|
||||||
|
}
|
||||||
|
respond "ok"
|
||||||
|
}`
|
||||||
|
|
||||||
|
out, _, err := adapter.Adapt([]byte(input), nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("adapting caddyfile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var config struct {
|
||||||
|
Apps struct {
|
||||||
|
TLS *caddytls.TLS `json:"tls"`
|
||||||
|
} `json:"apps"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(out, &config); err != nil {
|
||||||
|
t.Fatalf("unmarshaling adapted config: %v", err)
|
||||||
|
}
|
||||||
|
if config.Apps.TLS == nil || config.Apps.TLS.Automation == nil {
|
||||||
|
t.Fatal("expected tls automation config")
|
||||||
|
}
|
||||||
|
|
||||||
|
var subjectPolicy *caddytls.AutomationPolicy
|
||||||
|
for _, ap := range config.Apps.TLS.Automation.Policies {
|
||||||
|
if len(ap.SubjectsRaw) == 1 && ap.SubjectsRaw[0] == "report.company.intern" {
|
||||||
|
subjectPolicy = ap
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if subjectPolicy == nil {
|
||||||
|
t.Fatal("expected subject-specific automation policy")
|
||||||
|
}
|
||||||
|
if len(subjectPolicy.IssuersRaw) != 1 {
|
||||||
|
t.Fatalf("expected one issuer for subject-specific policy, got %d", len(subjectPolicy.IssuersRaw))
|
||||||
|
}
|
||||||
|
|
||||||
|
var issuer caddytls.ACMEIssuer
|
||||||
|
if err := json.Unmarshal(subjectPolicy.IssuersRaw[0], &issuer); err != nil {
|
||||||
|
t.Fatalf("unmarshaling issuer: %v", err)
|
||||||
|
}
|
||||||
|
if issuer.CA != "https://deglacme01.company.intern/acme/acme/directory" {
|
||||||
|
t.Fatalf("expected custom ACME CA, got %q", issuer.CA)
|
||||||
|
}
|
||||||
|
if len(issuer.TrustedRootsPEMFiles) != 1 || issuer.TrustedRootsPEMFiles[0] != "/etc/certs/company_root2.crt" {
|
||||||
|
t.Fatalf("expected trusted roots to include site CA root, got %v", issuer.TrustedRootsPEMFiles)
|
||||||
|
}
|
||||||
|
if issuer.Challenges == nil || issuer.Challenges.TLSALPN == nil || !issuer.Challenges.TLSALPN.Disabled {
|
||||||
|
t.Fatalf("expected tls-alpn challenge to be disabled, got %#v", issuer.Challenges)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeACMEIssuers(t *testing.T) {
|
||||||
|
base := &caddytls.ACMEIssuer{
|
||||||
|
Email: "ops@example.com",
|
||||||
|
Challenges: &caddytls.ChallengesConfig{
|
||||||
|
HTTP: &caddytls.HTTPChallengeConfig{
|
||||||
|
AlternatePort: 8080,
|
||||||
|
},
|
||||||
|
TLSALPN: &caddytls.TLSALPNChallengeConfig{
|
||||||
|
Disabled: true,
|
||||||
|
AlternatePort: 8443,
|
||||||
|
},
|
||||||
|
DNS: &caddytls.DNSChallengeConfig{
|
||||||
|
Resolvers: []string{"1.1.1.1"},
|
||||||
|
OverrideDomain: "_acme-challenge.example.net",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TrustedRootsPEMFiles: []string{"global.pem"},
|
||||||
|
}
|
||||||
|
overrides := &caddytls.ACMEIssuer{
|
||||||
|
CA: "https://deglacme01.company.intern/acme/acme/directory",
|
||||||
|
Challenges: &caddytls.ChallengesConfig{
|
||||||
|
HTTP: &caddytls.HTTPChallengeConfig{
|
||||||
|
Disabled: true,
|
||||||
|
},
|
||||||
|
DNS: &caddytls.DNSChallengeConfig{
|
||||||
|
PropagationTimeout: caddy.Duration(time.Minute),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TrustedRootsPEMFiles: []string{"site.pem"},
|
||||||
|
}
|
||||||
|
|
||||||
|
merged := mergeACMEIssuers(base, overrides)
|
||||||
|
if merged.CA != overrides.CA {
|
||||||
|
t.Fatalf("expected merged CA %q, got %q", overrides.CA, merged.CA)
|
||||||
|
}
|
||||||
|
if merged.Email != base.Email {
|
||||||
|
t.Fatalf("expected merged email %q, got %q", base.Email, merged.Email)
|
||||||
|
}
|
||||||
|
if len(merged.TrustedRootsPEMFiles) != 2 || merged.TrustedRootsPEMFiles[0] != "global.pem" || merged.TrustedRootsPEMFiles[1] != "site.pem" {
|
||||||
|
t.Fatalf("expected merged roots [global.pem site.pem], got %v", merged.TrustedRootsPEMFiles)
|
||||||
|
}
|
||||||
|
if merged.Challenges == nil || merged.Challenges.HTTP == nil || !merged.Challenges.HTTP.Disabled || merged.Challenges.HTTP.AlternatePort != 8080 {
|
||||||
|
t.Fatalf("expected merged HTTP challenge config to preserve alternate port and apply disable flag, got %#v", merged.Challenges)
|
||||||
|
}
|
||||||
|
if merged.Challenges.TLSALPN == nil || !merged.Challenges.TLSALPN.Disabled || merged.Challenges.TLSALPN.AlternatePort != 8443 {
|
||||||
|
t.Fatalf("expected merged TLS-ALPN challenge config to preserve global settings, got %#v", merged.Challenges)
|
||||||
|
}
|
||||||
|
if merged.Challenges.DNS == nil || merged.Challenges.DNS.PropagationTimeout != caddy.Duration(time.Minute) || len(merged.Challenges.DNS.Resolvers) != 1 || merged.Challenges.DNS.Resolvers[0] != "1.1.1.1" || merged.Challenges.DNS.OverrideDomain != "_acme-challenge.example.net" {
|
||||||
|
t.Fatalf("expected merged DNS challenge config to preserve global values and apply overrides, got %#v", merged.Challenges)
|
||||||
|
}
|
||||||
|
|
||||||
|
if base.CA != "" {
|
||||||
|
t.Fatalf("expected base issuer to remain unchanged, got CA %q", base.CA)
|
||||||
|
}
|
||||||
|
if len(base.TrustedRootsPEMFiles) != 1 || base.TrustedRootsPEMFiles[0] != "global.pem" {
|
||||||
|
t.Fatalf("expected base roots to remain unchanged, got %v", base.TrustedRootsPEMFiles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -334,6 +334,11 @@ func (st ServerType) buildTLSApp(
|
|||||||
tlsApp.DNSRaw = caddyconfig.JSONModuleObject(globalDNS, "name", globalDNS.(caddy.Module).CaddyModule().ID.Name(), nil)
|
tlsApp.DNSRaw = caddyconfig.JSONModuleObject(globalDNS, "name", globalDNS.(caddy.Module).CaddyModule().ID.Name(), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set up "global" (to the TLS app) DNS resolvers config
|
||||||
|
if globalResolvers, ok := options["tls_resolvers"]; ok && globalResolvers != nil {
|
||||||
|
tlsApp.Resolvers = globalResolvers.([]string)
|
||||||
|
}
|
||||||
|
|
||||||
// set up ECH from Caddyfile options
|
// set up ECH from Caddyfile options
|
||||||
if ech, ok := options["ech"].(*caddytls.ECH); ok {
|
if ech, ok := options["ech"].(*caddytls.ECH); ok {
|
||||||
tlsApp.EncryptedClientHello = ech
|
tlsApp.EncryptedClientHello = ech
|
||||||
@@ -595,9 +600,301 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
|
|||||||
if globalCertLifetime != nil && acmeIssuer.CertificateLifetime == 0 {
|
if globalCertLifetime != nil && acmeIssuer.CertificateLifetime == 0 {
|
||||||
acmeIssuer.CertificateLifetime = globalCertLifetime.(caddy.Duration)
|
acmeIssuer.CertificateLifetime = globalCertLifetime.(caddy.Duration)
|
||||||
}
|
}
|
||||||
|
// apply global resolvers if DNS challenge is configured and resolvers are not already set
|
||||||
|
globalResolvers := options["tls_resolvers"]
|
||||||
|
if globalResolvers != nil && acmeIssuer.Challenges != nil && acmeIssuer.Challenges.DNS != nil {
|
||||||
|
// Check if DNS challenge is actually configured
|
||||||
|
hasDNSChallenge := globalACMEDNSok || acmeIssuer.Challenges.DNS.ProviderRaw != nil
|
||||||
|
if hasDNSChallenge && len(acmeIssuer.Challenges.DNS.Resolvers) == 0 {
|
||||||
|
acmeIssuer.Challenges.DNS.Resolvers = globalResolvers.([]string)
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// implicitACMEIssuers returns the issuers to use for ACME-related tls
|
||||||
|
// shortcuts such as ca, ca_root, and dns. If any global cert_issuer options
|
||||||
|
// configure ACME issuers, those become the templates for the local shortcut
|
||||||
|
// configuration; otherwise, default ACME issuers are used.
|
||||||
|
func implicitACMEIssuers(h Helper, acmeIssuer *caddytls.ACMEIssuer) []certmagic.Issuer {
|
||||||
|
globalIssuers, _ := h.Option("cert_issuer").([]certmagic.Issuer)
|
||||||
|
|
||||||
|
var implicitIssuers []certmagic.Issuer
|
||||||
|
for _, issuer := range globalIssuers {
|
||||||
|
acmeWrapper, ok := issuer.(acmeCapable)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
baseIssuer := acmeWrapper.GetACMEIssuer()
|
||||||
|
if baseIssuer == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
implicitIssuers = append(implicitIssuers, mergeACMEIssuers(baseIssuer, acmeIssuer))
|
||||||
|
}
|
||||||
|
if len(implicitIssuers) > 0 {
|
||||||
|
return implicitIssuers
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an ACME CA endpoint was set locally, the user expects to use only that
|
||||||
|
// CA rather than the usual default fallback issuers.
|
||||||
|
defaultIssuers := caddytls.DefaultIssuers(acmeIssuer.Email)
|
||||||
|
if acmeIssuer.CA != "" {
|
||||||
|
defaultIssuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)}
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitIssuers = make([]certmagic.Issuer, 0, len(defaultIssuers))
|
||||||
|
for _, issuer := range defaultIssuers {
|
||||||
|
acmeWrapper, ok := issuer.(acmeCapable)
|
||||||
|
if !ok {
|
||||||
|
implicitIssuers = append(implicitIssuers, issuer)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
baseIssuer := acmeWrapper.GetACMEIssuer()
|
||||||
|
if baseIssuer == nil {
|
||||||
|
implicitIssuers = append(implicitIssuers, issuer)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
implicitIssuers = append(implicitIssuers, mergeACMEIssuers(baseIssuer, acmeIssuer))
|
||||||
|
}
|
||||||
|
return implicitIssuers
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeACMEIssuers(base, overrides *caddytls.ACMEIssuer) *caddytls.ACMEIssuer {
|
||||||
|
if base == nil {
|
||||||
|
return cloneACMEIssuer(overrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
merged := cloneACMEIssuer(base)
|
||||||
|
if overrides == nil {
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
if overrides.CA != "" {
|
||||||
|
merged.CA = overrides.CA
|
||||||
|
}
|
||||||
|
if overrides.TestCA != "" {
|
||||||
|
merged.TestCA = overrides.TestCA
|
||||||
|
}
|
||||||
|
if overrides.Email != "" {
|
||||||
|
merged.Email = overrides.Email
|
||||||
|
}
|
||||||
|
if overrides.Profile != "" {
|
||||||
|
merged.Profile = overrides.Profile
|
||||||
|
}
|
||||||
|
if overrides.AccountKey != "" {
|
||||||
|
merged.AccountKey = overrides.AccountKey
|
||||||
|
}
|
||||||
|
if overrides.ExternalAccount != nil {
|
||||||
|
merged.ExternalAccount = cloneACMEEAB(overrides.ExternalAccount)
|
||||||
|
}
|
||||||
|
if overrides.ACMETimeout != 0 {
|
||||||
|
merged.ACMETimeout = overrides.ACMETimeout
|
||||||
|
}
|
||||||
|
if len(overrides.TrustedRootsPEMFiles) > 0 {
|
||||||
|
merged.TrustedRootsPEMFiles = appendUniqueStrings(merged.TrustedRootsPEMFiles, overrides.TrustedRootsPEMFiles...)
|
||||||
|
}
|
||||||
|
if overrides.PreferredChains != nil {
|
||||||
|
merged.PreferredChains = cloneChainPreference(overrides.PreferredChains)
|
||||||
|
}
|
||||||
|
if overrides.CertificateLifetime != 0 {
|
||||||
|
merged.CertificateLifetime = overrides.CertificateLifetime
|
||||||
|
}
|
||||||
|
if len(overrides.NetworkProxyRaw) > 0 {
|
||||||
|
merged.NetworkProxyRaw = slices.Clone(overrides.NetworkProxyRaw)
|
||||||
|
}
|
||||||
|
merged.Challenges = mergeChallengesConfig(merged.Challenges, overrides.Challenges)
|
||||||
|
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeChallengesConfig(base, overrides *caddytls.ChallengesConfig) *caddytls.ChallengesConfig {
|
||||||
|
if base == nil {
|
||||||
|
return cloneChallengesConfig(overrides)
|
||||||
|
}
|
||||||
|
merged := cloneChallengesConfig(base)
|
||||||
|
if overrides == nil {
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
merged.HTTP = mergeHTTPChallengeConfig(merged.HTTP, overrides.HTTP)
|
||||||
|
merged.TLSALPN = mergeTLSALPNChallengeConfig(merged.TLSALPN, overrides.TLSALPN)
|
||||||
|
merged.DNS = mergeDNSChallengeConfig(merged.DNS, overrides.DNS)
|
||||||
|
if overrides.BindHost != "" {
|
||||||
|
merged.BindHost = overrides.BindHost
|
||||||
|
}
|
||||||
|
if overrides.Distributed != nil {
|
||||||
|
value := *overrides.Distributed
|
||||||
|
merged.Distributed = &value
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeHTTPChallengeConfig(base, overrides *caddytls.HTTPChallengeConfig) *caddytls.HTTPChallengeConfig {
|
||||||
|
if base == nil {
|
||||||
|
return cloneHTTPChallengeConfig(overrides)
|
||||||
|
}
|
||||||
|
merged := cloneHTTPChallengeConfig(base)
|
||||||
|
if overrides == nil {
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
if overrides.Disabled {
|
||||||
|
merged.Disabled = true
|
||||||
|
}
|
||||||
|
if overrides.AlternatePort != 0 {
|
||||||
|
merged.AlternatePort = overrides.AlternatePort
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeTLSALPNChallengeConfig(base, overrides *caddytls.TLSALPNChallengeConfig) *caddytls.TLSALPNChallengeConfig {
|
||||||
|
if base == nil {
|
||||||
|
return cloneTLSALPNChallengeConfig(overrides)
|
||||||
|
}
|
||||||
|
merged := cloneTLSALPNChallengeConfig(base)
|
||||||
|
if overrides == nil {
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
if overrides.Disabled {
|
||||||
|
merged.Disabled = true
|
||||||
|
}
|
||||||
|
if overrides.AlternatePort != 0 {
|
||||||
|
merged.AlternatePort = overrides.AlternatePort
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeDNSChallengeConfig(base, overrides *caddytls.DNSChallengeConfig) *caddytls.DNSChallengeConfig {
|
||||||
|
if base == nil {
|
||||||
|
return cloneDNSChallengeConfig(overrides)
|
||||||
|
}
|
||||||
|
merged := cloneDNSChallengeConfig(base)
|
||||||
|
if overrides == nil {
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(overrides.ProviderRaw) > 0 {
|
||||||
|
merged.ProviderRaw = slices.Clone(overrides.ProviderRaw)
|
||||||
|
}
|
||||||
|
if overrides.PropagationDelay != 0 {
|
||||||
|
merged.PropagationDelay = overrides.PropagationDelay
|
||||||
|
}
|
||||||
|
if overrides.PropagationTimeout != 0 {
|
||||||
|
merged.PropagationTimeout = overrides.PropagationTimeout
|
||||||
|
}
|
||||||
|
if overrides.Resolvers != nil {
|
||||||
|
merged.Resolvers = slices.Clone(overrides.Resolvers)
|
||||||
|
}
|
||||||
|
if overrides.OverrideDomain != "" {
|
||||||
|
merged.OverrideDomain = overrides.OverrideDomain
|
||||||
|
}
|
||||||
|
if overrides.TTL != 0 {
|
||||||
|
merged.TTL = overrides.TTL
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneACMEIssuer(iss *caddytls.ACMEIssuer) *caddytls.ACMEIssuer {
|
||||||
|
if iss == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cloned := *iss
|
||||||
|
cloned.Challenges = cloneChallengesConfig(iss.Challenges)
|
||||||
|
cloned.ExternalAccount = cloneACMEEAB(iss.ExternalAccount)
|
||||||
|
cloned.TrustedRootsPEMFiles = slices.Clone(iss.TrustedRootsPEMFiles)
|
||||||
|
cloned.PreferredChains = cloneChainPreference(iss.PreferredChains)
|
||||||
|
cloned.NetworkProxyRaw = slices.Clone(iss.NetworkProxyRaw)
|
||||||
|
|
||||||
|
return &cloned
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneChallengesConfig(cfg *caddytls.ChallengesConfig) *caddytls.ChallengesConfig {
|
||||||
|
if cfg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cloned := *cfg
|
||||||
|
cloned.HTTP = cloneHTTPChallengeConfig(cfg.HTTP)
|
||||||
|
cloned.TLSALPN = cloneTLSALPNChallengeConfig(cfg.TLSALPN)
|
||||||
|
cloned.DNS = cloneDNSChallengeConfig(cfg.DNS)
|
||||||
|
if cfg.Distributed != nil {
|
||||||
|
value := *cfg.Distributed
|
||||||
|
cloned.Distributed = &value
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cloned
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneHTTPChallengeConfig(cfg *caddytls.HTTPChallengeConfig) *caddytls.HTTPChallengeConfig {
|
||||||
|
if cfg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cloned := *cfg
|
||||||
|
return &cloned
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneTLSALPNChallengeConfig(cfg *caddytls.TLSALPNChallengeConfig) *caddytls.TLSALPNChallengeConfig {
|
||||||
|
if cfg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cloned := *cfg
|
||||||
|
return &cloned
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneDNSChallengeConfig(cfg *caddytls.DNSChallengeConfig) *caddytls.DNSChallengeConfig {
|
||||||
|
if cfg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cloned := *cfg
|
||||||
|
cloned.ProviderRaw = slices.Clone(cfg.ProviderRaw)
|
||||||
|
cloned.Resolvers = slices.Clone(cfg.Resolvers)
|
||||||
|
|
||||||
|
return &cloned
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneACMEEAB(eab *acme.EAB) *acme.EAB {
|
||||||
|
if eab == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cloned := *eab
|
||||||
|
return &cloned
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneChainPreference(pref *caddytls.ChainPreference) *caddytls.ChainPreference {
|
||||||
|
if pref == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cloned := *pref
|
||||||
|
cloned.RootCommonName = slices.Clone(pref.RootCommonName)
|
||||||
|
cloned.AnyCommonName = slices.Clone(pref.AnyCommonName)
|
||||||
|
if pref.Smallest != nil {
|
||||||
|
value := *pref.Smallest
|
||||||
|
cloned.Smallest = &value
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cloned
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendUniqueStrings(existing []string, additions ...string) []string {
|
||||||
|
for _, value := range additions {
|
||||||
|
if !slices.Contains(existing, value) {
|
||||||
|
existing = append(existing, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return existing
|
||||||
|
}
|
||||||
|
|
||||||
// newBaseAutomationPolicy returns a new TLS automation policy that gets
|
// newBaseAutomationPolicy returns a new TLS automation policy that gets
|
||||||
// its values from the global options map. It should be used as the base
|
// its values from the global options map. It should be used as the base
|
||||||
// for any other automation policies. A nil policy (and no error) will be
|
// for any other automation policies. A nil policy (and no error) will be
|
||||||
@@ -684,14 +981,31 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
|
|||||||
emptyAPCount := 0
|
emptyAPCount := 0
|
||||||
origLenAPs := len(aps)
|
origLenAPs := len(aps)
|
||||||
// compute the number of empty policies (disregarding subjects) - see #4128
|
// compute the number of empty policies (disregarding subjects) - see #4128
|
||||||
|
// while we're at it,
|
||||||
emptyAP := new(caddytls.AutomationPolicy)
|
emptyAP := new(caddytls.AutomationPolicy)
|
||||||
for i := 0; i < len(aps); i++ {
|
for i := 0; i < len(aps); i++ {
|
||||||
emptyAP.SubjectsRaw = aps[i].SubjectsRaw
|
emptyAP.SubjectsRaw = aps[i].SubjectsRaw
|
||||||
|
emptyAP.ManagersRaw = nil
|
||||||
if reflect.DeepEqual(aps[i], emptyAP) {
|
if reflect.DeepEqual(aps[i], emptyAP) {
|
||||||
|
// AP is empty
|
||||||
emptyAPCount++
|
emptyAPCount++
|
||||||
if !automationPolicyHasAllPublicNames(aps[i]) {
|
|
||||||
// if this automation policy has internal names, we might as well remove it
|
// see if this AP shadows something later
|
||||||
// so auto-https can implicitly use the internal issuer
|
shadowIdx := automationPolicyShadows(i, aps)
|
||||||
|
emptyAP.SubjectsRaw = nil
|
||||||
|
if shadowIdx >= 0 {
|
||||||
|
emptyAP.SubjectsRaw = aps[shadowIdx].SubjectsRaw
|
||||||
|
// allow the later policy, which is likely for a wildcard, to have cert
|
||||||
|
// managers ("get_certificate"), since wildcards now cover specific
|
||||||
|
// subdomains by default, when configured (see discussion in #7559)
|
||||||
|
emptyAP.ManagersRaw = aps[shadowIdx].ManagersRaw
|
||||||
|
}
|
||||||
|
|
||||||
|
// if this is the last AP, we can delete it, since auto-https should
|
||||||
|
// pick it up; if it shadows something later that is also empty, we
|
||||||
|
// can similarly delete this; but if it shadows something that is NOT
|
||||||
|
// empty, we must not delete it since the shadowing has a purpose
|
||||||
|
if i == len(aps)-1 || (shadowIdx >= 0 && reflect.DeepEqual(aps[shadowIdx], emptyAP)) {
|
||||||
aps = slices.Delete(aps, i, i+1)
|
aps = slices.Delete(aps, i, i+1)
|
||||||
i--
|
i--
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ func doHttpCallWithRetries(ctx caddy.Context, client *http.Client, request *http
|
|||||||
var err error
|
var err error
|
||||||
const maxAttempts = 10
|
const maxAttempts = 10
|
||||||
|
|
||||||
for i := 0; i < maxAttempts; i++ {
|
for i := range maxAttempts {
|
||||||
resp, err = attemptHttpCall(client, request)
|
resp, err = attemptHttpCall(client, request)
|
||||||
if err != nil && i < maxAttempts-1 {
|
if err != nil && i < maxAttempts-1 {
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package caddytest
|
package caddytest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -126,3 +127,118 @@ func TestLoadUnorderedJSON(t *testing.T) {
|
|||||||
}
|
}
|
||||||
tester.AssertResponseCode(req, 200)
|
tester.AssertResponseCode(req, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCheckID(t *testing.T) {
|
||||||
|
tester := NewTester(t)
|
||||||
|
tester.InitServer(`{
|
||||||
|
"admin": {
|
||||||
|
"listen": "localhost:2999"
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"http_port": 9080,
|
||||||
|
"servers": {
|
||||||
|
"s_server": {
|
||||||
|
"@id": "s_server",
|
||||||
|
"listen": [
|
||||||
|
":9080"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "static_response",
|
||||||
|
"body": "Hello"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, "json")
|
||||||
|
headers := []string{"Content-Type:application/json"}
|
||||||
|
sServer1 := []byte(`{"@id":"s_server","listen":[":9080"],"routes":[{"@id":"route1","handle":[{"handler":"static_response","body":"Hello 2"}]}]}`)
|
||||||
|
|
||||||
|
// PUT to an existing ID should fail with a 409 conflict
|
||||||
|
tester.AssertPutResponseBody(
|
||||||
|
"http://localhost:2999/id/s_server",
|
||||||
|
headers,
|
||||||
|
bytes.NewBuffer(sServer1),
|
||||||
|
409,
|
||||||
|
`{"error":"[/config/apps/http/servers/s_server] key already exists: s_server"}`+"\n")
|
||||||
|
|
||||||
|
// POST replaces the object fully
|
||||||
|
tester.AssertPostResponseBody(
|
||||||
|
"http://localhost:2999/id/s_server",
|
||||||
|
headers,
|
||||||
|
bytes.NewBuffer(sServer1),
|
||||||
|
200,
|
||||||
|
"")
|
||||||
|
|
||||||
|
// Verify the server is running the new route
|
||||||
|
tester.AssertGetResponse(
|
||||||
|
"http://localhost:9080/",
|
||||||
|
200,
|
||||||
|
"Hello 2")
|
||||||
|
|
||||||
|
// Update the existing route to ensure IDs are handled correctly when replaced
|
||||||
|
tester.AssertPostResponseBody(
|
||||||
|
"http://localhost:2999/id/s_server",
|
||||||
|
headers,
|
||||||
|
bytes.NewBuffer([]byte(`{"@id":"s_server","listen":[":9080"],"routes":[{"@id":"route1","handle":[{"handler":"static_response","body":"Hello2"}],"match":[{"path":["/route_1/*"]}]}]}`)),
|
||||||
|
200,
|
||||||
|
"")
|
||||||
|
|
||||||
|
sServer2 := []byte(`{"@id":"s_server","listen":[":9080"],"routes":[{"@id":"route1","handle":[{"handler":"static_response","body":"Hello2"}],"match":[{"path":["/route_1/*"]}]}]}`)
|
||||||
|
|
||||||
|
// Identical patch should succeed and return 200 (config is unchanged branch)
|
||||||
|
tester.AssertPatchResponseBody(
|
||||||
|
"http://localhost:2999/id/s_server",
|
||||||
|
headers,
|
||||||
|
bytes.NewBuffer(sServer2),
|
||||||
|
200,
|
||||||
|
"")
|
||||||
|
|
||||||
|
route2 := []byte(`{"@id":"route2","handle": [{"handler": "static_response","body": "route2"}],"match":[{"path":["/route_2/*"]}]}`)
|
||||||
|
|
||||||
|
// Put a new route2 object before the route1 object due to the path of /id/route1
|
||||||
|
// Being translated to: /config/apps/http/servers/s_server/routes/0
|
||||||
|
tester.AssertPutResponseBody(
|
||||||
|
"http://localhost:2999/id/route1",
|
||||||
|
headers,
|
||||||
|
bytes.NewBuffer(route2),
|
||||||
|
200,
|
||||||
|
"")
|
||||||
|
|
||||||
|
// Verify that the whole config looks correct, now containing both route1 and route2
|
||||||
|
tester.AssertGetResponse(
|
||||||
|
"http://localhost:2999/config/",
|
||||||
|
200,
|
||||||
|
`{"admin":{"listen":"localhost:2999"},"apps":{"http":{"http_port":9080,"servers":{"s_server":{"@id":"s_server","listen":[":9080"],"routes":[{"@id":"route2","handle":[{"body":"route2","handler":"static_response"}],"match":[{"path":["/route_2/*"]}]},{"@id":"route1","handle":[{"body":"Hello2","handler":"static_response"}],"match":[{"path":["/route_1/*"]}]}]}}}}}`+"\n")
|
||||||
|
|
||||||
|
// Try to add another copy of route2 using POST to test duplicate ID handling
|
||||||
|
// Since the first route2 ended up at array index 0, and we are appending to the array, the index for the new element would be 2
|
||||||
|
tester.AssertPostResponseBody(
|
||||||
|
"http://localhost:2999/id/route2",
|
||||||
|
headers,
|
||||||
|
bytes.NewBuffer(route2),
|
||||||
|
400,
|
||||||
|
`{"error":"indexing config: duplicate ID 'route2' found at /config/apps/http/servers/s_server/routes/0 and /config/apps/http/servers/s_server/routes/2"}`+"\n")
|
||||||
|
|
||||||
|
// Use PATCH to modify an existing object successfully
|
||||||
|
tester.AssertPatchResponseBody(
|
||||||
|
"http://localhost:2999/id/route1",
|
||||||
|
headers,
|
||||||
|
bytes.NewBuffer([]byte(`{"@id":"route1","handle":[{"handler":"static_response","body":"route1"}],"match":[{"path":["/route_1/*"]}]}`)),
|
||||||
|
200,
|
||||||
|
"")
|
||||||
|
|
||||||
|
// Verify the PATCH updated the server state
|
||||||
|
tester.AssertGetResponse(
|
||||||
|
"http://localhost:9080/route_1/",
|
||||||
|
200,
|
||||||
|
"route1")
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func TestACMEServerWithDefaults(t *testing.T) {
|
|||||||
Client: &acme.Client{
|
Client: &acme.Client{
|
||||||
Directory: "https://acme.localhost:9443/acme/local/directory",
|
Directory: "https://acme.localhost:9443/acme/local/directory",
|
||||||
HTTPClient: tester.Client,
|
HTTPClient: tester.Client,
|
||||||
Logger: slog.New(zapslog.NewHandler(logger.Core())),
|
Logger: slog.New(zapslog.NewHandler(logger.Core(), zapslog.WithName("acmez"))),
|
||||||
},
|
},
|
||||||
ChallengeSolvers: map[string]acmez.Solver{
|
ChallengeSolvers: map[string]acmez.Solver{
|
||||||
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
|
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
|
||||||
@@ -120,7 +120,7 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) {
|
|||||||
Client: &acme.Client{
|
Client: &acme.Client{
|
||||||
Directory: "https://acme.localhost:9443/acme/local/directory",
|
Directory: "https://acme.localhost:9443/acme/local/directory",
|
||||||
HTTPClient: tester.Client,
|
HTTPClient: tester.Client,
|
||||||
Logger: slog.New(zapslog.NewHandler(logger.Core())),
|
Logger: slog.New(zapslog.NewHandler(logger.Core(), zapslog.WithName("acmez"))),
|
||||||
},
|
},
|
||||||
ChallengeSolvers: map[string]acmez.Solver{
|
ChallengeSolvers: map[string]acmez.Solver{
|
||||||
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
|
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
|
||||||
|
|||||||
@@ -55,6 +55,28 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T
|
|||||||
tester.AssertRedirect("http://localhost:9080/", "https://localhost:1234/", http.StatusPermanentRedirect)
|
tester.AssertRedirect("http://localhost:9080/", "https://localhost:1234/", http.StatusPermanentRedirect)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAutoHTTPtoHTTPSRedirectsPreferHTTPSPortOverAlternatePort(t *testing.T) {
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(`
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
admin localhost:2999
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
localhost {
|
||||||
|
respond "Canonical"
|
||||||
|
}
|
||||||
|
|
||||||
|
localhost:10443 {
|
||||||
|
respond "Alternate"
|
||||||
|
}
|
||||||
|
`, "caddyfile")
|
||||||
|
|
||||||
|
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
|
||||||
|
}
|
||||||
|
|
||||||
func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
|
func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
|
||||||
tester := caddytest.NewTester(t)
|
tester := caddytest.NewTester(t)
|
||||||
tester.InitServer(`
|
tester.InitServer(`
|
||||||
@@ -143,3 +165,26 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSit
|
|||||||
tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo")
|
tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo")
|
||||||
tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Foo")
|
tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Foo")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAutoHTTPSRedirectSortingExactMatchOverWildcard(t *testing.T) {
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(`
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
admin localhost:2999
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
local_certs
|
||||||
|
}
|
||||||
|
*.localhost:10443 {
|
||||||
|
respond "Wildcard"
|
||||||
|
}
|
||||||
|
dev.localhost {
|
||||||
|
respond "Exact"
|
||||||
|
}
|
||||||
|
`, "caddyfile")
|
||||||
|
|
||||||
|
tester.AssertRedirect("http://dev.localhost:9080/", "https://dev.localhost/", http.StatusPermanentRedirect)
|
||||||
|
|
||||||
|
tester.AssertRedirect("http://foo.localhost:9080/", "https://foo.localhost:10443/", http.StatusPermanentRedirect)
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ encode gzip zstd {
|
|||||||
|
|
||||||
# Long way with a block for each encoding
|
# Long way with a block for each encoding
|
||||||
encode {
|
encode {
|
||||||
zstd
|
zstd {
|
||||||
|
disable_checksum
|
||||||
|
}
|
||||||
gzip 5
|
gzip 5
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +73,9 @@ encode
|
|||||||
"gzip": {
|
"gzip": {
|
||||||
"level": 5
|
"level": 5
|
||||||
},
|
},
|
||||||
"zstd": {}
|
"zstd": {
|
||||||
|
"checksum": false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"handler": "encode",
|
"handler": "encode",
|
||||||
"prefer": [
|
"prefer": [
|
||||||
|
|||||||
@@ -46,6 +46,18 @@ app.example.com {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"request": {
|
||||||
|
"delete": [
|
||||||
|
"Remote-Email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -73,6 +85,18 @@ app.example.com {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"request": {
|
||||||
|
"delete": [
|
||||||
|
"Remote-Groups"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -100,6 +124,18 @@ app.example.com {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"request": {
|
||||||
|
"delete": [
|
||||||
|
"Remote-Name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -127,6 +163,18 @@ app.example.com {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"request": {
|
||||||
|
"delete": [
|
||||||
|
"Remote-User"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -200,4 +248,4 @@ app.example.com {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
:8080
|
||||||
|
|
||||||
|
forward_auth 127.0.0.1:9091 {
|
||||||
|
uri /
|
||||||
|
copy_headers X-User-Id X-User-Role
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8080"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handle_response": [
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"status_code": [
|
||||||
|
2
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "vars"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"request": {
|
||||||
|
"delete": [
|
||||||
|
"X-User-Id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"request": {
|
||||||
|
"set": {
|
||||||
|
"X-User-Id": [
|
||||||
|
"{http.reverse_proxy.header.X-User-Id}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"not": [
|
||||||
|
{
|
||||||
|
"vars": {
|
||||||
|
"{http.reverse_proxy.header.X-User-Id}": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"request": {
|
||||||
|
"delete": [
|
||||||
|
"X-User-Role"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"request": {
|
||||||
|
"set": {
|
||||||
|
"X-User-Role": [
|
||||||
|
"{http.reverse_proxy.header.X-User-Role}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"not": [
|
||||||
|
{
|
||||||
|
"vars": {
|
||||||
|
"{http.reverse_proxy.header.X-User-Role}": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"headers": {
|
||||||
|
"request": {
|
||||||
|
"set": {
|
||||||
|
"X-Forwarded-Method": [
|
||||||
|
"{http.request.method}"
|
||||||
|
],
|
||||||
|
"X-Forwarded-Uri": [
|
||||||
|
"{http.request.uri}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rewrite": {
|
||||||
|
"method": "GET",
|
||||||
|
"uri": "/"
|
||||||
|
},
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "127.0.0.1:9091"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,6 +35,18 @@ forward_auth localhost:9000 {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"request": {
|
||||||
|
"delete": [
|
||||||
|
"1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -62,6 +74,18 @@ forward_auth localhost:9000 {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"request": {
|
||||||
|
"delete": [
|
||||||
|
"B"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -89,6 +113,18 @@ forward_auth localhost:9000 {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"request": {
|
||||||
|
"delete": [
|
||||||
|
"3"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -116,6 +152,18 @@ forward_auth localhost:9000 {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"request": {
|
||||||
|
"delete": [
|
||||||
|
"D"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -143,6 +191,18 @@ forward_auth localhost:9000 {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"request": {
|
||||||
|
"delete": [
|
||||||
|
"5"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -203,4 +263,4 @@ forward_auth localhost:9000 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
log {
|
log {
|
||||||
sampling {
|
sampling {
|
||||||
interval 300
|
interval 5m
|
||||||
first 50
|
first 50
|
||||||
thereafter 40
|
thereafter 40
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"logs": {
|
"logs": {
|
||||||
"default": {
|
"default": {
|
||||||
"sampling": {
|
"sampling": {
|
||||||
"interval": 300,
|
"interval": 300000000000,
|
||||||
"first": 50,
|
"first": 50,
|
||||||
"thereafter": 40
|
"thereafter": 40
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
email test@example.com
|
||||||
|
dns mock
|
||||||
|
tls_resolvers 1.1.1.1 8.8.8.8
|
||||||
|
acme_dns
|
||||||
|
}
|
||||||
|
|
||||||
|
example.com {
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"automation": {
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"issuers": [
|
||||||
|
{
|
||||||
|
"challenges": {
|
||||||
|
"dns": {
|
||||||
|
"resolvers": [
|
||||||
|
"1.1.1.1",
|
||||||
|
"8.8.8.8"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"email": "test@example.com",
|
||||||
|
"module": "acme"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ca": "https://acme.zerossl.com/v2/DV90",
|
||||||
|
"challenges": {
|
||||||
|
"dns": {
|
||||||
|
"resolvers": [
|
||||||
|
"1.1.1.1",
|
||||||
|
"8.8.8.8"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"email": "test@example.com",
|
||||||
|
"module": "acme"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"name": "mock"
|
||||||
|
},
|
||||||
|
"resolvers": [
|
||||||
|
"1.1.1.1",
|
||||||
|
"8.8.8.8"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
tls_resolvers 1.1.1.1 8.8.8.8
|
||||||
|
}
|
||||||
|
|
||||||
|
example.com {
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"resolvers": [
|
||||||
|
"1.1.1.1",
|
||||||
|
"8.8.8.8"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
email test@example.com
|
||||||
|
dns mock
|
||||||
|
tls_resolvers 1.1.1.1 8.8.8.8
|
||||||
|
}
|
||||||
|
|
||||||
|
example.com {
|
||||||
|
tls {
|
||||||
|
dns mock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"automation": {
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"subjects": [
|
||||||
|
"example.com"
|
||||||
|
],
|
||||||
|
"issuers": [
|
||||||
|
{
|
||||||
|
"challenges": {
|
||||||
|
"dns": {
|
||||||
|
"provider": {
|
||||||
|
"name": "mock"
|
||||||
|
},
|
||||||
|
"resolvers": [
|
||||||
|
"1.1.1.1",
|
||||||
|
"8.8.8.8"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"email": "test@example.com",
|
||||||
|
"module": "acme"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"name": "mock"
|
||||||
|
},
|
||||||
|
"resolvers": [
|
||||||
|
"1.1.1.1",
|
||||||
|
"8.8.8.8"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
{
|
||||||
|
email test@example.com
|
||||||
|
dns mock
|
||||||
|
tls_resolvers 1.1.1.1 8.8.8.8
|
||||||
|
acme_dns
|
||||||
|
}
|
||||||
|
|
||||||
|
example.com {
|
||||||
|
tls {
|
||||||
|
resolvers 9.9.9.9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"automation": {
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"subjects": [
|
||||||
|
"example.com"
|
||||||
|
],
|
||||||
|
"issuers": [
|
||||||
|
{
|
||||||
|
"challenges": {
|
||||||
|
"dns": {
|
||||||
|
"resolvers": [
|
||||||
|
"9.9.9.9"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"email": "test@example.com",
|
||||||
|
"module": "acme"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"issuers": [
|
||||||
|
{
|
||||||
|
"challenges": {
|
||||||
|
"dns": {
|
||||||
|
"resolvers": [
|
||||||
|
"1.1.1.1",
|
||||||
|
"8.8.8.8"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"email": "test@example.com",
|
||||||
|
"module": "acme"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ca": "https://acme.zerossl.com/v2/DV90",
|
||||||
|
"challenges": {
|
||||||
|
"dns": {
|
||||||
|
"resolvers": [
|
||||||
|
"1.1.1.1",
|
||||||
|
"8.8.8.8"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"email": "test@example.com",
|
||||||
|
"module": "acme"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"name": "mock"
|
||||||
|
},
|
||||||
|
"resolvers": [
|
||||||
|
"1.1.1.1",
|
||||||
|
"8.8.8.8"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
{
|
||||||
|
email test@example.com
|
||||||
|
dns mock
|
||||||
|
tls_resolvers 1.1.1.1 8.8.8.8
|
||||||
|
acme_dns
|
||||||
|
}
|
||||||
|
|
||||||
|
site1.example.com {
|
||||||
|
}
|
||||||
|
|
||||||
|
site2.example.com {
|
||||||
|
tls {
|
||||||
|
resolvers 9.9.9.9 8.8.4.4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"site1.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"site2.example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"automation": {
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"subjects": [
|
||||||
|
"site2.example.com"
|
||||||
|
],
|
||||||
|
"issuers": [
|
||||||
|
{
|
||||||
|
"challenges": {
|
||||||
|
"dns": {
|
||||||
|
"resolvers": [
|
||||||
|
"9.9.9.9",
|
||||||
|
"8.8.4.4"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"email": "test@example.com",
|
||||||
|
"module": "acme"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"issuers": [
|
||||||
|
{
|
||||||
|
"challenges": {
|
||||||
|
"dns": {
|
||||||
|
"resolvers": [
|
||||||
|
"1.1.1.1",
|
||||||
|
"8.8.8.8"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"email": "test@example.com",
|
||||||
|
"module": "acme"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ca": "https://acme.zerossl.com/v2/DV90",
|
||||||
|
"challenges": {
|
||||||
|
"dns": {
|
||||||
|
"resolvers": [
|
||||||
|
"1.1.1.1",
|
||||||
|
"8.8.8.8"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"email": "test@example.com",
|
||||||
|
"module": "acme"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"name": "mock"
|
||||||
|
},
|
||||||
|
"resolvers": [
|
||||||
|
"1.1.1.1",
|
||||||
|
"8.8.8.8"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
admin off
|
||||||
|
auto_https off
|
||||||
|
}
|
||||||
|
|
||||||
|
import testdata/issue_7557_invalid_subdirective_snippet.conf
|
||||||
|
|
||||||
|
:8080 {
|
||||||
|
import test {
|
||||||
|
this_is_nonsense
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
----------
|
||||||
|
parsing caddyfile tokens for 'reverse_proxy': unrecognized subdirective this_is_nonsense
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
import testdata/issue_7518_unused_block_panic_snippets.conf
|
||||||
|
|
||||||
|
example.com {
|
||||||
|
import snippet
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "headers",
|
||||||
|
"response": {
|
||||||
|
"set": {
|
||||||
|
"Reverse_proxy": [
|
||||||
|
"localhost:3000"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
log {
|
||||||
|
format journald {
|
||||||
|
wrap console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:80 {
|
||||||
|
respond "Hello, World!"
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"logging": {
|
||||||
|
"logs": {
|
||||||
|
"default": {
|
||||||
|
"encoder": {
|
||||||
|
"format": "journald",
|
||||||
|
"wrap": {
|
||||||
|
"format": "console"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "Hello, World!",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,47 @@
|
|||||||
:80
|
:80
|
||||||
|
|
||||||
log {
|
log one {
|
||||||
output file /var/log/access.log {
|
output file /var/log/access.log {
|
||||||
|
mode 0644
|
||||||
|
dir_mode 0755
|
||||||
roll_size 1gb
|
roll_size 1gb
|
||||||
roll_uncompressed
|
roll_uncompressed
|
||||||
|
roll_compression none
|
||||||
roll_local_time
|
roll_local_time
|
||||||
roll_keep 5
|
roll_keep 5
|
||||||
roll_keep_for 90d
|
roll_keep_for 90d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
log two {
|
||||||
|
output file /var/log/access-2.log {
|
||||||
|
mode 0777
|
||||||
|
dir_mode from_file
|
||||||
|
roll_size 1gib
|
||||||
|
roll_compression zstd
|
||||||
|
roll_interval 12h
|
||||||
|
roll_at 00:00 06:00 12:00,18:00
|
||||||
|
roll_minutes 10 40 45,46
|
||||||
|
roll_keep 10
|
||||||
|
roll_keep_for 90d
|
||||||
|
}
|
||||||
|
}
|
||||||
----------
|
----------
|
||||||
{
|
{
|
||||||
"logging": {
|
"logging": {
|
||||||
"logs": {
|
"logs": {
|
||||||
"default": {
|
"default": {
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"http.log.access.log0"
|
"http.log.access.one",
|
||||||
|
"http.log.access.two"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"log0": {
|
"one": {
|
||||||
"writer": {
|
"writer": {
|
||||||
|
"dir_mode": "0755",
|
||||||
"filename": "/var/log/access.log",
|
"filename": "/var/log/access.log",
|
||||||
|
"mode": "0644",
|
||||||
"output": "file",
|
"output": "file",
|
||||||
|
"roll_compression": "none",
|
||||||
"roll_gzip": false,
|
"roll_gzip": false,
|
||||||
"roll_keep": 5,
|
"roll_keep": 5,
|
||||||
"roll_keep_days": 90,
|
"roll_keep_days": 90,
|
||||||
@@ -29,7 +49,35 @@ log {
|
|||||||
"roll_size_mb": 954
|
"roll_size_mb": 954
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"http.log.access.log0"
|
"http.log.access.one"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"two": {
|
||||||
|
"writer": {
|
||||||
|
"dir_mode": "from_file",
|
||||||
|
"filename": "/var/log/access-2.log",
|
||||||
|
"mode": "0777",
|
||||||
|
"output": "file",
|
||||||
|
"roll_at": [
|
||||||
|
"00:00",
|
||||||
|
"06:00",
|
||||||
|
"12:00",
|
||||||
|
"18:00"
|
||||||
|
],
|
||||||
|
"roll_compression": "zstd",
|
||||||
|
"roll_interval": 43200000000000,
|
||||||
|
"roll_keep": 10,
|
||||||
|
"roll_keep_days": 90,
|
||||||
|
"roll_minutes": [
|
||||||
|
10,
|
||||||
|
40,
|
||||||
|
45,
|
||||||
|
46
|
||||||
|
],
|
||||||
|
"roll_size_mb": 1024
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"http.log.access.two"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,7 +90,7 @@ log {
|
|||||||
":80"
|
":80"
|
||||||
],
|
],
|
||||||
"logs": {
|
"logs": {
|
||||||
"default_logger_name": "log0"
|
"default_logger_name": "two"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
:80 {
|
:80 {
|
||||||
log {
|
log {
|
||||||
sampling {
|
sampling {
|
||||||
interval 300
|
interval 5m
|
||||||
first 50
|
first 50
|
||||||
thereafter 40
|
thereafter 40
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
"log0": {
|
"log0": {
|
||||||
"sampling": {
|
"sampling": {
|
||||||
"interval": 300,
|
"interval": 300000000000,
|
||||||
"first": 50,
|
"first": 50,
|
||||||
"thereafter": 40
|
"thereafter": 40
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
metrics {
|
||||||
|
otlp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:80 {
|
||||||
|
respond "Hello"
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":80"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "Hello",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"otlp": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
:8884
|
||||||
|
|
||||||
|
reverse_proxy 127.0.0.1:65535 {
|
||||||
|
lb_retries 3
|
||||||
|
lb_retry_match expression `{rp.status_code} in [502, 503]`
|
||||||
|
lb_retry_match expression `{rp.is_transport_error} || {rp.status_code} == 502`
|
||||||
|
lb_retry_match expression `method('POST') && {rp.status_code} == 503`
|
||||||
|
lb_retry_match `{rp.status_code} == 504`
|
||||||
|
lb_retry_match `{rp.is_transport_error} && method('PUT')`
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8884"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"load_balancing": {
|
||||||
|
"retries": 3,
|
||||||
|
"retry_match": [
|
||||||
|
{
|
||||||
|
"expression": "{http.reverse_proxy.status_code} in [502, 503]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "{http.reverse_proxy.is_transport_error} || {http.reverse_proxy.status_code} == 502"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "method('POST') \u0026\u0026 {http.reverse_proxy.status_code} == 503"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "{http.reverse_proxy.status_code} == 504"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "{http.reverse_proxy.is_transport_error} \u0026\u0026 method('PUT')"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "127.0.0.1:65535"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+147
@@ -0,0 +1,147 @@
|
|||||||
|
:8884
|
||||||
|
|
||||||
|
reverse_proxy 127.0.0.1:65535 {
|
||||||
|
lb_retries 5
|
||||||
|
|
||||||
|
# request matchers (backward-compatible, non-expression)
|
||||||
|
lb_retry_match {
|
||||||
|
method POST PUT
|
||||||
|
}
|
||||||
|
lb_retry_match {
|
||||||
|
path /foo*
|
||||||
|
}
|
||||||
|
lb_retry_match {
|
||||||
|
header X-Idempotency-Key *
|
||||||
|
}
|
||||||
|
|
||||||
|
# response status code via expression
|
||||||
|
lb_retry_match {
|
||||||
|
expression `{rp.status_code} in [502, 503, 504]`
|
||||||
|
}
|
||||||
|
|
||||||
|
# response header via expression
|
||||||
|
lb_retry_match {
|
||||||
|
expression `{rp.header.X-Retry} == "true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
# CEL request functions combined with response placeholders
|
||||||
|
lb_retry_match {
|
||||||
|
expression `method('POST') && {rp.status_code} >= 500`
|
||||||
|
}
|
||||||
|
lb_retry_match {
|
||||||
|
expression `path('/api*') && {rp.status_code} in [502, 503]`
|
||||||
|
}
|
||||||
|
lb_retry_match {
|
||||||
|
expression `host('example.com') && {rp.status_code} == 503`
|
||||||
|
}
|
||||||
|
lb_retry_match {
|
||||||
|
expression `query({'retry': 'true'}) && {rp.status_code} >= 500`
|
||||||
|
}
|
||||||
|
lb_retry_match {
|
||||||
|
expression `header({'X-Idempotency-Key': '*'}) && {rp.status_code} in [502, 503]`
|
||||||
|
}
|
||||||
|
lb_retry_match {
|
||||||
|
expression `protocol('https') && {rp.status_code} == 502`
|
||||||
|
}
|
||||||
|
lb_retry_match {
|
||||||
|
expression `path_regexp('^/api/v[0-9]+/') && {rp.status_code} >= 500`
|
||||||
|
}
|
||||||
|
lb_retry_match {
|
||||||
|
expression `header_regexp('Content-Type', '^application/json') && {rp.status_code} == 502`
|
||||||
|
}
|
||||||
|
|
||||||
|
# transport error handling via placeholder
|
||||||
|
lb_retry_match {
|
||||||
|
expression `{rp.is_transport_error} || {rp.status_code} in [502, 503]`
|
||||||
|
}
|
||||||
|
lb_retry_match {
|
||||||
|
expression `{rp.is_transport_error} && method('POST')`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8884"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"load_balancing": {
|
||||||
|
"retries": 5,
|
||||||
|
"retry_match": [
|
||||||
|
{
|
||||||
|
"method": [
|
||||||
|
"POST",
|
||||||
|
"PUT"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": [
|
||||||
|
"/foo*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"X-Idempotency-Key": [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "{http.reverse_proxy.status_code} in [502, 503, 504]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "{http.reverse_proxy.header.X-Retry} == \"true\""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "method('POST') \u0026\u0026 {http.reverse_proxy.status_code} \u003e= 500"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "path('/api*') \u0026\u0026 {http.reverse_proxy.status_code} in [502, 503]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "host('example.com') \u0026\u0026 {http.reverse_proxy.status_code} == 503"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "query({'retry': 'true'}) \u0026\u0026 {http.reverse_proxy.status_code} \u003e= 500"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "header({'X-Idempotency-Key': '*'}) \u0026\u0026 {http.reverse_proxy.status_code} in [502, 503]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "protocol('https') \u0026\u0026 {http.reverse_proxy.status_code} == 502"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "path_regexp('^/api/v[0-9]+/') \u0026\u0026 {http.reverse_proxy.status_code} \u003e= 500"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "header_regexp('Content-Type', '^application/json') \u0026\u0026 {http.reverse_proxy.status_code} == 502"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "{http.reverse_proxy.is_transport_error} || {http.reverse_proxy.status_code} in [502, 503]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "{http.reverse_proxy.is_transport_error} \u0026\u0026 method('POST')"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "127.0.0.1:65535"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
https://example.com {
|
||||||
|
reverse_proxy https://localhost:54321 {
|
||||||
|
stream_buffer_size 8KB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"example.com"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"stream_buffer_size": 8000,
|
||||||
|
"transport": {
|
||||||
|
"protocol": "http",
|
||||||
|
"tls": {}
|
||||||
|
},
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "localhost:54321"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,11 +54,6 @@ b.com {
|
|||||||
"via": "http"
|
"via": "http"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"subjects": [
|
|
||||||
"b.com"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# example from https://github.com/caddyserver/caddy/issues/7559
|
||||||
|
*.test.local {
|
||||||
|
tls {
|
||||||
|
get_certificate http http://cert-server:9000/certs
|
||||||
|
}
|
||||||
|
respond "wildcard"
|
||||||
|
}
|
||||||
|
|
||||||
|
# certificate for this subdomain is covered by wildcard above
|
||||||
|
subdomain.test.local {
|
||||||
|
respond "subdomain"
|
||||||
|
}
|
||||||
|
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"subdomain.test.local"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "subdomain",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"*.test.local"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "wildcard",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tls": {
|
||||||
|
"automation": {
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"subjects": [
|
||||||
|
"*.test.local"
|
||||||
|
],
|
||||||
|
"get_certificate": [
|
||||||
|
{
|
||||||
|
"url": "http://cert-server:9000/certs",
|
||||||
|
"via": "http"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
localhost
|
||||||
|
|
||||||
|
respond "hello from localhost"
|
||||||
|
tls {
|
||||||
|
client_auth {
|
||||||
|
mode request
|
||||||
|
trust_pool combined {
|
||||||
|
source inline {
|
||||||
|
trust_der MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==
|
||||||
|
}
|
||||||
|
source file {
|
||||||
|
pem_file ../caddy.ca.cer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "hello from localhost",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tls_connection_policies": [
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"sni": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"client_authentication": {
|
||||||
|
"ca": {
|
||||||
|
"provider": "combined",
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"provider": "inline",
|
||||||
|
"trusted_ca_certs": [
|
||||||
|
"MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ=="
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pem_files": [
|
||||||
|
"../caddy.ca.cer"
|
||||||
|
],
|
||||||
|
"provider": "file"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mode": "request"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
localhost
|
||||||
|
|
||||||
|
respond "hello from localhost"
|
||||||
|
tls {
|
||||||
|
client_auth {
|
||||||
|
mode require_and_verify
|
||||||
|
trust_pool combined {
|
||||||
|
source inline {
|
||||||
|
trust_der MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==
|
||||||
|
}
|
||||||
|
source pki_root {
|
||||||
|
authority local
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "hello from localhost",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tls_connection_policies": [
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"sni": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"client_authentication": {
|
||||||
|
"ca": {
|
||||||
|
"provider": "combined",
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"provider": "inline",
|
||||||
|
"trusted_ca_certs": [
|
||||||
|
"MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ=="
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"authority": [
|
||||||
|
"local"
|
||||||
|
],
|
||||||
|
"provider": "pki_root"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mode": "require_and_verify"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
localhost
|
||||||
|
|
||||||
|
respond "hello from localhost"
|
||||||
|
tls {
|
||||||
|
client_auth {
|
||||||
|
mode request
|
||||||
|
trust_pool system
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":443"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": [
|
||||||
|
{
|
||||||
|
"host": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "subroute",
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"body": "hello from localhost",
|
||||||
|
"handler": "static_response"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminal": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tls_connection_policies": [
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"sni": [
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"client_authentication": {
|
||||||
|
"ca": {
|
||||||
|
"provider": "system"
|
||||||
|
},
|
||||||
|
"mode": "request"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
// 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 integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/caddytest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestForwardAuthCopyHeadersStripsClientHeaders is a regression test for the
|
||||||
|
// header injection vulnerability in forward_auth copy_headers.
|
||||||
|
//
|
||||||
|
// When the auth service returns 200 OK without one of the copy_headers headers,
|
||||||
|
// the MatchNot guard skips the Set operation. Before this fix, the original
|
||||||
|
// client-supplied header survived unchanged into the backend request, allowing
|
||||||
|
// privilege escalation with only a valid (non-privileged) bearer token. After
|
||||||
|
// the fix, an unconditional delete route runs first, so the backend always
|
||||||
|
// sees an absent header rather than the attacker-supplied value.
|
||||||
|
func TestForwardAuthCopyHeadersStripsClientHeaders(t *testing.T) {
|
||||||
|
// Mock auth service: accepts any Bearer token, returns 200 OK with NO
|
||||||
|
// identity headers. This is the stateless JWT validator pattern that
|
||||||
|
// triggers the vulnerability.
|
||||||
|
authSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
}))
|
||||||
|
defer authSrv.Close()
|
||||||
|
|
||||||
|
// Mock backend: records the identity headers it receives. A real application
|
||||||
|
// would use X-User-Id / X-User-Role to make authorization decisions.
|
||||||
|
type received struct{ userID, userRole string }
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
last received
|
||||||
|
)
|
||||||
|
backendSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mu.Lock()
|
||||||
|
last = received{
|
||||||
|
userID: r.Header.Get("X-User-Id"),
|
||||||
|
userRole: r.Header.Get("X-User-Role"),
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
fmt.Fprint(w, "ok")
|
||||||
|
}))
|
||||||
|
defer backendSrv.Close()
|
||||||
|
|
||||||
|
authAddr := strings.TrimPrefix(authSrv.URL, "http://")
|
||||||
|
backendAddr := strings.TrimPrefix(backendSrv.URL, "http://")
|
||||||
|
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(fmt.Sprintf(`
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
admin localhost:2999
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
grace_period 1ns
|
||||||
|
}
|
||||||
|
http://localhost:9080 {
|
||||||
|
forward_auth %s {
|
||||||
|
uri /
|
||||||
|
copy_headers X-User-Id X-User-Role
|
||||||
|
}
|
||||||
|
reverse_proxy %s
|
||||||
|
}
|
||||||
|
`, authAddr, backendAddr), "caddyfile")
|
||||||
|
|
||||||
|
// Case 1: no token. Auth must still reject the request even when the client
|
||||||
|
// includes identity headers. This confirms the auth check is not bypassed.
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
|
||||||
|
req.Header.Set("X-User-Id", "injected")
|
||||||
|
req.Header.Set("X-User-Role", "injected")
|
||||||
|
resp := tester.AssertResponseCode(req, http.StatusUnauthorized)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
// Case 2: valid token, no injected headers. The backend should see absent
|
||||||
|
// identity headers (the auth service never returns them).
|
||||||
|
req, _ = http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer token123")
|
||||||
|
tester.AssertResponse(req, http.StatusOK, "ok")
|
||||||
|
mu.Lock()
|
||||||
|
gotID, gotRole := last.userID, last.userRole
|
||||||
|
mu.Unlock()
|
||||||
|
if gotID != "" {
|
||||||
|
t.Errorf("baseline: X-User-Id should be absent, got %q", gotID)
|
||||||
|
}
|
||||||
|
if gotRole != "" {
|
||||||
|
t.Errorf("baseline: X-User-Role should be absent, got %q", gotRole)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 3 (the security regression): valid token plus forged identity headers.
|
||||||
|
// The fix must strip those values so the backend never sees them.
|
||||||
|
req, _ = http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer token123")
|
||||||
|
req.Header.Set("X-User-Id", "admin") // forged
|
||||||
|
req.Header.Set("X-User-Role", "superadmin") // forged
|
||||||
|
tester.AssertResponse(req, http.StatusOK, "ok")
|
||||||
|
mu.Lock()
|
||||||
|
gotID, gotRole = last.userID, last.userRole
|
||||||
|
mu.Unlock()
|
||||||
|
if gotID != "" {
|
||||||
|
t.Errorf("injection: X-User-Id must be stripped, got %q", gotID)
|
||||||
|
}
|
||||||
|
if gotRole != "" {
|
||||||
|
t.Errorf("injection: X-User-Role must be stripped, got %q", gotRole)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestForwardAuthCopyHeadersAuthResponseWins verifies that when the auth
|
||||||
|
// service does include a copy_headers header in its response, that value
|
||||||
|
// is forwarded to the backend and takes precedence over any client-supplied
|
||||||
|
// value for the same header.
|
||||||
|
func TestForwardAuthCopyHeadersAuthResponseWins(t *testing.T) {
|
||||||
|
const wantUserID = "service-user-42"
|
||||||
|
const wantUserRole = "editor"
|
||||||
|
|
||||||
|
// Auth service: accepts bearer token and sets identity headers.
|
||||||
|
authSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
|
||||||
|
w.Header().Set("X-User-Id", wantUserID)
|
||||||
|
w.Header().Set("X-User-Role", wantUserRole)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
}))
|
||||||
|
defer authSrv.Close()
|
||||||
|
|
||||||
|
type received struct{ userID, userRole string }
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
last received
|
||||||
|
)
|
||||||
|
backendSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mu.Lock()
|
||||||
|
last = received{
|
||||||
|
userID: r.Header.Get("X-User-Id"),
|
||||||
|
userRole: r.Header.Get("X-User-Role"),
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
fmt.Fprint(w, "ok")
|
||||||
|
}))
|
||||||
|
defer backendSrv.Close()
|
||||||
|
|
||||||
|
authAddr := strings.TrimPrefix(authSrv.URL, "http://")
|
||||||
|
backendAddr := strings.TrimPrefix(backendSrv.URL, "http://")
|
||||||
|
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(fmt.Sprintf(`
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
admin localhost:2999
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
grace_period 1ns
|
||||||
|
}
|
||||||
|
http://localhost:9080 {
|
||||||
|
forward_auth %s {
|
||||||
|
uri /
|
||||||
|
copy_headers X-User-Id X-User-Role
|
||||||
|
}
|
||||||
|
reverse_proxy %s
|
||||||
|
}
|
||||||
|
`, authAddr, backendAddr), "caddyfile")
|
||||||
|
|
||||||
|
// The client sends forged headers; the auth service overrides them with
|
||||||
|
// its own values. The backend must receive the auth service values.
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer token123")
|
||||||
|
req.Header.Set("X-User-Id", "forged-id") // must be overwritten
|
||||||
|
req.Header.Set("X-User-Role", "forged-role") // must be overwritten
|
||||||
|
tester.AssertResponse(req, http.StatusOK, "ok")
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
gotID, gotRole := last.userID, last.userRole
|
||||||
|
mu.Unlock()
|
||||||
|
if gotID != wantUserID {
|
||||||
|
t.Errorf("X-User-Id: want %q, got %q", wantUserID, gotID)
|
||||||
|
}
|
||||||
|
if gotRole != wantUserRole {
|
||||||
|
t.Errorf("X-User-Role: want %q, got %q", wantUserRole, gotRole)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,595 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// Integration tests for Caddy's PROXY protocol support, covering two distinct
|
||||||
|
// roles that Caddy can play:
|
||||||
|
//
|
||||||
|
// 1. As a PROXY protocol *sender* (reverse proxy outbound transport):
|
||||||
|
// Caddy receives an inbound request from a test client and the
|
||||||
|
// reverse_proxy handler forwards it to an upstream with a PROXY protocol
|
||||||
|
// header (v1 or v2) prepended to the connection. A lightweight backend
|
||||||
|
// built with go-proxyproto validates that the header was received and
|
||||||
|
// carries the correct client address.
|
||||||
|
//
|
||||||
|
// Transport versions tested:
|
||||||
|
// - "1.1" -> plain HTTP/1.1 to the upstream
|
||||||
|
// - "h2c" -> HTTP/2 cleartext (h2c) to the upstream (regression for #7529)
|
||||||
|
// - "2" -> HTTP/2 over TLS (h2) to the upstream
|
||||||
|
//
|
||||||
|
// For each transport version both PROXY protocol v1 and v2 are exercised.
|
||||||
|
//
|
||||||
|
// HTTP/3 (h3) is not included because it uses QUIC/UDP and therefore
|
||||||
|
// bypasses the TCP-level dialContext that injects PROXY protocol headers;
|
||||||
|
// there is no meaningful h3 + proxy protocol sender combination to test.
|
||||||
|
//
|
||||||
|
// 2. As a PROXY protocol *receiver* (server-side listener wrapper):
|
||||||
|
// A raw TCP client dials Caddy directly, injects a PROXY v2 header
|
||||||
|
// spoofing a source address, and sends a normal HTTP/1.1 request. The
|
||||||
|
// Caddy server is configured with the proxy_protocol listener wrapper and
|
||||||
|
// is expected to surface the spoofed address via the
|
||||||
|
// {http.request.remote.host} placeholder.
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
goproxy "github.com/pires/go-proxyproto"
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
"golang.org/x/net/http2/h2c"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/caddytest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// proxyProtoBackend is a minimal HTTP server that sits behind a
|
||||||
|
// go-proxyproto listener and records the source address that was
|
||||||
|
// delivered in the PROXY header for each request.
|
||||||
|
type proxyProtoBackend struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
headerAddrs []string // host:port strings extracted from each PROXY header
|
||||||
|
|
||||||
|
ln net.Listener
|
||||||
|
srv *http.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
// newProxyProtoBackend starts a TCP listener wrapped with go-proxyproto on a
|
||||||
|
// random local port and serves requests with a simple "OK" body. The PROXY
|
||||||
|
// header source addresses are accumulated in headerAddrs so tests can
|
||||||
|
// inspect them.
|
||||||
|
func newProxyProtoBackend(t *testing.T) *proxyProtoBackend {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
b := &proxyProtoBackend{}
|
||||||
|
|
||||||
|
rawLn, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("backend: listen: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap with go-proxyproto so the PROXY header is stripped and parsed
|
||||||
|
// before the HTTP server sees the connection. We use REQUIRE so that a
|
||||||
|
// missing header returns an error instead of silently passing through.
|
||||||
|
pLn := &goproxy.Listener{
|
||||||
|
Listener: rawLn,
|
||||||
|
Policy: func(_ net.Addr) (goproxy.Policy, error) {
|
||||||
|
return goproxy.REQUIRE, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
b.ln = pLn
|
||||||
|
|
||||||
|
// Wrap the handler with h2c support so the backend can speak HTTP/2
|
||||||
|
// cleartext (h2c) as well as plain HTTP/1.1. Without this, Caddy's
|
||||||
|
// reverse proxy would receive a 'frame too large' error when the
|
||||||
|
// upstream transport is configured to use h2c.
|
||||||
|
h2Server := &http2.Server{}
|
||||||
|
handlerFn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// go-proxyproto has already updated the net.Conn's remote
|
||||||
|
// address to the value from the PROXY header; the HTTP server
|
||||||
|
// surfaces it in r.RemoteAddr.
|
||||||
|
b.mu.Lock()
|
||||||
|
b.headerAddrs = append(b.headerAddrs, r.RemoteAddr)
|
||||||
|
b.mu.Unlock()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = fmt.Fprint(w, "OK")
|
||||||
|
})
|
||||||
|
|
||||||
|
b.srv = &http.Server{
|
||||||
|
Handler: h2c.NewHandler(handlerFn, h2Server),
|
||||||
|
}
|
||||||
|
|
||||||
|
go b.srv.Serve(pLn) //nolint:errcheck
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = b.srv.Close()
|
||||||
|
_ = rawLn.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// addr returns the listening address (host:port) of the backend.
|
||||||
|
func (b *proxyProtoBackend) addr() string {
|
||||||
|
return b.ln.Addr().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// recordedAddrs returns a snapshot of all PROXY-header source addresses seen
|
||||||
|
// so far.
|
||||||
|
func (b *proxyProtoBackend) recordedAddrs() []string {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
cp := make([]string, len(b.headerAddrs))
|
||||||
|
copy(cp, b.headerAddrs)
|
||||||
|
return cp
|
||||||
|
}
|
||||||
|
|
||||||
|
// tlsProxyProtoBackend is a TLS-enabled backend that sits behind a
|
||||||
|
// go-proxyproto listener. The PROXY header is stripped before the TLS
|
||||||
|
// handshake so the layer order on a connection is:
|
||||||
|
//
|
||||||
|
// raw TCP → go-proxyproto (strips PROXY header) → TLS handshake → HTTP/2
|
||||||
|
type tlsProxyProtoBackend struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
headerAddrs []string
|
||||||
|
|
||||||
|
srv *httptest.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
// newTLSProxyProtoBackend starts a TLS listener that first reads and strips
|
||||||
|
// PROXY protocol headers (go-proxyproto, REQUIRE policy) and then performs a
|
||||||
|
// TLS handshake. The backend speaks HTTP/2 over TLS (h2).
|
||||||
|
//
|
||||||
|
// The certificate is the standard self-signed certificate generated by
|
||||||
|
// httptest.Server; the Caddy transport must be configured with
|
||||||
|
// insecure_skip_verify: true to trust it.
|
||||||
|
func newTLSProxyProtoBackend(t *testing.T) *tlsProxyProtoBackend {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
b := &tlsProxyProtoBackend{}
|
||||||
|
|
||||||
|
handlerFn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
b.mu.Lock()
|
||||||
|
b.headerAddrs = append(b.headerAddrs, r.RemoteAddr)
|
||||||
|
b.mu.Unlock()
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = fmt.Fprint(w, "OK")
|
||||||
|
})
|
||||||
|
|
||||||
|
rawLn, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("tlsBackend: listen: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap with go-proxyproto so the PROXY header is consumed before TLS.
|
||||||
|
pLn := &goproxy.Listener{
|
||||||
|
Listener: rawLn,
|
||||||
|
Policy: func(_ net.Addr) (goproxy.Policy, error) {
|
||||||
|
return goproxy.REQUIRE, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// httptest.NewUnstartedServer lets us replace the listener before
|
||||||
|
// calling StartTLS(), which wraps our proxyproto listener with
|
||||||
|
// tls.NewListener. This gives us the right layer order.
|
||||||
|
b.srv = httptest.NewUnstartedServer(handlerFn)
|
||||||
|
b.srv.Listener = pLn
|
||||||
|
|
||||||
|
// StartTLS enables HTTP/2 on the server automatically.
|
||||||
|
b.srv.StartTLS()
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
b.srv.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// addr returns the listening address (host:port) of the TLS backend.
|
||||||
|
func (b *tlsProxyProtoBackend) addr() string {
|
||||||
|
return b.srv.Listener.Addr().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// tlsConfig returns the *tls.Config used by the backend server.
|
||||||
|
// Tests can use it to verify cert details if needed.
|
||||||
|
func (b *tlsProxyProtoBackend) tlsConfig() *tls.Config {
|
||||||
|
return b.srv.TLS
|
||||||
|
}
|
||||||
|
|
||||||
|
// recordedAddrs returns a snapshot of all PROXY-header source addresses.
|
||||||
|
func (b *tlsProxyProtoBackend) recordedAddrs() []string {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
cp := make([]string, len(b.headerAddrs))
|
||||||
|
copy(cp, b.headerAddrs)
|
||||||
|
return cp
|
||||||
|
}
|
||||||
|
|
||||||
|
// proxyProtoTLSConfig builds a Caddy JSON configuration that proxies to a TLS
|
||||||
|
// upstream with PROXY protocol. The transport uses insecure_skip_verify so
|
||||||
|
// the self-signed certificate generated by httptest.Server is accepted.
|
||||||
|
func proxyProtoTLSConfig(listenPort int, backendAddr, ppVersion string, transportVersions []string) string {
|
||||||
|
versionsJSON, _ := json.Marshal(transportVersions)
|
||||||
|
return fmt.Sprintf(`{
|
||||||
|
"admin": {
|
||||||
|
"listen": "localhost:2999"
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"pki": {
|
||||||
|
"certificate_authorities": {
|
||||||
|
"local": {
|
||||||
|
"install_trust": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"grace_period": 1,
|
||||||
|
"servers": {
|
||||||
|
"proxy": {
|
||||||
|
"listen": [":%d"],
|
||||||
|
"automatic_https": {
|
||||||
|
"disable": true
|
||||||
|
},
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"upstreams": [{"dial": "%s"}],
|
||||||
|
"transport": {
|
||||||
|
"protocol": "http",
|
||||||
|
"proxy_protocol": "%s",
|
||||||
|
"versions": %s,
|
||||||
|
"tls": {
|
||||||
|
"insecure_skip_verify": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`, listenPort, backendAddr, ppVersion, string(versionsJSON))
|
||||||
|
}
|
||||||
|
|
||||||
|
// testTLSProxyProtocolMatrix is the shared implementation for TLS-based proxy
|
||||||
|
// protocol tests. It mirrors testProxyProtocolMatrix but uses a TLS backend.
|
||||||
|
func testTLSProxyProtocolMatrix(t *testing.T, ppVersion string, transportVersions []string, numRequests int) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
backend := newTLSProxyProtoBackend(t)
|
||||||
|
listenPort := freePort(t)
|
||||||
|
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.WithDefaultOverrides(caddytest.Config{
|
||||||
|
AdminPort: 2999,
|
||||||
|
})
|
||||||
|
cfg := proxyProtoTLSConfig(listenPort, backend.addr(), ppVersion, transportVersions)
|
||||||
|
tester.InitServer(cfg, "json")
|
||||||
|
|
||||||
|
proxyURL := fmt.Sprintf("http://127.0.0.1:%d/", listenPort)
|
||||||
|
|
||||||
|
for i := 0; i < numRequests; i++ {
|
||||||
|
resp, err := tester.Client.Get(proxyURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request %d/%d: GET %s: %v", i+1, numRequests, proxyURL, err)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("request %d/%d: expected status 200, got %d", i+1, numRequests, resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs := backend.recordedAddrs()
|
||||||
|
if len(addrs) == 0 {
|
||||||
|
t.Fatalf("backend recorded no PROXY protocol addresses (expected at least 1)")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, addr := range addrs {
|
||||||
|
host, _, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("addr[%d] %q: SplitHostPort: %v", i, addr, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if host != "127.0.0.1" {
|
||||||
|
t.Errorf("addr[%d]: expected source 127.0.0.1, got %q", i, host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// proxyProtoConfig builds a Caddy JSON configuration that:
|
||||||
|
// - listens on listenPort for inbound HTTP requests
|
||||||
|
// - proxies them to backendAddr with PROXY protocol ppVersion ("v1"/"v2")
|
||||||
|
// - uses the given transport versions (e.g. ["1.1"] or ["h2c"])
|
||||||
|
func proxyProtoConfig(listenPort int, backendAddr, ppVersion string, transportVersions []string) string {
|
||||||
|
versionsJSON, _ := json.Marshal(transportVersions)
|
||||||
|
return fmt.Sprintf(`{
|
||||||
|
"admin": {
|
||||||
|
"listen": "localhost:2999"
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"pki": {
|
||||||
|
"certificate_authorities": {
|
||||||
|
"local": {
|
||||||
|
"install_trust": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"grace_period": 1,
|
||||||
|
"servers": {
|
||||||
|
"proxy": {
|
||||||
|
"listen": [":%d"],
|
||||||
|
"automatic_https": {
|
||||||
|
"disable": true
|
||||||
|
},
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"upstreams": [{"dial": "%s"}],
|
||||||
|
"transport": {
|
||||||
|
"protocol": "http",
|
||||||
|
"proxy_protocol": "%s",
|
||||||
|
"versions": %s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`, listenPort, backendAddr, ppVersion, string(versionsJSON))
|
||||||
|
}
|
||||||
|
|
||||||
|
// freePort returns a free local TCP port by binding briefly and releasing it.
|
||||||
|
func freePort(t *testing.T) int {
|
||||||
|
t.Helper()
|
||||||
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("freePort: %v", err)
|
||||||
|
}
|
||||||
|
port := ln.Addr().(*net.TCPAddr).Port
|
||||||
|
_ = ln.Close()
|
||||||
|
return port
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProxyProtocolV1WithH1 verifies that PROXY protocol v1 headers are sent
|
||||||
|
// correctly when the transport uses HTTP/1.1 to the upstream.
|
||||||
|
func TestProxyProtocolV1WithH1(t *testing.T) {
|
||||||
|
testProxyProtocolMatrix(t, "v1", []string{"1.1"}, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProxyProtocolV2WithH1 verifies that PROXY protocol v2 headers are sent
|
||||||
|
// correctly when the transport uses HTTP/1.1 to the upstream.
|
||||||
|
func TestProxyProtocolV2WithH1(t *testing.T) {
|
||||||
|
testProxyProtocolMatrix(t, "v2", []string{"1.1"}, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProxyProtocolV1WithH2C verifies that PROXY protocol v1 headers are sent
|
||||||
|
// correctly when the transport uses h2c (HTTP/2 cleartext) to the upstream.
|
||||||
|
func TestProxyProtocolV1WithH2C(t *testing.T) {
|
||||||
|
testProxyProtocolMatrix(t, "v1", []string{"h2c"}, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProxyProtocolV2WithH2C verifies that PROXY protocol v2 headers are sent
|
||||||
|
// correctly when the transport uses h2c (HTTP/2 cleartext) to the upstream.
|
||||||
|
// This is the primary regression test for github.com/caddyserver/caddy/issues/7529:
|
||||||
|
// before the fix, the h2 transport opened a new TCP connection per request
|
||||||
|
// (because req.URL.Host was mangled differently for each request due to the
|
||||||
|
// varying client port), which caused file-descriptor exhaustion under load.
|
||||||
|
func TestProxyProtocolV2WithH2C(t *testing.T) {
|
||||||
|
testProxyProtocolMatrix(t, "v2", []string{"h2c"}, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProxyProtocolV2WithH2CMultipleRequests sends several sequential requests
|
||||||
|
// through the h2c + PROXY-protocol path and confirms that:
|
||||||
|
// 1. Every request receives a 200 response (no connection exhaustion).
|
||||||
|
// 2. The backend received at least one PROXY header (connection was reused).
|
||||||
|
//
|
||||||
|
// This is the core regression guard for issue #7529: without the fix, a new
|
||||||
|
// TCP connection was opened per request, quickly exhausting file descriptors.
|
||||||
|
func TestProxyProtocolV2WithH2CMultipleRequests(t *testing.T) {
|
||||||
|
testProxyProtocolMatrix(t, "v2", []string{"h2c"}, 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProxyProtocolV1WithH2 verifies that PROXY protocol v1 headers are sent
|
||||||
|
// correctly when the transport uses HTTP/2 over TLS (h2) to the upstream.
|
||||||
|
func TestProxyProtocolV1WithH2(t *testing.T) {
|
||||||
|
testTLSProxyProtocolMatrix(t, "v1", []string{"2"}, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProxyProtocolV2WithH2 verifies that PROXY protocol v2 headers are sent
|
||||||
|
// correctly when the transport uses HTTP/2 over TLS (h2) to the upstream.
|
||||||
|
func TestProxyProtocolV2WithH2(t *testing.T) {
|
||||||
|
testTLSProxyProtocolMatrix(t, "v2", []string{"2"}, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProxyProtocolServerAndProxy is an end-to-end matrix test that exercises
|
||||||
|
// all combinations of PROXY protocol version x transport version.
|
||||||
|
func TestProxyProtocolServerAndProxy(t *testing.T) {
|
||||||
|
plainTests := []struct {
|
||||||
|
name string
|
||||||
|
ppVersion string
|
||||||
|
transportVersions []string
|
||||||
|
numRequests int
|
||||||
|
}{
|
||||||
|
{"h1-v1", "v1", []string{"1.1"}, 3},
|
||||||
|
{"h1-v2", "v2", []string{"1.1"}, 3},
|
||||||
|
{"h2c-v1", "v1", []string{"h2c"}, 3},
|
||||||
|
{"h2c-v2", "v2", []string{"h2c"}, 3},
|
||||||
|
}
|
||||||
|
for _, tc := range plainTests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
testProxyProtocolMatrix(t, tc.ppVersion, tc.transportVersions, tc.numRequests)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsTests := []struct {
|
||||||
|
name string
|
||||||
|
ppVersion string
|
||||||
|
transportVersions []string
|
||||||
|
numRequests int
|
||||||
|
}{
|
||||||
|
{"h2-v1", "v1", []string{"2"}, 3},
|
||||||
|
{"h2-v2", "v2", []string{"2"}, 3},
|
||||||
|
}
|
||||||
|
for _, tc := range tlsTests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
testTLSProxyProtocolMatrix(t, tc.ppVersion, tc.transportVersions, tc.numRequests)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// testProxyProtocolMatrix is the shared implementation for the proxy protocol
|
||||||
|
// tests. It:
|
||||||
|
// 1. Starts a go-proxyproto-wrapped backend.
|
||||||
|
// 2. Configures Caddy as a reverse proxy with the given PROXY protocol
|
||||||
|
// version and transport versions.
|
||||||
|
// 3. Sends numRequests GET requests through Caddy and asserts 200 OK each time.
|
||||||
|
// 4. Asserts the backend recorded at least one PROXY header whose source host
|
||||||
|
// is 127.0.0.1 (the loopback address used by the test client).
|
||||||
|
func testProxyProtocolMatrix(t *testing.T, ppVersion string, transportVersions []string, numRequests int) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
backend := newProxyProtoBackend(t)
|
||||||
|
listenPort := freePort(t)
|
||||||
|
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.WithDefaultOverrides(caddytest.Config{
|
||||||
|
AdminPort: 2999,
|
||||||
|
})
|
||||||
|
cfg := proxyProtoConfig(listenPort, backend.addr(), ppVersion, transportVersions)
|
||||||
|
tester.InitServer(cfg, "json")
|
||||||
|
|
||||||
|
// If the test is h2c-only (no "1.1" in versions), reconfigure the test
|
||||||
|
// client transport to use unencrypted HTTP/2 so we actually exercise the
|
||||||
|
// h2c code path through Caddy.
|
||||||
|
if slices.Contains(transportVersions, "h2c") && !slices.Contains(transportVersions, "1.1") {
|
||||||
|
tr, ok := tester.Client.Transport.(*http.Transport)
|
||||||
|
if ok {
|
||||||
|
tr.Protocols = new(http.Protocols)
|
||||||
|
tr.Protocols.SetHTTP1(false)
|
||||||
|
tr.Protocols.SetUnencryptedHTTP2(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyURL := fmt.Sprintf("http://127.0.0.1:%d/", listenPort)
|
||||||
|
|
||||||
|
for i := 0; i < numRequests; i++ {
|
||||||
|
resp, err := tester.Client.Get(proxyURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("request %d/%d: GET %s: %v", i+1, numRequests, proxyURL, err)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Errorf("request %d/%d: expected status 200, got %d", i+1, numRequests, resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The backend must have seen at least one PROXY header. For h1, there is
|
||||||
|
// one per request; for h2c, requests share the same connection so only one
|
||||||
|
// header is written at connection establishment.
|
||||||
|
addrs := backend.recordedAddrs()
|
||||||
|
if len(addrs) == 0 {
|
||||||
|
t.Fatalf("backend recorded no PROXY protocol addresses (expected at least 1)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every PROXY-decoded source address must be the loopback address since
|
||||||
|
// the test client always connects from 127.0.0.1.
|
||||||
|
for i, addr := range addrs {
|
||||||
|
host, _, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("addr[%d] %q: SplitHostPort: %v", i, addr, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if host != "127.0.0.1" {
|
||||||
|
t.Errorf("addr[%d]: expected source 127.0.0.1, got %q", i, host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestProxyProtocolListenerWrapper verifies that Caddy's
|
||||||
|
// caddy.listeners.proxy_protocol listener wrapper can successfully parse
|
||||||
|
// incoming PROXY protocol headers.
|
||||||
|
//
|
||||||
|
// The test dials Caddy's listening port directly, injects a raw PROXY v2
|
||||||
|
// header spoofing source address 10.0.0.1:1234, then sends a normal
|
||||||
|
// HTTP/1.1 GET request. The Caddy server is configured to echo back the
|
||||||
|
// remote address ({http.request.remote.host}). The test asserts that the
|
||||||
|
// echoed address is the spoofed 10.0.0.1.
|
||||||
|
func TestProxyProtocolListenerWrapper(t *testing.T) {
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(`{
|
||||||
|
skip_install_trust
|
||||||
|
admin localhost:2999
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
grace_period 1ns
|
||||||
|
servers :9080 {
|
||||||
|
listener_wrappers {
|
||||||
|
proxy_protocol {
|
||||||
|
timeout 5s
|
||||||
|
allow 127.0.0.0/8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
http://localhost:9080 {
|
||||||
|
respond "{http.request.remote.host}"
|
||||||
|
}`, "caddyfile")
|
||||||
|
|
||||||
|
// Dial the Caddy listener directly and inject a PROXY v2 header that
|
||||||
|
// claims the connection originates from 10.0.0.1:1234.
|
||||||
|
conn, err := net.Dial("tcp", "127.0.0.1:9080")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dial: %v", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
spoofedSrc := &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 1234}
|
||||||
|
spoofedDst := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 9080}
|
||||||
|
hdr := goproxy.HeaderProxyFromAddrs(2, spoofedSrc, spoofedDst)
|
||||||
|
if _, err := hdr.WriteTo(conn); err != nil {
|
||||||
|
t.Fatalf("write proxy header: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write a minimal HTTP/1.1 GET request.
|
||||||
|
_, err = fmt.Fprintf(conn,
|
||||||
|
"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("write HTTP request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the raw response and look for the spoofed address in the body.
|
||||||
|
buf := make([]byte, 4096)
|
||||||
|
n, _ := conn.Read(buf)
|
||||||
|
raw := string(buf[:n])
|
||||||
|
|
||||||
|
if !strings.Contains(raw, "10.0.0.1") {
|
||||||
|
t.Errorf("expected spoofed address 10.0.0.1 in response body; full response:\n%s", raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
"golang.org/x/net/http2/hpack"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/caddytest"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errExtendedConnectUnsupportedByPeer = errors.New("peer did not advertise RFC 8441 extended CONNECT support")
|
||||||
|
|
||||||
|
func TestReverseProxyExtendedConnectOverH2(t *testing.T) {
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
backend := newWebsocketUpgradeEchoBackend(t)
|
||||||
|
defer backend.Close()
|
||||||
|
|
||||||
|
tester.InitServer(fmt.Sprintf(`
|
||||||
|
{
|
||||||
|
admin localhost:2999
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
grace_period 1ns
|
||||||
|
skip_install_trust
|
||||||
|
servers :9443 {
|
||||||
|
protocols h2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
https://localhost:9443 {
|
||||||
|
reverse_proxy %s
|
||||||
|
}
|
||||||
|
`, backend.addr), "caddyfile")
|
||||||
|
|
||||||
|
const payload = "extended-connect-echo\n"
|
||||||
|
if err := assertExtendedConnectH2Echo("localhost:9443", payload); err != nil {
|
||||||
|
if errors.Is(err, errExtendedConnectUnsupportedByPeer) {
|
||||||
|
t.Skipf("skipping extended CONNECT integration test: %v", err)
|
||||||
|
}
|
||||||
|
t.Fatalf("extended connect h2 echo failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertExtendedConnectH2Echo(addr, payload string) error {
|
||||||
|
conn, err := tlsDialH2(addr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("dialing h2 tls: %w", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
|
||||||
|
return fmt.Errorf("setting deadline: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fr := http2.NewFramer(conn, conn)
|
||||||
|
|
||||||
|
if _, err := conn.Write([]byte(http2.ClientPreface)); err != nil {
|
||||||
|
return fmt.Errorf("writing client preface: %w", err)
|
||||||
|
}
|
||||||
|
if err := fr.WriteSettings(http2.Setting{ID: http2.SettingEnableConnectProtocol, Val: 1}); err != nil {
|
||||||
|
return fmt.Errorf("writing client settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
supported, err := waitForServerSettings(fr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !supported {
|
||||||
|
return errExtendedConnectUnsupportedByPeer
|
||||||
|
}
|
||||||
|
if err := waitForSettingsAck(fr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeExtendedConnectHeaders(fr, addr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := readResponseStatus(fr, 1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if status != "200" {
|
||||||
|
return fmt.Errorf("unexpected extended connect status: got=%s want=200", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fr.WriteData(1, false, []byte(payload)); err != nil {
|
||||||
|
return fmt.Errorf("writing stream data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
echo, err := readStreamData(fr, 1, len(payload))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if echo != payload {
|
||||||
|
return fmt.Errorf("unexpected echoed payload: got=%q want=%q", echo, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = fr.WriteRSTStream(1, http2.ErrCodeNo)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tlsDialH2(addr string) (net.Conn, error) {
|
||||||
|
var lastErr error
|
||||||
|
for i := 0; i < 30; i++ {
|
||||||
|
dialer := &net.Dialer{Timeout: 2 * time.Second}
|
||||||
|
conn, err := tls.DialWithDialer(dialer, "tcp", addr, &tls.Config{
|
||||||
|
ServerName: "localhost",
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
NextProtos: []string{"h2"},
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
return nil, lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForServerSettings(fr *http2.Framer) (bool, error) {
|
||||||
|
for {
|
||||||
|
frame, err := fr.ReadFrame()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("reading frame before connect: %w", err)
|
||||||
|
}
|
||||||
|
settings, ok := frame.(*http2.SettingsFrame)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if settings.IsAck() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
supported := false
|
||||||
|
if err := settings.ForeachSetting(func(s http2.Setting) error {
|
||||||
|
if s.ID == http2.SettingEnableConnectProtocol && s.Val == 1 {
|
||||||
|
supported = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return false, fmt.Errorf("reading server settings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fr.WriteSettingsAck(); err != nil {
|
||||||
|
return false, fmt.Errorf("writing settings ack: %w", err)
|
||||||
|
}
|
||||||
|
return supported, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForSettingsAck(fr *http2.Framer) error {
|
||||||
|
for {
|
||||||
|
frame, err := fr.ReadFrame()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading settings ack: %w", err)
|
||||||
|
}
|
||||||
|
settings, ok := frame.(*http2.SettingsFrame)
|
||||||
|
if ok && settings.IsAck() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeExtendedConnectHeaders(fr *http2.Framer, addr string) error {
|
||||||
|
var hb bytes.Buffer
|
||||||
|
enc := hpack.NewEncoder(&hb)
|
||||||
|
for _, hf := range []hpack.HeaderField{
|
||||||
|
{Name: ":method", Value: "CONNECT"},
|
||||||
|
{Name: ":scheme", Value: "https"},
|
||||||
|
{Name: ":authority", Value: addr},
|
||||||
|
{Name: ":path", Value: "/upgrade"},
|
||||||
|
{Name: ":protocol", Value: "websocket"},
|
||||||
|
} {
|
||||||
|
if err := enc.WriteField(hf); err != nil {
|
||||||
|
return fmt.Errorf("encoding request headers: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fr.WriteHeaders(http2.HeadersFrameParam{
|
||||||
|
StreamID: 1,
|
||||||
|
BlockFragment: hb.Bytes(),
|
||||||
|
EndHeaders: true,
|
||||||
|
EndStream: false,
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("writing extended connect headers: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readResponseStatus(fr *http2.Framer, streamID uint32) (string, error) {
|
||||||
|
var block bytes.Buffer
|
||||||
|
|
||||||
|
for {
|
||||||
|
frame, err := fr.ReadFrame()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("reading response headers: %w", err)
|
||||||
|
}
|
||||||
|
if rst, ok := frame.(*http2.RSTStreamFrame); ok && rst.StreamID == streamID {
|
||||||
|
return "", fmt.Errorf("stream reset before response headers: %s", rst.ErrCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
h, ok := frame.(*http2.HeadersFrame)
|
||||||
|
if !ok || h.StreamID != streamID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := block.Write(h.HeaderBlockFragment()); err != nil {
|
||||||
|
return "", fmt.Errorf("buffering response header fragment: %w", err)
|
||||||
|
}
|
||||||
|
for !h.HeadersEnded() {
|
||||||
|
next, err := fr.ReadFrame()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("reading continuation frame: %w", err)
|
||||||
|
}
|
||||||
|
c, ok := next.(*http2.ContinuationFrame)
|
||||||
|
if !ok || c.StreamID != streamID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := block.Write(c.HeaderBlockFragment()); err != nil {
|
||||||
|
return "", fmt.Errorf("buffering continuation fragment: %w", err)
|
||||||
|
}
|
||||||
|
if c.HeadersEnded() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var status string
|
||||||
|
dec := hpack.NewDecoder(4096, func(f hpack.HeaderField) {
|
||||||
|
if f.Name == ":status" {
|
||||||
|
status = f.Value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if _, err := dec.Write(block.Bytes()); err != nil {
|
||||||
|
return "", fmt.Errorf("decoding response header block: %w", err)
|
||||||
|
}
|
||||||
|
if status == "" {
|
||||||
|
return "", fmt.Errorf("missing :status in response headers")
|
||||||
|
}
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readStreamData(fr *http2.Framer, streamID uint32, n int) (string, error) {
|
||||||
|
buf := make([]byte, 0, n)
|
||||||
|
for len(buf) < n {
|
||||||
|
frame, err := fr.ReadFrame()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("reading stream data: %w", err)
|
||||||
|
}
|
||||||
|
d, ok := frame.(*http2.DataFrame)
|
||||||
|
if !ok || d.StreamID != streamID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
buf = append(buf, d.Data()...)
|
||||||
|
}
|
||||||
|
return string(buf[:n]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type websocketUpgradeEchoBackend struct {
|
||||||
|
addr string
|
||||||
|
ln net.Listener
|
||||||
|
server *http.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWebsocketUpgradeEchoBackend(t *testing.T) *websocketUpgradeEchoBackend {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
backend := &websocketUpgradeEchoBackend{}
|
||||||
|
backend.server = &http.Server{
|
||||||
|
Handler: http.HandlerFunc(backend.serveHTTP),
|
||||||
|
}
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("listening for websocket backend: %v", err)
|
||||||
|
}
|
||||||
|
backend.ln = ln
|
||||||
|
backend.addr = ln.Addr().String()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_ = backend.server.Serve(ln)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return backend
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *websocketUpgradeEchoBackend) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !strings.EqualFold(r.Header.Get("Connection"), "Upgrade") || !strings.EqualFold(r.Header.Get("Upgrade"), "websocket") {
|
||||||
|
http.Error(w, "upgrade required", http.StatusUpgradeRequired)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hijacker, ok := w.(http.Hijacker)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "hijacking not supported", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, rw, err := hijacker.Hijack()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = rw.WriteString("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n")
|
||||||
|
_ = rw.Flush()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer conn.Close()
|
||||||
|
_, _ = io.Copy(conn, conn)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *websocketUpgradeEchoBackend) Close() {
|
||||||
|
_ = b.server.Close()
|
||||||
|
_ = b.ln.Close()
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/caddytest"
|
"github.com/caddyserver/caddy/v2/caddytest"
|
||||||
@@ -386,6 +387,68 @@ func TestReverseProxyHealthCheck(t *testing.T) {
|
|||||||
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
|
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestReverseProxyHealthCheckPortUsed verifies that health_port is actually
|
||||||
|
// used for active health checks and not the upstream's main port. This is a
|
||||||
|
// regression test for https://github.com/caddyserver/caddy/issues/7524.
|
||||||
|
func TestReverseProxyHealthCheckPortUsed(t *testing.T) {
|
||||||
|
// upstream server: serves proxied requests normally, but returns 503 for
|
||||||
|
// /health so that if health checks mistakenly hit this port the upstream
|
||||||
|
// gets marked unhealthy and the proxy returns 503.
|
||||||
|
upstreamSrv := &http.Server{
|
||||||
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.URL.Path == "/health" {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = w.Write([]byte("Hello, World!"))
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
ln0, err := net.Listen("tcp", "127.0.0.1:2022")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to listen on 127.0.0.1:2022: %v", err)
|
||||||
|
}
|
||||||
|
go upstreamSrv.Serve(ln0)
|
||||||
|
t.Cleanup(func() { upstreamSrv.Close(); ln0.Close() })
|
||||||
|
|
||||||
|
// separate health check server on the configured health_port: returns 200
|
||||||
|
// so the upstream is marked healthy only if health checks go to this port.
|
||||||
|
healthSrv := &http.Server{
|
||||||
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
_, _ = w.Write([]byte("ok"))
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
ln1, err := net.Listen("tcp", "127.0.0.1:2023")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to listen on 127.0.0.1:2023: %v", err)
|
||||||
|
}
|
||||||
|
go healthSrv.Serve(ln1)
|
||||||
|
t.Cleanup(func() { healthSrv.Close(); ln1.Close() })
|
||||||
|
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(`
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
admin localhost:2999
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
grace_period 1ns
|
||||||
|
}
|
||||||
|
http://localhost:9080 {
|
||||||
|
reverse_proxy {
|
||||||
|
to localhost:2022
|
||||||
|
|
||||||
|
health_uri /health
|
||||||
|
health_port 2023
|
||||||
|
health_interval 10ms
|
||||||
|
health_timeout 100ms
|
||||||
|
health_passes 1
|
||||||
|
health_fails 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, "caddyfile")
|
||||||
|
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
|
||||||
|
}
|
||||||
|
|
||||||
func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
|
func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
t.SkipNow()
|
t.SkipNow()
|
||||||
@@ -500,3 +563,233 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) {
|
|||||||
|
|
||||||
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
|
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestReverseProxyRetryMatchStatusCode verifies that lb_retry_match with a
|
||||||
|
// CEL expression matching on {rp.status_code} causes the request to be
|
||||||
|
// retried on the next upstream when the first upstream returns a matching
|
||||||
|
// status code
|
||||||
|
func TestReverseProxyRetryMatchStatusCode(t *testing.T) {
|
||||||
|
// Bad upstream: returns 502
|
||||||
|
badSrv := &http.Server{
|
||||||
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusBadGateway)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
badLn, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to listen: %v", err)
|
||||||
|
}
|
||||||
|
go badSrv.Serve(badLn)
|
||||||
|
t.Cleanup(func() { badSrv.Close(); badLn.Close() })
|
||||||
|
|
||||||
|
// Good upstream: returns 200
|
||||||
|
goodSrv := &http.Server{
|
||||||
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("ok"))
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
goodLn, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to listen: %v", err)
|
||||||
|
}
|
||||||
|
go goodSrv.Serve(goodLn)
|
||||||
|
t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
|
||||||
|
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(fmt.Sprintf(`
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
admin localhost:2999
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
grace_period 1ns
|
||||||
|
}
|
||||||
|
http://localhost:9080 {
|
||||||
|
reverse_proxy %s %s {
|
||||||
|
lb_policy round_robin
|
||||||
|
lb_retries 1
|
||||||
|
lb_retry_match {
|
||||||
|
expression `+"`{rp.status_code} in [502, 503]`"+`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, goodLn.Addr().String(), badLn.Addr().String()), "caddyfile")
|
||||||
|
|
||||||
|
tester.AssertGetResponse("http://localhost:9080/", 200, "ok")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReverseProxyRetryMatchHeader verifies that lb_retry_match with a CEL
|
||||||
|
// expression matching on {rp.header.*} causes the request to be retried when
|
||||||
|
// the upstream sets a matching response header
|
||||||
|
func TestReverseProxyRetryMatchHeader(t *testing.T) {
|
||||||
|
var badHits atomic.Int32
|
||||||
|
|
||||||
|
// Bad upstream: returns 200 but signals retry via header
|
||||||
|
badSrv := &http.Server{
|
||||||
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
badHits.Add(1)
|
||||||
|
w.Header().Set("X-Upstream-Retry", "true")
|
||||||
|
w.Write([]byte("bad"))
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
badLn, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to listen: %v", err)
|
||||||
|
}
|
||||||
|
go badSrv.Serve(badLn)
|
||||||
|
t.Cleanup(func() { badSrv.Close(); badLn.Close() })
|
||||||
|
|
||||||
|
// Good upstream: returns 200 without retry header
|
||||||
|
goodSrv := &http.Server{
|
||||||
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("good"))
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
goodLn, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to listen: %v", err)
|
||||||
|
}
|
||||||
|
go goodSrv.Serve(goodLn)
|
||||||
|
t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
|
||||||
|
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(fmt.Sprintf(`
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
admin localhost:2999
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
grace_period 1ns
|
||||||
|
}
|
||||||
|
http://localhost:9080 {
|
||||||
|
reverse_proxy %s %s {
|
||||||
|
lb_policy round_robin
|
||||||
|
lb_retries 1
|
||||||
|
lb_retry_match {
|
||||||
|
expression `+"`{rp.header.X-Upstream-Retry} == \"true\"`"+`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, goodLn.Addr().String(), badLn.Addr().String()), "caddyfile")
|
||||||
|
|
||||||
|
tester.AssertGetResponse("http://localhost:9080/", 200, "good")
|
||||||
|
|
||||||
|
if badHits.Load() != 1 {
|
||||||
|
t.Errorf("bad upstream hits: got %d, want 1", badHits.Load())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReverseProxyRetryMatchCombined verifies that a CEL expression combining
|
||||||
|
// request path matching with response status code matching works correctly -
|
||||||
|
// only retrying when both conditions are met
|
||||||
|
func TestReverseProxyRetryMatchCombined(t *testing.T) {
|
||||||
|
// Upstream: returns 502 for all requests
|
||||||
|
srv := &http.Server{
|
||||||
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusBadGateway)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to listen: %v", err)
|
||||||
|
}
|
||||||
|
go srv.Serve(ln)
|
||||||
|
t.Cleanup(func() { srv.Close(); ln.Close() })
|
||||||
|
|
||||||
|
// Good upstream
|
||||||
|
goodSrv := &http.Server{
|
||||||
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("ok"))
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
goodLn, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to listen: %v", err)
|
||||||
|
}
|
||||||
|
go goodSrv.Serve(goodLn)
|
||||||
|
t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
|
||||||
|
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(fmt.Sprintf(`
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
admin localhost:2999
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
grace_period 1ns
|
||||||
|
}
|
||||||
|
http://localhost:9080 {
|
||||||
|
reverse_proxy %s %s {
|
||||||
|
lb_policy round_robin
|
||||||
|
lb_retries 1
|
||||||
|
lb_retry_match {
|
||||||
|
expression `+"`path('/retry*') && {rp.status_code} in [502, 503]`"+`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, goodLn.Addr().String(), ln.Addr().String()), "caddyfile")
|
||||||
|
|
||||||
|
// /retry path matches the expression - should retry to good upstream
|
||||||
|
tester.AssertGetResponse("http://localhost:9080/retry", 200, "ok")
|
||||||
|
|
||||||
|
// /other path does NOT match - should return the 502
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/other", nil)
|
||||||
|
tester.AssertResponse(req, 502, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReverseProxyRetryMatchIsTransportError verifies that the
|
||||||
|
// {rp.is_transport_error} == true CEL function correctly identifies transport errors
|
||||||
|
// and allows retrying them alongside response-based matching
|
||||||
|
func TestReverseProxyRetryMatchIsTransportError(t *testing.T) {
|
||||||
|
// Good upstream: returns 200
|
||||||
|
goodSrv := &http.Server{
|
||||||
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("ok"))
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
goodLn, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to listen: %v", err)
|
||||||
|
}
|
||||||
|
go goodSrv.Serve(goodLn)
|
||||||
|
t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
|
||||||
|
|
||||||
|
// Broken upstream: accepts connections but closes immediately
|
||||||
|
brokenLn, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to listen: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { brokenLn.Close() })
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
conn, err := brokenLn.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
tester.InitServer(fmt.Sprintf(`
|
||||||
|
{
|
||||||
|
skip_install_trust
|
||||||
|
admin localhost:2999
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
grace_period 1ns
|
||||||
|
}
|
||||||
|
http://localhost:9080 {
|
||||||
|
reverse_proxy %s %s {
|
||||||
|
lb_policy round_robin
|
||||||
|
lb_retries 1
|
||||||
|
lb_retry_match {
|
||||||
|
expression `+"`{rp.is_transport_error} || {rp.status_code} in [502, 503]`"+`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, goodLn.Addr().String(), brokenLn.Addr().String()), "caddyfile")
|
||||||
|
|
||||||
|
// Transport error on broken upstream should be retried to good upstream
|
||||||
|
tester.AssertGetResponse("http://localhost:9080/", 200, "ok")
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/textproto"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/caddytest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReverseProxyUpgradeWithEncode(t *testing.T) {
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
backend := newUpgradeEchoBackend(t)
|
||||||
|
defer backend.Close()
|
||||||
|
|
||||||
|
tester.InitServer(fmt.Sprintf(`
|
||||||
|
{
|
||||||
|
admin localhost:2999
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
grace_period 1ns
|
||||||
|
skip_install_trust
|
||||||
|
}
|
||||||
|
|
||||||
|
localhost:9080 {
|
||||||
|
route {
|
||||||
|
encode gzip
|
||||||
|
reverse_proxy %s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, backend.addr), "caddyfile")
|
||||||
|
|
||||||
|
client := newUpgradedStreamClientWithHeaders(t, map[string]string{
|
||||||
|
"Accept-Encoding": "gzip",
|
||||||
|
})
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
if err := client.echo("encode-upgrade\n"); err != nil {
|
||||||
|
t.Fatalf("upgraded stream echo through encode failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReverseProxyUpgradeWithInterceptHandleResponse(t *testing.T) {
|
||||||
|
tester := caddytest.NewTester(t)
|
||||||
|
backend := newUpgradeEchoBackend(t)
|
||||||
|
defer backend.Close()
|
||||||
|
|
||||||
|
tester.InitServer(fmt.Sprintf(`
|
||||||
|
{
|
||||||
|
admin localhost:2999
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
grace_period 1ns
|
||||||
|
skip_install_trust
|
||||||
|
}
|
||||||
|
|
||||||
|
localhost:9080 {
|
||||||
|
route {
|
||||||
|
intercept {
|
||||||
|
@upgrade status 101
|
||||||
|
handle_response @upgrade {
|
||||||
|
respond "should-not-run"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reverse_proxy %s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, backend.addr), "caddyfile")
|
||||||
|
|
||||||
|
client := newUpgradedStreamClientWithHeaders(t, nil)
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
if err := client.echo("intercept-upgrade\n"); err != nil {
|
||||||
|
t.Fatalf("upgraded stream echo through intercept failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUpgradedStreamClientWithHeaders(t *testing.T, extraHeaders map[string]string) *upgradedStreamClient {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
conn, err := net.DialTimeout("tcp", "127.0.0.1:9080", 5*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dialing caddy: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
requestLines := []string{
|
||||||
|
"GET /upgrade HTTP/1.1",
|
||||||
|
"Host: localhost:9080",
|
||||||
|
"Connection: Upgrade",
|
||||||
|
"Upgrade: stress-stream",
|
||||||
|
}
|
||||||
|
for k, v := range extraHeaders {
|
||||||
|
requestLines = append(requestLines, k+": "+v)
|
||||||
|
}
|
||||||
|
requestLines = append(requestLines, "", "")
|
||||||
|
|
||||||
|
if _, err := io.WriteString(conn, strings.Join(requestLines, "\r\n")); err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
t.Fatalf("writing upgrade request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := bufio.NewReader(conn)
|
||||||
|
tproto := textproto.NewReader(reader)
|
||||||
|
statusLine, err := tproto.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
t.Fatalf("reading upgrade status line: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(statusLine, "101") {
|
||||||
|
_ = conn.Close()
|
||||||
|
t.Fatalf("unexpected upgrade status: %s", statusLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
headers, err := tproto.ReadMIMEHeader()
|
||||||
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
t.Fatalf("reading upgrade headers: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(headers.Get("Connection"), "Upgrade") {
|
||||||
|
_ = conn.Close()
|
||||||
|
t.Fatalf("unexpected upgrade response headers: %v", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &upgradedStreamClient{conn: conn, reader: reader}
|
||||||
|
}
|
||||||
@@ -0,0 +1,504 @@
|
|||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/textproto"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
|
"runtime/pprof"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/caddytest"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultStressStreamCount = 1
|
||||||
|
defaultStressReloadCount = 1
|
||||||
|
defaultStressCloseDelay = 500 * time.Millisecond
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReverseProxyReloadStressUpgradedStreamsHeapProfiles(t *testing.T) {
|
||||||
|
tester := caddytest.NewTester(t).WithDefaultOverrides(caddytest.Config{
|
||||||
|
LoadRequestTimeout: 30 * time.Second,
|
||||||
|
TestRequestTimeout: 30 * time.Second,
|
||||||
|
})
|
||||||
|
|
||||||
|
backend := newUpgradeEchoBackend(t)
|
||||||
|
defer backend.Close()
|
||||||
|
|
||||||
|
// Three scenarios, each sequential so they don't share Caddy state:
|
||||||
|
//
|
||||||
|
// legacy – no delay, close on reload immediately (old default)
|
||||||
|
// close_delay – stream_close_delay, the old "keep-alive workaround"
|
||||||
|
// detached – stream_detached, the new explicit detached flag
|
||||||
|
//
|
||||||
|
// Reloads are spread across time and interleaved with echo-checks so
|
||||||
|
// stream health is exercised at each reload boundary, not only at the end.
|
||||||
|
legacy := runReloadStress(t, tester, backend.addr, "legacy", false, 0)
|
||||||
|
closeDelay := runReloadStress(t, tester, backend.addr, "close_delay", false, stressCloseDelay(t))
|
||||||
|
detached := runReloadStress(t, tester, backend.addr, "detached", true, 0)
|
||||||
|
|
||||||
|
if legacy.aliveAfterReloads != 0 {
|
||||||
|
t.Fatalf("legacy mode left %d upgraded streams alive after reloads", legacy.aliveAfterReloads)
|
||||||
|
}
|
||||||
|
if closeDelay.aliveBeforeDelayExpiry == 0 {
|
||||||
|
t.Fatalf("close_delay mode: all streams closed before delay expired (expected them alive)")
|
||||||
|
}
|
||||||
|
if closeDelay.aliveAfterReloads != 0 {
|
||||||
|
t.Fatalf("close_delay mode left %d upgraded streams alive after delay expiry", closeDelay.aliveAfterReloads)
|
||||||
|
}
|
||||||
|
if detached.aliveAfterReloads != detached.streamCount {
|
||||||
|
t.Fatalf("detached mode kept %d/%d upgraded streams alive after reloads", detached.aliveAfterReloads, detached.streamCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("legacy heap: before=%s mid=%s after=%s delta(before→after)=%s objects(before=%d after=%d) handler_frames(before=%d after=%d)",
|
||||||
|
formatBytes(legacy.beforeReload.HeapInuse),
|
||||||
|
formatBytes(legacy.midReload.HeapInuse),
|
||||||
|
formatBytes(legacy.afterReload.HeapInuse),
|
||||||
|
formatBytesDiff(legacy.beforeReload.HeapInuse, legacy.afterReload.HeapInuse),
|
||||||
|
legacy.beforeReload.HeapObjects, legacy.afterReload.HeapObjects,
|
||||||
|
legacy.beforeReload.handlerFrames, legacy.afterReload.handlerFrames,
|
||||||
|
)
|
||||||
|
t.Logf("close_delay heap: before=%s mid=%s after=%s delta(before→after)=%s objects(before=%d after=%d) handler_frames(before=%d after=%d)",
|
||||||
|
formatBytes(closeDelay.beforeReload.HeapInuse),
|
||||||
|
formatBytes(closeDelay.midReload.HeapInuse),
|
||||||
|
formatBytes(closeDelay.afterReload.HeapInuse),
|
||||||
|
formatBytesDiff(closeDelay.beforeReload.HeapInuse, closeDelay.afterReload.HeapInuse),
|
||||||
|
closeDelay.beforeReload.HeapObjects, closeDelay.afterReload.HeapObjects,
|
||||||
|
closeDelay.beforeReload.handlerFrames, closeDelay.afterReload.handlerFrames,
|
||||||
|
)
|
||||||
|
t.Logf("detached heap: before=%s mid=%s after=%s delta(before→after)=%s objects(before=%d after=%d) handler_frames(before=%d after=%d)",
|
||||||
|
formatBytes(detached.beforeReload.HeapInuse),
|
||||||
|
formatBytes(detached.midReload.HeapInuse),
|
||||||
|
formatBytes(detached.afterReload.HeapInuse),
|
||||||
|
formatBytesDiff(detached.beforeReload.HeapInuse, detached.afterReload.HeapInuse),
|
||||||
|
detached.beforeReload.HeapObjects, detached.afterReload.HeapObjects,
|
||||||
|
detached.beforeReload.handlerFrames, detached.afterReload.handlerFrames,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type stressRunResult struct {
|
||||||
|
streamCount int
|
||||||
|
aliveAfterReloads int
|
||||||
|
aliveBeforeDelayExpiry int // only meaningful for close_delay mode
|
||||||
|
beforeReload heapSnapshot
|
||||||
|
midReload heapSnapshot // after all reloads, before delay expiry clean-up
|
||||||
|
afterReload heapSnapshot // after all streams have been fully cleaned up
|
||||||
|
}
|
||||||
|
|
||||||
|
type heapSnapshot struct {
|
||||||
|
HeapInuse uint64
|
||||||
|
HeapObjects uint64
|
||||||
|
handlerFrames int
|
||||||
|
profileBytes int
|
||||||
|
}
|
||||||
|
|
||||||
|
// runReloadStress opens streamCount upgraded streams, then performs reloadCount
|
||||||
|
// config reloads spread over time. An echo check is performed every 6 reloads so
|
||||||
|
// stream health is exercised at each reload boundary rather than only at the end.
|
||||||
|
// closeDelay mirrors the stream_close_delay config option; pass 0 to disable.
|
||||||
|
func runReloadStress(t *testing.T, tester *caddytest.Tester, backendAddr, mode string, detach bool, closeDelay time.Duration) stressRunResult {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
const echoEvery = 6 // perform an echo check every N reloads
|
||||||
|
|
||||||
|
streamCount := envIntOrDefault(t, "CADDY_STRESS_STREAM_COUNT", defaultStressStreamCount)
|
||||||
|
reloadCount := envIntOrDefault(t, "CADDY_STRESS_RELOAD_COUNT", defaultStressReloadCount)
|
||||||
|
|
||||||
|
tester.InitServer(reloadStressConfig(backendAddr, detach, closeDelay, 0), "caddyfile")
|
||||||
|
|
||||||
|
clients := make([]*upgradedStreamClient, 0, streamCount)
|
||||||
|
for i := 0; i < streamCount; i++ {
|
||||||
|
client := newUpgradedStreamClient(t)
|
||||||
|
clients = append(clients, client)
|
||||||
|
if err := client.echo(fmt.Sprintf("%s-warmup-%02d\n", mode, i)); err != nil {
|
||||||
|
closeClients(clients)
|
||||||
|
t.Fatalf("warmup echo failed in %s mode: %v", mode, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer closeClients(clients)
|
||||||
|
|
||||||
|
before := captureHeapSnapshot(t)
|
||||||
|
|
||||||
|
// Reloads are spread across time; between batches of echoEvery reloads we
|
||||||
|
// pause briefly and measure stream health so the snapshot reflects real-world
|
||||||
|
// reload cadence rather than a tight loop.
|
||||||
|
for i := 1; i <= reloadCount; i++ {
|
||||||
|
loadCaddyfileConfig(t, reloadStressConfig(backendAddr, detach, closeDelay, i))
|
||||||
|
|
||||||
|
// Small pause after each reload to let connection teardown propagate.
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
if i%echoEvery == 0 {
|
||||||
|
alive := countAliveStreams(clients)
|
||||||
|
t.Logf("%s mode: %d/%d streams alive after reload %d", mode, alive, streamCount, i)
|
||||||
|
|
||||||
|
// In detached mode, every stream must survive every reload (upstream unchanged).
|
||||||
|
if detach {
|
||||||
|
for j, client := range clients {
|
||||||
|
if err := client.echo(fmt.Sprintf("%s-mid-%02d-%02d\n", mode, i, j)); err != nil {
|
||||||
|
t.Fatalf("detached mode stream %d died at reload %d: %v", j, i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mid snapshot: after all reloads but before any close_delay timer has fired
|
||||||
|
// (the delay is long enough to still be running at this point).
|
||||||
|
mid := captureHeapSnapshot(t)
|
||||||
|
|
||||||
|
// For legacy mode: the reloads close streams immediately; wait for that to complete.
|
||||||
|
// For close_delay mode: streams are still alive here; wait for the delay to fire.
|
||||||
|
// For detached mode: streams survive indefinitely; no wait needed.
|
||||||
|
var aliveBeforeDelayExpiry int
|
||||||
|
aliveAfterReloads := countAliveStreams(clients)
|
||||||
|
switch {
|
||||||
|
case detach:
|
||||||
|
// nothing to wait for
|
||||||
|
case closeDelay > 0:
|
||||||
|
// streams should still be alive at this point (delay hasn't expired)
|
||||||
|
aliveBeforeDelayExpiry = aliveAfterReloads
|
||||||
|
t.Logf("%s mode: %d/%d streams alive before close_delay expires; waiting %v for cleanup",
|
||||||
|
mode, aliveBeforeDelayExpiry, streamCount, closeDelay)
|
||||||
|
time.Sleep(closeDelay + 200*time.Millisecond)
|
||||||
|
aliveAfterReloads = countAliveStreams(clients)
|
||||||
|
default:
|
||||||
|
deadline := time.Now().Add(2 * time.Second)
|
||||||
|
for aliveAfterReloads > 0 && time.Now().Before(deadline) {
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
aliveAfterReloads = countAliveStreams(clients)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
after := captureHeapSnapshot(t)
|
||||||
|
t.Logf("%s mode heap profile size: before=%dB mid=%dB after=%dB objects(before=%d mid=%d after=%d)",
|
||||||
|
mode,
|
||||||
|
before.profileBytes, mid.profileBytes, after.profileBytes,
|
||||||
|
before.HeapObjects, mid.HeapObjects, after.HeapObjects,
|
||||||
|
)
|
||||||
|
|
||||||
|
return stressRunResult{
|
||||||
|
streamCount: streamCount,
|
||||||
|
aliveAfterReloads: aliveAfterReloads,
|
||||||
|
aliveBeforeDelayExpiry: aliveBeforeDelayExpiry,
|
||||||
|
beforeReload: before,
|
||||||
|
midReload: mid,
|
||||||
|
afterReload: after,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func envIntOrDefault(t *testing.T, key string, def int) int {
|
||||||
|
t.Helper()
|
||||||
|
raw := strings.TrimSpace(os.Getenv(key))
|
||||||
|
if raw == "" {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
v, err := strconv.Atoi(raw)
|
||||||
|
if err != nil || v <= 0 {
|
||||||
|
t.Fatalf("invalid %s=%q: must be a positive integer", key, raw)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func stressCloseDelay(t *testing.T) time.Duration {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
const key = "CADDY_STRESS_CLOSE_DELAY"
|
||||||
|
raw := strings.TrimSpace(os.Getenv(key))
|
||||||
|
if raw == "" {
|
||||||
|
return defaultStressCloseDelay
|
||||||
|
}
|
||||||
|
v, err := time.ParseDuration(raw)
|
||||||
|
if err != nil || v <= 0 {
|
||||||
|
t.Fatalf("invalid %s=%q: must be a positive duration", key, raw)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadCaddyfileConfig(t *testing.T, rawConfig string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
|
req, err := http.NewRequest(http.MethodPost, "http://localhost:2999/load", strings.NewReader(rawConfig))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("creating load request: %v", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "text/caddyfile")
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("loading config: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("reading load response: %v", err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("loading config failed: status=%d body=%s", resp.StatusCode, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func reloadStressConfig(backendAddr string, detach bool, closeDelay time.Duration, revision int) string {
|
||||||
|
var directives string
|
||||||
|
if detach {
|
||||||
|
directives += "\n\t\tstream_detached"
|
||||||
|
}
|
||||||
|
if closeDelay > 0 {
|
||||||
|
directives += fmt.Sprintf("\n\t\tstream_close_delay %s", closeDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(`
|
||||||
|
{
|
||||||
|
admin localhost:2999
|
||||||
|
http_port 9080
|
||||||
|
https_port 9443
|
||||||
|
grace_period 1ns
|
||||||
|
skip_install_trust
|
||||||
|
}
|
||||||
|
|
||||||
|
localhost:9080 {
|
||||||
|
reverse_proxy %s {
|
||||||
|
header_up X-Reload-Revision %d%s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, backendAddr, revision, directives)
|
||||||
|
}
|
||||||
|
|
||||||
|
func captureHeapSnapshot(t *testing.T) heapSnapshot {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
runtime.GC()
|
||||||
|
debug.FreeOSMemory()
|
||||||
|
|
||||||
|
var mem runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&mem)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := pprof.Lookup("heap").WriteTo(&buf, 1); err != nil {
|
||||||
|
t.Fatalf("capturing heap profile: %v", err)
|
||||||
|
}
|
||||||
|
profile := buf.String()
|
||||||
|
|
||||||
|
return heapSnapshot{
|
||||||
|
HeapInuse: mem.HeapInuse,
|
||||||
|
HeapObjects: mem.HeapObjects,
|
||||||
|
handlerFrames: strings.Count(profile, "modules/caddyhttp/reverseproxy.(*Handler)"),
|
||||||
|
profileBytes: buf.Len(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func countAliveStreams(clients []*upgradedStreamClient) int {
|
||||||
|
alive := 0
|
||||||
|
for index, client := range clients {
|
||||||
|
if err := client.echo(fmt.Sprintf("alive-check-%02d\n", index)); err == nil {
|
||||||
|
alive++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return alive
|
||||||
|
}
|
||||||
|
|
||||||
|
func closeClients(clients []*upgradedStreamClient) {
|
||||||
|
for _, client := range clients {
|
||||||
|
if client != nil {
|
||||||
|
_ = client.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatBytes(value uint64) string {
|
||||||
|
const unit = 1024
|
||||||
|
if value < unit {
|
||||||
|
return fmt.Sprintf("%d B", value)
|
||||||
|
}
|
||||||
|
div, exp := uint64(unit), 0
|
||||||
|
for n := value / unit; n >= unit; n /= unit {
|
||||||
|
div *= unit
|
||||||
|
exp++
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1f %ciB", float64(value)/float64(div), "KMGTPE"[exp])
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatBytesDiff(before, after uint64) string {
|
||||||
|
if after >= before {
|
||||||
|
return "+" + formatBytes(after-before)
|
||||||
|
}
|
||||||
|
return "-" + formatBytes(before-after)
|
||||||
|
}
|
||||||
|
|
||||||
|
type upgradedStreamClient struct {
|
||||||
|
conn net.Conn
|
||||||
|
reader *bufio.Reader
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUpgradedStreamClient(t *testing.T) *upgradedStreamClient {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
conn, err := net.DialTimeout("tcp", "127.0.0.1:9080", 5*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dialing caddy: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
request := strings.Join([]string{
|
||||||
|
"GET /upgrade HTTP/1.1",
|
||||||
|
"Host: localhost:9080",
|
||||||
|
"Connection: Upgrade",
|
||||||
|
"Upgrade: stress-stream",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
}, "\r\n")
|
||||||
|
if _, err := io.WriteString(conn, request); err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
t.Fatalf("writing upgrade request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := bufio.NewReader(conn)
|
||||||
|
tproto := textproto.NewReader(reader)
|
||||||
|
statusLine, err := tproto.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
t.Fatalf("reading upgrade status line: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(statusLine, "101") {
|
||||||
|
_ = conn.Close()
|
||||||
|
t.Fatalf("unexpected upgrade status: %s", statusLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
headers, err := tproto.ReadMIMEHeader()
|
||||||
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
t.Fatalf("reading upgrade headers: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(headers.Get("Connection"), "Upgrade") {
|
||||||
|
_ = conn.Close()
|
||||||
|
t.Fatalf("unexpected upgrade response headers: %v", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &upgradedStreamClient{conn: conn, reader: reader}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *upgradedStreamClient) echo(payload string) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
deadline := time.Now().Add(1 * time.Second)
|
||||||
|
if err := c.conn.SetWriteDeadline(deadline); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.WriteString(c.conn, payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.conn.SetReadDeadline(deadline); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, len(payload))
|
||||||
|
if _, err := io.ReadFull(c.reader, buf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if string(buf) != payload {
|
||||||
|
return fmt.Errorf("unexpected echoed payload: got %q want %q", string(buf), payload)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *upgradedStreamClient) Close() error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
return c.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
type upgradeEchoBackend struct {
|
||||||
|
addr string
|
||||||
|
ln net.Listener
|
||||||
|
mu sync.Mutex
|
||||||
|
conns map[net.Conn]struct{}
|
||||||
|
server *http.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUpgradeEchoBackend(t *testing.T) *upgradeEchoBackend {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
backend := &upgradeEchoBackend{conns: make(map[net.Conn]struct{})}
|
||||||
|
backend.server = &http.Server{
|
||||||
|
Handler: http.HandlerFunc(backend.serveHTTP),
|
||||||
|
}
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("listening for backend: %v", err)
|
||||||
|
}
|
||||||
|
backend.ln = ln
|
||||||
|
backend.addr = ln.Addr().String()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_ = backend.server.Serve(ln)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return backend
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *upgradeEchoBackend) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !strings.EqualFold(r.Header.Get("Connection"), "Upgrade") || !strings.EqualFold(r.Header.Get("Upgrade"), "stress-stream") {
|
||||||
|
http.Error(w, "upgrade required", http.StatusUpgradeRequired)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hijacker, ok := w.(http.Hijacker)
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "hijacking not supported", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, rw, err := hijacker.Hijack()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b.trackConn(conn)
|
||||||
|
_, _ = rw.WriteString("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: stress-stream\r\n\r\n")
|
||||||
|
_ = rw.Flush()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer b.untrackConn(conn)
|
||||||
|
defer conn.Close()
|
||||||
|
_, _ = io.Copy(conn, conn)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *upgradeEchoBackend) trackConn(conn net.Conn) {
|
||||||
|
b.mu.Lock()
|
||||||
|
b.conns[conn] = struct{}{}
|
||||||
|
b.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *upgradeEchoBackend) untrackConn(conn net.Conn) {
|
||||||
|
b.mu.Lock()
|
||||||
|
delete(b.conns, conn)
|
||||||
|
b.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *upgradeEchoBackend) Close() {
|
||||||
|
_ = b.server.Close()
|
||||||
|
_ = b.ln.Close()
|
||||||
|
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
for conn := range b.conns {
|
||||||
|
_ = conn.Close()
|
||||||
|
}
|
||||||
|
clear(b.conns)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Used by import_block_snippet_non_replaced_block_from_separate_file.caddyfiletest
|
||||||
|
|
||||||
|
(snippet) {
|
||||||
|
header {
|
||||||
|
reverse_proxy localhost:3000
|
||||||
|
{block}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# This snippet being unused by the test Caddyfile is intentional.
|
||||||
|
# This is to test that a panic runtime error triggered by an out-of-range slice index access
|
||||||
|
# will not happen again, please see issue #7518 and pull request #7543 for more information
|
||||||
|
(unused_snippet) {
|
||||||
|
header SomeHeader SomeValue
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Used by import_block_snippet_invalid_subdirective.caddyfiletest
|
||||||
|
|
||||||
|
(test) {
|
||||||
|
reverse_proxy {
|
||||||
|
{block}
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-4
@@ -9,9 +9,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var defaultFactory = newRootCommandFactory(func() *cobra.Command {
|
var defaultFactory = newRootCommandFactory(func() *cobra.Command {
|
||||||
return &cobra.Command{
|
bin := caddy.CustomBinaryName
|
||||||
Use: "caddy",
|
if bin == "" {
|
||||||
Long: `Caddy is an extensible server platform written in Go.
|
bin = "caddy"
|
||||||
|
}
|
||||||
|
|
||||||
|
long := caddy.CustomLongDescription
|
||||||
|
if long == "" {
|
||||||
|
long = `Caddy is an extensible server platform written in Go.
|
||||||
|
|
||||||
At its core, Caddy merely manages configuration. Modules are plugged
|
At its core, Caddy merely manages configuration. Modules are plugged
|
||||||
in statically at compile-time to provide useful functionality. Caddy's
|
in statically at compile-time to provide useful functionality. Caddy's
|
||||||
@@ -91,7 +96,12 @@ package installers: https://caddyserver.com/docs/install
|
|||||||
|
|
||||||
Instructions for running Caddy in production are also available:
|
Instructions for running Caddy in production are also available:
|
||||||
https://caddyserver.com/docs/running
|
https://caddyserver.com/docs/running
|
||||||
`,
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: bin,
|
||||||
|
Long: long,
|
||||||
Example: ` $ caddy run
|
Example: ` $ caddy run
|
||||||
$ caddy run --config caddy.json
|
$ caddy run --config caddy.json
|
||||||
$ caddy reload --config caddy.json
|
$ caddy reload --config caddy.json
|
||||||
|
|||||||
+2
-2
@@ -372,7 +372,7 @@ func cmdReload(fl Flags) (int, error) {
|
|||||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
|
||||||
}
|
}
|
||||||
|
|
||||||
adminAddr, err := DetermineAdminAPIAddress(addressFlag, config, configFlag, configAdapterFlag)
|
adminAddr, err := DetermineAdminAPIAddress(addressFlag, config, configFile, 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)
|
||||||
}
|
}
|
||||||
@@ -697,7 +697,7 @@ func cmdFmt(fl Flags) (int, error) {
|
|||||||
output := caddyfile.Format(input)
|
output := caddyfile.Format(input)
|
||||||
|
|
||||||
if fl.Bool("overwrite") {
|
if fl.Bool("overwrite") {
|
||||||
if err := os.WriteFile(configFile, output, 0o600); err != nil {
|
if err := os.WriteFile(configFile, output, 0o600); err != nil { //nolint:gosec // path traversal is not really a thing here, this is either "Caddyfile" or admin-controlled
|
||||||
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
|
||||||
|
|||||||
+7
-1
@@ -484,7 +484,13 @@ func setResourceLimits(logger *zap.Logger) func() {
|
|||||||
// See https://pkg.go.dev/runtime/debug#SetMemoryLimit
|
// See https://pkg.go.dev/runtime/debug#SetMemoryLimit
|
||||||
_, _ = memlimit.SetGoMemLimitWithOpts(
|
_, _ = memlimit.SetGoMemLimitWithOpts(
|
||||||
memlimit.WithLogger(
|
memlimit.WithLogger(
|
||||||
slog.New(zapslog.NewHandler(logger.Core())),
|
slog.New(zapslog.NewHandler(
|
||||||
|
logger.Core(),
|
||||||
|
zapslog.WithName("memlimit"),
|
||||||
|
// the default enables traces at ERROR level, this disables
|
||||||
|
// them by setting it to a level higher than any other level
|
||||||
|
zapslog.AddStacktraceAt(slog.Level(127)),
|
||||||
|
)),
|
||||||
),
|
),
|
||||||
memlimit.WithProvider(
|
memlimit.WithProvider(
|
||||||
memlimit.ApplyFallback(
|
memlimit.ApplyFallback(
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
|
|||||||
// not sure why), and since New() should return a pointer
|
// not sure why), and since New() should return a pointer
|
||||||
// value, we need to dereference it first
|
// value, we need to dereference it first
|
||||||
iface := any(modInfo.New())
|
iface := any(modInfo.New())
|
||||||
if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr {
|
if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Pointer {
|
||||||
iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
|
iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
|
||||||
}
|
}
|
||||||
modPkgPath := reflect.TypeOf(iface).PkgPath()
|
modPkgPath := reflect.TypeOf(iface).PkgPath()
|
||||||
|
|||||||
+19
-7
@@ -63,10 +63,17 @@ type Context struct {
|
|||||||
// modules which are loaded will be properly unloaded.
|
// modules which are loaded will be properly unloaded.
|
||||||
// See standard library context package's documentation.
|
// See standard library context package's documentation.
|
||||||
func NewContext(ctx Context) (Context, context.CancelFunc) {
|
func NewContext(ctx Context) (Context, context.CancelFunc) {
|
||||||
|
newCtx, cancelCause := NewContextWithCause(ctx)
|
||||||
|
return newCtx, func() { cancelCause(nil) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContextWithCause is like NewContext but returns a context.CancelCauseFunc.
|
||||||
|
// EXPERIMENTAL: This API is subject to change.
|
||||||
|
func NewContextWithCause(ctx Context) (Context, context.CancelCauseFunc) {
|
||||||
newCtx := Context{moduleInstances: make(map[string][]Module), cfg: ctx.cfg, metricsRegistry: prometheus.NewPedanticRegistry()}
|
newCtx := Context{moduleInstances: make(map[string][]Module), cfg: ctx.cfg, metricsRegistry: prometheus.NewPedanticRegistry()}
|
||||||
c, cancel := context.WithCancel(ctx.Context)
|
c, cancel := context.WithCancelCause(ctx.Context)
|
||||||
wrappedCancel := func() {
|
wrappedCancel := func(cause error) {
|
||||||
cancel()
|
cancel(cause)
|
||||||
|
|
||||||
for _, f := range ctx.cleanupFuncs {
|
for _, f := range ctx.cleanupFuncs {
|
||||||
f()
|
f()
|
||||||
@@ -371,7 +378,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
|
|||||||
// value must be a pointer for unmarshaling into concrete type, even if
|
// value must be a pointer for unmarshaling into concrete type, even if
|
||||||
// the module's concrete type is a slice or map; New() *should* return
|
// the module's concrete type is a slice or map; New() *should* return
|
||||||
// a pointer, otherwise unmarshaling errors or panics will occur
|
// a pointer, otherwise unmarshaling errors or panics will occur
|
||||||
if rv := reflect.ValueOf(val); rv.Kind() != reflect.Ptr {
|
if rv := reflect.ValueOf(val); rv.Kind() != reflect.Pointer {
|
||||||
log.Printf("[WARNING] ModuleInfo.New() for module '%s' did not return a pointer,"+
|
log.Printf("[WARNING] ModuleInfo.New() for module '%s' did not return a pointer,"+
|
||||||
" so we are using reflection to make a pointer instead; please fix this by"+
|
" so we are using reflection to make a pointer instead; please fix this by"+
|
||||||
" using new(Type) or &Type notation in your module's New() function.", id)
|
" using new(Type) or &Type notation in your module's New() function.", id)
|
||||||
@@ -608,6 +615,11 @@ func (ctx Context) Slogger() *slog.Logger {
|
|||||||
core zapcore.Core
|
core zapcore.Core
|
||||||
moduleID string
|
moduleID string
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// the default enables traces at ERROR level, this disables
|
||||||
|
// them by setting it to a level higher than any other level
|
||||||
|
tracesOpt := zapslog.AddStacktraceAt(slog.Level(127))
|
||||||
|
|
||||||
if ctx.cfg == nil {
|
if ctx.cfg == nil {
|
||||||
// often the case in tests; just use a dev logger
|
// often the case in tests; just use a dev logger
|
||||||
l, err := zap.NewDevelopment()
|
l, err := zap.NewDevelopment()
|
||||||
@@ -616,16 +628,16 @@ func (ctx Context) Slogger() *slog.Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
core = l.Core()
|
core = l.Core()
|
||||||
handler = zapslog.NewHandler(core)
|
handler = zapslog.NewHandler(core, tracesOpt)
|
||||||
} else {
|
} else {
|
||||||
mod := ctx.Module()
|
mod := ctx.Module()
|
||||||
if mod == nil {
|
if mod == nil {
|
||||||
core = Log().Core()
|
core = Log().Core()
|
||||||
handler = zapslog.NewHandler(core)
|
handler = zapslog.NewHandler(core, tracesOpt)
|
||||||
} else {
|
} else {
|
||||||
moduleID = string(mod.CaddyModule().ID)
|
moduleID = string(mod.CaddyModule().ID)
|
||||||
core = ctx.cfg.Logging.Logger(mod).Core()
|
core = ctx.cfg.Logging.Logger(mod).Core()
|
||||||
handler = zapslog.NewHandler(core, zapslog.WithName(moduleID))
|
handler = zapslog.NewHandler(core, zapslog.WithName(moduleID), tracesOpt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +1,58 @@
|
|||||||
module github.com/caddyserver/caddy/v2
|
module github.com/caddyserver/caddy/v2
|
||||||
|
|
||||||
go 1.25
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.6.0
|
github.com/BurntSushi/toml v1.6.0
|
||||||
github.com/DeRuina/timberjack v1.3.9
|
github.com/DeRuina/timberjack v1.4.2
|
||||||
github.com/KimMachineGun/automemlimit v0.7.5
|
github.com/KimMachineGun/automemlimit v0.7.5
|
||||||
github.com/Masterminds/sprig/v3 v3.3.0
|
github.com/Masterminds/sprig/v3 v3.3.0
|
||||||
github.com/alecthomas/chroma/v2 v2.23.1
|
github.com/alecthomas/chroma/v2 v2.23.1
|
||||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
|
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
|
||||||
github.com/caddyserver/certmagic v0.25.1
|
github.com/caddyserver/certmagic v0.25.3
|
||||||
github.com/caddyserver/zerossl v0.1.5
|
github.com/caddyserver/zerossl v0.1.5
|
||||||
github.com/cloudflare/circl v1.6.3
|
github.com/cloudflare/circl v1.6.3
|
||||||
github.com/dustin/go-humanize v1.0.1
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/go-chi/chi/v5 v5.2.5
|
github.com/go-chi/chi/v5 v5.2.5
|
||||||
github.com/google/cel-go v0.27.0
|
github.com/google/cel-go v0.28.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/klauspost/compress v1.18.4
|
github.com/klauspost/compress v1.18.5
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0
|
github.com/klauspost/cpuid/v2 v2.3.0
|
||||||
github.com/mholt/acmez/v3 v3.1.4
|
github.com/mholt/acmez/v3 v3.1.6
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/quic-go/quic-go v0.59.0
|
github.com/quic-go/quic-go v0.59.1
|
||||||
github.com/smallstep/certificates v0.30.0-rc2.0.20260211214201-20608299c29c
|
github.com/smallstep/certificates v0.30.2
|
||||||
github.com/smallstep/nosql v0.7.0
|
github.com/smallstep/nosql v0.8.0
|
||||||
github.com/smallstep/truststore v0.13.0
|
github.com/smallstep/truststore v0.13.0
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/pflag v1.0.10
|
github.com/spf13/pflag v1.0.10
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747
|
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747
|
||||||
github.com/yuin/goldmark v1.7.16
|
github.com/yuin/goldmark v1.8.2
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||||
|
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0
|
||||||
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0
|
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
|
||||||
go.opentelemetry.io/contrib/propagators/autoprop v0.65.0
|
go.opentelemetry.io/contrib/propagators/autoprop v0.65.0
|
||||||
go.opentelemetry.io/otel v1.40.0
|
go.opentelemetry.io/otel v1.43.0
|
||||||
go.opentelemetry.io/otel/sdk v1.40.0
|
go.opentelemetry.io/otel/sdk v1.43.0
|
||||||
go.step.sm/crypto v0.76.0
|
go.opentelemetry.io/otel/sdk/metric v1.43.0
|
||||||
|
go.step.sm/crypto v0.77.1
|
||||||
go.uber.org/automaxprocs v1.6.0
|
go.uber.org/automaxprocs v1.6.0
|
||||||
go.uber.org/zap v1.27.1
|
go.uber.org/zap v1.27.1
|
||||||
go.uber.org/zap/exp v0.3.0
|
go.uber.org/zap/exp v0.3.0
|
||||||
golang.org/x/crypto v0.48.0
|
golang.org/x/crypto v0.50.0
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541
|
||||||
golang.org/x/net v0.50.0
|
golang.org/x/net v0.53.0
|
||||||
golang.org/x/sync v0.19.0
|
golang.org/x/sync v0.20.0
|
||||||
golang.org/x/term v0.40.0
|
golang.org/x/term v0.42.0
|
||||||
golang.org/x/time v0.14.0
|
golang.org/x/time v0.15.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cel.dev/expr v0.25.1 // indirect
|
cel.dev/expr v0.25.1 // indirect
|
||||||
cloud.google.com/go/auth v0.18.1 // indirect
|
cloud.google.com/go/auth v0.18.2 // indirect
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||||
dario.cat/mergo v1.0.2 // indirect
|
dario.cat/mergo v1.0.2 // indirect
|
||||||
@@ -62,16 +64,16 @@ require (
|
|||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||||
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect
|
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect
|
||||||
github.com/google/go-tpm v0.9.8 // indirect
|
github.com/google/go-tpm v0.9.8 // indirect
|
||||||
github.com/google/go-tspi v0.3.0 // indirect
|
github.com/google/go-tspi v0.3.0 // indirect
|
||||||
github.com/google/s2a-go v0.1.9 // indirect
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
github.com/googleapis/gax-go/v2 v2.18.0 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
github.com/jackc/pgx/v5 v5.9.2 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||||
@@ -87,39 +89,37 @@ require (
|
|||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 // indirect
|
go.opentelemetry.io/contrib/propagators/aws v1.43.0 // indirect
|
||||||
go.opentelemetry.io/contrib/propagators/aws v1.40.0 // indirect
|
go.opentelemetry.io/contrib/propagators/b3 v1.43.0 // indirect
|
||||||
go.opentelemetry.io/contrib/propagators/b3 v1.40.0 // indirect
|
go.opentelemetry.io/contrib/propagators/jaeger v1.43.0 // indirect
|
||||||
go.opentelemetry.io/contrib/propagators/jaeger v1.40.0 // indirect
|
go.opentelemetry.io/contrib/propagators/ot v1.43.0 // indirect
|
||||||
go.opentelemetry.io/contrib/propagators/ot v1.40.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
|
go.opentelemetry.io/otel/exporters/prometheus v0.65.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.62.0 // indirect
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 // indirect
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 // indirect
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 // indirect
|
go.opentelemetry.io/otel/log v0.19.0 // indirect
|
||||||
go.opentelemetry.io/otel/log v0.16.0 // indirect
|
go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk/log v0.16.0 // indirect
|
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
|
|
||||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||||
golang.org/x/oauth2 v0.34.0 // indirect
|
golang.org/x/oauth2 v0.36.0 // indirect
|
||||||
google.golang.org/api v0.265.0 // indirect
|
google.golang.org/api v0.271.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
|
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.2.0 // indirect
|
||||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.3.1 // indirect
|
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash v1.1.0 // indirect
|
github.com/cespare/xxhash v1.1.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0
|
github.com/cespare/xxhash/v2 v2.3.0
|
||||||
@@ -133,19 +133,19 @@ require (
|
|||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||||
github.com/golang/protobuf v1.5.4 // indirect
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
github.com/huandu/xstrings v1.5.0 // indirect
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/libdns/libdns v1.1.1
|
github.com/libdns/libdns v1.1.1
|
||||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
github.com/miekg/dns v1.1.70 // indirect
|
github.com/miekg/dns v1.1.72 // 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
|
||||||
@@ -153,7 +153,7 @@ require (
|
|||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2
|
github.com/prometheus/client_model v0.6.2
|
||||||
github.com/prometheus/common v0.67.5 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
github.com/prometheus/procfs v0.19.2 // indirect
|
github.com/prometheus/procfs v0.20.1 // indirect
|
||||||
github.com/rs/xid v1.6.0 // indirect
|
github.com/rs/xid v1.6.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.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
@@ -162,17 +162,17 @@ require (
|
|||||||
github.com/slackhq/nebula v1.10.3 // indirect
|
github.com/slackhq/nebula v1.10.3 // indirect
|
||||||
github.com/spf13/cast v1.7.0 // indirect
|
github.com/spf13/cast v1.7.0 // indirect
|
||||||
github.com/urfave/cli v1.22.17 // indirect
|
github.com/urfave/cli v1.22.17 // indirect
|
||||||
go.etcd.io/bbolt v1.3.10 // indirect
|
go.etcd.io/bbolt v1.4.3 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.40.0
|
go.opentelemetry.io/otel/trace v1.43.0
|
||||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/mod v0.33.0 // indirect
|
golang.org/x/mod v0.35.0 // indirect
|
||||||
golang.org/x/sys v0.41.0
|
golang.org/x/sys v0.43.0
|
||||||
golang.org/x/text v0.34.0
|
golang.org/x/text v0.36.0 // indirect
|
||||||
golang.org/x/tools v0.42.0 // indirect
|
golang.org/x/tools v0.44.0 // indirect
|
||||||
google.golang.org/grpc v1.78.0 // indirect
|
google.golang.org/grpc v1.80.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
howett.net/plist v1.0.0 // indirect
|
howett.net/plist v1.0.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,38 +2,40 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
|||||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||||
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
|
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
|
||||||
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
|
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||||
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
|
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
|
||||||
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
|
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
|
||||||
cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ=
|
cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU=
|
||||||
cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk=
|
cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58=
|
||||||
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
|
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
|
||||||
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
|
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
|
||||||
|
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
|
||||||
|
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
|
||||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||||
filippo.io/bigmod v0.1.0 h1:UNzDk7y9ADKST+axd9skUpBQeW7fG2KrTZyOE4uGQy8=
|
filippo.io/bigmod v0.1.0 h1:UNzDk7y9ADKST+axd9skUpBQeW7fG2KrTZyOE4uGQy8=
|
||||||
filippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI=
|
filippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI=
|
||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
|
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
|
||||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/DeRuina/timberjack v1.3.9 h1:6UXZ1I7ExPGTX/1UNYawR58LlOJUHKBPiYC7WQ91eBo=
|
github.com/DeRuina/timberjack v1.4.2 h1:4bKlzhKdsR+2oNkgef9mqb4n11ICow8VK88RfzJPzN8=
|
||||||
github.com/DeRuina/timberjack v1.3.9/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=
|
github.com/DeRuina/timberjack v1.4.2/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=
|
||||||
github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk=
|
github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk=
|
||||||
github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
|
github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
|
||||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||||
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
|
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||||
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
|
||||||
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
|
||||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||||
@@ -51,40 +53,40 @@ github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmO
|
|||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
|
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
|
||||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
|
github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY=
|
github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY=
|
github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw=
|
github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk=
|
||||||
github.com/aws/aws-sdk-go-v2/service/kms v1.49.5 h1:DKibav4XF66XSeaXcrn9GlWGHos6D/vJ4r7jsK7z5CE=
|
github.com/aws/aws-sdk-go-v2/service/kms v1.50.3 h1:s/zDSG/a/Su9aX+v0Ld9cimUCdkr5FWPmBV8owaEbZY=
|
||||||
github.com/aws/aws-sdk-go-v2/service/kms v1.49.5/go.mod h1:1SdcmEGUEQE1mrU2sIgeHtcMSxHuybhPvuEPANzIDfI=
|
github.com/aws/aws-sdk-go-v2/service/kms v1.50.3/go.mod h1:/iSgiUor15ZuxFGQSTf3lA2FmKxFsQoc2tADOarQBSw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=
|
||||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
|
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk=
|
||||||
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
||||||
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/caddyserver/certmagic v0.25.1 h1:4sIKKbOt5pg6+sL7tEwymE1x2bj6CHr80da1CRRIPbY=
|
github.com/caddyserver/certmagic v0.25.3 h1:mGf5ba8F7xA4c5jfDZZbK2buY1VEkbnwpMDixaju94A=
|
||||||
github.com/caddyserver/certmagic v0.25.1/go.mod h1:VhyvndxtVton/Fo/wKhRoC46Rbw1fmjvQ3GjHYSQTEY=
|
github.com/caddyserver/certmagic v0.25.3/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA=
|
||||||
github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=
|
github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=
|
||||||
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||||
github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0=
|
github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0=
|
||||||
@@ -149,15 +151,15 @@ github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
|||||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||||
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
||||||
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
@@ -166,8 +168,8 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
|||||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||||
github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo=
|
github.com/google/cel-go v0.28.0 h1:KjSWstCpz/MN5t4a8gnGJNIYUsJRpdi/r97xWDphIQc=
|
||||||
github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw=
|
github.com/google/cel-go v0.28.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
|
||||||
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
|
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
|
||||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
|
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
|
||||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
|
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
|
||||||
@@ -185,12 +187,12 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
|||||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
|
||||||
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
|
github.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI=
|
||||||
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
|
github.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
@@ -201,16 +203,16 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
|||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
@@ -223,6 +225,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU=
|
||||||
|
github.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk=
|
||||||
|
github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYqCLCnj/U=
|
||||||
|
github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ=
|
||||||
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
|
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
|
||||||
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
@@ -234,10 +240,10 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||||
github.com/mholt/acmez/v3 v3.1.4 h1:DyzZe/RnAzT3rpZj/2Ii5xZpiEvvYk3cQEN/RmqxwFQ=
|
github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk=
|
||||||
github.com/mholt/acmez/v3 v3.1.4/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY=
|
||||||
github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
|
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||||
github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
@@ -270,12 +276,12 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU
|
|||||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
|
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
|
||||||
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
|
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
|
||||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
|
||||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||||
@@ -295,16 +301,16 @@ github.com/slackhq/nebula v1.10.3 h1:EstYj8ODEcv6T0R9X5BVq1zgWZnyU5gtPzk99QF1PMU
|
|||||||
github.com/slackhq/nebula v1.10.3/go.mod h1:IL5TUQm4x9IFx2kCKPYm1gP47pwd5b8QGnnBH2RHnvs=
|
github.com/slackhq/nebula v1.10.3/go.mod h1:IL5TUQm4x9IFx2kCKPYm1gP47pwd5b8QGnnBH2RHnvs=
|
||||||
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
|
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
|
||||||
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
|
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
|
||||||
github.com/smallstep/certificates v0.30.0-rc2.0.20260211214201-20608299c29c h1:XQpX0IPYUAoJ661YlgfOJmY48ZOhIbglw4E2gw9mcyc=
|
github.com/smallstep/certificates v0.30.2 h1:1G3xBi8sJ740iA1mMPW2Svv7EIZKJ4Zf/iQtA5QlN0Y=
|
||||||
github.com/smallstep/certificates v0.30.0-rc2.0.20260211214201-20608299c29c/go.mod h1:75NRLmYJq6ZcCb8ApJc+W1eL4oMYwjeufMJDHpv4rx4=
|
github.com/smallstep/certificates v0.30.2/go.mod h1:oyaE/aEYUGDr+YiCZLAxxP22bOQqcSHTeDgp8Vv2rlY=
|
||||||
github.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k=
|
github.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k=
|
||||||
github.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y=
|
github.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y=
|
||||||
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4=
|
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4=
|
||||||
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
|
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
|
||||||
github.com/smallstep/linkedca v0.25.0 h1:txT9QHGbCsJq0MhAghBq7qhurGY727tQuqUi+n4BVBo=
|
github.com/smallstep/linkedca v0.25.0 h1:txT9QHGbCsJq0MhAghBq7qhurGY727tQuqUi+n4BVBo=
|
||||||
github.com/smallstep/linkedca v0.25.0/go.mod h1:Q3jVAauFKNlF86W5/RFtgQeyDKz98GL/KN3KG4mJOvc=
|
github.com/smallstep/linkedca v0.25.0/go.mod h1:Q3jVAauFKNlF86W5/RFtgQeyDKz98GL/KN3KG4mJOvc=
|
||||||
github.com/smallstep/nosql v0.7.0 h1:YiWC9ZAHcrLCrayfaF+QJUv16I2bZ7KdLC3RpJcnAnE=
|
github.com/smallstep/nosql v0.8.0 h1:FBTCUfKPmWYbrozW+RBKu+fnvbn+zr5rVli/XB4Jp4A=
|
||||||
github.com/smallstep/nosql v0.7.0/go.mod h1:H5VnKMCbeq9QA6SRY5iqPylfxLfYcLwvUff3onQ8+HU=
|
github.com/smallstep/nosql v0.8.0/go.mod h1:5dUpNotHLHhOUapP0PLBVVfp3tG1DFC31VRccg+Cqwo=
|
||||||
github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA=
|
github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA=
|
||||||
github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0=
|
github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0=
|
||||||
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 h1:k23+s51sgYix4Zgbvpmy+1ZgXLjr4ZTkBTqXmpnImwA=
|
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 h1:k23+s51sgYix4Zgbvpmy+1ZgXLjr4ZTkBTqXmpnImwA=
|
||||||
@@ -353,8 +359,8 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY
|
|||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
||||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||||
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
||||||
@@ -363,12 +369,12 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
|||||||
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
|
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
|
||||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||||
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
|
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||||
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
|
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 h1:I/7S/yWobR3QHFLqHsJ8QOndoiFsj1VgHpQiq43KlUI=
|
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 h1:w3zlHYETbDwXyWHZlyyR58ZC39XGi8rAhkBgUgJ9d5w=
|
||||||
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0/go.mod h1:jPF6gn3y1E+nozCAEQj3c6NZ8KY+tvAgSVfvoOJUFac=
|
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0/go.mod h1:GR/mClR2nn7vE8RLwxKjoBNg+QtgdDhRzxVa93koy5o=
|
||||||
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 h1:2gApdml7SznX9szEKFjKjM4qGcGSvAybYLBY319XG3g=
|
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 h1:2gApdml7SznX9szEKFjKjM4qGcGSvAybYLBY319XG3g=
|
||||||
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0/go.mod h1:0QqAGlbHXhmPYACG3n5hNzO5DnEqqtg4VcK5pr22RI0=
|
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0/go.mod h1:0QqAGlbHXhmPYACG3n5hNzO5DnEqqtg4VcK5pr22RI0=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||||
@@ -377,56 +383,56 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb
|
|||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||||
go.opentelemetry.io/contrib/propagators/autoprop v0.65.0 h1:kTaCycF9Xkm8VBBvH0rJ4wFeRjtIV55Erk3uuVsIs5s=
|
go.opentelemetry.io/contrib/propagators/autoprop v0.65.0 h1:kTaCycF9Xkm8VBBvH0rJ4wFeRjtIV55Erk3uuVsIs5s=
|
||||||
go.opentelemetry.io/contrib/propagators/autoprop v0.65.0/go.mod h1:rooPzAbXfxMX9fsPJjmOBg2SN4RhFEV8D7cfGK+N3tE=
|
go.opentelemetry.io/contrib/propagators/autoprop v0.65.0/go.mod h1:rooPzAbXfxMX9fsPJjmOBg2SN4RhFEV8D7cfGK+N3tE=
|
||||||
go.opentelemetry.io/contrib/propagators/aws v1.40.0 h1:4VIrh75jW4RTimUNx1DSk+6H9/nDr1FvmKoOVDh3K04=
|
go.opentelemetry.io/contrib/propagators/aws v1.43.0 h1:EwnsB3cXRLAh7/Nr/9rMuGw73nfb3z6uAvVDjRrbeUg=
|
||||||
go.opentelemetry.io/contrib/propagators/aws v1.40.0/go.mod h1:B0dCov9KNQGlut3T8wZZjDnLXEXdBroM7bFsHh/gRos=
|
go.opentelemetry.io/contrib/propagators/aws v1.43.0/go.mod h1:CJjTym6F87tEdm61Qvnz5xrV8vKlH4C92djiqcn62k8=
|
||||||
go.opentelemetry.io/contrib/propagators/b3 v1.40.0 h1:xariChe8OOVF3rNlfzGFgQc61npQmXhzZj/i82mxMfg=
|
go.opentelemetry.io/contrib/propagators/b3 v1.43.0 h1:CETqV3QLLPTy5yNrqyMr41VnAOOD4lsRved7n4QG00A=
|
||||||
go.opentelemetry.io/contrib/propagators/b3 v1.40.0/go.mod h1:72WvbdxbOfXaELEQfonFfOL6osvcVjI7uJEE8C2nkrs=
|
go.opentelemetry.io/contrib/propagators/b3 v1.43.0/go.mod h1:Q4mCiCdziYzpNR0g+6UqVotAlCDZdzz6L8jwY4knOrw=
|
||||||
go.opentelemetry.io/contrib/propagators/jaeger v1.40.0 h1:aXl9uobjJs5vquMLt9ZkI/3zIuz8XQ3TqOKSWx0/xdU=
|
go.opentelemetry.io/contrib/propagators/jaeger v1.43.0 h1:peiLMz1+aqJE+3L4mOVtR9wlmv+yh/JVYXCBjqmzJJE=
|
||||||
go.opentelemetry.io/contrib/propagators/jaeger v1.40.0/go.mod h1:ioMePqe6k6c/ovXSkmkMr1mbN5qRBGJxNTVop7/2XO0=
|
go.opentelemetry.io/contrib/propagators/jaeger v1.43.0/go.mod h1:Agvif+4A8p/3UtZzJ0MCcDEuQwgtrzM71DueU41DCs8=
|
||||||
go.opentelemetry.io/contrib/propagators/ot v1.40.0 h1:Lon8J5SPmWaL1Ko2TIlCNHJ42/J1b5XbJlgJaE/9m7I=
|
go.opentelemetry.io/contrib/propagators/ot v1.43.0 h1:Hh1HahlGc81AOE7siqi1tVOlbanY/UxMMWedpb0d5oQ=
|
||||||
go.opentelemetry.io/contrib/propagators/ot v1.40.0/go.mod h1:dKWtJTlp1Yj+8Cneye5idO46eRPIbi23qVuJYKjNnvY=
|
go.opentelemetry.io/contrib/propagators/ot v1.43.0/go.mod h1:58MlyS7lghzYvAm5LN9gGmZpCMQEMB5vpZp9SRgOyE4=
|
||||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 h1:ZVg+kCXxd9LtAaQNKBxAvJ5NpMf7LpvEr4MIZqb0TMQ=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0/go.mod h1:hh0tMeZ75CCXrHd9OXRYxTlCAdxcXioWHFIpYw2rZu8=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.62.0 h1:krvC4JMfIOVdEuNPTtQ0ZjCiXrybhv+uOHMfHRmnvVo=
|
go.opentelemetry.io/otel/exporters/prometheus v0.65.0 h1:jOveH/b4lU9HT7y+Gfamf18BqlOuz2PWEvs8yM7Q6XE=
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.62.0/go.mod h1:fgOE6FM/swEnsVQCqCnbOfRV4tOnWPg7bVeo4izBuhQ=
|
go.opentelemetry.io/otel/exporters/prometheus v0.65.0/go.mod h1:i1P8pcumauPtUI4YNopea1dhzEMuEqWP1xoUZDylLHo=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 h1:ivlbaajBWJqhcCPniDqDJmRwj4lc6sRT+dCAVKNmxlQ=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 h1:GJkybS+crDMdExT/BUNCEgfrmfboztcS6PhvSo88HKM=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0/go.mod h1:u/G56dEKDDwXNCVLsbSrllB2o8pbtFLUC4HpR66r2dc=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0/go.mod h1:NuAyxRYIG2lKX3YQkB+83StTxM7s52PUUkRRiC0wnYI=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ=
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8=
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU=
|
||||||
go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4=
|
go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4=
|
||||||
go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes=
|
go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk=
|
||||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||||
go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI=
|
go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko=
|
||||||
go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4=
|
go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg=
|
||||||
go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4=
|
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk=
|
||||||
go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y=
|
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||||
go.step.sm/crypto v0.76.0 h1:K23BSaeoiY7Y5dvvijTeYC9EduDBetNwQYMBwMhi1aA=
|
go.step.sm/crypto v0.77.1 h1:4EEqfKdv0egQ1lqz2RhnU8Jv6QgXZfrgoxWMqJF9aDs=
|
||||||
go.step.sm/crypto v0.76.0/go.mod h1:PXYJdKkK8s+GHLwLguFaLxHNAFsFL3tL1vSBrYfey5k=
|
go.step.sm/crypto v0.77.1/go.mod h1:U/SsmEm80mNnfD5WIkbhuW/B1eFp3fgFvdXyDLpU1AQ=
|
||||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
@@ -439,8 +445,8 @@ go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
|||||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
|
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
|
||||||
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
||||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
@@ -450,10 +456,10 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
|||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99 h1:CH0o4/bZX6KIUCjjgjmtNtfM/kXSkTYlzTOB9vZF45g=
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 h1:FmKxj9ocLKn45jiR2jQMwCVhDvaK7fKQFzfuT9GvyK8=
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18=
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
@@ -461,8 +467,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
@@ -471,10 +477,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
|||||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -482,8 +488,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
|||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -500,8 +506,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -511,8 +517,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
|||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
@@ -522,31 +528,31 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
google.golang.org/api v0.265.0 h1:FZvfUdI8nfmuNrE34aOWFPmLC+qRBEiNm3JdivTvAAU=
|
google.golang.org/api v0.271.0 h1:cIPN4qcUc61jlh7oXu6pwOQqbJW2GqYh5PS6rB2C/JY=
|
||||||
google.golang.org/api v0.265.0/go.mod h1:uAvfEl3SLUj/7n6k+lJutcswVojHPp2Sp08jWCu8hLY=
|
google.golang.org/api v0.271.0/go.mod h1:CGT29bhwkbF+i11qkRUJb2KMKqcJ1hdFceEIRd9u64Q=
|
||||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
|
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d h1:vsOm753cOAMkt76efriTCDKjpCbK18XGHMJHo0JUKhc=
|
||||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
|
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:0oz9d7g9QLSdv9/lgbIjowW1JoxMbxmBVNe8i6tORJI=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
|
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
|
||||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
|
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoggableHTTPHeader makes an HTTP header loggable with zap.Object().
|
||||||
|
// Headers with potentially sensitive information (Cookie, Set-Cookie,
|
||||||
|
// Authorization, and Proxy-Authorization) are logged with empty values.
|
||||||
|
type LoggableHTTPHeader struct {
|
||||||
|
http.Header
|
||||||
|
|
||||||
|
ShouldLogCredentials bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
|
||||||
|
func (h LoggableHTTPHeader) MarshalLogObject(enc zapcore.ObjectEncoder) error {
|
||||||
|
if h.Header == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for key, val := range h.Header {
|
||||||
|
if !h.ShouldLogCredentials {
|
||||||
|
switch strings.ToLower(key) {
|
||||||
|
case "cookie", "set-cookie", "authorization", "proxy-authorization":
|
||||||
|
val = []string{"REDACTED"} // see #5669. I still think ▒▒▒▒ would be cool.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enc.AddArray(key, LoggableStringArray(val))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoggableStringArray makes a slice of strings marshalable for logging.
|
||||||
|
type LoggableStringArray []string
|
||||||
|
|
||||||
|
// MarshalLogArray satisfies the zapcore.ArrayMarshaler interface.
|
||||||
|
func (sa LoggableStringArray) MarshalLogArray(enc zapcore.ArrayEncoder) error {
|
||||||
|
if sa == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, s := range sa {
|
||||||
|
enc.AppendString(s)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards
|
||||||
|
var (
|
||||||
|
_ zapcore.ObjectMarshaler = (*LoggableHTTPHeader)(nil)
|
||||||
|
_ zapcore.ArrayMarshaler = (*LoggableStringArray)(nil)
|
||||||
|
)
|
||||||
@@ -30,10 +30,6 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func reuseUnixSocket(_, _ string) (any, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func listenReusable(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (any, error) {
|
func listenReusable(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (any, error) {
|
||||||
var socketFile *os.File
|
var socketFile *os.File
|
||||||
|
|
||||||
@@ -120,8 +116,8 @@ func listenReusable(ctx context.Context, lnKey string, network, address string,
|
|||||||
// re-wrapped in a new fakeCloseListener each time the listener
|
// re-wrapped in a new fakeCloseListener each time the listener
|
||||||
// is reused. This type is atomic and values must not be copied.
|
// is reused. This type is atomic and values must not be copied.
|
||||||
type fakeCloseListener struct {
|
type fakeCloseListener struct {
|
||||||
closed int32 // accessed atomically; belongs to this struct only
|
closed atomic.Bool
|
||||||
*sharedListener // embedded, so we also become a net.Listener
|
*sharedListener // embedded, so we also become a net.Listener
|
||||||
keepAliveConfig net.KeepAliveConfig
|
keepAliveConfig net.KeepAliveConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +127,7 @@ type canSetKeepAliveConfig interface {
|
|||||||
|
|
||||||
func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
|
func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
|
||||||
// if the listener is already "closed", return error
|
// if the listener is already "closed", return error
|
||||||
if atomic.LoadInt32(&fcl.closed) == 1 {
|
if fcl.closed.Load() {
|
||||||
return nil, fakeClosedErr(fcl)
|
return nil, fakeClosedErr(fcl)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +151,7 @@ func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
|
|||||||
// that we set when Close() was called, and return a non-temporary and
|
// that we set when Close() was called, and return a non-temporary and
|
||||||
// non-timeout error value to the caller, masking the "true" error, so
|
// non-timeout error value to the caller, masking the "true" error, so
|
||||||
// that server loops / goroutines won't retry, linger, and leak
|
// that server loops / goroutines won't retry, linger, and leak
|
||||||
if atomic.LoadInt32(&fcl.closed) == 1 {
|
if fcl.closed.Load() {
|
||||||
// we dereference the sharedListener explicitly even though it's embedded
|
// we dereference the sharedListener explicitly even though it's embedded
|
||||||
// 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
|
||||||
@@ -175,7 +171,7 @@ func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
|
|||||||
// underlying listener. The underlying listener is only closed
|
// underlying listener. The underlying listener is only closed
|
||||||
// if the caller is the last known user of the socket.
|
// if the caller is the last known user of the socket.
|
||||||
func (fcl *fakeCloseListener) Close() error {
|
func (fcl *fakeCloseListener) Close() error {
|
||||||
if atomic.CompareAndSwapInt32(&fcl.closed, 0, 1) {
|
if fcl.closed.CompareAndSwap(false, true) {
|
||||||
// There are two ways I know of to get an Accept()
|
// There are two ways I know of to get an Accept()
|
||||||
// function to return to the server loop that called
|
// function to return to the server loop that called
|
||||||
// it: close the listener, or set a deadline in the
|
// it: close the listener, or set a deadline in the
|
||||||
@@ -238,13 +234,13 @@ func (sl *sharedListener) Destruct() error {
|
|||||||
// fakeClosePacketConn is like fakeCloseListener, but for PacketConns,
|
// fakeClosePacketConn is like fakeCloseListener, but for PacketConns,
|
||||||
// or more specifically, *net.UDPConn
|
// or more specifically, *net.UDPConn
|
||||||
type fakeClosePacketConn struct {
|
type fakeClosePacketConn struct {
|
||||||
closed int32 // accessed atomically; belongs to this struct only
|
closed atomic.Bool
|
||||||
*sharedPacketConn // embedded, so we also become a net.PacketConn; its key is used in Close
|
*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) {
|
func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||||
// if the listener is already "closed", return error
|
// if the listener is already "closed", return error
|
||||||
if atomic.LoadInt32(&fcpc.closed) == 1 {
|
if fcpc.closed.Load() {
|
||||||
return 0, nil, &net.OpError{
|
return 0, nil, &net.OpError{
|
||||||
Op: "readfrom",
|
Op: "readfrom",
|
||||||
Net: fcpc.LocalAddr().Network(),
|
Net: fcpc.LocalAddr().Network(),
|
||||||
@@ -258,7 +254,7 @@ func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
// this server was stopped, so clear the deadline and let
|
// this server was stopped, so clear the deadline and let
|
||||||
// any new server continue reading; but we will exit
|
// any new server continue reading; but we will exit
|
||||||
if atomic.LoadInt32(&fcpc.closed) == 1 {
|
if fcpc.closed.Load() {
|
||||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||||
if err = fcpc.SetReadDeadline(time.Time{}); err != nil {
|
if err = fcpc.SetReadDeadline(time.Time{}); err != nil {
|
||||||
return n, addr, err
|
return n, addr, err
|
||||||
@@ -273,7 +269,7 @@ func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err e
|
|||||||
|
|
||||||
// Close won't close the underlying socket unless there is no more reference, then listenerPool will close it.
|
// Close won't close the underlying socket unless there is no more reference, then listenerPool will close it.
|
||||||
func (fcpc *fakeClosePacketConn) Close() error {
|
func (fcpc *fakeClosePacketConn) Close() error {
|
||||||
if atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) {
|
if fcpc.closed.CompareAndSwap(false, true) {
|
||||||
_ = fcpc.SetReadDeadline(time.Now()) // unblock ReadFrom() calls to kick old servers out of their loops
|
_ = fcpc.SetReadDeadline(time.Now()) // unblock ReadFrom() calls to kick old servers out of their loops
|
||||||
_, _ = listenerPool.Delete(fcpc.sharedPacketConn.key)
|
_, _ = listenerPool.Delete(fcpc.sharedPacketConn.key)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// 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 (!unix || solaris) && !windows
|
||||||
|
|
||||||
|
package caddy
|
||||||
|
|
||||||
|
func reuseUnixSocket(_, _ string) (any, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// 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 windows
|
||||||
|
|
||||||
|
package caddy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errUnixSocketAlreadyInUse = errors.New("unix socket is already in use by another process")
|
||||||
|
|
||||||
|
func reuseUnixSocket(network, addr string) (any, error) {
|
||||||
|
if !IsUnixNetwork(network) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: This is here mainly for proper compatibility, because Unix sockets with abstract names are in an interesting limbo state on Windows:
|
||||||
|
// Go already translates `@` characters to `\0` for Windows: https://github.com/golang/go/blob/65d5c5f6dd8aa7b221cff6ec3f5101ea2e5f3efa/src/syscall/syscall_windows.go#L910
|
||||||
|
// ...but there still is an open issue about the fact that this is not properly supported: https://github.com/microsoft/WSL/issues/4240#issuecomment-620805115
|
||||||
|
// The main issue is that the original announcement proclaimed support for this feature, but it was (apparently) never implemented: https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/
|
||||||
|
isAbstractUnixSocket := strings.HasPrefix(addr, "@")
|
||||||
|
|
||||||
|
if isAbstractUnixSocket {
|
||||||
|
// Abstract Unix sockets do not require us to remove stale socket files.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Windows, we're using the `fakeCloseListener` wrappers around a single, ever-living listener.
|
||||||
|
// So, if there's an active listener entry in the pool, we're the current owner of the Unix socket file.
|
||||||
|
_, socketBelongsToCurrentProcess := listenerPool.References(listenerKey(network, addr))
|
||||||
|
|
||||||
|
if socketBelongsToCurrentProcess {
|
||||||
|
// Reuse/cleanup is entirely handled by the refcounting mechanism in `listenerPool`.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the socket file does not exist or has no backing server process, this will fail instantly.
|
||||||
|
connection, err := net.DialTimeout("unix", addr, 10*time.Millisecond)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
connection.Close()
|
||||||
|
return nil, fmt.Errorf("cannot reuse socket %v: %w", addr, errUnixSocketAlreadyInUse)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows returns this error code both if the socket file does not exist and if it isn't backed by a server process anymore.
|
||||||
|
// See: https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2#wsaeconnrefused
|
||||||
|
const WSAECONNREFUSED syscall.Errno = 10061
|
||||||
|
|
||||||
|
var errno syscall.Errno
|
||||||
|
hasNoListeningServerProcess := errors.As(err, &errno) && errno == WSAECONNREFUSED
|
||||||
|
|
||||||
|
if !hasNoListeningServerProcess {
|
||||||
|
return nil, fmt.Errorf("cannot reuse socket %v: %w", addr, errUnixSocketAlreadyInUse)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the socket file exists, it hasn't been created by our process, and it seemingly
|
||||||
|
// isn't backed by a server process anymore. Try to delete it so we can bind to it later.
|
||||||
|
err = os.Remove(addr)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return nil, nil
|
||||||
|
} else if errors.Is(err, fs.ErrNotExist) {
|
||||||
|
// Either the file didn't exist in the first place, or it was deleted before we were able to.
|
||||||
|
return nil, nil
|
||||||
|
} else {
|
||||||
|
// We failed to delete the file. Likely, it belongs to another (active) process.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-10
@@ -63,7 +63,7 @@ func reuseUnixSocket(network, addr string) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
atomic.AddInt32(unixSocket.count, 1)
|
unixSocket.count.Add(1)
|
||||||
unixSockets[socketKey] = &unixListener{ln.(*net.UnixListener), socketKey, unixSocket.count}
|
unixSockets[socketKey] = &unixListener{ln.(*net.UnixListener), socketKey, unixSocket.count}
|
||||||
|
|
||||||
case *unixConn:
|
case *unixConn:
|
||||||
@@ -71,7 +71,7 @@ func reuseUnixSocket(network, addr string) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
atomic.AddInt32(unixSocket.count, 1)
|
unixSocket.count.Add(1)
|
||||||
unixSockets[socketKey] = &unixConn{pc.(*net.UnixConn), socketKey, unixSocket.count}
|
unixSockets[socketKey] = &unixConn{pc.(*net.UnixConn), socketKey, unixSocket.count}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,8 +165,9 @@ func listenReusable(ctx context.Context, lnKey string, network, address string,
|
|||||||
if !fd {
|
if !fd {
|
||||||
// TODO: Not 100% sure this is necessary, but we do this for net.UnixListener, so...
|
// TODO: Not 100% sure this is necessary, but we do this for net.UnixListener, so...
|
||||||
if unix, ok := ln.(*net.UnixConn); ok {
|
if unix, ok := ln.(*net.UnixConn); ok {
|
||||||
one := int32(1)
|
cnt := new(atomic.Int32)
|
||||||
ln = &unixConn{unix, lnKey, &one}
|
cnt.Store(1)
|
||||||
|
ln = &unixConn{unix, lnKey, cnt}
|
||||||
unixSockets[lnKey] = ln.(*unixConn)
|
unixSockets[lnKey] = ln.(*unixConn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,8 +182,9 @@ func listenReusable(ctx context.Context, lnKey string, network, address string,
|
|||||||
// (we do our own "unlink on close" -- not required, but more tidy)
|
// (we do our own "unlink on close" -- not required, but more tidy)
|
||||||
if unix, ok := ln.(*net.UnixListener); ok {
|
if unix, ok := ln.(*net.UnixListener); ok {
|
||||||
unix.SetUnlinkOnClose(false)
|
unix.SetUnlinkOnClose(false)
|
||||||
one := int32(1)
|
cnt := new(atomic.Int32)
|
||||||
ln = &unixListener{unix, lnKey, &one}
|
cnt.Store(1)
|
||||||
|
ln = &unixListener{unix, lnKey, cnt}
|
||||||
unixSockets[lnKey] = ln.(*unixListener)
|
unixSockets[lnKey] = ln.(*unixListener)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,11 +218,11 @@ func reusePort(network, address string, conn syscall.RawConn) error {
|
|||||||
type unixListener struct {
|
type unixListener struct {
|
||||||
*net.UnixListener
|
*net.UnixListener
|
||||||
mapKey string
|
mapKey string
|
||||||
count *int32 // accessed atomically
|
count *atomic.Int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uln *unixListener) Close() error {
|
func (uln *unixListener) Close() error {
|
||||||
newCount := atomic.AddInt32(uln.count, -1)
|
newCount := uln.count.Add(-1)
|
||||||
if newCount == 0 {
|
if newCount == 0 {
|
||||||
file, err := uln.File()
|
file, err := uln.File()
|
||||||
var name string
|
var name string
|
||||||
@@ -242,11 +244,11 @@ func (uln *unixListener) Close() error {
|
|||||||
type unixConn struct {
|
type unixConn struct {
|
||||||
*net.UnixConn
|
*net.UnixConn
|
||||||
mapKey string
|
mapKey string
|
||||||
count *int32 // accessed atomically
|
count *atomic.Int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *unixConn) Close() error {
|
func (uc *unixConn) Close() error {
|
||||||
newCount := atomic.AddInt32(uc.count, -1)
|
newCount := uc.count.Add(-1)
|
||||||
if newCount == 0 {
|
if newCount == 0 {
|
||||||
file, err := uc.File()
|
file, err := uc.File()
|
||||||
var name string
|
var name string
|
||||||
|
|||||||
+30
-97
@@ -38,10 +38,6 @@ import (
|
|||||||
"github.com/caddyserver/caddy/v2/internal"
|
"github.com/caddyserver/caddy/v2/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
// listenFdsStart is the first file descriptor number for systemd socket activation.
|
|
||||||
// File descriptors 0, 1, 2 are reserved for stdin, stdout, stderr.
|
|
||||||
const listenFdsStart = 3
|
|
||||||
|
|
||||||
// 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().
|
||||||
@@ -233,7 +229,7 @@ func (na NetworkAddress) JoinHostPort(offset uint) string {
|
|||||||
func (na NetworkAddress) Expand() []NetworkAddress {
|
func (na NetworkAddress) Expand() []NetworkAddress {
|
||||||
size := na.PortRangeSize()
|
size := na.PortRangeSize()
|
||||||
addrs := make([]NetworkAddress, size)
|
addrs := make([]NetworkAddress, size)
|
||||||
for portOffset := uint(0); portOffset < size; portOffset++ {
|
for portOffset := range size {
|
||||||
addrs[portOffset] = na.At(portOffset)
|
addrs[portOffset] = na.At(portOffset)
|
||||||
}
|
}
|
||||||
return addrs
|
return addrs
|
||||||
@@ -309,64 +305,6 @@ func IsFdNetwork(netw string) bool {
|
|||||||
return strings.HasPrefix(netw, "fd")
|
return strings.HasPrefix(netw, "fd")
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFdByName returns the file descriptor number for the given
|
|
||||||
// socket name from systemd's LISTEN_FDNAMES environment variable.
|
|
||||||
// Socket names are provided by systemd via socket activation.
|
|
||||||
//
|
|
||||||
// The name can optionally include an index to handle multiple sockets
|
|
||||||
// with the same name: "web:0" for first, "web:1" for second, etc.
|
|
||||||
// If no index is specified, defaults to index 0 (first occurrence).
|
|
||||||
func getFdByName(nameWithIndex string) (int, error) {
|
|
||||||
if nameWithIndex == "" {
|
|
||||||
return 0, fmt.Errorf("socket name cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
fdNamesStr := os.Getenv("LISTEN_FDNAMES")
|
|
||||||
if fdNamesStr == "" {
|
|
||||||
return 0, fmt.Errorf("LISTEN_FDNAMES environment variable not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse name and optional index
|
|
||||||
parts := strings.Split(nameWithIndex, ":")
|
|
||||||
if len(parts) > 2 {
|
|
||||||
return 0, fmt.Errorf("invalid socket name format '%s': too many colons", nameWithIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
name := parts[0]
|
|
||||||
targetIndex := 0
|
|
||||||
|
|
||||||
if len(parts) > 1 {
|
|
||||||
var err error
|
|
||||||
targetIndex, err = strconv.Atoi(parts[1])
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("invalid socket index '%s': %v", parts[1], err)
|
|
||||||
}
|
|
||||||
if targetIndex < 0 {
|
|
||||||
return 0, fmt.Errorf("socket index cannot be negative: %d", targetIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the socket names
|
|
||||||
names := strings.Split(fdNamesStr, ":")
|
|
||||||
|
|
||||||
// Find the Nth occurrence of the requested name
|
|
||||||
matchCount := 0
|
|
||||||
for i, fdName := range names {
|
|
||||||
if fdName == name {
|
|
||||||
if matchCount == targetIndex {
|
|
||||||
return listenFdsStart + i, nil
|
|
||||||
}
|
|
||||||
matchCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if matchCount == 0 {
|
|
||||||
return 0, fmt.Errorf("socket name '%s' not found in LISTEN_FDNAMES", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, fmt.Errorf("socket name '%s' found %d times, but index %d requested", name, matchCount, targetIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseNetworkAddress parses addr into its individual
|
// ParseNetworkAddress parses addr into its individual
|
||||||
// components. The input string is expected to be of
|
// components. The input string is expected to be of
|
||||||
// the form "network/host:port-range" where any part is
|
// the form "network/host:port-range" where any part is
|
||||||
@@ -398,27 +336,9 @@ func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort ui
|
|||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
if IsFdNetwork(network) {
|
if IsFdNetwork(network) {
|
||||||
fdAddr := host
|
|
||||||
|
|
||||||
// Handle named socket activation (fdname/name, fdgramname/name)
|
|
||||||
if strings.HasPrefix(network, "fdname") || strings.HasPrefix(network, "fdgramname") {
|
|
||||||
fdNum, err := getFdByName(host)
|
|
||||||
if err != nil {
|
|
||||||
return NetworkAddress{}, fmt.Errorf("named socket activation: %v", err)
|
|
||||||
}
|
|
||||||
fdAddr = strconv.Itoa(fdNum)
|
|
||||||
|
|
||||||
// Normalize network to standard fd/fdgram
|
|
||||||
if strings.HasPrefix(network, "fdname") {
|
|
||||||
network = "fd"
|
|
||||||
} else {
|
|
||||||
network = "fdgram"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NetworkAddress{
|
return NetworkAddress{
|
||||||
Network: network,
|
Network: network,
|
||||||
Host: fdAddr,
|
Host: host,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
var start, end uint64
|
var start, end uint64
|
||||||
@@ -542,7 +462,10 @@ func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config
|
|||||||
sqs := newSharedQUICState(tlsConf)
|
sqs := newSharedQUICState(tlsConf)
|
||||||
// http3.ConfigureTLSConfig only uses this field and tls App sets this field as well
|
// http3.ConfigureTLSConfig only uses this field and tls App sets this field as well
|
||||||
//nolint:gosec
|
//nolint:gosec
|
||||||
quicTlsConfig := &tls.Config{GetConfigForClient: sqs.getConfigForClient}
|
quicTlsConfig := &tls.Config{
|
||||||
|
GetConfigForClient: sqs.getConfigForClient,
|
||||||
|
GetEncryptedClientHelloKeys: sqs.getEncryptedClientHelloKeys,
|
||||||
|
}
|
||||||
// Require clients to verify their source address when we're handling more than 1000 handshakes per second.
|
// Require clients to verify their source address when we're handling more than 1000 handshakes per second.
|
||||||
// TODO: make tunable?
|
// TODO: make tunable?
|
||||||
limiter := rate.NewLimiter(1000, 1000)
|
limiter := rate.NewLimiter(1000, 1000)
|
||||||
@@ -592,7 +515,7 @@ func ListenerUsage(network, addr string) int {
|
|||||||
// contextAndCancelFunc groups context and its cancelFunc
|
// contextAndCancelFunc groups context and its cancelFunc
|
||||||
type contextAndCancelFunc struct {
|
type contextAndCancelFunc struct {
|
||||||
context.Context
|
context.Context
|
||||||
context.CancelFunc
|
context.CancelCauseFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// sharedQUICState manages GetConfigForClient
|
// sharedQUICState manages GetConfigForClient
|
||||||
@@ -620,19 +543,29 @@ func (sqs *sharedQUICState) getConfigForClient(ch *tls.ClientHelloInfo) (*tls.Co
|
|||||||
return sqs.activeTlsConf.GetConfigForClient(ch)
|
return sqs.activeTlsConf.GetConfigForClient(ch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getEncryptedClientHelloKeys is used as tls.Config's GetEncryptedClientHelloKeys field.
|
||||||
|
func (sqs *sharedQUICState) getEncryptedClientHelloKeys(ch *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
|
||||||
|
sqs.rmu.RLock()
|
||||||
|
defer sqs.rmu.RUnlock()
|
||||||
|
if sqs.activeTlsConf.GetEncryptedClientHelloKeys == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return sqs.activeTlsConf.GetEncryptedClientHelloKeys(ch)
|
||||||
|
}
|
||||||
|
|
||||||
// addState adds tls.Config and activeRequests to the map if not present and returns the corresponding context and its cancelFunc
|
// 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 will change
|
// so that when cancelled, the active tls.Config will change
|
||||||
func (sqs *sharedQUICState) addState(tlsConfig *tls.Config) (context.Context, context.CancelFunc) {
|
func (sqs *sharedQUICState) addState(tlsConfig *tls.Config) (context.Context, context.CancelCauseFunc) {
|
||||||
sqs.rmu.Lock()
|
sqs.rmu.Lock()
|
||||||
defer sqs.rmu.Unlock()
|
defer sqs.rmu.Unlock()
|
||||||
|
|
||||||
if cacc, ok := sqs.tlsConfs[tlsConfig]; ok {
|
if cacc, ok := sqs.tlsConfs[tlsConfig]; ok {
|
||||||
return cacc.Context, cacc.CancelFunc
|
return cacc.Context, cacc.CancelCauseFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancelCause(context.Background())
|
||||||
wrappedCancel := func() {
|
wrappedCancel := func(cause error) {
|
||||||
cancel()
|
cancel(cause)
|
||||||
|
|
||||||
sqs.rmu.Lock()
|
sqs.rmu.Lock()
|
||||||
defer sqs.rmu.Unlock()
|
defer sqs.rmu.Unlock()
|
||||||
@@ -688,13 +621,13 @@ func fakeClosedErr(l interface{ Addr() net.Addr }) error {
|
|||||||
// indicating that it is pretending to be closed so that the
|
// indicating that it is pretending to be closed so that the
|
||||||
// server using it can terminate, while the underlying
|
// server using it can terminate, while the underlying
|
||||||
// socket is actually left open.
|
// socket is actually left open.
|
||||||
var errFakeClosed = fmt.Errorf("listener 'closed' 😉")
|
var errFakeClosed = fmt.Errorf("QUIC listener 'closed' 😉")
|
||||||
|
|
||||||
type fakeCloseQuicListener struct {
|
type fakeCloseQuicListener struct {
|
||||||
closed int32 // accessed atomically; belongs to this struct only
|
closed atomic.Int32
|
||||||
*sharedQuicListener // embedded, so we also become a quic.EarlyListener
|
*sharedQuicListener // embedded, so we also become a quic.EarlyListener
|
||||||
context context.Context
|
context context.Context
|
||||||
contextCancel context.CancelFunc
|
contextCancel context.CancelCauseFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// Currently Accept ignores the passed context, however a situation where
|
// Currently Accept ignores the passed context, however a situation where
|
||||||
@@ -709,16 +642,16 @@ func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (*quic.Conn, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if the listener is "closed", return a fake closed error instead
|
// if the listener is "closed", return a fake closed error instead
|
||||||
if atomic.LoadInt32(&fcql.closed) == 1 && errors.Is(err, context.Canceled) {
|
if fcql.closed.Load() == 1 && errors.Is(err, context.Canceled) {
|
||||||
return nil, fakeClosedErr(fcql)
|
return nil, fakeClosedErr(fcql)
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fcql *fakeCloseQuicListener) Close() error {
|
func (fcql *fakeCloseQuicListener) Close() error {
|
||||||
if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) {
|
if fcql.closed.CompareAndSwap(0, 1) {
|
||||||
fcql.contextCancel()
|
fcql.contextCancel(errFakeClosed)
|
||||||
} else if atomic.CompareAndSwapInt32(&fcql.closed, 1, 2) {
|
} else if fcql.closed.CompareAndSwap(1, 2) {
|
||||||
_, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
|
_, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+58
-284
@@ -15,7 +15,7 @@
|
|||||||
package caddy
|
package caddy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"crypto/tls"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -176,6 +176,63 @@ func TestJoinNetworkAddress(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSharedQUICStateGetEncryptedClientHelloKeys(t *testing.T) {
|
||||||
|
hello := &tls.ClientHelloInfo{ServerName: "example.com"}
|
||||||
|
initialKeys := []tls.EncryptedClientHelloKey{{Config: []byte("initial"), PrivateKey: []byte("initial-key")}}
|
||||||
|
updatedKeys := []tls.EncryptedClientHelloKey{{Config: []byte("updated"), PrivateKey: []byte("updated-key")}}
|
||||||
|
|
||||||
|
initialConfig := &tls.Config{
|
||||||
|
GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
GetEncryptedClientHelloKeys: func(*tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
|
||||||
|
return initialKeys, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sqs := newSharedQUICState(initialConfig)
|
||||||
|
|
||||||
|
keys, err := sqs.getEncryptedClientHelloKeys(hello)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("getting initial ECH keys: %v", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(keys, initialKeys) {
|
||||||
|
t.Fatalf("unexpected initial ECH keys: got %#v, want %#v", keys, initialKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedConfig := &tls.Config{
|
||||||
|
GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
GetEncryptedClientHelloKeys: func(*tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
|
||||||
|
return updatedKeys, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, cancel := sqs.addState(updatedConfig)
|
||||||
|
sqs.rmu.Lock()
|
||||||
|
sqs.activeTlsConf = updatedConfig
|
||||||
|
sqs.rmu.Unlock()
|
||||||
|
|
||||||
|
keys, err = sqs.getEncryptedClientHelloKeys(hello)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("getting updated ECH keys: %v", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(keys, updatedKeys) {
|
||||||
|
t.Fatalf("unexpected updated ECH keys: got %#v, want %#v", keys, updatedKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(nil)
|
||||||
|
|
||||||
|
keys, err = sqs.getEncryptedClientHelloKeys(hello)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("getting restored ECH keys: %v", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(keys, initialKeys) {
|
||||||
|
t.Fatalf("unexpected restored ECH keys: got %#v, want %#v", keys, initialKeys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseNetworkAddress(t *testing.T) {
|
func TestParseNetworkAddress(t *testing.T) {
|
||||||
for i, tc := range []struct {
|
for i, tc := range []struct {
|
||||||
input string
|
input string
|
||||||
@@ -653,286 +710,3 @@ func TestSplitUnixSocketPermissionsBits(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestGetFdByName tests the getFdByName function for systemd socket activation.
|
|
||||||
func TestGetFdByName(t *testing.T) {
|
|
||||||
// Save original environment
|
|
||||||
originalFdNames := os.Getenv("LISTEN_FDNAMES")
|
|
||||||
|
|
||||||
// Restore environment after test
|
|
||||||
defer func() {
|
|
||||||
if originalFdNames != "" {
|
|
||||||
os.Setenv("LISTEN_FDNAMES", originalFdNames)
|
|
||||||
} else {
|
|
||||||
os.Unsetenv("LISTEN_FDNAMES")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fdNames string
|
|
||||||
socketName string
|
|
||||||
expectedFd int
|
|
||||||
expectError bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "simple http socket",
|
|
||||||
fdNames: "http",
|
|
||||||
socketName: "http",
|
|
||||||
expectedFd: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple different sockets - first",
|
|
||||||
fdNames: "http:https:dns",
|
|
||||||
socketName: "http",
|
|
||||||
expectedFd: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple different sockets - second",
|
|
||||||
fdNames: "http:https:dns",
|
|
||||||
socketName: "https",
|
|
||||||
expectedFd: 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple different sockets - third",
|
|
||||||
fdNames: "http:https:dns",
|
|
||||||
socketName: "dns",
|
|
||||||
expectedFd: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "duplicate names - first occurrence (no index)",
|
|
||||||
fdNames: "web:web:api",
|
|
||||||
socketName: "web",
|
|
||||||
expectedFd: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "duplicate names - first occurrence (explicit index 0)",
|
|
||||||
fdNames: "web:web:api",
|
|
||||||
socketName: "web:0",
|
|
||||||
expectedFd: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "duplicate names - second occurrence (index 1)",
|
|
||||||
fdNames: "web:web:api",
|
|
||||||
socketName: "web:1",
|
|
||||||
expectedFd: 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "complex duplicates - first api",
|
|
||||||
fdNames: "web:api:web:api:dns",
|
|
||||||
socketName: "api:0",
|
|
||||||
expectedFd: 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "complex duplicates - second api",
|
|
||||||
fdNames: "web:api:web:api:dns",
|
|
||||||
socketName: "api:1",
|
|
||||||
expectedFd: 6,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "complex duplicates - first web",
|
|
||||||
fdNames: "web:api:web:api:dns",
|
|
||||||
socketName: "web:0",
|
|
||||||
expectedFd: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "complex duplicates - second web",
|
|
||||||
fdNames: "web:api:web:api:dns",
|
|
||||||
socketName: "web:1",
|
|
||||||
expectedFd: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "socket not found",
|
|
||||||
fdNames: "http:https",
|
|
||||||
socketName: "missing",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty socket name",
|
|
||||||
fdNames: "http",
|
|
||||||
socketName: "",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing LISTEN_FDNAMES",
|
|
||||||
fdNames: "",
|
|
||||||
socketName: "http",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "index out of range",
|
|
||||||
fdNames: "web:web",
|
|
||||||
socketName: "web:2",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "negative index",
|
|
||||||
fdNames: "web",
|
|
||||||
socketName: "web:-1",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid index format",
|
|
||||||
fdNames: "web",
|
|
||||||
socketName: "web:abc",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "too many colons",
|
|
||||||
fdNames: "web",
|
|
||||||
socketName: "web:0:extra",
|
|
||||||
expectError: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
// Set up environment
|
|
||||||
if tc.fdNames != "" {
|
|
||||||
os.Setenv("LISTEN_FDNAMES", tc.fdNames)
|
|
||||||
} else {
|
|
||||||
os.Unsetenv("LISTEN_FDNAMES")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test the function
|
|
||||||
fd, err := getFdByName(tc.socketName)
|
|
||||||
|
|
||||||
if tc.expectError {
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Expected error but got none")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
if fd != tc.expectedFd {
|
|
||||||
t.Errorf("Expected FD %d but got %d", tc.expectedFd, fd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestParseNetworkAddressFdName tests parsing of fdname and fdgramname addresses.
|
|
||||||
func TestParseNetworkAddressFdName(t *testing.T) {
|
|
||||||
// Save and restore environment
|
|
||||||
originalFdNames := os.Getenv("LISTEN_FDNAMES")
|
|
||||||
defer func() {
|
|
||||||
if originalFdNames != "" {
|
|
||||||
os.Setenv("LISTEN_FDNAMES", originalFdNames)
|
|
||||||
} else {
|
|
||||||
os.Unsetenv("LISTEN_FDNAMES")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Set up test environment
|
|
||||||
os.Setenv("LISTEN_FDNAMES", "http:https:dns")
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
input string
|
|
||||||
expectAddr NetworkAddress
|
|
||||||
expectErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
input: "fdname/http",
|
|
||||||
expectAddr: NetworkAddress{
|
|
||||||
Network: "fd",
|
|
||||||
Host: "3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdname/https",
|
|
||||||
expectAddr: NetworkAddress{
|
|
||||||
Network: "fd",
|
|
||||||
Host: "4",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdname/dns",
|
|
||||||
expectAddr: NetworkAddress{
|
|
||||||
Network: "fd",
|
|
||||||
Host: "5",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdname/http:0",
|
|
||||||
expectAddr: NetworkAddress{
|
|
||||||
Network: "fd",
|
|
||||||
Host: "3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdname/https:0",
|
|
||||||
expectAddr: NetworkAddress{
|
|
||||||
Network: "fd",
|
|
||||||
Host: "4",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdgramname/http",
|
|
||||||
expectAddr: NetworkAddress{
|
|
||||||
Network: "fdgram",
|
|
||||||
Host: "3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdgramname/https",
|
|
||||||
expectAddr: NetworkAddress{
|
|
||||||
Network: "fdgram",
|
|
||||||
Host: "4",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdgramname/http:0",
|
|
||||||
expectAddr: NetworkAddress{
|
|
||||||
Network: "fdgram",
|
|
||||||
Host: "3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdname/nonexistent",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdgramname/nonexistent",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdname/http:99",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdname/invalid:abc",
|
|
||||||
expectErr: true,
|
|
||||||
},
|
|
||||||
// Test that old fd/N syntax still works
|
|
||||||
{
|
|
||||||
input: "fd/7",
|
|
||||||
expectAddr: NetworkAddress{
|
|
||||||
Network: "fd",
|
|
||||||
Host: "7",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "fdgram/8",
|
|
||||||
expectAddr: NetworkAddress{
|
|
||||||
Network: "fdgram",
|
|
||||||
Host: "8",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, tc := range tests {
|
|
||||||
actualAddr, err := ParseNetworkAddress(tc.input)
|
|
||||||
|
|
||||||
if tc.expectErr && err == nil {
|
|
||||||
t.Errorf("Test %d (%s): Expected error but got none", i, tc.input)
|
|
||||||
}
|
|
||||||
if !tc.expectErr && err != nil {
|
|
||||||
t.Errorf("Test %d (%s): Expected no error but got: %v", i, tc.input, err)
|
|
||||||
}
|
|
||||||
if !tc.expectErr && !reflect.DeepEqual(tc.expectAddr, actualAddr) {
|
|
||||||
t.Errorf("Test %d (%s): Expected %+v but got %+v", i, tc.input, tc.expectAddr, actualAddr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+27
-39
@@ -18,8 +18,8 @@ import (
|
|||||||
"cmp"
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -68,6 +68,7 @@ func init() {
|
|||||||
// `{http.request.orig_uri.path.dir}` | The request's original directory
|
// `{http.request.orig_uri.path.dir}` | The request's original directory
|
||||||
// `{http.request.orig_uri.path.file}` | The request's original filename
|
// `{http.request.orig_uri.path.file}` | The request's original filename
|
||||||
// `{http.request.orig_uri.query}` | The request's original query string (without `?`)
|
// `{http.request.orig_uri.query}` | The request's original query string (without `?`)
|
||||||
|
// `{http.request.orig_uri.prefixed_query}` | The request's original query string with a `?` prefix, if non-empty
|
||||||
// `{http.request.port}` | The port part of the request's Host header
|
// `{http.request.port}` | The port part of the request's Host header
|
||||||
// `{http.request.proto}` | The protocol of the request
|
// `{http.request.proto}` | The protocol of the request
|
||||||
// `{http.request.local.host}` | The host (IP) part of the local address the connection arrived on
|
// `{http.request.local.host}` | The host (IP) part of the local address the connection arrived on
|
||||||
@@ -97,11 +98,15 @@ func init() {
|
|||||||
// `{http.request.tls.client.san.ips.*}` | SAN IP addresses (index optional)
|
// `{http.request.tls.client.san.ips.*}` | SAN IP addresses (index optional)
|
||||||
// `{http.request.tls.client.san.uris.*}` | SAN URIs (index optional)
|
// `{http.request.tls.client.san.uris.*}` | SAN URIs (index optional)
|
||||||
// `{http.request.uri}` | The full request URI
|
// `{http.request.uri}` | The full request URI
|
||||||
|
// `{http.request.uri_escaped}` | The full request URI with query-style URL encoding applied (using url.QueryEscape)
|
||||||
// `{http.request.uri.path}` | The path component of the request URI
|
// `{http.request.uri.path}` | The path component of the request URI
|
||||||
|
// `{http.request.uri.path_escaped}` | The path component of the request URI with query-style URL encoding applied (using url.QueryEscape)
|
||||||
// `{http.request.uri.path.*}` | Parts of the path, split by `/` (0-based from left)
|
// `{http.request.uri.path.*}` | Parts of the path, split by `/` (0-based from left)
|
||||||
// `{http.request.uri.path.dir}` | The directory, excluding leaf filename
|
// `{http.request.uri.path.dir}` | The directory, excluding leaf filename
|
||||||
// `{http.request.uri.path.file}` | The filename of the path, excluding directory
|
// `{http.request.uri.path.file}` | The filename of the path, excluding directory
|
||||||
// `{http.request.uri.query}` | The query string (without `?`)
|
// `{http.request.uri.query}` | The query string (without `?`)
|
||||||
|
// `{http.request.uri.query_escaped}` | The query string with query-style URL encoding applied (using url.QueryEscape)
|
||||||
|
// `{http.request.uri.prefixed_query}` | The query string with a `?` prefix, if non-empty
|
||||||
// `{http.request.uri.query.*}` | Individual query string value
|
// `{http.request.uri.query.*}` | Individual query string value
|
||||||
// `{http.response.header.*}` | Specific response header field
|
// `{http.response.header.*}` | Specific response header field
|
||||||
// `{http.vars.*}` | Custom variables in the HTTP handler chain
|
// `{http.vars.*}` | Custom variables in the HTTP handler chain
|
||||||
@@ -202,6 +207,9 @@ func (app *App) Provision(ctx caddy.Context) error {
|
|||||||
app.Metrics.httpMetrics = &httpMetrics{}
|
app.Metrics.httpMetrics = &httpMetrics{}
|
||||||
// Scan config for allowed hosts to prevent cardinality explosion
|
// Scan config for allowed hosts to prevent cardinality explosion
|
||||||
app.Metrics.scanConfigForHosts(app)
|
app.Metrics.scanConfigForHosts(app)
|
||||||
|
if err := app.Metrics.provisionOTLP(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// prepare each server
|
// prepare each server
|
||||||
oldContext := ctx.Context
|
oldContext := ctx.Context
|
||||||
@@ -213,8 +221,6 @@ func (app *App) Provision(ctx caddy.Context) error {
|
|||||||
srv.ctx = ctx
|
srv.ctx = ctx
|
||||||
srv.logger = app.logger.Named("log")
|
srv.logger = app.logger.Named("log")
|
||||||
srv.errorLogger = app.logger.Named("log.error")
|
srv.errorLogger = app.logger.Named("log.error")
|
||||||
srv.shutdownAtMu = new(sync.RWMutex)
|
|
||||||
|
|
||||||
if srv.Metrics != nil {
|
if srv.Metrics != nil {
|
||||||
srv.logger.Warn("per-server 'metrics' is deprecated; use 'metrics' in the root 'http' app instead")
|
srv.logger.Warn("per-server 'metrics' is deprecated; use 'metrics' in the root 'http' app instead")
|
||||||
app.Metrics = cmp.Or(app.Metrics, &Metrics{
|
app.Metrics = cmp.Or(app.Metrics, &Metrics{
|
||||||
@@ -234,12 +240,7 @@ func (app *App) Provision(ctx caddy.Context) error {
|
|||||||
|
|
||||||
// if no protocols configured explicitly, enable all except h2c
|
// if no protocols configured explicitly, enable all except h2c
|
||||||
if len(srv.Protocols) == 0 {
|
if len(srv.Protocols) == 0 {
|
||||||
srv.Protocols = []string{"h1", "h2", "h3"}
|
srv.Protocols = srv.protocolsWithDefaults()
|
||||||
}
|
|
||||||
|
|
||||||
srvProtocolsUnique := map[string]struct{}{}
|
|
||||||
for _, srvProtocol := range srv.Protocols {
|
|
||||||
srvProtocolsUnique[srvProtocol] = struct{}{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if srv.ListenProtocols != nil {
|
if srv.ListenProtocols != nil {
|
||||||
@@ -250,31 +251,7 @@ func (app *App) Provision(ctx caddy.Context) error {
|
|||||||
|
|
||||||
for i, lnProtocols := range srv.ListenProtocols {
|
for i, lnProtocols := range srv.ListenProtocols {
|
||||||
if lnProtocols != nil {
|
if lnProtocols != nil {
|
||||||
// populate empty listen protocols with server protocols
|
srv.ListenProtocols[i] = srv.listenerProtocolsWithDefaults(lnProtocols)
|
||||||
lnProtocolsDefault := false
|
|
||||||
var lnProtocolsInclude []string
|
|
||||||
srvProtocolsInclude := maps.Clone(srvProtocolsUnique)
|
|
||||||
|
|
||||||
// keep existing listener protocols unless they are empty
|
|
||||||
for _, lnProtocol := range lnProtocols {
|
|
||||||
if lnProtocol == "" {
|
|
||||||
lnProtocolsDefault = true
|
|
||||||
} else {
|
|
||||||
lnProtocolsInclude = append(lnProtocolsInclude, lnProtocol)
|
|
||||||
delete(srvProtocolsInclude, lnProtocol)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// append server protocols to listener protocols if any listener protocols were empty
|
|
||||||
if lnProtocolsDefault {
|
|
||||||
for _, srvProtocol := range srv.Protocols {
|
|
||||||
if _, ok := srvProtocolsInclude[srvProtocol]; ok {
|
|
||||||
lnProtocolsInclude = append(lnProtocolsInclude, srvProtocol)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
srv.ListenProtocols[i] = lnProtocolsInclude
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -688,9 +665,7 @@ func (app *App) Stop() error {
|
|||||||
for _, addr := range na.Expand() {
|
for _, addr := range na.Expand() {
|
||||||
if caddy.ListenerUsage(addr.Network, addr.JoinHostPort(0)) < 2 {
|
if caddy.ListenerUsage(addr.Network, addr.JoinHostPort(0)) < 2 {
|
||||||
app.logger.Debug("listener closing and shutdown delay is configured", zap.String("address", addr.String()))
|
app.logger.Debug("listener closing and shutdown delay is configured", zap.String("address", addr.String()))
|
||||||
server.shutdownAtMu.Lock()
|
server.shutdownAt.Store(&scheduledTime)
|
||||||
server.shutdownAt = scheduledTime
|
|
||||||
server.shutdownAtMu.Unlock()
|
|
||||||
delay = true
|
delay = true
|
||||||
} else {
|
} else {
|
||||||
app.logger.Debug("shutdown delay configured but listener will remain open", zap.String("address", addr.String()))
|
app.logger.Debug("shutdown delay configured but listener will remain open", zap.String("address", addr.String()))
|
||||||
@@ -711,9 +686,10 @@ func (app *App) Stop() error {
|
|||||||
// enforce grace period if configured
|
// enforce grace period if configured
|
||||||
if app.GracePeriod > 0 {
|
if app.GracePeriod > 0 {
|
||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
ctx, cancel = context.WithTimeout(ctx, time.Duration(app.GracePeriod))
|
timeout := time.Duration(app.GracePeriod)
|
||||||
|
ctx, cancel = context.WithTimeoutCause(ctx, timeout, fmt.Errorf("server graceful shutdown %ds timeout", int(timeout.Seconds())))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
app.logger.Info("servers shutting down; grace period initiated", zap.Duration("duration", time.Duration(app.GracePeriod)))
|
app.logger.Info("servers shutting down; grace period initiated", zap.Duration("duration", timeout))
|
||||||
} else {
|
} else {
|
||||||
app.logger.Info("servers shutting down with eternal grace period")
|
app.logger.Info("servers shutting down with eternal grace period")
|
||||||
}
|
}
|
||||||
@@ -739,6 +715,9 @@ func (app *App) Stop() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := server.server.Shutdown(ctx); err != nil {
|
if err := server.server.Shutdown(ctx); err != nil {
|
||||||
|
if cause := context.Cause(ctx); cause != nil && errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
err = cause
|
||||||
|
}
|
||||||
app.logger.Error("server shutdown",
|
app.logger.Error("server shutdown",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.Strings("addresses", server.Listen))
|
zap.Strings("addresses", server.Listen))
|
||||||
@@ -762,6 +741,9 @@ func (app *App) Stop() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := server.h3server.Shutdown(ctx); err != nil {
|
if err := server.h3server.Shutdown(ctx); err != nil {
|
||||||
|
if cause := context.Cause(ctx); cause != nil && errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
err = cause
|
||||||
|
}
|
||||||
app.logger.Error("HTTP/3 server shutdown",
|
app.logger.Error("HTTP/3 server shutdown",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.Strings("addresses", server.Listen))
|
zap.Strings("addresses", server.Listen))
|
||||||
@@ -808,6 +790,12 @@ func (app *App) Stop() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// flush and shut down the OTLP metrics exporter (if configured) so any
|
||||||
|
// last data point reaches the collector before the process exits
|
||||||
|
if err := app.Metrics.shutdown(ctx); err != nil {
|
||||||
|
app.logger.Error("shutting down OTLP metrics", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
app.stopped = true
|
app.stopped = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
|
|||||||
for d := range serverDomainSet {
|
for d := range serverDomainSet {
|
||||||
echDomains = append(echDomains, d)
|
echDomains = append(echDomains, d)
|
||||||
}
|
}
|
||||||
app.tlsApp.RegisterServerNames(echDomains)
|
app.tlsApp.RegisterServerNames(echDomains, httpsRRALPNs(srv))
|
||||||
|
|
||||||
// nothing more to do here if there are no domains that qualify for
|
// nothing more to do here if there are no domains that qualify for
|
||||||
// automatic HTTPS and there are no explicit TLS connection policies:
|
// automatic HTTPS and there are no explicit TLS connection policies:
|
||||||
@@ -258,18 +258,13 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
|
|||||||
// an empty string to indicate a catch-all, which we have to
|
// an empty string to indicate a catch-all, which we have to
|
||||||
// treat special later
|
// treat special later
|
||||||
if len(serverDomainSet) == 0 {
|
if len(serverDomainSet) == 0 {
|
||||||
redirDomains[""] = append(redirDomains[""], addr)
|
app.recordAutoHTTPSRedirectAddress(redirDomains, "", addr)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// ...and associate it with each domain in this server
|
// ...and associate it with each domain in this server
|
||||||
for d := range serverDomainSet {
|
for d := range serverDomainSet {
|
||||||
// if this domain is used on more than one HTTPS-enabled
|
app.recordAutoHTTPSRedirectAddress(redirDomains, d, addr)
|
||||||
// port, we'll have to choose one, so prefer the HTTPS port
|
|
||||||
if _, ok := redirDomains[d]; !ok ||
|
|
||||||
addr.StartPort == uint(app.httpsPort()) {
|
|
||||||
redirDomains[d] = append(redirDomains[d], addr)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -424,6 +419,40 @@ redirServersLoop:
|
|||||||
// we'll create a new server for all the listener addresses
|
// we'll create a new server for all the listener addresses
|
||||||
// that are unused and serve the remaining redirects from it
|
// that are unused and serve the remaining redirects from it
|
||||||
|
|
||||||
|
// Sort redirect routes by host specificity to ensure exact matches
|
||||||
|
// take precedence over wildcards, preventing ambiguous routing.
|
||||||
|
slices.SortFunc(routes, func(a, b Route) int {
|
||||||
|
hostA := getFirstHostFromRoute(a)
|
||||||
|
hostB := getFirstHostFromRoute(b)
|
||||||
|
|
||||||
|
// Catch-all routes (empty host) have the lowest priority
|
||||||
|
if hostA == "" && hostB != "" {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if hostB == "" && hostA != "" {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
hasWildcardA := strings.Contains(hostA, "*")
|
||||||
|
hasWildcardB := strings.Contains(hostB, "*")
|
||||||
|
|
||||||
|
// Exact domains take precedence over wildcards
|
||||||
|
if !hasWildcardA && hasWildcardB {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if hasWildcardA && !hasWildcardB {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// If both are exact or both are wildcards, the longer one is more specific
|
||||||
|
if len(hostA) != len(hostB) {
|
||||||
|
return len(hostB) - len(hostA)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tie-breaker: alphabetical order to ensure determinism
|
||||||
|
return strings.Compare(hostA, hostB)
|
||||||
|
})
|
||||||
|
|
||||||
// Use the sorted srvNames to consistently find the target server
|
// Use the sorted srvNames to consistently find the target server
|
||||||
for _, srvName := range srvNames {
|
for _, srvName := range srvNames {
|
||||||
srv := app.Servers[srvName]
|
srv := app.Servers[srvName]
|
||||||
@@ -483,6 +512,35 @@ redirServersLoop:
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recordAutoHTTPSRedirectAddress stores redirect destinations for one domain
|
||||||
|
// using a single winning port while keeping all bind addresses on that port.
|
||||||
|
//
|
||||||
|
// This is needed to avoid two opposite regressions in auto-HTTPS redirects:
|
||||||
|
// preserve all listener addresses when a site binds multiple addresses on the
|
||||||
|
// same HTTPS port, but do not mix in alternate HTTPS ports when the canonical
|
||||||
|
// app HTTPS port is also available.
|
||||||
|
func (app *App) recordAutoHTTPSRedirectAddress(redirDomains map[string][]caddy.NetworkAddress, domain string, addr caddy.NetworkAddress) {
|
||||||
|
existing := redirDomains[domain]
|
||||||
|
if len(existing) == 0 {
|
||||||
|
redirDomains[domain] = []caddy.NetworkAddress{addr}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
existingPort := existing[0].StartPort
|
||||||
|
if addr.StartPort != existingPort {
|
||||||
|
if addr.StartPort == uint(app.httpsPort()) && existingPort != uint(app.httpsPort()) {
|
||||||
|
redirDomains[domain] = []caddy.NetworkAddress{addr}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(existing, addr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
redirDomains[domain] = append(existing, addr)
|
||||||
|
}
|
||||||
|
|
||||||
func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route {
|
func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route {
|
||||||
redirTo := "https://{http.request.host}"
|
redirTo := "https://{http.request.host}"
|
||||||
|
|
||||||
@@ -516,6 +574,20 @@ func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func httpsRRALPNs(srv *Server) []string {
|
||||||
|
alpn := make(map[string]struct{}, 3)
|
||||||
|
if srv.protocol("h3") {
|
||||||
|
alpn["h3"] = struct{}{}
|
||||||
|
}
|
||||||
|
if srv.protocol("h2") {
|
||||||
|
alpn["h2"] = struct{}{}
|
||||||
|
}
|
||||||
|
if srv.protocol("h1") {
|
||||||
|
alpn["http/1.1"] = struct{}{}
|
||||||
|
}
|
||||||
|
return caddytls.OrderedHTTPSRRALPN(alpn)
|
||||||
|
}
|
||||||
|
|
||||||
// createAutomationPolicies ensures that automated certificates for this
|
// createAutomationPolicies ensures that automated certificates for this
|
||||||
// app are managed properly. This adds up to two automation policies:
|
// app are managed properly. This adds up to two automation policies:
|
||||||
// one for the public names, and one for the internal names. If a catch-all
|
// one for the public names, and one for the internal names. If a catch-all
|
||||||
@@ -580,6 +652,27 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames, tails
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure automation policies' CertMagic configs are rebuilt when
|
||||||
|
// ACME issuer templates may have been modified above (for example,
|
||||||
|
// alternate ports filled in by the HTTP app). If a policy is already
|
||||||
|
// provisioned, perform a lightweight rebuild of the CertMagic config
|
||||||
|
// so issuers receive SetConfig with the updated templates; otherwise
|
||||||
|
// run a normal Provision to initialize the policy.
|
||||||
|
for i, ap := range app.tlsApp.Automation.Policies {
|
||||||
|
// If the policy is already provisioned, rebuild only the CertMagic
|
||||||
|
// config so issuers get SetConfig with updated templates. Otherwise
|
||||||
|
// provision the policy normally (which may load modules).
|
||||||
|
if ap.IsProvisioned() {
|
||||||
|
if err := ap.RebuildCertMagic(app.tlsApp); err != nil {
|
||||||
|
return fmt.Errorf("rebuilding certmagic config for automation policy %d: %v", i, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := ap.Provision(app.tlsApp); err != nil {
|
||||||
|
return fmt.Errorf("provisioning automation policy %d after auto-HTTPS defaults: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if basePolicy == nil {
|
if basePolicy == nil {
|
||||||
// no base policy found; we will make one
|
// no base policy found; we will make one
|
||||||
basePolicy = new(caddytls.AutomationPolicy)
|
basePolicy = new(caddytls.AutomationPolicy)
|
||||||
@@ -793,3 +886,26 @@ func isTailscaleDomain(name string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
|
type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
|
||||||
|
|
||||||
|
// getFirstHostFromRoute traverses a route's matchers to find the Host rule.
|
||||||
|
// Since we are dealing with internally generated redirect routes, the host
|
||||||
|
// is typically the first string within the MatchHost.
|
||||||
|
func getFirstHostFromRoute(r Route) string {
|
||||||
|
for _, matcherSet := range r.MatcherSets {
|
||||||
|
for _, m := range matcherSet {
|
||||||
|
// Check if the matcher is of type MatchHost (value or pointer)
|
||||||
|
switch hm := m.(type) {
|
||||||
|
case MatchHost:
|
||||||
|
if len(hm) > 0 {
|
||||||
|
return hm[0]
|
||||||
|
}
|
||||||
|
case *MatchHost:
|
||||||
|
if len(*hm) > 0 {
|
||||||
|
return (*hm)[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Return an empty string if it's a catch-all route (no specific host)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package caddyhttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHTTPSRRALPNsDefaultProtocols(t *testing.T) {
|
||||||
|
srv := &Server{}
|
||||||
|
|
||||||
|
got := httpsRRALPNs(srv)
|
||||||
|
want := []string{"h3", "h2", "http/1.1"}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("unexpected ALPN values: got %v want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPSRRALPNsListenProtocolOverrides(t *testing.T) {
|
||||||
|
srv := &Server{
|
||||||
|
Protocols: []string{"h1", "h2"},
|
||||||
|
ListenProtocols: [][]string{
|
||||||
|
{"h1"},
|
||||||
|
nil,
|
||||||
|
{},
|
||||||
|
{"h3", ""},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := httpsRRALPNs(srv)
|
||||||
|
want := []string{"h3", "h2", "http/1.1"}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("unexpected ALPN values: got %v want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPSRRALPNsIgnoresH2COnly(t *testing.T) {
|
||||||
|
srv := &Server{
|
||||||
|
Protocols: []string{"h2c"},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := httpsRRALPNs(srv)
|
||||||
|
if len(got) != 0 {
|
||||||
|
t.Fatalf("unexpected ALPN values: got %v want none", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,7 +41,10 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
|||||||
//
|
//
|
||||||
// encode [<matcher>] <formats...> {
|
// encode [<matcher>] <formats...> {
|
||||||
// gzip [<level>]
|
// gzip [<level>]
|
||||||
// zstd
|
// zstd [<level>] {
|
||||||
|
// level <level>
|
||||||
|
// disable_checksum
|
||||||
|
// }
|
||||||
// minimum_length <length>
|
// minimum_length <length>
|
||||||
// # response matcher block
|
// # response matcher block
|
||||||
// match {
|
// match {
|
||||||
|
|||||||
@@ -307,14 +307,6 @@ func (rw *responseWriter) FlushError() error {
|
|||||||
return http.NewResponseController(rw.ResponseWriter).Flush()
|
return http.NewResponseController(rw.ResponseWriter).Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush calls FlushError() and simply discards any error. It is only implemented for backwards
|
|
||||||
// compatibility with legacy code that does not use FlushError; we know at least one sponsor
|
|
||||||
// needs this. It should not be relied upon as a stable part of the exported API, as it may be
|
|
||||||
// removed in the future.
|
|
||||||
func (rw *responseWriter) Flush() {
|
|
||||||
_ = rw.FlushError()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write writes to the response. If the response qualifies,
|
// Write writes to the response. If the response qualifies,
|
||||||
// it is encoded using the encoder, which is initialized
|
// it is encoded using the encoder, which is initialized
|
||||||
// if not done so already.
|
// if not done so already.
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ type Zstd struct {
|
|||||||
// The compression level. Accepted values: fastest, better, best, default.
|
// The compression level. Accepted values: fastest, better, best, default.
|
||||||
Level string `json:"level,omitempty"`
|
Level string `json:"level,omitempty"`
|
||||||
|
|
||||||
|
// Whether to include the optional 4-byte zstd frame checksum trailer.
|
||||||
|
// If unset, the upstream zstd library default is preserved.
|
||||||
|
Checksum *bool `json:"checksum,omitempty"`
|
||||||
|
|
||||||
// Compression level refer to type constants value from zstd.SpeedFastest to zstd.SpeedBestCompression
|
// Compression level refer to type constants value from zstd.SpeedFastest to zstd.SpeedBestCompression
|
||||||
level zstd.EncoderLevel
|
level zstd.EncoderLevel
|
||||||
}
|
}
|
||||||
@@ -48,19 +52,48 @@ func (Zstd) CaddyModule() caddy.ModuleInfo {
|
|||||||
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens.
|
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens.
|
||||||
func (z *Zstd) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
func (z *Zstd) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
d.Next() // consume option name
|
d.Next() // consume option name
|
||||||
if !d.NextArg() {
|
args := d.RemainingArgs()
|
||||||
return nil
|
switch len(args) {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
if _, err := parseEncoderLevel(args[0]); err != nil {
|
||||||
|
return d.Err(err.Error())
|
||||||
|
}
|
||||||
|
z.Level = args[0]
|
||||||
|
default:
|
||||||
|
return d.ArgErr()
|
||||||
}
|
}
|
||||||
levelStr := d.Val()
|
|
||||||
if ok, _ := zstd.EncoderLevelFromString(levelStr); !ok {
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
||||||
return d.Errf("unexpected compression level, use one of '%s', '%s', '%s', '%s'",
|
switch d.Val() {
|
||||||
zstd.SpeedFastest,
|
case "level":
|
||||||
zstd.SpeedBetterCompression,
|
args := d.RemainingArgs()
|
||||||
zstd.SpeedBestCompression,
|
if len(args) != 1 {
|
||||||
zstd.SpeedDefault,
|
return d.ArgErr()
|
||||||
)
|
}
|
||||||
|
if z.Level != "" {
|
||||||
|
return d.Err("compression level already specified")
|
||||||
|
}
|
||||||
|
if _, err := parseEncoderLevel(args[0]); err != nil {
|
||||||
|
return d.Err(err.Error())
|
||||||
|
}
|
||||||
|
z.Level = args[0]
|
||||||
|
|
||||||
|
case "disable_checksum":
|
||||||
|
if d.NextArg() {
|
||||||
|
return d.ArgErr()
|
||||||
|
}
|
||||||
|
if z.Checksum != nil {
|
||||||
|
return d.Err("checksum already specified")
|
||||||
|
}
|
||||||
|
disabled := false
|
||||||
|
z.Checksum = &disabled
|
||||||
|
|
||||||
|
default:
|
||||||
|
return d.Errf("unknown subdirective '%s'", d.Val())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
z.Level = levelStr
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,15 +102,11 @@ func (z *Zstd) Provision(ctx caddy.Context) error {
|
|||||||
if z.Level == "" {
|
if z.Level == "" {
|
||||||
z.Level = zstd.SpeedDefault.String()
|
z.Level = zstd.SpeedDefault.String()
|
||||||
}
|
}
|
||||||
var ok bool
|
level, err := parseEncoderLevel(z.Level)
|
||||||
if ok, z.level = zstd.EncoderLevelFromString(z.Level); !ok {
|
if err != nil {
|
||||||
return fmt.Errorf("unexpected compression level, use one of '%s', '%s', '%s', '%s'",
|
return err
|
||||||
zstd.SpeedFastest,
|
|
||||||
zstd.SpeedDefault,
|
|
||||||
zstd.SpeedBetterCompression,
|
|
||||||
zstd.SpeedBestCompression,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
z.level = level
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,14 +119,45 @@ func (z Zstd) NewEncoder() encode.Encoder {
|
|||||||
// The default of 8MB for the window is
|
// The default of 8MB for the window is
|
||||||
// too large for many clients, so we limit
|
// too large for many clients, so we limit
|
||||||
// it to 128K to lighten their load.
|
// it to 128K to lighten their load.
|
||||||
writer, _ := zstd.NewWriter(
|
writer, _ := zstd.NewWriter(nil, z.writerOptions(128<<10)...)
|
||||||
nil,
|
return writer
|
||||||
zstd.WithWindowSize(128<<10),
|
}
|
||||||
|
|
||||||
|
func (z Zstd) writerOptions(windowSize int) []zstd.EOption {
|
||||||
|
opts := []zstd.EOption{
|
||||||
|
zstd.WithWindowSize(windowSize),
|
||||||
zstd.WithEncoderConcurrency(1),
|
zstd.WithEncoderConcurrency(1),
|
||||||
zstd.WithZeroFrames(true),
|
zstd.WithZeroFrames(true),
|
||||||
zstd.WithEncoderLevel(z.level),
|
zstd.WithEncoderLevel(z.encoderLevel()),
|
||||||
|
}
|
||||||
|
if z.Checksum != nil {
|
||||||
|
opts = append(opts, zstd.WithEncoderCRC(*z.Checksum))
|
||||||
|
}
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (z Zstd) encoderLevel() zstd.EncoderLevel {
|
||||||
|
if z.level != 0 {
|
||||||
|
return z.level
|
||||||
|
}
|
||||||
|
if z.Level != "" {
|
||||||
|
if level, err := parseEncoderLevel(z.Level); err == nil {
|
||||||
|
return level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return zstd.SpeedDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseEncoderLevel(level string) (zstd.EncoderLevel, error) {
|
||||||
|
if ok, encLevel := zstd.EncoderLevelFromString(level); ok {
|
||||||
|
return encLevel, nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("unexpected compression level, use one of '%s', '%s', '%s', '%s'",
|
||||||
|
zstd.SpeedFastest,
|
||||||
|
zstd.SpeedBetterCompression,
|
||||||
|
zstd.SpeedBestCompression,
|
||||||
|
zstd.SpeedDefault,
|
||||||
)
|
)
|
||||||
return writer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guards
|
// Interface guards
|
||||||
|
|||||||
@@ -281,7 +281,13 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re
|
|||||||
sortParam = sortCookie.Value
|
sortParam = sortCookie.Value
|
||||||
}
|
}
|
||||||
case sortByName, sortByNameDirFirst, sortBySize, sortByTime:
|
case sortByName, sortByNameDirFirst, sortBySize, sortByTime:
|
||||||
http.SetCookie(w, &http.Cookie{Name: "sort", Value: sortParam, Secure: r.TLS != nil})
|
http.SetCookie(w, &http.Cookie{ //nolint:gosec // Secure depends on whether the request itself used TLS
|
||||||
|
Name: "sort",
|
||||||
|
Value: sortParam,
|
||||||
|
Secure: r.TLS != nil,
|
||||||
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// then figure out the order
|
// then figure out the order
|
||||||
@@ -292,7 +298,13 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re
|
|||||||
orderParam = orderCookie.Value
|
orderParam = orderCookie.Value
|
||||||
}
|
}
|
||||||
case sortOrderAsc, sortOrderDesc:
|
case sortOrderAsc, sortOrderDesc:
|
||||||
http.SetCookie(w, &http.Cookie{Name: "order", Value: orderParam, Secure: r.TLS != nil})
|
http.SetCookie(w, &http.Cookie{ //nolint:gosec // Secure depends on whether the request itself used TLS
|
||||||
|
Name: "order",
|
||||||
|
Value: orderParam,
|
||||||
|
Secure: r.TLS != nil,
|
||||||
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// finally, apply the sorting and limiting
|
// finally, apply the sorting and limiting
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -100,7 +99,7 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if fsrv.Browse.RevealSymlinks {
|
if fsrv.Browse.RevealSymlinks {
|
||||||
symLinkTarget, err := filepath.EvalSymlinks(path)
|
symLinkTarget, err := os.Readlink(path)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
symlinkPath = symLinkTarget
|
symlinkPath = symLinkTarget
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -720,6 +720,7 @@ var globSafeRepl = strings.NewReplacer(
|
|||||||
"*", "\\*",
|
"*", "\\*",
|
||||||
"[", "\\[",
|
"[", "\\[",
|
||||||
"?", "\\?",
|
"?", "\\?",
|
||||||
|
"\\", "\\\\",
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -20,14 +20,24 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/internal/filesystems"
|
"github.com/caddyserver/caddy/v2/internal/filesystems"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
path string
|
||||||
|
expectedPath string
|
||||||
|
expectedType string
|
||||||
|
matched bool
|
||||||
|
}
|
||||||
|
|
||||||
func TestFileMatcher(t *testing.T) {
|
func TestFileMatcher(t *testing.T) {
|
||||||
// Windows doesn't like colons in files names
|
// Windows doesn't like colons in files names
|
||||||
isWindows := runtime.GOOS == "windows"
|
isWindows := runtime.GOOS == "windows"
|
||||||
@@ -45,12 +55,7 @@ func TestFileMatcher(t *testing.T) {
|
|||||||
f.Close()
|
f.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, tc := range []struct {
|
for i, tc := range []testCase{
|
||||||
path string
|
|
||||||
expectedPath string
|
|
||||||
expectedType string
|
|
||||||
matched bool
|
|
||||||
}{
|
|
||||||
{
|
{
|
||||||
path: "/foo.txt",
|
path: "/foo.txt",
|
||||||
expectedPath: "/foo.txt",
|
expectedPath: "/foo.txt",
|
||||||
@@ -116,44 +121,170 @@ func TestFileMatcher(t *testing.T) {
|
|||||||
matched: !isWindows,
|
matched: !isWindows,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
m := &MatchFile{
|
fileMatcherTest(t, i, tc)
|
||||||
fsmap: &filesystems.FileSystemMap{},
|
}
|
||||||
Root: "./testdata",
|
}
|
||||||
TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"},
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := url.Parse(tc.path)
|
func TestFileMatcherNonWindows(t *testing.T) {
|
||||||
if err != nil {
|
if runtime.GOOS == "windows" {
|
||||||
t.Errorf("Test %d: parsing path: %v", i, err)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
req := &http.Request{URL: u}
|
// this is impossible to test on Windows, but tests a security patch for other platforms
|
||||||
repl := caddyhttp.NewTestReplacer(req)
|
tc := testCase{
|
||||||
|
path: "/foodir/secr%5Cet.txt",
|
||||||
|
expectedPath: "/foodir/secr\\et.txt",
|
||||||
|
expectedType: "file",
|
||||||
|
matched: true,
|
||||||
|
}
|
||||||
|
|
||||||
result, err := m.MatchWithError(req)
|
f, err := os.Create(filepath.Join("testdata", strings.TrimPrefix(tc.expectedPath, "/")))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Test %d: unexpected error: %v", i, err)
|
t.Fatalf("could not create test file: %v", err)
|
||||||
}
|
}
|
||||||
if result != tc.matched {
|
defer f.Close()
|
||||||
t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result)
|
defer os.Remove(f.Name())
|
||||||
}
|
|
||||||
|
|
||||||
rel, ok := repl.Get("http.matchers.file.relative")
|
fileMatcherTest(t, 0, tc)
|
||||||
if !ok && result {
|
}
|
||||||
t.Errorf("Test %d: expected replacer value", i)
|
|
||||||
}
|
|
||||||
if !result {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if rel != tc.expectedPath {
|
func fileMatcherTest(t *testing.T, i int, tc testCase) {
|
||||||
t.Errorf("Test %d: actual path: %v, expected: %v", i, rel, tc.expectedPath)
|
m := &MatchFile{
|
||||||
}
|
fsmap: &filesystems.FileSystemMap{},
|
||||||
|
Root: "./testdata",
|
||||||
|
TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"},
|
||||||
|
}
|
||||||
|
|
||||||
fileType, _ := repl.Get("http.matchers.file.type")
|
u, err := url.Parse(tc.path)
|
||||||
if fileType != tc.expectedType {
|
if err != nil {
|
||||||
t.Errorf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType)
|
t.Errorf("Test %d: parsing path: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
req := &http.Request{URL: u}
|
||||||
|
repl := caddyhttp.NewTestReplacer(req)
|
||||||
|
|
||||||
|
result, err := m.MatchWithError(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Test %d: unexpected error: %v", i, err)
|
||||||
|
}
|
||||||
|
if result != tc.matched {
|
||||||
|
t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
rel, ok := repl.Get("http.matchers.file.relative")
|
||||||
|
if !ok && result {
|
||||||
|
t.Errorf("Test %d: expected replacer value", i)
|
||||||
|
}
|
||||||
|
if !result {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if rel != tc.expectedPath {
|
||||||
|
t.Errorf("Test %d: actual path: %v, expected: %v", i, rel, tc.expectedPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileType, _ := repl.Get("http.matchers.file.type")
|
||||||
|
if fileType != tc.expectedType {
|
||||||
|
t.Errorf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTryFilesRewriteEscapesMatchedPath(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
requestTarget string
|
||||||
|
filename string
|
||||||
|
extraFiles []string
|
||||||
|
wantPath string
|
||||||
|
wantRequestURI string
|
||||||
|
skipWindows bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "question mark in path",
|
||||||
|
requestTarget: "/%3F.html",
|
||||||
|
filename: "?.html",
|
||||||
|
wantPath: "/?.html",
|
||||||
|
wantRequestURI: "/%3F.html",
|
||||||
|
skipWindows: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "percent in path",
|
||||||
|
requestTarget: "/%25.html",
|
||||||
|
filename: "%.html",
|
||||||
|
wantPath: "/%.html",
|
||||||
|
wantRequestURI: "/%25.html",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "encoded question mark remains percent-encoded",
|
||||||
|
requestTarget: "/%253F.html",
|
||||||
|
filename: "%3F.html",
|
||||||
|
wantPath: "/%3F.html",
|
||||||
|
wantRequestURI: "/%253F.html",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "question mark in nested path",
|
||||||
|
requestTarget: "/nested/%3F.html",
|
||||||
|
filename: filepath.Join("nested", "?.html"),
|
||||||
|
wantPath: "/nested/?.html",
|
||||||
|
wantRequestURI: "/nested/%3F.html",
|
||||||
|
skipWindows: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "encoded slash in filename does not conflict with nesting",
|
||||||
|
requestTarget: "/nested%252Ffile.html",
|
||||||
|
filename: "nested%2Ffile.html",
|
||||||
|
extraFiles: []string{filepath.Join("nested", "file.html")},
|
||||||
|
wantPath: "/nested%2Ffile.html",
|
||||||
|
wantRequestURI: "/nested%252Ffile.html",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if tc.skipWindows && runtime.GOOS == "windows" {
|
||||||
|
t.Skip("Windows file names cannot contain question marks")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range append([]string{tc.filename}, tc.extraFiles...) {
|
||||||
|
filename := filepath.Join(root, name)
|
||||||
|
if err := os.MkdirAll(filepath.Dir(filename), 0o700); err != nil {
|
||||||
|
t.Fatalf("creating test file parent directory: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filename, []byte(name), 0o600); err != nil {
|
||||||
|
t.Fatalf("writing test file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m := &MatchFile{
|
||||||
|
fsmap: &filesystems.FileSystemMap{},
|
||||||
|
Root: root,
|
||||||
|
TryFiles: []string{"{http.request.uri.path}"},
|
||||||
|
}
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "http://example.com"+tc.requestTarget, nil)
|
||||||
|
repl := caddyhttp.NewTestReplacer(req)
|
||||||
|
|
||||||
|
matched, err := m.MatchWithError(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("matching file: %v", err)
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
t.Fatalf("expected request %s to match %s", tc.requestTarget, tc.filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
rewrite.Rewrite{URI: "{http.matchers.file.relative}"}.Rewrite(req, repl)
|
||||||
|
|
||||||
|
if req.URL.Path != tc.wantPath {
|
||||||
|
t.Errorf("rewritten path = %q, want %q", req.URL.Path, tc.wantPath)
|
||||||
|
}
|
||||||
|
if req.RequestURI != tc.wantRequestURI {
|
||||||
|
t.Errorf("rewritten request URI = %q, want %q", req.RequestURI, tc.wantRequestURI)
|
||||||
|
}
|
||||||
|
if req.URL.RawQuery != "" {
|
||||||
|
t.Errorf("rewritten raw query = %q, want empty", req.URL.RawQuery)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,11 @@ type FileServer struct {
|
|||||||
// When possible, all paths are resolved to their absolute form before
|
// When possible, all paths are resolved to their absolute form before
|
||||||
// comparisons are made. For maximum clarity and explictness, use complete,
|
// comparisons are made. For maximum clarity and explictness, use complete,
|
||||||
// absolute paths; or, for greater portability, use relative paths instead.
|
// absolute paths; or, for greater portability, use relative paths instead.
|
||||||
|
//
|
||||||
|
// Note that hide comparisons are case-sensitive. On case-insensitive
|
||||||
|
// filesystems, requests with different path casing may still resolve to the
|
||||||
|
// same file or directory on disk, so hide should not be treated as a
|
||||||
|
// security boundary for sensitive paths.
|
||||||
Hide []string `json:"hide,omitempty"`
|
Hide []string `json:"hide,omitempty"`
|
||||||
|
|
||||||
// The names of files to try as index files if a folder is requested.
|
// The names of files to try as index files if a folder is requested.
|
||||||
@@ -780,7 +785,7 @@ func redirect(w http.ResponseWriter, r *http.Request, toPath string) error {
|
|||||||
if r.URL.RawQuery != "" {
|
if r.URL.RawQuery != "" {
|
||||||
toPath += "?" + r.URL.RawQuery
|
toPath += "?" + r.URL.RawQuery
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, toPath, http.StatusPermanentRedirect)
|
http.Redirect(w, r, toPath, http.StatusPermanentRedirect) //nolint:gosec // toPath is a same-origin path and leading // is stripped above
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -161,11 +161,11 @@ func (ops *HeaderOps) Provision(_ caddy.Context) error {
|
|||||||
|
|
||||||
// containsPlaceholders checks if the string contains Caddy placeholder syntax {key}
|
// containsPlaceholders checks if the string contains Caddy placeholder syntax {key}
|
||||||
func containsPlaceholders(s string) bool {
|
func containsPlaceholders(s string) bool {
|
||||||
openIdx := strings.Index(s, "{")
|
_, after, ok := strings.Cut(s, "{")
|
||||||
if openIdx == -1 {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
closeIdx := strings.Index(s[openIdx+1:], "}")
|
closeIdx := strings.Index(after, "}")
|
||||||
if closeIdx == -1 {
|
if closeIdx == -1 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LoggableHTTPRequest makes an HTTP request loggable with zap.Object().
|
// LoggableHTTPRequest makes an HTTP request loggable with zap.Object().
|
||||||
@@ -47,12 +48,12 @@ func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error {
|
|||||||
enc.AddString("method", r.Method)
|
enc.AddString("method", r.Method)
|
||||||
enc.AddString("host", r.Host)
|
enc.AddString("host", r.Host)
|
||||||
enc.AddString("uri", r.RequestURI)
|
enc.AddString("uri", r.RequestURI)
|
||||||
enc.AddObject("headers", LoggableHTTPHeader{
|
enc.AddObject("headers", internal.LoggableHTTPHeader{
|
||||||
Header: r.Header,
|
Header: r.Header,
|
||||||
ShouldLogCredentials: r.ShouldLogCredentials,
|
ShouldLogCredentials: r.ShouldLogCredentials,
|
||||||
})
|
})
|
||||||
if r.TransferEncoding != nil {
|
if r.TransferEncoding != nil {
|
||||||
enc.AddArray("transfer_encoding", LoggableStringArray(r.TransferEncoding))
|
enc.AddArray("transfer_encoding", internal.LoggableStringArray(r.TransferEncoding))
|
||||||
}
|
}
|
||||||
if r.TLS != nil {
|
if r.TLS != nil {
|
||||||
enc.AddObject("tls", LoggableTLSConnState(*r.TLS))
|
enc.AddObject("tls", LoggableTLSConnState(*r.TLS))
|
||||||
@@ -61,44 +62,10 @@ func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoggableHTTPHeader makes an HTTP header loggable with zap.Object().
|
// LoggableHTTPHeader makes an HTTP header loggable with zap.Object().
|
||||||
// Headers with potentially sensitive information (Cookie, Set-Cookie,
|
type LoggableHTTPHeader = internal.LoggableHTTPHeader
|
||||||
// Authorization, and Proxy-Authorization) are logged with empty values.
|
|
||||||
type LoggableHTTPHeader struct {
|
|
||||||
http.Header
|
|
||||||
|
|
||||||
ShouldLogCredentials bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
|
|
||||||
func (h LoggableHTTPHeader) MarshalLogObject(enc zapcore.ObjectEncoder) error {
|
|
||||||
if h.Header == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for key, val := range h.Header {
|
|
||||||
if !h.ShouldLogCredentials {
|
|
||||||
switch strings.ToLower(key) {
|
|
||||||
case "cookie", "set-cookie", "authorization", "proxy-authorization":
|
|
||||||
val = []string{"REDACTED"} // see #5669. I still think ▒▒▒▒ would be cool.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enc.AddArray(key, LoggableStringArray(val))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoggableStringArray makes a slice of strings marshalable for logging.
|
// LoggableStringArray makes a slice of strings marshalable for logging.
|
||||||
type LoggableStringArray []string
|
type LoggableStringArray = internal.LoggableStringArray
|
||||||
|
|
||||||
// MarshalLogArray satisfies the zapcore.ArrayMarshaler interface.
|
|
||||||
func (sa LoggableStringArray) MarshalLogArray(enc zapcore.ArrayEncoder) error {
|
|
||||||
if sa == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, s := range sa {
|
|
||||||
enc.AppendString(s)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoggableTLSConnState makes a TLS connection state loggable with zap.Object().
|
// LoggableTLSConnState makes a TLS connection state loggable with zap.Object().
|
||||||
type LoggableTLSConnState tls.ConnectionState
|
type LoggableTLSConnState tls.ConnectionState
|
||||||
@@ -121,7 +88,5 @@ func (t LoggableTLSConnState) MarshalLogObject(enc zapcore.ObjectEncoder) error
|
|||||||
// Interface guards
|
// Interface guards
|
||||||
var (
|
var (
|
||||||
_ zapcore.ObjectMarshaler = (*LoggableHTTPRequest)(nil)
|
_ zapcore.ObjectMarshaler = (*LoggableHTTPRequest)(nil)
|
||||||
_ zapcore.ObjectMarshaler = (*LoggableHTTPHeader)(nil)
|
|
||||||
_ zapcore.ArrayMarshaler = (*LoggableStringArray)(nil)
|
|
||||||
_ zapcore.ObjectMarshaler = (*LoggableTLSConnState)(nil)
|
_ zapcore.ObjectMarshaler = (*LoggableTLSConnState)(nil)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -266,7 +266,13 @@ func (m MatchHost) Provision(_ caddy.Context) error {
|
|||||||
if firstI, ok := seen[normalizedHost]; ok {
|
if firstI, ok := seen[normalizedHost]; ok {
|
||||||
return fmt.Errorf("host at index %d is repeated at index %d: %s", firstI, i, host)
|
return fmt.Errorf("host at index %d is repeated at index %d: %s", firstI, i, host)
|
||||||
}
|
}
|
||||||
m[i] = normalizedHost // normalize for all comparisons while matching
|
// Normalize exact hosts for standardized comparison in large-list fastpath later on.
|
||||||
|
// Keep wildcards/placeholders untouched.
|
||||||
|
if m.fuzzy(asciiHost) {
|
||||||
|
m[i] = asciiHost
|
||||||
|
} else {
|
||||||
|
m[i] = normalizedHost
|
||||||
|
}
|
||||||
seen[normalizedHost] = i
|
seen[normalizedHost] = i
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,14 +316,15 @@ func (m MatchHost) MatchWithError(r *http.Request) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if m.large() {
|
if m.large() {
|
||||||
|
reqHostLower := strings.ToLower(reqHost)
|
||||||
// fast path: locate exact match using binary search (about 100-1000x faster for large lists)
|
// fast path: locate exact match using binary search (about 100-1000x faster for large lists)
|
||||||
pos := sort.Search(len(m), func(i int) bool {
|
pos := sort.Search(len(m), func(i int) bool {
|
||||||
if m.fuzzy(m[i]) {
|
if m.fuzzy(m[i]) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return m[i] >= reqHost
|
return m[i] >= reqHostLower
|
||||||
})
|
})
|
||||||
if pos < len(m) && strings.EqualFold(m[pos], reqHost) {
|
if pos < len(m) && m[pos] == reqHostLower {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -531,6 +538,7 @@ func (m MatchPath) MatchWithError(r *http.Request) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) bool {
|
func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) bool {
|
||||||
|
escapedPath = strings.ToLower(escapedPath)
|
||||||
// We would just compare the pattern against r.URL.Path,
|
// We would just compare the pattern against r.URL.Path,
|
||||||
// but the pattern contains %, indicating that we should
|
// but the pattern contains %, indicating that we should
|
||||||
// compare at least some part of the path in raw/escaped
|
// compare at least some part of the path in raw/escaped
|
||||||
@@ -1554,6 +1562,14 @@ func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, er
|
|||||||
// instances of the matcher in this set
|
// instances of the matcher in this set
|
||||||
tokensByMatcherName := make(map[string][]caddyfile.Token)
|
tokensByMatcherName := make(map[string][]caddyfile.Token)
|
||||||
for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
|
for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
|
||||||
|
// if the token is quoted (backtick), treat it as a shorthand
|
||||||
|
// for an expression matcher, same as @named matcher parsing
|
||||||
|
if d.Token().Quoted() {
|
||||||
|
expressionToken := d.Token().Clone()
|
||||||
|
expressionToken.Text = "expression"
|
||||||
|
tokensByMatcherName["expression"] = append(tokensByMatcherName["expression"], expressionToken, d.Token())
|
||||||
|
continue
|
||||||
|
}
|
||||||
matcherName := d.Val()
|
matcherName := d.Val()
|
||||||
tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...)
|
tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -417,6 +417,11 @@ func TestPathMatcher(t *testing.T) {
|
|||||||
input: "/ADMIN%2fpanel",
|
input: "/ADMIN%2fpanel",
|
||||||
expect: true,
|
expect: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
match: MatchPath{"/admin%2fpa*el"},
|
||||||
|
input: "/ADMIN%2fPaAzZLm123NEL",
|
||||||
|
expect: true,
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
err := tc.match.Provision(caddy.Context{})
|
err := tc.match.Provision(caddy.Context{})
|
||||||
if err == nil && tc.provisionErr {
|
if err == nil && tc.provisionErr {
|
||||||
@@ -962,6 +967,7 @@ func TestVarREMatcher(t *testing.T) {
|
|||||||
desc string
|
desc string
|
||||||
match MatchVarsRE
|
match MatchVarsRE
|
||||||
input VarsMiddleware
|
input VarsMiddleware
|
||||||
|
headers http.Header
|
||||||
expect bool
|
expect bool
|
||||||
expectRepl map[string]string
|
expectRepl map[string]string
|
||||||
}{
|
}{
|
||||||
@@ -996,6 +1002,14 @@ func TestVarREMatcher(t *testing.T) {
|
|||||||
input: VarsMiddleware{"Var1": "var1Value"},
|
input: VarsMiddleware{"Var1": "var1Value"},
|
||||||
expect: true,
|
expect: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "placeholder key value containing braces is not double-expanded",
|
||||||
|
match: MatchVarsRE{"{http.request.header.X-Input}": &MatchRegexp{Pattern: ".+", Name: "val"}},
|
||||||
|
input: VarsMiddleware{},
|
||||||
|
headers: http.Header{"X-Input": []string{"{env.HOME}"}},
|
||||||
|
expect: true,
|
||||||
|
expectRepl: map[string]string{"val.0": "{env.HOME}"},
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -1012,7 +1026,7 @@ func TestVarREMatcher(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set up the fake request and its Replacer
|
// set up the fake request and its Replacer
|
||||||
req := &http.Request{URL: new(url.URL), Method: http.MethodGet}
|
req := &http.Request{URL: new(url.URL), Method: http.MethodGet, Header: tc.headers}
|
||||||
repl := caddy.NewReplacer()
|
repl := caddy.NewReplacer()
|
||||||
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
|
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
|
||||||
ctx = context.WithValue(ctx, VarsCtxKey, make(map[string]any))
|
ctx = context.WithValue(ctx, VarsCtxKey, make(map[string]any))
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package caddyhttp
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -10,9 +11,14 @@ import (
|
|||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
otelprom "go.opentelemetry.io/contrib/bridges/prometheus"
|
||||||
|
"go.opentelemetry.io/contrib/exporters/autoexport"
|
||||||
|
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/internal/metrics"
|
caddymetrics "github.com/caddyserver/caddy/v2/internal/metrics"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Metrics configures metrics observations.
|
// Metrics configures metrics observations.
|
||||||
@@ -67,10 +73,20 @@ type Metrics struct {
|
|||||||
// for production environments exposed to the internet).
|
// for production environments exposed to the internet).
|
||||||
ObserveCatchallHosts bool `json:"observe_catchall_hosts,omitempty"`
|
ObserveCatchallHosts bool `json:"observe_catchall_hosts,omitempty"`
|
||||||
|
|
||||||
|
// Enable pushing metrics via OTLP in addition to the existing Prometheus
|
||||||
|
// scrape endpoints. When set, a PeriodicReader is attached to the shared
|
||||||
|
// Prometheus registry (via a Prometheus -> OpenTelemetry bridge), and the
|
||||||
|
// exporter is autoconfigured from the standard OTEL_* environment
|
||||||
|
// variables (OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_PROTOCOL,
|
||||||
|
// OTEL_METRICS_EXPORTER, ...). Set OTEL_METRICS_EXPORTER=none or simply
|
||||||
|
// keep this field false to disable OTLP export.
|
||||||
|
OTLP bool `json:"otlp,omitempty"`
|
||||||
|
|
||||||
init sync.Once
|
init sync.Once
|
||||||
httpMetrics *httpMetrics
|
httpMetrics *httpMetrics
|
||||||
allowedHosts map[string]struct{}
|
allowedHosts map[string]struct{}
|
||||||
hasHTTPSServer bool
|
hasHTTPSServer bool
|
||||||
|
meterProvider *sdkmetric.MeterProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
type httpMetrics struct {
|
type httpMetrics struct {
|
||||||
@@ -147,6 +163,70 @@ func initHTTPMetrics(ctx caddy.Context, metrics *Metrics) {
|
|||||||
}, httpLabels)
|
}, httpLabels)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// provisionOTLP wires a MeterProvider that periodically reads the process-wide
|
||||||
|
// Prometheus registry and pushes the result via OTLP. The exporter and reader
|
||||||
|
// are autoconfigured from the standard OTEL_* environment variables, matching
|
||||||
|
// the ergonomics of the existing `tracing` directive. It is a no-op when
|
||||||
|
// m.OTLP is false, and honors OTEL_METRICS_EXPORTER=none (autoexport
|
||||||
|
// short-circuits to a no-op reader in that case).
|
||||||
|
func (m *Metrics) provisionOTLP(ctx caddy.Context) error {
|
||||||
|
if !m.OTLP {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a Prometheus -> OpenTelemetry bridge against the process-wide
|
||||||
|
// Prometheus registry as the *default* source the NewMetricReader below
|
||||||
|
// will read from.
|
||||||
|
//
|
||||||
|
// NB: despite the "With*" naming, autoexport.WithFallbackMetricProducer is
|
||||||
|
// a package-level setter (it returns nothing) — it mutates autoexport's
|
||||||
|
// internal producer registry and takes effect on the very next call to
|
||||||
|
// NewMetricReader. It is NOT a MetricOption and must not be passed as one.
|
||||||
|
// Users can still override the source by setting OTEL_METRICS_PRODUCERS.
|
||||||
|
reg := ctx.GetMetricsRegistry()
|
||||||
|
autoexport.WithFallbackMetricProducer(func(context.Context) (sdkmetric.Producer, error) {
|
||||||
|
return otelprom.NewMetricProducer(otelprom.WithGatherer(reg)), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
reader, err := autoexport.NewMetricReader(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating OTLP metric reader: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
version, _ := caddy.Version()
|
||||||
|
res, err := resource.Merge(resource.Default(), resource.NewSchemaless(
|
||||||
|
semconv.WebEngineName(ServerHeader),
|
||||||
|
semconv.WebEngineVersion(version),
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("building OTLP metrics resource: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.meterProvider = sdkmetric.NewMeterProvider(
|
||||||
|
sdkmetric.WithResource(res),
|
||||||
|
sdkmetric.WithReader(reader),
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// shutdown flushes and tears down the OTLP MeterProvider if one was provisioned.
|
||||||
|
// Both ForceFlush and Shutdown are always attempted so that a flush failure
|
||||||
|
// does not prevent the reader goroutines from being stopped; errors from both
|
||||||
|
// are returned joined.
|
||||||
|
func (m *Metrics) shutdown(ctx context.Context) error {
|
||||||
|
if m == nil || m.meterProvider == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForceFlush gives the final collection a chance to reach the collector
|
||||||
|
// before the reader goroutine is stopped by Shutdown.
|
||||||
|
return errors.Join(
|
||||||
|
m.meterProvider.ForceFlush(ctx),
|
||||||
|
m.meterProvider.Shutdown(ctx),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// scanConfigForHosts scans the HTTP app configuration to build a set of allowed hosts
|
// scanConfigForHosts scans the HTTP app configuration to build a set of allowed hosts
|
||||||
// for metrics collection, similar to how auto-HTTPS scans for domain names.
|
// for metrics collection, similar to how auto-HTTPS scans for domain names.
|
||||||
func (m *Metrics) scanConfigForHosts(app *App) {
|
func (m *Metrics) scanConfigForHosts(app *App) {
|
||||||
@@ -214,24 +294,27 @@ func serverNameFromContext(ctx context.Context) string {
|
|||||||
return srv.name
|
return srv.name
|
||||||
}
|
}
|
||||||
|
|
||||||
type metricsInstrumentedHandler struct {
|
// metricsInstrumentedRoute wraps a compiled route Handler with metrics
|
||||||
|
// instrumentation. It wraps the entire compiled route chain once,
|
||||||
|
// collecting metrics only once per route match.
|
||||||
|
type metricsInstrumentedRoute struct {
|
||||||
handler string
|
handler string
|
||||||
mh MiddlewareHandler
|
next Handler
|
||||||
metrics *Metrics
|
metrics *Metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMetricsInstrumentedHandler(ctx caddy.Context, handler string, mh MiddlewareHandler, metrics *Metrics) *metricsInstrumentedHandler {
|
func newMetricsInstrumentedRoute(ctx caddy.Context, handler string, next Handler, m *Metrics) *metricsInstrumentedRoute {
|
||||||
metrics.init.Do(func() {
|
m.init.Do(func() {
|
||||||
initHTTPMetrics(ctx, metrics)
|
initHTTPMetrics(ctx, m)
|
||||||
})
|
})
|
||||||
|
|
||||||
return &metricsInstrumentedHandler{handler, mh, metrics}
|
return &metricsInstrumentedRoute{handler: handler, next: next, metrics: m}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error {
|
func (h *metricsInstrumentedRoute) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||||
server := serverNameFromContext(r.Context())
|
server := serverNameFromContext(r.Context())
|
||||||
labels := prometheus.Labels{"server": server, "handler": h.handler}
|
labels := prometheus.Labels{"server": server, "handler": h.handler}
|
||||||
method := metrics.SanitizeMethod(r.Method)
|
method := caddymetrics.SanitizeMethod(r.Method)
|
||||||
// the "code" value is set later, but initialized here to eliminate the possibility
|
// the "code" value is set later, but initialized here to eliminate the possibility
|
||||||
// of a panic
|
// of a panic
|
||||||
statusLabels := prometheus.Labels{"server": server, "handler": h.handler, "method": method, "code": ""}
|
statusLabels := prometheus.Labels{"server": server, "handler": h.handler, "method": method, "code": ""}
|
||||||
@@ -261,13 +344,13 @@ func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
|
|||||||
// being called when the headers are written.
|
// being called when the headers are written.
|
||||||
// Effectively the same behaviour as promhttp.InstrumentHandlerTimeToWriteHeader.
|
// Effectively the same behaviour as promhttp.InstrumentHandlerTimeToWriteHeader.
|
||||||
writeHeaderRecorder := ShouldBufferFunc(func(status int, header http.Header) bool {
|
writeHeaderRecorder := ShouldBufferFunc(func(status int, header http.Header) bool {
|
||||||
statusLabels["code"] = metrics.SanitizeCode(status)
|
statusLabels["code"] = caddymetrics.SanitizeCode(status)
|
||||||
ttfb := time.Since(start).Seconds()
|
ttfb := time.Since(start).Seconds()
|
||||||
h.metrics.httpMetrics.responseDuration.With(statusLabels).Observe(ttfb)
|
h.metrics.httpMetrics.responseDuration.With(statusLabels).Observe(ttfb)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
wrec := NewResponseRecorder(w, nil, writeHeaderRecorder)
|
wrec := NewResponseRecorder(w, nil, writeHeaderRecorder)
|
||||||
err := h.mh.ServeHTTP(wrec, r, next)
|
err := h.next.ServeHTTP(wrec, r)
|
||||||
dur := time.Since(start).Seconds()
|
dur := time.Since(start).Seconds()
|
||||||
h.metrics.httpMetrics.requestCount.With(labels).Inc()
|
h.metrics.httpMetrics.requestCount.With(labels).Inc()
|
||||||
|
|
||||||
@@ -277,7 +360,7 @@ func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
|
|||||||
if statusLabels["code"] == "" {
|
if statusLabels["code"] == "" {
|
||||||
// we still sanitize it, even though it's likely to be 0. A 200 is
|
// we still sanitize it, even though it's likely to be 0. A 200 is
|
||||||
// returned on fallthrough so we want to reflect that.
|
// returned on fallthrough so we want to reflect that.
|
||||||
statusLabels["code"] = metrics.SanitizeCode(status)
|
statusLabels["code"] = caddymetrics.SanitizeCode(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
h.metrics.httpMetrics.requestDuration.With(statusLabels).Observe(dur)
|
h.metrics.httpMetrics.requestDuration.With(statusLabels).Observe(dur)
|
||||||
|
|||||||
@@ -47,16 +47,12 @@ func TestMetricsInstrumentedHandler(t *testing.T) {
|
|||||||
return handlerErr
|
return handlerErr
|
||||||
})
|
})
|
||||||
|
|
||||||
mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
|
ih := newMetricsInstrumentedRoute(ctx, "bar", h, metrics)
|
||||||
return h.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
|
|
||||||
ih := newMetricsInstrumentedHandler(ctx, "bar", mh, metrics)
|
|
||||||
|
|
||||||
r := httptest.NewRequest("GET", "/", nil)
|
r := httptest.NewRequest("GET", "/", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
if actual := ih.ServeHTTP(w, r, h); actual != handlerErr {
|
if actual := ih.ServeHTTP(w, r); actual != handlerErr {
|
||||||
t.Errorf("Not same: expected %#v, but got %#v", handlerErr, actual)
|
t.Errorf("Not same: expected %#v, but got %#v", handlerErr, actual)
|
||||||
}
|
}
|
||||||
if actual := testutil.ToFloat64(metrics.httpMetrics.requestInFlight); actual != 0.0 {
|
if actual := testutil.ToFloat64(metrics.httpMetrics.requestInFlight); actual != 0.0 {
|
||||||
@@ -64,19 +60,19 @@ func TestMetricsInstrumentedHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handlerErr = nil
|
handlerErr = nil
|
||||||
if err := ih.ServeHTTP(w, r, h); err != nil {
|
if err := ih.ServeHTTP(w, r); err != nil {
|
||||||
t.Errorf("Received unexpected error: %v", err)
|
t.Errorf("Received unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// an empty handler - no errors, no header written
|
// an empty handler - no errors, no header written
|
||||||
mh = middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
|
emptyHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
ih = newMetricsInstrumentedHandler(ctx, "empty", mh, metrics)
|
ih = newMetricsInstrumentedRoute(ctx, "empty", emptyHandler, metrics)
|
||||||
r = httptest.NewRequest("GET", "/", nil)
|
r = httptest.NewRequest("GET", "/", nil)
|
||||||
w = httptest.NewRecorder()
|
w = httptest.NewRecorder()
|
||||||
|
|
||||||
if err := ih.ServeHTTP(w, r, h); err != nil {
|
if err := ih.ServeHTTP(w, r); err != nil {
|
||||||
t.Errorf("Received unexpected error: %v", err)
|
t.Errorf("Received unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
if actual := w.Result().StatusCode; actual != 200 {
|
if actual := w.Result().StatusCode; actual != 200 {
|
||||||
@@ -87,16 +83,16 @@ func TestMetricsInstrumentedHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handler returning an error with an HTTP status
|
// handler returning an error with an HTTP status
|
||||||
mh = middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
|
errHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
return Error(http.StatusTooManyRequests, nil)
|
return Error(http.StatusTooManyRequests, nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
ih = newMetricsInstrumentedHandler(ctx, "foo", mh, metrics)
|
ih = newMetricsInstrumentedRoute(ctx, "foo", errHandler, metrics)
|
||||||
|
|
||||||
r = httptest.NewRequest("GET", "/", nil)
|
r = httptest.NewRequest("GET", "/", nil)
|
||||||
w = httptest.NewRecorder()
|
w = httptest.NewRecorder()
|
||||||
|
|
||||||
if err := ih.ServeHTTP(w, r, nil); err == nil {
|
if err := ih.ServeHTTP(w, r); err == nil {
|
||||||
t.Errorf("expected error to be propagated")
|
t.Errorf("expected error to be propagated")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,16 +221,12 @@ func TestMetricsInstrumentedHandlerPerHost(t *testing.T) {
|
|||||||
return handlerErr
|
return handlerErr
|
||||||
})
|
})
|
||||||
|
|
||||||
mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
|
ih := newMetricsInstrumentedRoute(ctx, "bar", h, metrics)
|
||||||
return h.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
|
|
||||||
ih := newMetricsInstrumentedHandler(ctx, "bar", mh, metrics)
|
|
||||||
|
|
||||||
r := httptest.NewRequest("GET", "/", nil)
|
r := httptest.NewRequest("GET", "/", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
if actual := ih.ServeHTTP(w, r, h); actual != handlerErr {
|
if actual := ih.ServeHTTP(w, r); actual != handlerErr {
|
||||||
t.Errorf("Not same: expected %#v, but got %#v", handlerErr, actual)
|
t.Errorf("Not same: expected %#v, but got %#v", handlerErr, actual)
|
||||||
}
|
}
|
||||||
if actual := testutil.ToFloat64(metrics.httpMetrics.requestInFlight); actual != 0.0 {
|
if actual := testutil.ToFloat64(metrics.httpMetrics.requestInFlight); actual != 0.0 {
|
||||||
@@ -242,19 +234,19 @@ func TestMetricsInstrumentedHandlerPerHost(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handlerErr = nil
|
handlerErr = nil
|
||||||
if err := ih.ServeHTTP(w, r, h); err != nil {
|
if err := ih.ServeHTTP(w, r); err != nil {
|
||||||
t.Errorf("Received unexpected error: %v", err)
|
t.Errorf("Received unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// an empty handler - no errors, no header written
|
// an empty handler - no errors, no header written
|
||||||
mh = middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
|
emptyHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
ih = newMetricsInstrumentedHandler(ctx, "empty", mh, metrics)
|
ih = newMetricsInstrumentedRoute(ctx, "empty", emptyHandler, metrics)
|
||||||
r = httptest.NewRequest("GET", "/", nil)
|
r = httptest.NewRequest("GET", "/", nil)
|
||||||
w = httptest.NewRecorder()
|
w = httptest.NewRecorder()
|
||||||
|
|
||||||
if err := ih.ServeHTTP(w, r, h); err != nil {
|
if err := ih.ServeHTTP(w, r); err != nil {
|
||||||
t.Errorf("Received unexpected error: %v", err)
|
t.Errorf("Received unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
if actual := w.Result().StatusCode; actual != 200 {
|
if actual := w.Result().StatusCode; actual != 200 {
|
||||||
@@ -265,16 +257,16 @@ func TestMetricsInstrumentedHandlerPerHost(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handler returning an error with an HTTP status
|
// handler returning an error with an HTTP status
|
||||||
mh = middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
|
errHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
return Error(http.StatusTooManyRequests, nil)
|
return Error(http.StatusTooManyRequests, nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
ih = newMetricsInstrumentedHandler(ctx, "foo", mh, metrics)
|
ih = newMetricsInstrumentedRoute(ctx, "foo", errHandler, metrics)
|
||||||
|
|
||||||
r = httptest.NewRequest("GET", "/", nil)
|
r = httptest.NewRequest("GET", "/", nil)
|
||||||
w = httptest.NewRecorder()
|
w = httptest.NewRecorder()
|
||||||
|
|
||||||
if err := ih.ServeHTTP(w, r, nil); err == nil {
|
if err := ih.ServeHTTP(w, r); err == nil {
|
||||||
t.Errorf("expected error to be propagated")
|
t.Errorf("expected error to be propagated")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,30 +389,30 @@ func TestMetricsCardinalityProtection(t *testing.T) {
|
|||||||
// Add one allowed host
|
// Add one allowed host
|
||||||
metrics.allowedHosts["allowed.com"] = struct{}{}
|
metrics.allowedHosts["allowed.com"] = struct{}{}
|
||||||
|
|
||||||
mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
|
h := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
w.Write([]byte("hello"))
|
w.Write([]byte("hello"))
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
ih := newMetricsInstrumentedHandler(ctx, "test", mh, metrics)
|
ih := newMetricsInstrumentedRoute(ctx, "test", h, metrics)
|
||||||
|
|
||||||
// Test request to allowed host
|
// Test request to allowed host
|
||||||
r1 := httptest.NewRequest("GET", "http://allowed.com/", nil)
|
r1 := httptest.NewRequest("GET", "http://allowed.com/", nil)
|
||||||
r1.Host = "allowed.com"
|
r1.Host = "allowed.com"
|
||||||
w1 := httptest.NewRecorder()
|
w1 := httptest.NewRecorder()
|
||||||
ih.ServeHTTP(w1, r1, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
|
ih.ServeHTTP(w1, r1)
|
||||||
|
|
||||||
// Test request to unknown host (should be mapped to "_other")
|
// Test request to unknown host (should be mapped to "_other")
|
||||||
r2 := httptest.NewRequest("GET", "http://attacker.com/", nil)
|
r2 := httptest.NewRequest("GET", "http://attacker.com/", nil)
|
||||||
r2.Host = "attacker.com"
|
r2.Host = "attacker.com"
|
||||||
w2 := httptest.NewRecorder()
|
w2 := httptest.NewRecorder()
|
||||||
ih.ServeHTTP(w2, r2, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
|
ih.ServeHTTP(w2, r2)
|
||||||
|
|
||||||
// Test request to another unknown host (should also be mapped to "_other")
|
// Test request to another unknown host (should also be mapped to "_other")
|
||||||
r3 := httptest.NewRequest("GET", "http://evil.com/", nil)
|
r3 := httptest.NewRequest("GET", "http://evil.com/", nil)
|
||||||
r3.Host = "evil.com"
|
r3.Host = "evil.com"
|
||||||
w3 := httptest.NewRecorder()
|
w3 := httptest.NewRecorder()
|
||||||
ih.ServeHTTP(w3, r3, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
|
ih.ServeHTTP(w3, r3)
|
||||||
|
|
||||||
// Check that metrics contain:
|
// Check that metrics contain:
|
||||||
// - One entry for "allowed.com"
|
// - One entry for "allowed.com"
|
||||||
@@ -452,26 +444,26 @@ func TestMetricsHTTPSCatchAll(t *testing.T) {
|
|||||||
allowedHosts: make(map[string]struct{}), // Empty - no explicitly allowed hosts
|
allowedHosts: make(map[string]struct{}), // Empty - no explicitly allowed hosts
|
||||||
}
|
}
|
||||||
|
|
||||||
mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
|
h := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
w.Write([]byte("hello"))
|
w.Write([]byte("hello"))
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
ih := newMetricsInstrumentedHandler(ctx, "test", mh, metrics)
|
ih := newMetricsInstrumentedRoute(ctx, "test", h, metrics)
|
||||||
|
|
||||||
// Test HTTPS request (should be allowed even though not in allowedHosts)
|
// Test HTTPS request (should be allowed even though not in allowedHosts)
|
||||||
r1 := httptest.NewRequest("GET", "https://unknown.com/", nil)
|
r1 := httptest.NewRequest("GET", "https://unknown.com/", nil)
|
||||||
r1.Host = "unknown.com"
|
r1.Host = "unknown.com"
|
||||||
r1.TLS = &tls.ConnectionState{} // Mark as TLS/HTTPS
|
r1.TLS = &tls.ConnectionState{} // Mark as TLS/HTTPS
|
||||||
w1 := httptest.NewRecorder()
|
w1 := httptest.NewRecorder()
|
||||||
ih.ServeHTTP(w1, r1, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
|
ih.ServeHTTP(w1, r1)
|
||||||
|
|
||||||
// Test HTTP request (should be mapped to "_other")
|
// Test HTTP request (should be mapped to "_other")
|
||||||
r2 := httptest.NewRequest("GET", "http://unknown.com/", nil)
|
r2 := httptest.NewRequest("GET", "http://unknown.com/", nil)
|
||||||
r2.Host = "unknown.com"
|
r2.Host = "unknown.com"
|
||||||
// No TLS field = HTTP request
|
// No TLS field = HTTP request
|
||||||
w2 := httptest.NewRecorder()
|
w2 := httptest.NewRecorder()
|
||||||
ih.ServeHTTP(w2, r2, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
|
ih.ServeHTTP(w2, r2)
|
||||||
|
|
||||||
// Check that HTTPS request gets real host, HTTP gets "_other"
|
// Check that HTTPS request gets real host, HTTP gets "_other"
|
||||||
expected := `
|
expected := `
|
||||||
@@ -488,8 +480,152 @@ func TestMetricsHTTPSCatchAll(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type middlewareHandlerFunc func(http.ResponseWriter, *http.Request, Handler) error
|
func TestMetricsInstrumentedRoute(t *testing.T) {
|
||||||
|
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||||
|
m := &Metrics{
|
||||||
|
init: sync.Once{},
|
||||||
|
httpMetrics: &httpMetrics{},
|
||||||
|
}
|
||||||
|
|
||||||
func (f middlewareHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, h Handler) error {
|
handlerErr := errors.New("oh noes")
|
||||||
return f(w, r, h)
|
response := []byte("hello world!")
|
||||||
|
innerHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
if actual := testutil.ToFloat64(m.httpMetrics.requestInFlight); actual != 1.0 {
|
||||||
|
t.Errorf("Expected requestInFlight to be 1.0, got %v", actual)
|
||||||
|
}
|
||||||
|
if handlerErr == nil {
|
||||||
|
w.Write(response)
|
||||||
|
}
|
||||||
|
return handlerErr
|
||||||
|
})
|
||||||
|
|
||||||
|
ih := newMetricsInstrumentedRoute(ctx, "test_handler", innerHandler, m)
|
||||||
|
|
||||||
|
r := httptest.NewRequest("GET", "/", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// Test with error
|
||||||
|
if actual := ih.ServeHTTP(w, r); actual != handlerErr {
|
||||||
|
t.Errorf("Expected error %v, got %v", handlerErr, actual)
|
||||||
|
}
|
||||||
|
if actual := testutil.ToFloat64(m.httpMetrics.requestInFlight); actual != 0.0 {
|
||||||
|
t.Errorf("Expected requestInFlight to be 0.0 after request, got %v", actual)
|
||||||
|
}
|
||||||
|
if actual := testutil.ToFloat64(m.httpMetrics.requestErrors); actual != 1.0 {
|
||||||
|
t.Errorf("Expected requestErrors to be 1.0, got %v", actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test without error
|
||||||
|
handlerErr = nil
|
||||||
|
w = httptest.NewRecorder()
|
||||||
|
if err := ih.ServeHTTP(w, r); err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMetricsProvisionOTLPDisabled(t *testing.T) {
|
||||||
|
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||||
|
|
||||||
|
m := &Metrics{OTLP: false}
|
||||||
|
|
||||||
|
if err := m.provisionOTLP(ctx); err != nil {
|
||||||
|
t.Fatalf("provisionOTLP returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if m.meterProvider != nil {
|
||||||
|
t.Fatalf("meterProvider should remain nil when OTLP is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
// shutdown must be safe on a never-provisioned Metrics.
|
||||||
|
if err := m.shutdown(context.Background()); err != nil {
|
||||||
|
t.Fatalf("shutdown returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMetricsProvisionOTLPNoopExporter(t *testing.T) {
|
||||||
|
// OTEL_METRICS_EXPORTER=none makes autoexport return its built-in
|
||||||
|
// no-op reader, which avoids any network I/O while still exercising
|
||||||
|
// the full provisionOTLP -> shutdown lifecycle.
|
||||||
|
t.Setenv("OTEL_METRICS_EXPORTER", "none")
|
||||||
|
|
||||||
|
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||||
|
|
||||||
|
m := &Metrics{OTLP: true}
|
||||||
|
|
||||||
|
if err := m.provisionOTLP(ctx); err != nil {
|
||||||
|
t.Fatalf("provisionOTLP returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if m.meterProvider == nil {
|
||||||
|
t.Fatalf("provisionOTLP did not create a MeterProvider")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.shutdown(context.Background()); err != nil {
|
||||||
|
t.Fatalf("shutdown returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// shutdown on a nil receiver is a convenience so App.Stop can call it
|
||||||
|
// without guarding against app.Metrics being unset.
|
||||||
|
func TestMetricsShutdownNilReceiver(t *testing.T) {
|
||||||
|
var m *Metrics
|
||||||
|
|
||||||
|
if err := m.shutdown(context.Background()); err != nil {
|
||||||
|
t.Fatalf("shutdown on nil Metrics returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkMetricsInstrumentedRoute(b *testing.B) {
|
||||||
|
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||||
|
m := &Metrics{
|
||||||
|
init: sync.Once{},
|
||||||
|
httpMetrics: &httpMetrics{},
|
||||||
|
}
|
||||||
|
|
||||||
|
noopHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
w.Write([]byte("ok"))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
ih := newMetricsInstrumentedRoute(ctx, "bench_handler", noopHandler, m)
|
||||||
|
|
||||||
|
r := httptest.NewRequest("GET", "/", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
ih.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkSingleRouteMetrics simulates the new behavior where metrics
|
||||||
|
// are collected once for the entire route.
|
||||||
|
func BenchmarkSingleRouteMetrics(b *testing.B) {
|
||||||
|
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||||
|
m := &Metrics{
|
||||||
|
init: sync.Once{},
|
||||||
|
httpMetrics: &httpMetrics{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a chain of 5 plain middleware handlers (no per-handler metrics)
|
||||||
|
var next Handler = HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
capturedNext := next
|
||||||
|
next = HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
return capturedNext.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap the entire chain with a single route-level metrics handler
|
||||||
|
ih := newMetricsInstrumentedRoute(ctx, "handler", next, m)
|
||||||
|
|
||||||
|
r := httptest.NewRequest("GET", "/", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
ih.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -387,17 +387,14 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
|||||||
switch key {
|
switch key {
|
||||||
case "http.shutting_down":
|
case "http.shutting_down":
|
||||||
server := req.Context().Value(ServerCtxKey).(*Server)
|
server := req.Context().Value(ServerCtxKey).(*Server)
|
||||||
server.shutdownAtMu.RLock()
|
return server.shutdownAt.Load() != nil, true
|
||||||
defer server.shutdownAtMu.RUnlock()
|
|
||||||
return !server.shutdownAt.IsZero(), true
|
|
||||||
case "http.time_until_shutdown":
|
case "http.time_until_shutdown":
|
||||||
server := req.Context().Value(ServerCtxKey).(*Server)
|
server := req.Context().Value(ServerCtxKey).(*Server)
|
||||||
server.shutdownAtMu.RLock()
|
t := server.shutdownAt.Load()
|
||||||
defer server.shutdownAtMu.RUnlock()
|
if t == nil {
|
||||||
if server.shutdownAt.IsZero() {
|
|
||||||
return nil, true
|
return nil, true
|
||||||
}
|
}
|
||||||
return time.Until(server.shutdownAt), true
|
return time.Until(*t), true
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, false
|
return nil, false
|
||||||
@@ -420,7 +417,16 @@ func getReqTLSReplacement(req *http.Request, key string) (any, bool) {
|
|||||||
if strings.HasPrefix(field, "client.") {
|
if strings.HasPrefix(field, "client.") {
|
||||||
cert := getTLSPeerCert(req.TLS)
|
cert := getTLSPeerCert(req.TLS)
|
||||||
if cert == nil {
|
if cert == nil {
|
||||||
return nil, false
|
// Instead of returning (nil, false) here, we set it to a dummy
|
||||||
|
// value to fix #7530. This way, even if there is no client cert,
|
||||||
|
// evaluating placeholders with ReplaceKnown() will still remove
|
||||||
|
// the placeholder, which would be expected. It is not expected
|
||||||
|
// for the placeholder to sometimes get removed based on whether
|
||||||
|
// the client presented a cert. We also do not return true here
|
||||||
|
// because we probably should remain accurate about whether a
|
||||||
|
// placeholder is, in fact, known or not.
|
||||||
|
// (This allocation may be slightly inefficient.)
|
||||||
|
cert = new(x509.Certificate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// subject alternate names (SANs)
|
// subject alternate names (SANs)
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ResponseWriterWrapper wraps an underlying ResponseWriter and
|
// ResponseWriterWrapper wraps an underlying ResponseWriter and
|
||||||
@@ -70,6 +72,8 @@ type responseRecorder struct {
|
|||||||
size int
|
size int
|
||||||
wroteHeader bool
|
wroteHeader bool
|
||||||
stream bool
|
stream bool
|
||||||
|
hijacked bool
|
||||||
|
detached bool
|
||||||
|
|
||||||
readSize *int
|
readSize *int
|
||||||
}
|
}
|
||||||
@@ -144,7 +148,8 @@ func NewResponseRecorder(w http.ResponseWriter, buf *bytes.Buffer, shouldBuffer
|
|||||||
|
|
||||||
// WriteHeader writes the headers with statusCode to the wrapped
|
// WriteHeader writes the headers with statusCode to the wrapped
|
||||||
// ResponseWriter unless the response is to be buffered instead.
|
// ResponseWriter unless the response is to be buffered instead.
|
||||||
// 1xx responses are never buffered.
|
// 1xx responses are never buffered, except 101 which is treated
|
||||||
|
// as a final upgrade response.
|
||||||
func (rr *responseRecorder) WriteHeader(statusCode int) {
|
func (rr *responseRecorder) WriteHeader(statusCode int) {
|
||||||
if rr.wroteHeader {
|
if rr.wroteHeader {
|
||||||
return
|
return
|
||||||
@@ -161,12 +166,12 @@ func (rr *responseRecorder) WriteHeader(statusCode int) {
|
|||||||
rr.stream = !rr.shouldBuffer(rr.statusCode, rr.ResponseWriterWrapper.Header())
|
rr.stream = !rr.shouldBuffer(rr.statusCode, rr.ResponseWriterWrapper.Header())
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1xx responses aren't final; just informational
|
// 1xx responses except 101 aren't final; just informational
|
||||||
if statusCode < 100 || statusCode > 199 {
|
if statusCode < 100 || statusCode > 199 || statusCode == http.StatusSwitchingProtocols {
|
||||||
rr.wroteHeader = true
|
rr.wroteHeader = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// if informational or not buffered, immediately write header
|
// if 1xx or not buffered, immediately write header
|
||||||
if rr.stream || (100 <= statusCode && statusCode <= 199) {
|
if rr.stream || (100 <= statusCode && statusCode <= 199) {
|
||||||
rr.ResponseWriterWrapper.WriteHeader(statusCode)
|
rr.ResponseWriterWrapper.WriteHeader(statusCode)
|
||||||
}
|
}
|
||||||
@@ -222,7 +227,18 @@ func (rr *responseRecorder) Buffered() bool {
|
|||||||
return !rr.stream
|
return !rr.stream
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rr *responseRecorder) DetachAfterHijack(detached bool) bool {
|
||||||
|
if rr.hijacked {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
rr.detached = detached
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (rr *responseRecorder) WriteResponse() error {
|
func (rr *responseRecorder) WriteResponse() error {
|
||||||
|
if rr.hijacked {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if rr.statusCode == 0 {
|
if rr.statusCode == 0 {
|
||||||
// could happen if no handlers actually wrote anything,
|
// could happen if no handlers actually wrote anything,
|
||||||
// and this prevents a panic; status must be > 0
|
// and this prevents a panic; status must be > 0
|
||||||
@@ -253,11 +269,25 @@ func (rr *responseRecorder) setReadSize(size *int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rr *responseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
func (rr *responseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
|
if !rr.wroteHeader {
|
||||||
|
// hijacking without writing status code first works as long as
|
||||||
|
// subsequent writes follows http1.1 wire format, but it will
|
||||||
|
// show up with a status code of 0 in the access log and bytes
|
||||||
|
// written will include response headers. Response headers won't
|
||||||
|
// be present in the log if not set on the response writer.
|
||||||
|
caddy.Log().Warn("hijacking without writing status code first")
|
||||||
|
}
|
||||||
//nolint:bodyclose
|
//nolint:bodyclose
|
||||||
conn, brw, err := http.NewResponseController(rr.ResponseWriterWrapper).Hijack()
|
conn, brw, err := http.NewResponseController(rr.ResponseWriterWrapper).Hijack()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
rr.hijacked = true
|
||||||
|
rr.stream = true
|
||||||
|
rr.wroteHeader = true
|
||||||
|
if rr.detached {
|
||||||
|
return conn, brw, nil
|
||||||
|
}
|
||||||
// Per http documentation, returned bufio.Writer is empty, but bufio.Read maybe not
|
// Per http documentation, returned bufio.Writer is empty, but bufio.Read maybe not
|
||||||
conn = &hijackedConn{conn, rr}
|
conn = &hijackedConn{conn, rr}
|
||||||
brw.Writer.Reset(conn)
|
brw.Writer.Reset(conn)
|
||||||
@@ -311,6 +341,29 @@ func (hc *hijackedConn) ReadFrom(r io.Reader) (int64, error) {
|
|||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DetachResponseWriterAfterHijack detaches w or one of its wrapped
|
||||||
|
// response writers when it's hijacked. Returns true if not already
|
||||||
|
// hijacked. When detached, bytes read or written stats will not be
|
||||||
|
// recorded for the hijacked connection, and it's safe to use the
|
||||||
|
// connection after http middleware returns.
|
||||||
|
func DetachResponseWriterAfterHijack(w http.ResponseWriter, detached bool) bool {
|
||||||
|
for w != nil {
|
||||||
|
if detacher, ok := w.(interface{ DetachAfterHijack(bool) bool }); ok {
|
||||||
|
return detacher.DetachAfterHijack(detached)
|
||||||
|
}
|
||||||
|
unwrapper, ok := w.(interface{ Unwrap() http.ResponseWriter })
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
next := unwrapper.Unwrap()
|
||||||
|
if next == w {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
w = next
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// ResponseRecorder is a http.ResponseWriter that records
|
// ResponseRecorder is a http.ResponseWriter that records
|
||||||
// responses instead of writing them to the client. See
|
// responses instead of writing them to the client. See
|
||||||
// docs for NewResponseRecorder for proper usage.
|
// docs for NewResponseRecorder for proper usage.
|
||||||
@@ -319,6 +372,7 @@ type ResponseRecorder interface {
|
|||||||
Status() int
|
Status() int
|
||||||
Buffer() *bytes.Buffer
|
Buffer() *bytes.Buffer
|
||||||
Buffered() bool
|
Buffered() bool
|
||||||
|
DetachAfterHijack(bool) bool
|
||||||
Size() int
|
Size() int
|
||||||
WriteResponse() error
|
WriteResponse() error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package caddyhttp
|
package caddyhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type responseWriterSpy interface {
|
type responseWriterSpy interface {
|
||||||
@@ -44,6 +47,50 @@ func (rf *readFromRespWriter) ReadFrom(r io.Reader) (int64, error) {
|
|||||||
|
|
||||||
func (rf *readFromRespWriter) CalledReadFrom() bool { return rf.called }
|
func (rf *readFromRespWriter) CalledReadFrom() bool { return rf.called }
|
||||||
|
|
||||||
|
type hijackRespWriter struct {
|
||||||
|
baseRespWriter
|
||||||
|
header http.Header
|
||||||
|
status int
|
||||||
|
conn net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHijackRespWriter() *hijackRespWriter {
|
||||||
|
return &hijackRespWriter{
|
||||||
|
header: make(http.Header),
|
||||||
|
conn: stubConn{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hrw *hijackRespWriter) Header() http.Header {
|
||||||
|
return hrw.header
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hrw *hijackRespWriter) WriteHeader(statusCode int) {
|
||||||
|
hrw.status = statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hrw *hijackRespWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
|
br := bufio.NewReader(hrw.conn)
|
||||||
|
bw := bufio.NewWriter(hrw.conn)
|
||||||
|
return hrw.conn, bufio.NewReadWriter(br, bw), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type stubConn struct{}
|
||||||
|
|
||||||
|
func (stubConn) Read(_ []byte) (int, error) { return 0, io.EOF }
|
||||||
|
func (stubConn) Write(p []byte) (int, error) { return len(p), nil }
|
||||||
|
func (stubConn) Close() error { return nil }
|
||||||
|
func (stubConn) LocalAddr() net.Addr { return stubAddr("local") }
|
||||||
|
func (stubConn) RemoteAddr() net.Addr { return stubAddr("remote") }
|
||||||
|
func (stubConn) SetDeadline(time.Time) error { return nil }
|
||||||
|
func (stubConn) SetReadDeadline(time.Time) error { return nil }
|
||||||
|
func (stubConn) SetWriteDeadline(time.Time) error { return nil }
|
||||||
|
|
||||||
|
type stubAddr string
|
||||||
|
|
||||||
|
func (a stubAddr) Network() string { return "tcp" }
|
||||||
|
func (a stubAddr) String() string { return string(a) }
|
||||||
|
|
||||||
func TestResponseWriterWrapperReadFrom(t *testing.T) {
|
func TestResponseWriterWrapperReadFrom(t *testing.T) {
|
||||||
tests := map[string]struct {
|
tests := map[string]struct {
|
||||||
responseWriter responseWriterSpy
|
responseWriter responseWriterSpy
|
||||||
@@ -169,3 +216,49 @@ func TestResponseRecorderReadFrom(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResponseRecorderSwitchingProtocolsIsHijackAware(t *testing.T) {
|
||||||
|
w := newHijackRespWriter()
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
rr := NewResponseRecorder(w, &buf, func(status int, header http.Header) bool {
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
rr.WriteHeader(http.StatusSwitchingProtocols)
|
||||||
|
|
||||||
|
if rr.Status() != http.StatusSwitchingProtocols {
|
||||||
|
t.Fatalf("status = %d, want %d", rr.Status(), http.StatusSwitchingProtocols)
|
||||||
|
}
|
||||||
|
if w.status != http.StatusSwitchingProtocols {
|
||||||
|
t.Fatalf("underlying status = %d, want %d", w.status, http.StatusSwitchingProtocols)
|
||||||
|
}
|
||||||
|
|
||||||
|
hj, ok := rr.(http.Hijacker)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("response recorder does not implement http.Hijacker")
|
||||||
|
}
|
||||||
|
conn, _, err := hj.Hijack()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Hijack() error = %v", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
if rr.Buffered() {
|
||||||
|
t.Fatal("hijacked response should not remain buffered")
|
||||||
|
}
|
||||||
|
if rr.DetachAfterHijack(true) {
|
||||||
|
t.Fatal("response recorder should report hijacked state by returning false")
|
||||||
|
}
|
||||||
|
if DetachResponseWriterAfterHijack(rr, true) {
|
||||||
|
t.Fatal("DetachResponseWriterAfterHijack() should report false after hijack")
|
||||||
|
}
|
||||||
|
if err := rr.WriteResponse(); err != nil {
|
||||||
|
t.Fatalf("WriteResponse() after hijack returned error: %v", err)
|
||||||
|
}
|
||||||
|
if rr.Size() != 0 {
|
||||||
|
t.Fatalf("size = %d, want 0 after hijack handshake", rr.Size())
|
||||||
|
}
|
||||||
|
if got := w.Written(); got != "" {
|
||||||
|
t.Fatalf("unexpected buffered body write after hijack: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -73,8 +73,9 @@ func (adminUpstreams) handleUpstreams(w http.ResponseWriter, r *http.Request) er
|
|||||||
|
|
||||||
// Collect the results to respond with
|
// Collect the results to respond with
|
||||||
results := []upstreamStatus{}
|
results := []upstreamStatus{}
|
||||||
|
knownHosts := make(map[string]struct{})
|
||||||
|
|
||||||
// Iterate over the upstream pool (needs to be fast)
|
// Iterate over the static upstream pool (needs to be fast)
|
||||||
var rangeErr error
|
var rangeErr error
|
||||||
hosts.Range(func(key, val any) bool {
|
hosts.Range(func(key, val any) bool {
|
||||||
address, ok := key.(string)
|
address, ok := key.(string)
|
||||||
@@ -95,6 +96,8 @@ func (adminUpstreams) handleUpstreams(w http.ResponseWriter, r *http.Request) er
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
knownHosts[address] = struct{}{}
|
||||||
|
|
||||||
results = append(results, upstreamStatus{
|
results = append(results, upstreamStatus{
|
||||||
Address: address,
|
Address: address,
|
||||||
NumRequests: upstream.NumRequests(),
|
NumRequests: upstream.NumRequests(),
|
||||||
@@ -103,11 +106,32 @@ func (adminUpstreams) handleUpstreams(w http.ResponseWriter, r *http.Request) er
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
// If an error happened during the range, return it
|
currentInFlight := getInFlightRequests()
|
||||||
|
for address, count := range currentInFlight {
|
||||||
|
if _, exists := knownHosts[address]; !exists && count > 0 {
|
||||||
|
results = append(results, upstreamStatus{
|
||||||
|
Address: address,
|
||||||
|
NumRequests: int(count),
|
||||||
|
Fails: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if rangeErr != nil {
|
if rangeErr != nil {
|
||||||
return rangeErr
|
return rangeErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also include dynamic upstreams
|
||||||
|
dynamicHostsMu.RLock()
|
||||||
|
for address, entry := range dynamicHosts {
|
||||||
|
results = append(results, upstreamStatus{
|
||||||
|
Address: address,
|
||||||
|
NumRequests: entry.host.NumRequests(),
|
||||||
|
Fails: entry.host.Fails(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
dynamicHostsMu.RUnlock()
|
||||||
|
|
||||||
err := enc.Encode(results)
|
err := enc.Encode(results)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.APIError{
|
return caddy.APIError{
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
// 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 reverseproxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// adminHandlerFixture sets up the global host state for an admin endpoint test
|
||||||
|
// and returns a cleanup function that must be deferred by the caller.
|
||||||
|
//
|
||||||
|
// staticAddrs are inserted into the UsagePool (as a static upstream would be).
|
||||||
|
// dynamicAddrs are inserted into the dynamicHosts map (as a dynamic upstream would be).
|
||||||
|
func adminHandlerFixture(t *testing.T, staticAddrs, dynamicAddrs []string) func() {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for _, addr := range staticAddrs {
|
||||||
|
u := &Upstream{Dial: addr}
|
||||||
|
u.fillHost()
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamicHostsMu.Lock()
|
||||||
|
for _, addr := range dynamicAddrs {
|
||||||
|
dynamicHosts[addr] = dynamicHostEntry{host: new(Host), lastSeen: time.Now()}
|
||||||
|
}
|
||||||
|
dynamicHostsMu.Unlock()
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
// Remove static entries from the UsagePool.
|
||||||
|
for _, addr := range staticAddrs {
|
||||||
|
_, _ = hosts.Delete(addr)
|
||||||
|
}
|
||||||
|
// Remove dynamic entries.
|
||||||
|
dynamicHostsMu.Lock()
|
||||||
|
for _, addr := range dynamicAddrs {
|
||||||
|
delete(dynamicHosts, addr)
|
||||||
|
}
|
||||||
|
dynamicHostsMu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// callAdminUpstreams fires a GET against handleUpstreams and returns the
|
||||||
|
// decoded response body.
|
||||||
|
func callAdminUpstreams(t *testing.T) []upstreamStatus {
|
||||||
|
t.Helper()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/reverse_proxy/upstreams", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler := adminUpstreams{}
|
||||||
|
if err := handler.handleUpstreams(w, req); err != nil {
|
||||||
|
t.Fatalf("handleUpstreams returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200, got %d", w.Code)
|
||||||
|
}
|
||||||
|
if ct := w.Header().Get("Content-Type"); ct != "application/json" {
|
||||||
|
t.Fatalf("expected Content-Type application/json, got %q", ct)
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []upstreamStatus
|
||||||
|
if err := json.NewDecoder(w.Body).Decode(&results); err != nil {
|
||||||
|
t.Fatalf("failed to decode response: %v", err)
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// resultsByAddress indexes a slice of upstreamStatus by address for easier
|
||||||
|
// lookup in assertions.
|
||||||
|
func resultsByAddress(statuses []upstreamStatus) map[string]upstreamStatus {
|
||||||
|
m := make(map[string]upstreamStatus, len(statuses))
|
||||||
|
for _, s := range statuses {
|
||||||
|
m[s.Address] = s
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAdminUpstreamsMethodNotAllowed verifies that non-GET methods are rejected.
|
||||||
|
func TestAdminUpstreamsMethodNotAllowed(t *testing.T) {
|
||||||
|
for _, method := range []string{http.MethodPost, http.MethodPut, http.MethodDelete} {
|
||||||
|
req := httptest.NewRequest(method, "/reverse_proxy/upstreams", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
err := (adminUpstreams{}).handleUpstreams(w, req)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("method %s: expected an error, got nil", method)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
apiErr, ok := err.(interface{ HTTPStatus() int })
|
||||||
|
if !ok {
|
||||||
|
// caddy.APIError stores the code in HTTPStatus field, access via the
|
||||||
|
// exported interface it satisfies indirectly; just check non-nil.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if code := apiErr.HTTPStatus(); code != http.StatusMethodNotAllowed {
|
||||||
|
t.Errorf("method %s: expected 405, got %d", method, code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAdminUpstreamsEmpty verifies that an empty response is valid JSON when
|
||||||
|
// no upstreams are registered.
|
||||||
|
func TestAdminUpstreamsEmpty(t *testing.T) {
|
||||||
|
resetDynamicHosts()
|
||||||
|
|
||||||
|
results := callAdminUpstreams(t)
|
||||||
|
if results == nil {
|
||||||
|
t.Error("expected non-nil (empty) slice, got nil")
|
||||||
|
}
|
||||||
|
if len(results) != 0 {
|
||||||
|
t.Errorf("expected 0 results with empty pools, got %d", len(results))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAdminUpstreamsStaticOnly verifies that static upstreams (from the
|
||||||
|
// UsagePool) appear in the response with correct addresses.
|
||||||
|
func TestAdminUpstreamsStaticOnly(t *testing.T) {
|
||||||
|
resetDynamicHosts()
|
||||||
|
cleanup := adminHandlerFixture(t,
|
||||||
|
[]string{"10.0.0.1:80", "10.0.0.2:80"},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
results := callAdminUpstreams(t)
|
||||||
|
byAddr := resultsByAddress(results)
|
||||||
|
|
||||||
|
for _, addr := range []string{"10.0.0.1:80", "10.0.0.2:80"} {
|
||||||
|
if _, ok := byAddr[addr]; !ok {
|
||||||
|
t.Errorf("expected static upstream %q in response", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Errorf("expected exactly 2 results, got %d", len(results))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAdminUpstreamsDynamicOnly verifies that dynamic upstreams (from
|
||||||
|
// dynamicHosts) appear in the response with correct addresses.
|
||||||
|
func TestAdminUpstreamsDynamicOnly(t *testing.T) {
|
||||||
|
resetDynamicHosts()
|
||||||
|
cleanup := adminHandlerFixture(t,
|
||||||
|
nil,
|
||||||
|
[]string{"10.0.1.1:80", "10.0.1.2:80"},
|
||||||
|
)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
results := callAdminUpstreams(t)
|
||||||
|
byAddr := resultsByAddress(results)
|
||||||
|
|
||||||
|
for _, addr := range []string{"10.0.1.1:80", "10.0.1.2:80"} {
|
||||||
|
if _, ok := byAddr[addr]; !ok {
|
||||||
|
t.Errorf("expected dynamic upstream %q in response", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Errorf("expected exactly 2 results, got %d", len(results))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAdminUpstreamsBothPools verifies that static and dynamic upstreams are
|
||||||
|
// both present in the same response and that there is no overlap or omission.
|
||||||
|
func TestAdminUpstreamsBothPools(t *testing.T) {
|
||||||
|
resetDynamicHosts()
|
||||||
|
cleanup := adminHandlerFixture(t,
|
||||||
|
[]string{"10.0.2.1:80"},
|
||||||
|
[]string{"10.0.2.2:80"},
|
||||||
|
)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
results := callAdminUpstreams(t)
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Fatalf("expected 2 results (1 static + 1 dynamic), got %d", len(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
byAddr := resultsByAddress(results)
|
||||||
|
if _, ok := byAddr["10.0.2.1:80"]; !ok {
|
||||||
|
t.Error("static upstream missing from response")
|
||||||
|
}
|
||||||
|
if _, ok := byAddr["10.0.2.2:80"]; !ok {
|
||||||
|
t.Error("dynamic upstream missing from response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAdminUpstreamsNoOverlapBetweenPools verifies that an address registered
|
||||||
|
// only as a static upstream does not also appear as a dynamic entry, and
|
||||||
|
// vice-versa.
|
||||||
|
func TestAdminUpstreamsNoOverlapBetweenPools(t *testing.T) {
|
||||||
|
resetDynamicHosts()
|
||||||
|
cleanup := adminHandlerFixture(t,
|
||||||
|
[]string{"10.0.3.1:80"},
|
||||||
|
[]string{"10.0.3.2:80"},
|
||||||
|
)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
results := callAdminUpstreams(t)
|
||||||
|
seen := make(map[string]int)
|
||||||
|
for _, r := range results {
|
||||||
|
seen[r.Address]++
|
||||||
|
}
|
||||||
|
for addr, count := range seen {
|
||||||
|
if count > 1 {
|
||||||
|
t.Errorf("address %q appeared %d times; expected exactly once", addr, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAdminUpstreamsReportsFailCounts verifies that fail counts accumulated on
|
||||||
|
// a dynamic upstream's Host are reflected in the response.
|
||||||
|
func TestAdminUpstreamsReportsFailCounts(t *testing.T) {
|
||||||
|
resetDynamicHosts()
|
||||||
|
|
||||||
|
const addr = "10.0.4.1:80"
|
||||||
|
h := new(Host)
|
||||||
|
_ = h.countFail(3)
|
||||||
|
|
||||||
|
dynamicHostsMu.Lock()
|
||||||
|
dynamicHosts[addr] = dynamicHostEntry{host: h, lastSeen: time.Now()}
|
||||||
|
dynamicHostsMu.Unlock()
|
||||||
|
defer func() {
|
||||||
|
dynamicHostsMu.Lock()
|
||||||
|
delete(dynamicHosts, addr)
|
||||||
|
dynamicHostsMu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
results := callAdminUpstreams(t)
|
||||||
|
byAddr := resultsByAddress(results)
|
||||||
|
|
||||||
|
status, ok := byAddr[addr]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected %q in response", addr)
|
||||||
|
}
|
||||||
|
if status.Fails != 3 {
|
||||||
|
t.Errorf("expected Fails=3, got %d", status.Fails)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAdminUpstreamsReportsNumRequests verifies that the active request count
|
||||||
|
// for a static upstream is reflected in the response.
|
||||||
|
func TestAdminUpstreamsReportsNumRequests(t *testing.T) {
|
||||||
|
resetDynamicHosts()
|
||||||
|
|
||||||
|
const addr = "10.0.4.2:80"
|
||||||
|
u := &Upstream{Dial: addr}
|
||||||
|
u.fillHost()
|
||||||
|
defer func() { _, _ = hosts.Delete(addr) }()
|
||||||
|
|
||||||
|
_ = u.Host.countRequest(2)
|
||||||
|
defer func() { _ = u.Host.countRequest(-2) }()
|
||||||
|
|
||||||
|
results := callAdminUpstreams(t)
|
||||||
|
byAddr := resultsByAddress(results)
|
||||||
|
|
||||||
|
status, ok := byAddr[addr]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected %q in response", addr)
|
||||||
|
}
|
||||||
|
if status.NumRequests != 2 {
|
||||||
|
t.Errorf("expected NumRequests=2, got %d", status.NumRequests)
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user