mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-26 08:42:31 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d6f2a05f8d |
+7
-8
@@ -1,14 +1,15 @@
|
|||||||
# Security Policy
|
# Security Policy
|
||||||
|
|
||||||
The Caddy project would like to make sure that it stays on top of all relevant and practically-exploitable vulnerabilities.
|
The Caddy project would like to make sure that it stays on top of all practically-exploitable vulnerabilities.
|
||||||
|
|
||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ----------- | ----------|
|
| -------- | ----------|
|
||||||
| 2.latest | ✔️ |
|
| 2.latest | ✔️ |
|
||||||
| <= 2.latest | :x: |
|
| 1.x | :x: |
|
||||||
|
| < 1.x | :x: |
|
||||||
|
|
||||||
|
|
||||||
## Acceptable Scope
|
## Acceptable Scope
|
||||||
@@ -25,8 +26,6 @@ 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.
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -34,7 +33,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).
|
||||||
|
|
||||||
: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.
|
**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.**
|
||||||
|
|
||||||
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||||
- uses: github/ai-moderator@81159c370785e295c97461ade67d7c33576e9319
|
- uses: github/ai-moderator@6bcdb2a79c2e564db8d76d7d4439d91a044c4eb6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
spam-label: 'spam'
|
spam-label: 'spam'
|
||||||
|
|||||||
+11
-11
@@ -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@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
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 }}
|
||||||
@@ -162,13 +162,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@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: |
|
run: |
|
||||||
set +e
|
set +e
|
||||||
@@ -221,19 +221,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@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
|
- uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: check
|
args: check
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||||
with:
|
with:
|
||||||
go-version: "~1.26"
|
go-version: "~1.26"
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -241,7 +241,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@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
|
- uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.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@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.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@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.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@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
|
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.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@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
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@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: 'Checkout Repository'
|
- name: 'Checkout Repository'
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: 'Dependency Review'
|
- name: 'Dependency Review'
|
||||||
uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3
|
uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0
|
||||||
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@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
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@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 runs this line:
|
# tl;dr: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 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@17ae1740179002c89186b61233e0f892c3118b11 # main
|
uses: anchore/sbom-action/download-syft@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # 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@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
|
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.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@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
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@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
|
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
|
||||||
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@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
|
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
|
||||||
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@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
|
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
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@89a39a4e59826350b863aa6b6252a07ad50cf83e # v3.29.5
|
uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.29.5
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ linters:
|
|||||||
- importas
|
- importas
|
||||||
- ineffassign
|
- ineffassign
|
||||||
- misspell
|
- misspell
|
||||||
- modernize
|
|
||||||
- prealloc
|
- prealloc
|
||||||
- promlinter
|
- promlinter
|
||||||
- sloglint
|
- sloglint
|
||||||
|
|||||||
+1
-3
@@ -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 arm64; do XCADDY_SKIP_BUILD=1 GOOS=windows GOARCH=$a xcaddy build {{.Env.TAG}}; done'
|
- /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 '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,8 +67,6 @@ 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
|
||||||
|
|||||||
@@ -749,14 +749,10 @@ func stopAdminServer(srv *http.Server) error {
|
|||||||
if srv == nil {
|
if srv == nil {
|
||||||
return fmt.Errorf("no admin server")
|
return fmt.Errorf("no admin server")
|
||||||
}
|
}
|
||||||
timeout := 10 * time.Second
|
ctx, cancel := context.WithTimeout(context.Background(), 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))
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ type Config struct {
|
|||||||
storage certmagic.Storage
|
storage certmagic.Storage
|
||||||
eventEmitter eventEmitter
|
eventEmitter eventEmitter
|
||||||
|
|
||||||
cancelFunc context.CancelCauseFunc
|
cancelFunc context.CancelFunc
|
||||||
|
|
||||||
// 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
|
||||||
@@ -227,18 +227,8 @@ 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.StatusBadRequest,
|
HTTPStatus: http.StatusInternalServerError,
|
||||||
Err: fmt.Errorf("indexing config: %v", err),
|
Err: fmt.Errorf("indexing config: %v", err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,8 +248,6 @@ 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)
|
||||||
@@ -293,19 +281,14 @@ 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:
|
||||||
idStr = idVal
|
index[idVal] = configPath
|
||||||
case float64: // all JSON numbers decode as float64
|
case float64: // all JSON numbers decode as float64
|
||||||
idStr = fmt.Sprintf("%v", idVal)
|
index[fmt.Sprintf("%v", idVal)] = configPath
|
||||||
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
|
||||||
@@ -433,7 +416,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(fmt.Errorf("configuration start error: %w", err))
|
ctx.cfg.cancelFunc()
|
||||||
|
|
||||||
if currentCtx.cfg != nil {
|
if currentCtx.cfg != nil {
|
||||||
certmagic.Default.Storage = currentCtx.cfg.storage
|
certmagic.Default.Storage = currentCtx.cfg.storage
|
||||||
@@ -509,7 +492,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, cancelCause := NewContextWithCause(Context{Context: context.Background(), cfg: newCfg})
|
ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg})
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
globalMetrics.configSuccess.Set(0)
|
globalMetrics.configSuccess.Set(0)
|
||||||
@@ -518,7 +501,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
|
||||||
cancelCause(fmt.Errorf("configuration error: %w", err))
|
cancel()
|
||||||
|
|
||||||
// also undo any other state changes we made
|
// also undo any other state changes we made
|
||||||
if currentCtx.cfg != nil {
|
if currentCtx.cfg != nil {
|
||||||
@@ -526,7 +509,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
newCfg.cancelFunc = cancelCause // clean up later
|
newCfg.cancelFunc = cancel // 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 {
|
||||||
@@ -746,7 +729,7 @@ func unsyncedStop(ctx Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// clean up all modules
|
// clean up all modules
|
||||||
ctx.cfg.cancelFunc(fmt.Errorf("stopping apps"))
|
ctx.cfg.cancelFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate loads, provisions, and validates
|
// Validate loads, provisions, and validates
|
||||||
@@ -754,7 +737,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(fmt.Errorf("validation complete")) // call Cleanup on all modules
|
cfg.cancelFunc() // call Cleanup on all modules
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -962,34 +945,6 @@ 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 := range targets {
|
for i := 0; i < len(targets); i++ {
|
||||||
if !d.NextArg() {
|
if !d.NextArg() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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[len(tokensCopy)-1], token) {
|
if i == 0 || isNextOnNewLine(tokensCopy[i-1], token) {
|
||||||
index = 0
|
index = 0
|
||||||
} else {
|
} else {
|
||||||
index++
|
index++
|
||||||
@@ -616,7 +616,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 := range importedTokens {
|
for i := 0; i < len(importedTokens); i++ {
|
||||||
importedTokens[i].File = filename
|
importedTokens[i].File = filename
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -822,7 +822,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,11 +1,9 @@
|
|||||||
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) {
|
||||||
@@ -211,53 +209,3 @@ 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,7 +64,6 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -307,15 +306,6 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
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/caddytls"
|
|
||||||
_ "github.com/caddyserver/caddy/v2/modules/logging"
|
_ "github.com/caddyserver/caddy/v2/modules/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -64,105 +62,3 @@ 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])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -334,11 +334,6 @@ 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
|
||||||
@@ -600,15 +595,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 := range maxAttempts {
|
for i := 0; i < maxAttempts; i++ {
|
||||||
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,7 +1,6 @@
|
|||||||
package caddytest
|
package caddytest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -127,118 +126,3 @@ 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(), zapslog.WithName("acmez"))),
|
Logger: slog.New(zapslog.NewHandler(logger.Core())),
|
||||||
},
|
},
|
||||||
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(), zapslog.WithName("acmez"))),
|
Logger: slog.New(zapslog.NewHandler(logger.Core())),
|
||||||
},
|
},
|
||||||
ChallengeSolvers: map[string]acmez.Solver{
|
ChallengeSolvers: map[string]acmez.Solver{
|
||||||
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
|
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
|
||||||
|
|||||||
@@ -143,26 +143,3 @@ 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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -46,18 +46,6 @@ app.example.com {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"delete": [
|
|
||||||
"Remote-Email"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -85,18 +73,6 @@ app.example.com {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"delete": [
|
|
||||||
"Remote-Groups"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -124,18 +100,6 @@ app.example.com {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"delete": [
|
|
||||||
"Remote-Name"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -163,18 +127,6 @@ app.example.com {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"delete": [
|
|
||||||
"Remote-User"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -248,4 +200,4 @@ app.example.com {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
: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,18 +35,6 @@ forward_auth localhost:9000 {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"delete": [
|
|
||||||
"1"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -74,18 +62,6 @@ forward_auth localhost:9000 {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"delete": [
|
|
||||||
"B"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -113,18 +89,6 @@ forward_auth localhost:9000 {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"delete": [
|
|
||||||
"3"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -152,18 +116,6 @@ forward_auth localhost:9000 {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"delete": [
|
|
||||||
"D"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -191,18 +143,6 @@ forward_auth localhost:9000 {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"handle": [
|
|
||||||
{
|
|
||||||
"handler": "headers",
|
|
||||||
"request": {
|
|
||||||
"delete": [
|
|
||||||
"5"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"handle": [
|
"handle": [
|
||||||
{
|
{
|
||||||
@@ -263,4 +203,4 @@ forward_auth localhost:9000 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
{
|
|
||||||
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
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
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
@@ -1,72 +0,0 @@
|
|||||||
{
|
|
||||||
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
@@ -1,98 +0,0 @@
|
|||||||
{
|
|
||||||
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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
{
|
|
||||||
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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-52
@@ -1,52 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +1,27 @@
|
|||||||
:80
|
:80
|
||||||
|
|
||||||
log one {
|
log {
|
||||||
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.one",
|
"http.log.access.log0"
|
||||||
"http.log.access.two"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"one": {
|
"log0": {
|
||||||
"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,
|
||||||
@@ -49,35 +29,7 @@ log two {
|
|||||||
"roll_size_mb": 954
|
"roll_size_mb": 954
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"http.log.access.one"
|
"http.log.access.log0"
|
||||||
]
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,7 +42,7 @@ log two {
|
|||||||
":80"
|
":80"
|
||||||
],
|
],
|
||||||
"logs": {
|
"logs": {
|
||||||
"default_logger_name": "two"
|
"default_logger_name": "log0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,206 +0,0 @@
|
|||||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,595 +0,0 @@
|
|||||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -386,68 +386,6 @@ 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()
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
# 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
|
|
||||||
}
|
|
||||||
+4
-14
@@ -9,14 +9,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var defaultFactory = newRootCommandFactory(func() *cobra.Command {
|
var defaultFactory = newRootCommandFactory(func() *cobra.Command {
|
||||||
bin := caddy.CustomBinaryName
|
return &cobra.Command{
|
||||||
if bin == "" {
|
Use: "caddy",
|
||||||
bin = "caddy"
|
Long: `Caddy is an extensible server platform written in Go.
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
@@ -96,12 +91,7 @@ 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, configFile, configAdapterFlag)
|
adminAddr, err := DetermineAdminAPIAddress(addressFlag, config, configFlag, configAdapterFlag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
|
||||||
}
|
}
|
||||||
@@ -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 { //nolint:gosec // path traversal is not really a thing here, this is either "Caddyfile" or admin-controlled
|
if err := os.WriteFile(configFile, output, 0o600); err != nil {
|
||||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err)
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err)
|
||||||
}
|
}
|
||||||
return caddy.ExitCodeSuccess, nil
|
return caddy.ExitCodeSuccess, nil
|
||||||
|
|||||||
+1
-7
@@ -484,13 +484,7 @@ 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(
|
slog.New(zapslog.NewHandler(logger.Core())),
|
||||||
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(
|
||||||
|
|||||||
+6
-18
@@ -63,17 +63,10 @@ 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.WithCancelCause(ctx.Context)
|
c, cancel := context.WithCancel(ctx.Context)
|
||||||
wrappedCancel := func(cause error) {
|
wrappedCancel := func() {
|
||||||
cancel(cause)
|
cancel()
|
||||||
|
|
||||||
for _, f := range ctx.cleanupFuncs {
|
for _, f := range ctx.cleanupFuncs {
|
||||||
f()
|
f()
|
||||||
@@ -615,11 +608,6 @@ 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()
|
||||||
@@ -628,16 +616,16 @@ func (ctx Context) Slogger() *slog.Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
core = l.Core()
|
core = l.Core()
|
||||||
handler = zapslog.NewHandler(core, tracesOpt)
|
handler = zapslog.NewHandler(core)
|
||||||
} else {
|
} else {
|
||||||
mod := ctx.Module()
|
mod := ctx.Module()
|
||||||
if mod == nil {
|
if mod == nil {
|
||||||
core = Log().Core()
|
core = Log().Core()
|
||||||
handler = zapslog.NewHandler(core, tracesOpt)
|
handler = zapslog.NewHandler(core)
|
||||||
} 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), tracesOpt)
|
handler = zapslog.NewHandler(core, zapslog.WithName(moduleID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module github.com/caddyserver/caddy/v2
|
module github.com/caddyserver/caddy/v2
|
||||||
|
|
||||||
go 1.25.0
|
go 1.25
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.6.0
|
github.com/BurntSushi/toml v1.6.0
|
||||||
@@ -9,7 +9,7 @@ require (
|
|||||||
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.2
|
github.com/caddyserver/certmagic v0.25.1
|
||||||
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
|
||||||
@@ -18,10 +18,10 @@ require (
|
|||||||
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.4
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0
|
github.com/klauspost/cpuid/v2 v2.3.0
|
||||||
github.com/mholt/acmez/v3 v3.1.6
|
github.com/mholt/acmez/v3 v3.1.4
|
||||||
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.0
|
||||||
github.com/smallstep/certificates v0.30.0-rc3
|
github.com/smallstep/certificates v0.30.0-rc2.0.20260211214201-20608299c29c
|
||||||
github.com/smallstep/nosql v0.7.0
|
github.com/smallstep/nosql v0.7.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
|
||||||
@@ -35,13 +35,13 @@ require (
|
|||||||
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.40.0
|
||||||
go.opentelemetry.io/otel/sdk v1.40.0
|
go.opentelemetry.io/otel/sdk v1.40.0
|
||||||
go.step.sm/crypto v0.76.2
|
go.step.sm/crypto v0.76.0
|
||||||
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.48.0
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99
|
||||||
golang.org/x/net v0.51.0
|
golang.org/x/net v0.50.0
|
||||||
golang.org/x/sync v0.19.0
|
golang.org/x/sync v0.19.0
|
||||||
golang.org/x/term v0.40.0
|
golang.org/x/term v0.40.0
|
||||||
golang.org/x/time v0.14.0
|
golang.org/x/time v0.14.0
|
||||||
@@ -108,18 +108,18 @@ require (
|
|||||||
go.yaml.in/yaml/v2 v2.4.3 // 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.35.0 // indirect
|
golang.org/x/oauth2 v0.34.0 // indirect
|
||||||
google.golang.org/api v0.266.0 // indirect
|
google.golang.org/api v0.265.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // 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.2.0 // indirect
|
filippo.io/edwards25519 v1.1.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.4.0 // indirect
|
github.com/Masterminds/semver/v3 v3.3.1 // 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
|
||||||
@@ -145,7 +145,7 @@ require (
|
|||||||
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.72 // indirect
|
github.com/miekg/dns v1.1.70 // 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
|
||||||
@@ -172,7 +172,7 @@ require (
|
|||||||
golang.org/x/sys v0.41.0
|
golang.org/x/sys v0.41.0
|
||||||
golang.org/x/text v0.34.0
|
golang.org/x/text v0.34.0
|
||||||
golang.org/x/tools v0.42.0 // indirect
|
golang.org/x/tools v0.42.0 // indirect
|
||||||
google.golang.org/grpc v1.79.1 // indirect
|
google.golang.org/grpc v1.78.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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,14 +14,12 @@ cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ=
|
|||||||
cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk=
|
cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk=
|
||||||
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.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
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=
|
||||||
@@ -34,8 +32,8 @@ github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43Dw
|
|||||||
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.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
|
||||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
github.com/Masterminds/semver/v3 v3.3.1/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=
|
||||||
@@ -85,8 +83,8 @@ github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
|||||||
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||||
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.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc=
|
github.com/caddyserver/certmagic v0.25.1 h1:4sIKKbOt5pg6+sL7tEwymE1x2bj6CHr80da1CRRIPbY=
|
||||||
github.com/caddyserver/certmagic v0.25.2/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg=
|
github.com/caddyserver/certmagic v0.25.1/go.mod h1:VhyvndxtVton/Fo/wKhRoC46Rbw1fmjvQ3GjHYSQTEY=
|
||||||
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=
|
||||||
@@ -225,10 +223,6 @@ 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=
|
||||||
@@ -240,10 +234,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.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk=
|
github.com/mholt/acmez/v3 v3.1.4 h1:DyzZe/RnAzT3rpZj/2Ii5xZpiEvvYk3cQEN/RmqxwFQ=
|
||||||
github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY=
|
github.com/mholt/acmez/v3 v3.1.4/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
||||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
|
||||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
github.com/miekg/dns v1.1.70/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=
|
||||||
@@ -301,8 +295,8 @@ 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-rc3 h1:Lx/NNJ4n+L3Pyx5NtVRGXeqviPPXTFFGLRiC1fCwU50=
|
github.com/smallstep/certificates v0.30.0-rc2.0.20260211214201-20608299c29c h1:XQpX0IPYUAoJ661YlgfOJmY48ZOhIbglw4E2gw9mcyc=
|
||||||
github.com/smallstep/certificates v0.30.0-rc3/go.mod h1:e5/ylYYpvnjCVZz6RpyOkpTe73EGPYoL+8TZZ5EtLjI=
|
github.com/smallstep/certificates v0.30.0-rc2.0.20260211214201-20608299c29c/go.mod h1:75NRLmYJq6ZcCb8ApJc+W1eL4oMYwjeufMJDHpv4rx4=
|
||||||
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=
|
||||||
@@ -431,8 +425,8 @@ go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZY
|
|||||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||||
go.step.sm/crypto v0.76.2 h1:JJ/yMcs/rmcCAwlo+afrHjq74XBFRTJw5B2y4Q4Z4c4=
|
go.step.sm/crypto v0.76.0 h1:K23BSaeoiY7Y5dvvijTeYC9EduDBetNwQYMBwMhi1aA=
|
||||||
go.step.sm/crypto v0.76.2/go.mod h1:m6KlB/HzIuGFep0UWI5e0SYi38UxpoKeCg6qUaHV6/Q=
|
go.step.sm/crypto v0.76.0/go.mod h1:PXYJdKkK8s+GHLwLguFaLxHNAFsFL3tL1vSBrYfey5k=
|
||||||
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=
|
||||||
@@ -458,8 +452,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
|
|||||||
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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 h1:FmKxj9ocLKn45jiR2jQMwCVhDvaK7fKQFzfuT9GvyK8=
|
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/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs=
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20250927194341-2beaa59a3c99/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18=
|
||||||
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=
|
||||||
@@ -477,10 +471,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.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
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=
|
||||||
@@ -543,16 +537,16 @@ golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0
|
|||||||
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.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/api v0.266.0 h1:hco+oNCf9y7DmLeAtHJi/uBAY7n/7XC9mZPxu1ROiyk=
|
google.golang.org/api v0.265.0 h1:FZvfUdI8nfmuNrE34aOWFPmLC+qRBEiNm3JdivTvAAU=
|
||||||
google.golang.org/api v0.266.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
|
google.golang.org/api v0.265.0/go.mod h1:uAvfEl3SLUj/7n6k+lJutcswVojHPp2Sp08jWCu8hLY=
|
||||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
|
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
|
||||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
|
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
|
||||||
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-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
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=
|
||||||
|
|||||||
+91
-11
@@ -38,6 +38,10 @@ 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().
|
||||||
@@ -229,7 +233,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 := range size {
|
for portOffset := uint(0); portOffset < size; portOffset++ {
|
||||||
addrs[portOffset] = na.At(portOffset)
|
addrs[portOffset] = na.At(portOffset)
|
||||||
}
|
}
|
||||||
return addrs
|
return addrs
|
||||||
@@ -305,6 +309,64 @@ 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
|
||||||
@@ -336,9 +398,27 @@ 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: host,
|
Host: fdAddr,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
var start, end uint64
|
var start, end uint64
|
||||||
@@ -512,7 +592,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.CancelCauseFunc
|
context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// sharedQUICState manages GetConfigForClient
|
// sharedQUICState manages GetConfigForClient
|
||||||
@@ -542,17 +622,17 @@ func (sqs *sharedQUICState) getConfigForClient(ch *tls.ClientHelloInfo) (*tls.Co
|
|||||||
|
|
||||||
// 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.CancelCauseFunc) {
|
func (sqs *sharedQUICState) addState(tlsConfig *tls.Config) (context.Context, context.CancelFunc) {
|
||||||
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.CancelCauseFunc
|
return cacc.Context, cacc.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancelCause(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
wrappedCancel := func(cause error) {
|
wrappedCancel := func() {
|
||||||
cancel(cause)
|
cancel()
|
||||||
|
|
||||||
sqs.rmu.Lock()
|
sqs.rmu.Lock()
|
||||||
defer sqs.rmu.Unlock()
|
defer sqs.rmu.Unlock()
|
||||||
@@ -608,13 +688,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("QUIC listener 'closed' 😉")
|
var errFakeClosed = fmt.Errorf("listener 'closed' 😉")
|
||||||
|
|
||||||
type fakeCloseQuicListener struct {
|
type fakeCloseQuicListener struct {
|
||||||
closed int32 // accessed atomically; belongs to this struct only
|
closed int32 // accessed atomically; belongs to this struct only
|
||||||
*sharedQuicListener // embedded, so we also become a quic.EarlyListener
|
*sharedQuicListener // embedded, so we also become a quic.EarlyListener
|
||||||
context context.Context
|
context context.Context
|
||||||
contextCancel context.CancelCauseFunc
|
contextCancel context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// Currently Accept ignores the passed context, however a situation where
|
// Currently Accept ignores the passed context, however a situation where
|
||||||
@@ -637,7 +717,7 @@ func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (*quic.Conn, error)
|
|||||||
|
|
||||||
func (fcql *fakeCloseQuicListener) Close() error {
|
func (fcql *fakeCloseQuicListener) Close() error {
|
||||||
if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) {
|
if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) {
|
||||||
fcql.contextCancel(errFakeClosed)
|
fcql.contextCancel()
|
||||||
} else if atomic.CompareAndSwapInt32(&fcql.closed, 1, 2) {
|
} else if atomic.CompareAndSwapInt32(&fcql.closed, 1, 2) {
|
||||||
_, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
|
_, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
package caddy
|
package caddy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -652,3 +653,286 @@ 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import (
|
|||||||
"cmp"
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"net"
|
"net"
|
||||||
@@ -712,10 +711,9 @@ 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
|
||||||
timeout := time.Duration(app.GracePeriod)
|
ctx, cancel = context.WithTimeout(ctx, 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", timeout))
|
app.logger.Info("servers shutting down; grace period initiated", zap.Duration("duration", time.Duration(app.GracePeriod)))
|
||||||
} else {
|
} else {
|
||||||
app.logger.Info("servers shutting down with eternal grace period")
|
app.logger.Info("servers shutting down with eternal grace period")
|
||||||
}
|
}
|
||||||
@@ -741,9 +739,6 @@ 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))
|
||||||
@@ -767,9 +762,6 @@ 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))
|
||||||
|
|||||||
@@ -424,40 +424,6 @@ 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]
|
||||||
@@ -614,27 +580,6 @@ 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)
|
||||||
@@ -848,26 +793,3 @@ 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 ""
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -307,6 +307,14 @@ 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.
|
||||||
|
|||||||
@@ -720,7 +720,6 @@ var globSafeRepl = strings.NewReplacer(
|
|||||||
"*", "\\*",
|
"*", "\\*",
|
||||||
"[", "\\[",
|
"[", "\\[",
|
||||||
"?", "\\?",
|
"?", "\\?",
|
||||||
"\\", "\\\\",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -20,9 +20,7 @@ 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"
|
||||||
@@ -30,13 +28,6 @@ import (
|
|||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
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"
|
||||||
@@ -54,7 +45,12 @@ func TestFileMatcher(t *testing.T) {
|
|||||||
f.Close()
|
f.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, tc := range []testCase{
|
for i, tc := range []struct {
|
||||||
|
path string
|
||||||
|
expectedPath string
|
||||||
|
expectedType string
|
||||||
|
matched bool
|
||||||
|
}{
|
||||||
{
|
{
|
||||||
path: "/foo.txt",
|
path: "/foo.txt",
|
||||||
expectedPath: "/foo.txt",
|
expectedPath: "/foo.txt",
|
||||||
@@ -120,71 +116,44 @@ func TestFileMatcher(t *testing.T) {
|
|||||||
matched: !isWindows,
|
matched: !isWindows,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
fileMatcherTest(t, i, tc)
|
m := &MatchFile{
|
||||||
}
|
fsmap: &filesystems.FileSystemMap{},
|
||||||
}
|
Root: "./testdata",
|
||||||
|
TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"},
|
||||||
|
}
|
||||||
|
|
||||||
func TestFileMatcherNonWindows(t *testing.T) {
|
u, err := url.Parse(tc.path)
|
||||||
if runtime.GOOS == "windows" {
|
if err != nil {
|
||||||
return
|
t.Errorf("Test %d: parsing path: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is impossible to test on Windows, but tests a security patch for other platforms
|
req := &http.Request{URL: u}
|
||||||
tc := testCase{
|
repl := caddyhttp.NewTestReplacer(req)
|
||||||
path: "/foodir/secr%5Cet.txt",
|
|
||||||
expectedPath: "/foodir/secr\\et.txt",
|
|
||||||
expectedType: "file",
|
|
||||||
matched: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.Create(filepath.Join("testdata", strings.TrimPrefix(tc.expectedPath, "/")))
|
result, err := m.MatchWithError(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("could not create test file: %v", err)
|
t.Errorf("Test %d: unexpected error: %v", i, err)
|
||||||
}
|
}
|
||||||
defer f.Close()
|
if result != tc.matched {
|
||||||
defer os.Remove(f.Name())
|
t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result)
|
||||||
|
}
|
||||||
|
|
||||||
fileMatcherTest(t, 0, tc)
|
rel, ok := repl.Get("http.matchers.file.relative")
|
||||||
}
|
if !ok && result {
|
||||||
|
t.Errorf("Test %d: expected replacer value", i)
|
||||||
|
}
|
||||||
|
if !result {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
func fileMatcherTest(t *testing.T, i int, tc testCase) {
|
if rel != tc.expectedPath {
|
||||||
m := &MatchFile{
|
t.Errorf("Test %d: actual path: %v, expected: %v", i, rel, tc.expectedPath)
|
||||||
fsmap: &filesystems.FileSystemMap{},
|
}
|
||||||
Root: "./testdata",
|
|
||||||
TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"},
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := url.Parse(tc.path)
|
fileType, _ := repl.Get("http.matchers.file.type")
|
||||||
if err != nil {
|
if fileType != tc.expectedType {
|
||||||
t.Errorf("Test %d: parsing path: %v", i, err)
|
t.Errorf("Test %d: actual file type: %v, expected: %v", i, fileType, tc.expectedType)
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,11 +125,6 @@ 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.
|
||||||
|
|||||||
@@ -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 {
|
||||||
_, after, ok := strings.Cut(s, "{")
|
openIdx := strings.Index(s, "{")
|
||||||
if !ok {
|
if openIdx == -1 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
closeIdx := strings.Index(after, "}")
|
closeIdx := strings.Index(s[openIdx+1:], "}")
|
||||||
if closeIdx == -1 {
|
if closeIdx == -1 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,13 +266,7 @@ 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)
|
||||||
}
|
}
|
||||||
// Normalize exact hosts for standardized comparison in large-list fastpath later on.
|
m[i] = normalizedHost // normalize for all comparisons while matching
|
||||||
// Keep wildcards/placeholders untouched.
|
|
||||||
if m.fuzzy(asciiHost) {
|
|
||||||
m[i] = asciiHost
|
|
||||||
} else {
|
|
||||||
m[i] = normalizedHost
|
|
||||||
}
|
|
||||||
seen[normalizedHost] = i
|
seen[normalizedHost] = i
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,15 +310,14 @@ 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] >= reqHostLower
|
return m[i] >= reqHost
|
||||||
})
|
})
|
||||||
if pos < len(m) && m[pos] == reqHostLower {
|
if pos < len(m) && strings.EqualFold(m[pos], reqHost) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -538,7 +531,6 @@ 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
|
||||||
|
|||||||
@@ -417,11 +417,6 @@ 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 {
|
||||||
@@ -967,7 +962,6 @@ 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
|
||||||
}{
|
}{
|
||||||
@@ -1002,14 +996,6 @@ 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()
|
||||||
@@ -1026,7 +1012,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, Header: tc.headers}
|
req := &http.Request{URL: new(url.URL), Method: http.MethodGet}
|
||||||
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))
|
||||||
|
|||||||
@@ -214,24 +214,21 @@ func serverNameFromContext(ctx context.Context) string {
|
|||||||
return srv.name
|
return srv.name
|
||||||
}
|
}
|
||||||
|
|
||||||
// metricsInstrumentedRoute wraps a compiled route Handler with metrics
|
type metricsInstrumentedHandler struct {
|
||||||
// instrumentation. It wraps the entire compiled route chain once,
|
|
||||||
// collecting metrics only once per route match.
|
|
||||||
type metricsInstrumentedRoute struct {
|
|
||||||
handler string
|
handler string
|
||||||
next Handler
|
mh MiddlewareHandler
|
||||||
metrics *Metrics
|
metrics *Metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMetricsInstrumentedRoute(ctx caddy.Context, handler string, next Handler, m *Metrics) *metricsInstrumentedRoute {
|
func newMetricsInstrumentedHandler(ctx caddy.Context, handler string, mh MiddlewareHandler, metrics *Metrics) *metricsInstrumentedHandler {
|
||||||
m.init.Do(func() {
|
metrics.init.Do(func() {
|
||||||
initHTTPMetrics(ctx, m)
|
initHTTPMetrics(ctx, metrics)
|
||||||
})
|
})
|
||||||
|
|
||||||
return &metricsInstrumentedRoute{handler: handler, next: next, metrics: m}
|
return &metricsInstrumentedHandler{handler, mh, metrics}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *metricsInstrumentedRoute) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
|
func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) 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 := metrics.SanitizeMethod(r.Method)
|
||||||
@@ -270,7 +267,7 @@ func (h *metricsInstrumentedRoute) ServeHTTP(w http.ResponseWriter, r *http.Requ
|
|||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
wrec := NewResponseRecorder(w, nil, writeHeaderRecorder)
|
wrec := NewResponseRecorder(w, nil, writeHeaderRecorder)
|
||||||
err := h.next.ServeHTTP(wrec, r)
|
err := h.mh.ServeHTTP(wrec, r, next)
|
||||||
dur := time.Since(start).Seconds()
|
dur := time.Since(start).Seconds()
|
||||||
h.metrics.httpMetrics.requestCount.With(labels).Inc()
|
h.metrics.httpMetrics.requestCount.With(labels).Inc()
|
||||||
|
|
||||||
|
|||||||
@@ -47,12 +47,16 @@ func TestMetricsInstrumentedHandler(t *testing.T) {
|
|||||||
return handlerErr
|
return handlerErr
|
||||||
})
|
})
|
||||||
|
|
||||||
ih := newMetricsInstrumentedRoute(ctx, "bar", h, metrics)
|
mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
|
||||||
|
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); actual != handlerErr {
|
if actual := ih.ServeHTTP(w, r, h); 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 {
|
||||||
@@ -60,19 +64,19 @@ func TestMetricsInstrumentedHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handlerErr = nil
|
handlerErr = nil
|
||||||
if err := ih.ServeHTTP(w, r); err != nil {
|
if err := ih.ServeHTTP(w, r, h); 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
|
||||||
emptyHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
mh = middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
ih = newMetricsInstrumentedRoute(ctx, "empty", emptyHandler, metrics)
|
ih = newMetricsInstrumentedHandler(ctx, "empty", mh, metrics)
|
||||||
r = httptest.NewRequest("GET", "/", nil)
|
r = httptest.NewRequest("GET", "/", nil)
|
||||||
w = httptest.NewRecorder()
|
w = httptest.NewRecorder()
|
||||||
|
|
||||||
if err := ih.ServeHTTP(w, r); err != nil {
|
if err := ih.ServeHTTP(w, r, h); 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 {
|
||||||
@@ -83,16 +87,16 @@ func TestMetricsInstrumentedHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handler returning an error with an HTTP status
|
// handler returning an error with an HTTP status
|
||||||
errHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
mh = middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
|
||||||
return Error(http.StatusTooManyRequests, nil)
|
return Error(http.StatusTooManyRequests, nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
ih = newMetricsInstrumentedRoute(ctx, "foo", errHandler, metrics)
|
ih = newMetricsInstrumentedHandler(ctx, "foo", mh, metrics)
|
||||||
|
|
||||||
r = httptest.NewRequest("GET", "/", nil)
|
r = httptest.NewRequest("GET", "/", nil)
|
||||||
w = httptest.NewRecorder()
|
w = httptest.NewRecorder()
|
||||||
|
|
||||||
if err := ih.ServeHTTP(w, r); err == nil {
|
if err := ih.ServeHTTP(w, r, nil); err == nil {
|
||||||
t.Errorf("expected error to be propagated")
|
t.Errorf("expected error to be propagated")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,12 +225,16 @@ func TestMetricsInstrumentedHandlerPerHost(t *testing.T) {
|
|||||||
return handlerErr
|
return handlerErr
|
||||||
})
|
})
|
||||||
|
|
||||||
ih := newMetricsInstrumentedRoute(ctx, "bar", h, metrics)
|
mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
|
||||||
|
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); actual != handlerErr {
|
if actual := ih.ServeHTTP(w, r, h); 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 {
|
||||||
@@ -234,19 +242,19 @@ func TestMetricsInstrumentedHandlerPerHost(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handlerErr = nil
|
handlerErr = nil
|
||||||
if err := ih.ServeHTTP(w, r); err != nil {
|
if err := ih.ServeHTTP(w, r, h); 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
|
||||||
emptyHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
mh = middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
ih = newMetricsInstrumentedRoute(ctx, "empty", emptyHandler, metrics)
|
ih = newMetricsInstrumentedHandler(ctx, "empty", mh, metrics)
|
||||||
r = httptest.NewRequest("GET", "/", nil)
|
r = httptest.NewRequest("GET", "/", nil)
|
||||||
w = httptest.NewRecorder()
|
w = httptest.NewRecorder()
|
||||||
|
|
||||||
if err := ih.ServeHTTP(w, r); err != nil {
|
if err := ih.ServeHTTP(w, r, h); 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 {
|
||||||
@@ -257,16 +265,16 @@ func TestMetricsInstrumentedHandlerPerHost(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handler returning an error with an HTTP status
|
// handler returning an error with an HTTP status
|
||||||
errHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
mh = middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
|
||||||
return Error(http.StatusTooManyRequests, nil)
|
return Error(http.StatusTooManyRequests, nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
ih = newMetricsInstrumentedRoute(ctx, "foo", errHandler, metrics)
|
ih = newMetricsInstrumentedHandler(ctx, "foo", mh, metrics)
|
||||||
|
|
||||||
r = httptest.NewRequest("GET", "/", nil)
|
r = httptest.NewRequest("GET", "/", nil)
|
||||||
w = httptest.NewRecorder()
|
w = httptest.NewRecorder()
|
||||||
|
|
||||||
if err := ih.ServeHTTP(w, r); err == nil {
|
if err := ih.ServeHTTP(w, r, nil); err == nil {
|
||||||
t.Errorf("expected error to be propagated")
|
t.Errorf("expected error to be propagated")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,30 +397,30 @@ func TestMetricsCardinalityProtection(t *testing.T) {
|
|||||||
// Add one allowed host
|
// Add one allowed host
|
||||||
metrics.allowedHosts["allowed.com"] = struct{}{}
|
metrics.allowedHosts["allowed.com"] = struct{}{}
|
||||||
|
|
||||||
h := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
|
||||||
w.Write([]byte("hello"))
|
w.Write([]byte("hello"))
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
ih := newMetricsInstrumentedRoute(ctx, "test", h, metrics)
|
ih := newMetricsInstrumentedHandler(ctx, "test", mh, 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)
|
ih.ServeHTTP(w1, r1, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
|
||||||
|
|
||||||
// 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)
|
ih.ServeHTTP(w2, r2, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
|
||||||
|
|
||||||
// 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)
|
ih.ServeHTTP(w3, r3, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
|
||||||
|
|
||||||
// Check that metrics contain:
|
// Check that metrics contain:
|
||||||
// - One entry for "allowed.com"
|
// - One entry for "allowed.com"
|
||||||
@@ -444,26 +452,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
|
||||||
}
|
}
|
||||||
|
|
||||||
h := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
|
||||||
w.Write([]byte("hello"))
|
w.Write([]byte("hello"))
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
ih := newMetricsInstrumentedRoute(ctx, "test", h, metrics)
|
ih := newMetricsInstrumentedHandler(ctx, "test", mh, 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)
|
ih.ServeHTTP(w1, r1, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
|
||||||
|
|
||||||
// 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)
|
ih.ServeHTTP(w2, r2, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
|
||||||
|
|
||||||
// Check that HTTPS request gets real host, HTTP gets "_other"
|
// Check that HTTPS request gets real host, HTTP gets "_other"
|
||||||
expected := `
|
expected := `
|
||||||
@@ -480,102 +488,8 @@ func TestMetricsHTTPSCatchAll(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMetricsInstrumentedRoute(t *testing.T) {
|
type middlewareHandlerFunc func(http.ResponseWriter, *http.Request, Handler) error
|
||||||
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
|
||||||
m := &Metrics{
|
|
||||||
init: sync.Once{},
|
|
||||||
httpMetrics: &httpMetrics{},
|
|
||||||
}
|
|
||||||
|
|
||||||
handlerErr := errors.New("oh noes")
|
func (f middlewareHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, h Handler) error {
|
||||||
response := []byte("hello world!")
|
return f(w, r, h)
|
||||||
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 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -420,16 +420,7 @@ 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 {
|
||||||
// Instead of returning (nil, false) here, we set it to a dummy
|
return nil, false
|
||||||
// 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)
|
||||||
|
|||||||
@@ -73,9 +73,8 @@ 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 static upstream pool (needs to be fast)
|
// Iterate over the 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)
|
||||||
@@ -96,8 +95,6 @@ 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(),
|
||||||
@@ -106,32 +103,11 @@ func (adminUpstreams) handleUpstreams(w http.ResponseWriter, r *http.Request) er
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
currentInFlight := getInFlightRequests()
|
// If an error happened during the range, return it
|
||||||
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{
|
||||||
|
|||||||
@@ -1,275 +0,0 @@
|
|||||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,345 +0,0 @@
|
|||||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package reverseproxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// resetDynamicHosts clears global dynamic host state between tests.
|
|
||||||
func resetDynamicHosts() {
|
|
||||||
dynamicHostsMu.Lock()
|
|
||||||
dynamicHosts = make(map[string]dynamicHostEntry)
|
|
||||||
dynamicHostsMu.Unlock()
|
|
||||||
// Reset the Once so cleanup goroutine tests can re-trigger if needed.
|
|
||||||
dynamicHostsCleanerOnce = sync.Once{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestFillDynamicHostCreatesEntry verifies that calling fillDynamicHost on a
|
|
||||||
// new address inserts an entry into dynamicHosts and assigns a non-nil Host.
|
|
||||||
func TestFillDynamicHostCreatesEntry(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
|
|
||||||
u := &Upstream{Dial: "192.0.2.1:80"}
|
|
||||||
u.fillDynamicHost()
|
|
||||||
|
|
||||||
if u.Host == nil {
|
|
||||||
t.Fatal("expected Host to be set after fillDynamicHost")
|
|
||||||
}
|
|
||||||
|
|
||||||
dynamicHostsMu.RLock()
|
|
||||||
entry, ok := dynamicHosts["192.0.2.1:80"]
|
|
||||||
dynamicHostsMu.RUnlock()
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
t.Fatal("expected entry in dynamicHosts map")
|
|
||||||
}
|
|
||||||
if entry.host != u.Host {
|
|
||||||
t.Error("dynamicHosts entry host should be the same pointer assigned to Upstream.Host")
|
|
||||||
}
|
|
||||||
if entry.lastSeen.IsZero() {
|
|
||||||
t.Error("expected lastSeen to be set")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestFillDynamicHostReusesSameHost verifies that two calls for the same address
|
|
||||||
// return the exact same *Host pointer so that state (e.g. fail counts) is shared.
|
|
||||||
func TestFillDynamicHostReusesSameHost(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
|
|
||||||
u1 := &Upstream{Dial: "192.0.2.2:80"}
|
|
||||||
u1.fillDynamicHost()
|
|
||||||
|
|
||||||
u2 := &Upstream{Dial: "192.0.2.2:80"}
|
|
||||||
u2.fillDynamicHost()
|
|
||||||
|
|
||||||
if u1.Host != u2.Host {
|
|
||||||
t.Error("expected both upstreams to share the same *Host pointer")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestFillDynamicHostUpdatesLastSeen verifies that a second call for the same
|
|
||||||
// address advances the lastSeen timestamp.
|
|
||||||
func TestFillDynamicHostUpdatesLastSeen(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
|
|
||||||
u := &Upstream{Dial: "192.0.2.3:80"}
|
|
||||||
u.fillDynamicHost()
|
|
||||||
|
|
||||||
dynamicHostsMu.RLock()
|
|
||||||
first := dynamicHosts["192.0.2.3:80"].lastSeen
|
|
||||||
dynamicHostsMu.RUnlock()
|
|
||||||
|
|
||||||
// Ensure measurable time passes.
|
|
||||||
time.Sleep(2 * time.Millisecond)
|
|
||||||
|
|
||||||
u2 := &Upstream{Dial: "192.0.2.3:80"}
|
|
||||||
u2.fillDynamicHost()
|
|
||||||
|
|
||||||
dynamicHostsMu.RLock()
|
|
||||||
second := dynamicHosts["192.0.2.3:80"].lastSeen
|
|
||||||
dynamicHostsMu.RUnlock()
|
|
||||||
|
|
||||||
if !second.After(first) {
|
|
||||||
t.Error("expected lastSeen to be updated on second fillDynamicHost call")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestFillDynamicHostIndependentAddresses verifies that different addresses get
|
|
||||||
// independent Host entries.
|
|
||||||
func TestFillDynamicHostIndependentAddresses(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
|
|
||||||
u1 := &Upstream{Dial: "192.0.2.4:80"}
|
|
||||||
u1.fillDynamicHost()
|
|
||||||
|
|
||||||
u2 := &Upstream{Dial: "192.0.2.5:80"}
|
|
||||||
u2.fillDynamicHost()
|
|
||||||
|
|
||||||
if u1.Host == u2.Host {
|
|
||||||
t.Error("different addresses should have different *Host entries")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestFillDynamicHostPreservesFailCount verifies that fail counts on a dynamic
|
|
||||||
// host survive across multiple fillDynamicHost calls (simulating sequential
|
|
||||||
// requests), which is the core behaviour fixed by this change.
|
|
||||||
func TestFillDynamicHostPreservesFailCount(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
|
|
||||||
// First "request": provision and record a failure.
|
|
||||||
u1 := &Upstream{Dial: "192.0.2.6:80"}
|
|
||||||
u1.fillDynamicHost()
|
|
||||||
_ = u1.Host.countFail(1)
|
|
||||||
|
|
||||||
if u1.Host.Fails() != 1 {
|
|
||||||
t.Fatalf("expected 1 fail, got %d", u1.Host.Fails())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second "request": provision the same address again (new *Upstream, same address).
|
|
||||||
u2 := &Upstream{Dial: "192.0.2.6:80"}
|
|
||||||
u2.fillDynamicHost()
|
|
||||||
|
|
||||||
if u2.Host.Fails() != 1 {
|
|
||||||
t.Errorf("expected fail count to persist across fillDynamicHost calls, got %d", u2.Host.Fails())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestProvisionUpstreamDynamic verifies that provisionUpstream with dynamic=true
|
|
||||||
// uses fillDynamicHost (not the UsagePool) and sets healthCheckPolicy /
|
|
||||||
// MaxRequests correctly from handler config.
|
|
||||||
func TestProvisionUpstreamDynamic(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
|
|
||||||
passive := &PassiveHealthChecks{
|
|
||||||
FailDuration: caddy.Duration(10 * time.Second),
|
|
||||||
MaxFails: 3,
|
|
||||||
UnhealthyRequestCount: 5,
|
|
||||||
}
|
|
||||||
h := Handler{
|
|
||||||
HealthChecks: &HealthChecks{
|
|
||||||
Passive: passive,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
u := &Upstream{Dial: "192.0.2.7:80"}
|
|
||||||
h.provisionUpstream(u, true)
|
|
||||||
|
|
||||||
if u.Host == nil {
|
|
||||||
t.Fatal("Host should be set after provisionUpstream")
|
|
||||||
}
|
|
||||||
if u.healthCheckPolicy != passive {
|
|
||||||
t.Error("healthCheckPolicy should point to the handler's PassiveHealthChecks")
|
|
||||||
}
|
|
||||||
if u.MaxRequests != 5 {
|
|
||||||
t.Errorf("expected MaxRequests=5 from UnhealthyRequestCount, got %d", u.MaxRequests)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Must be in dynamicHosts, not in the static UsagePool.
|
|
||||||
dynamicHostsMu.RLock()
|
|
||||||
_, inDynamic := dynamicHosts["192.0.2.7:80"]
|
|
||||||
dynamicHostsMu.RUnlock()
|
|
||||||
if !inDynamic {
|
|
||||||
t.Error("dynamic upstream should be stored in dynamicHosts")
|
|
||||||
}
|
|
||||||
_, inPool := hosts.References("192.0.2.7:80")
|
|
||||||
if inPool {
|
|
||||||
t.Error("dynamic upstream should NOT be stored in the static UsagePool")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestProvisionUpstreamStatic verifies that provisionUpstream with dynamic=false
|
|
||||||
// uses the UsagePool and does NOT insert into dynamicHosts.
|
|
||||||
func TestProvisionUpstreamStatic(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
|
|
||||||
h := Handler{}
|
|
||||||
|
|
||||||
u := &Upstream{Dial: "192.0.2.8:80"}
|
|
||||||
h.provisionUpstream(u, false)
|
|
||||||
|
|
||||||
if u.Host == nil {
|
|
||||||
t.Fatal("Host should be set after provisionUpstream")
|
|
||||||
}
|
|
||||||
|
|
||||||
refs, inPool := hosts.References("192.0.2.8:80")
|
|
||||||
if !inPool {
|
|
||||||
t.Error("static upstream should be in the UsagePool")
|
|
||||||
}
|
|
||||||
if refs != 1 {
|
|
||||||
t.Errorf("expected ref count 1, got %d", refs)
|
|
||||||
}
|
|
||||||
|
|
||||||
dynamicHostsMu.RLock()
|
|
||||||
_, inDynamic := dynamicHosts["192.0.2.8:80"]
|
|
||||||
dynamicHostsMu.RUnlock()
|
|
||||||
if inDynamic {
|
|
||||||
t.Error("static upstream should NOT be in dynamicHosts")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up the pool entry we just added.
|
|
||||||
_, _ = hosts.Delete("192.0.2.8:80")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDynamicHostHealthyConsultsFails verifies the end-to-end passive health
|
|
||||||
// check path: after enough failures are recorded against a dynamic upstream's
|
|
||||||
// shared *Host, Healthy() returns false for a newly provisioned *Upstream with
|
|
||||||
// the same address.
|
|
||||||
func TestDynamicHostHealthyConsultsFails(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
|
|
||||||
passive := &PassiveHealthChecks{
|
|
||||||
FailDuration: caddy.Duration(time.Minute),
|
|
||||||
MaxFails: 2,
|
|
||||||
}
|
|
||||||
h := Handler{
|
|
||||||
HealthChecks: &HealthChecks{Passive: passive},
|
|
||||||
}
|
|
||||||
|
|
||||||
// First request: provision and record two failures.
|
|
||||||
u1 := &Upstream{Dial: "192.0.2.9:80"}
|
|
||||||
h.provisionUpstream(u1, true)
|
|
||||||
|
|
||||||
_ = u1.Host.countFail(1)
|
|
||||||
_ = u1.Host.countFail(1)
|
|
||||||
|
|
||||||
// Second request: fresh *Upstream, same address.
|
|
||||||
u2 := &Upstream{Dial: "192.0.2.9:80"}
|
|
||||||
h.provisionUpstream(u2, true)
|
|
||||||
|
|
||||||
if u2.Healthy() {
|
|
||||||
t.Error("upstream should be unhealthy after MaxFails failures have been recorded against its shared Host")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDynamicHostCleanupEvictsStaleEntries verifies that the cleanup sweep
|
|
||||||
// removes entries whose lastSeen is older than dynamicHostIdleExpiry.
|
|
||||||
func TestDynamicHostCleanupEvictsStaleEntries(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
|
|
||||||
const addr = "192.0.2.10:80"
|
|
||||||
|
|
||||||
// Insert an entry directly with a lastSeen far in the past.
|
|
||||||
dynamicHostsMu.Lock()
|
|
||||||
dynamicHosts[addr] = dynamicHostEntry{
|
|
||||||
host: new(Host),
|
|
||||||
lastSeen: time.Now().Add(-2 * dynamicHostIdleExpiry),
|
|
||||||
}
|
|
||||||
dynamicHostsMu.Unlock()
|
|
||||||
|
|
||||||
// Run the cleanup logic inline (same logic as the goroutine).
|
|
||||||
dynamicHostsMu.Lock()
|
|
||||||
for a, entry := range dynamicHosts {
|
|
||||||
if time.Since(entry.lastSeen) > dynamicHostIdleExpiry {
|
|
||||||
delete(dynamicHosts, a)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dynamicHostsMu.Unlock()
|
|
||||||
|
|
||||||
dynamicHostsMu.RLock()
|
|
||||||
_, stillPresent := dynamicHosts[addr]
|
|
||||||
dynamicHostsMu.RUnlock()
|
|
||||||
|
|
||||||
if stillPresent {
|
|
||||||
t.Error("stale dynamic host entry should have been evicted by cleanup sweep")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDynamicHostCleanupRetainsFreshEntries verifies that the cleanup sweep
|
|
||||||
// keeps entries whose lastSeen is within dynamicHostIdleExpiry.
|
|
||||||
func TestDynamicHostCleanupRetainsFreshEntries(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
|
|
||||||
const addr = "192.0.2.11:80"
|
|
||||||
|
|
||||||
dynamicHostsMu.Lock()
|
|
||||||
dynamicHosts[addr] = dynamicHostEntry{
|
|
||||||
host: new(Host),
|
|
||||||
lastSeen: time.Now(),
|
|
||||||
}
|
|
||||||
dynamicHostsMu.Unlock()
|
|
||||||
|
|
||||||
// Run the cleanup logic inline.
|
|
||||||
dynamicHostsMu.Lock()
|
|
||||||
for a, entry := range dynamicHosts {
|
|
||||||
if time.Since(entry.lastSeen) > dynamicHostIdleExpiry {
|
|
||||||
delete(dynamicHosts, a)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dynamicHostsMu.Unlock()
|
|
||||||
|
|
||||||
dynamicHostsMu.RLock()
|
|
||||||
_, stillPresent := dynamicHosts[addr]
|
|
||||||
dynamicHostsMu.RUnlock()
|
|
||||||
|
|
||||||
if !stillPresent {
|
|
||||||
t.Error("fresh dynamic host entry should be retained by cleanup sweep")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDynamicHostConcurrentFillHost verifies that concurrent calls to
|
|
||||||
// fillDynamicHost for the same address all get the same *Host pointer and
|
|
||||||
// don't race (run with -race).
|
|
||||||
func TestDynamicHostConcurrentFillHost(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
|
|
||||||
const addr = "192.0.2.12:80"
|
|
||||||
const goroutines = 50
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
hosts := make([]*Host, goroutines)
|
|
||||||
|
|
||||||
for i := range goroutines {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(idx int) {
|
|
||||||
defer wg.Done()
|
|
||||||
u := &Upstream{Dial: addr}
|
|
||||||
u.fillDynamicHost()
|
|
||||||
hosts[idx] = u.Host
|
|
||||||
}(i)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
first := hosts[0]
|
|
||||||
for i, h := range hosts {
|
|
||||||
if h != first {
|
|
||||||
t.Errorf("goroutine %d got a different *Host pointer; expected all to share the same entry", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -442,7 +442,7 @@ func (t Transport) splitPos(path string) int {
|
|||||||
for _, split := range t.SplitPath {
|
for _, split := range t.SplitPath {
|
||||||
splitLen := len(split)
|
splitLen := len(split)
|
||||||
|
|
||||||
for i := range pathLen {
|
for i := 0; i < pathLen; i++ {
|
||||||
if path[i] >= utf8.RuneSelf {
|
if path[i] >= utf8.RuneSelf {
|
||||||
if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 {
|
if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 {
|
||||||
return end
|
return end
|
||||||
@@ -456,7 +456,7 @@ func (t Transport) splitPos(path string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match := true
|
match := true
|
||||||
for j := range splitLen {
|
for j := 0; j < splitLen; j++ {
|
||||||
c := path[i+j]
|
c := path[i+j]
|
||||||
|
|
||||||
if c >= utf8.RuneSelf {
|
if c >= utf8.RuneSelf {
|
||||||
|
|||||||
@@ -208,24 +208,6 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
|
|||||||
for _, from := range sortedHeadersToCopy {
|
for _, from := range sortedHeadersToCopy {
|
||||||
to := http.CanonicalHeaderKey(headersToCopy[from])
|
to := http.CanonicalHeaderKey(headersToCopy[from])
|
||||||
placeholderName := "http.reverse_proxy.header." + http.CanonicalHeaderKey(from)
|
placeholderName := "http.reverse_proxy.header." + http.CanonicalHeaderKey(from)
|
||||||
|
|
||||||
// Always delete the client-supplied header before conditionally setting
|
|
||||||
// it from the auth response. Without this, a client that pre-supplies a
|
|
||||||
// header listed in copy_headers can inject arbitrary values when the auth
|
|
||||||
// service does not return that header: the MatchNot guard below would
|
|
||||||
// skip the Set entirely, leaving the original client-controlled value
|
|
||||||
// intact and forwarding it to the backend.
|
|
||||||
copyHeaderRoutes = append(copyHeaderRoutes, caddyhttp.Route{
|
|
||||||
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
|
|
||||||
&headers.Handler{
|
|
||||||
Request: &headers.HeaderOps{
|
|
||||||
Delete: []string{to},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"handler", "headers", nil,
|
|
||||||
)},
|
|
||||||
})
|
|
||||||
|
|
||||||
handler := &headers.Handler{
|
handler := &headers.Handler{
|
||||||
Request: &headers.HeaderOps{
|
Request: &headers.HeaderOps{
|
||||||
Set: http.Header{
|
Set: http.Header{
|
||||||
|
|||||||
@@ -359,12 +359,6 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
|
|||||||
dialInfoUpstream = &Upstream{
|
dialInfoUpstream = &Upstream{
|
||||||
Dial: h.HealthChecks.Active.Upstream,
|
Dial: h.HealthChecks.Active.Upstream,
|
||||||
}
|
}
|
||||||
} else if upstream.activeHealthCheckPort != 0 {
|
|
||||||
// health_port overrides the port; addr has already been updated
|
|
||||||
// with the health port, so use its address for dialing
|
|
||||||
dialInfoUpstream = &Upstream{
|
|
||||||
Dial: addr.JoinHostPort(0),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
dialInfo, _ := dialInfoUpstream.fillDialInfo(repl)
|
dialInfo, _ := dialInfoUpstream.fillDialInfo(repl)
|
||||||
|
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
@@ -134,43 +132,6 @@ func (u *Upstream) fillHost() {
|
|||||||
u.Host = host
|
u.Host = host
|
||||||
}
|
}
|
||||||
|
|
||||||
// fillDynamicHost is like fillHost, but stores the host in the separate
|
|
||||||
// dynamicHosts map rather than the reference-counted UsagePool. Dynamic
|
|
||||||
// hosts are not reference-counted; instead, they are retained as long as
|
|
||||||
// they are actively seen and are evicted by a background cleanup goroutine
|
|
||||||
// after dynamicHostIdleExpiry of inactivity. This preserves health state
|
|
||||||
// (e.g. passive fail counts) across sequential requests.
|
|
||||||
func (u *Upstream) fillDynamicHost() {
|
|
||||||
dynamicHostsMu.Lock()
|
|
||||||
entry, ok := dynamicHosts[u.String()]
|
|
||||||
if ok {
|
|
||||||
entry.lastSeen = time.Now()
|
|
||||||
dynamicHosts[u.String()] = entry
|
|
||||||
u.Host = entry.host
|
|
||||||
} else {
|
|
||||||
h := new(Host)
|
|
||||||
dynamicHosts[u.String()] = dynamicHostEntry{host: h, lastSeen: time.Now()}
|
|
||||||
u.Host = h
|
|
||||||
}
|
|
||||||
dynamicHostsMu.Unlock()
|
|
||||||
|
|
||||||
// ensure the cleanup goroutine is running
|
|
||||||
dynamicHostsCleanerOnce.Do(func() {
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
time.Sleep(dynamicHostCleanupInterval)
|
|
||||||
dynamicHostsMu.Lock()
|
|
||||||
for addr, entry := range dynamicHosts {
|
|
||||||
if time.Since(entry.lastSeen) > dynamicHostIdleExpiry {
|
|
||||||
delete(dynamicHosts, addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dynamicHostsMu.Unlock()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Host is the basic, in-memory representation of the state of a remote host.
|
// Host is the basic, in-memory representation of the state of a remote host.
|
||||||
// Its fields are accessed atomically and Host values must not be copied.
|
// Its fields are accessed atomically and Host values must not be copied.
|
||||||
type Host struct {
|
type Host struct {
|
||||||
@@ -307,28 +268,6 @@ func GetDialInfo(ctx context.Context) (DialInfo, bool) {
|
|||||||
// through config reloads.
|
// through config reloads.
|
||||||
var hosts = caddy.NewUsagePool()
|
var hosts = caddy.NewUsagePool()
|
||||||
|
|
||||||
// dynamicHosts tracks hosts that were provisioned from dynamic upstream
|
|
||||||
// sources. Unlike static upstreams which are reference-counted via the
|
|
||||||
// UsagePool, dynamic upstream hosts are not reference-counted. Instead,
|
|
||||||
// their last-seen time is updated on each request, and a background
|
|
||||||
// goroutine evicts entries that have been idle for dynamicHostIdleExpiry.
|
|
||||||
// This preserves health state (e.g. passive fail counts) across requests
|
|
||||||
// to the same dynamic backend.
|
|
||||||
var (
|
|
||||||
dynamicHosts = make(map[string]dynamicHostEntry)
|
|
||||||
dynamicHostsMu sync.RWMutex
|
|
||||||
dynamicHostsCleanerOnce sync.Once
|
|
||||||
dynamicHostCleanupInterval = 5 * time.Minute
|
|
||||||
dynamicHostIdleExpiry = time.Hour
|
|
||||||
)
|
|
||||||
|
|
||||||
// dynamicHostEntry holds a Host and the last time it was seen
|
|
||||||
// in a set of dynamic upstreams returned for a request.
|
|
||||||
type dynamicHostEntry struct {
|
|
||||||
host *Host
|
|
||||||
lastSeen time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// dialInfoVarKey is the key used for the variable that holds
|
// dialInfoVarKey is the key used for the variable that holds
|
||||||
// the dial info for the upstream connection.
|
// the dial info for the upstream connection.
|
||||||
const dialInfoVarKey = "reverse_proxy.dial_info"
|
const dialInfoVarKey = "reverse_proxy.dial_info"
|
||||||
|
|||||||
@@ -384,9 +384,6 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
|
|||||||
}
|
}
|
||||||
// we need to keep track if a proxy is used for a request
|
// we need to keep track if a proxy is used for a request
|
||||||
proxyWrapper := func(req *http.Request) (*url.URL, error) {
|
proxyWrapper := func(req *http.Request) (*url.URL, error) {
|
||||||
if proxy == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
u, err := proxy(req)
|
u, err := proxy(req)
|
||||||
if u == nil || err != nil {
|
if u == nil || err != nil {
|
||||||
return u, err
|
return u, err
|
||||||
@@ -415,13 +412,8 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
|
|||||||
return nil, fmt.Errorf("making TLS client config: %v", err)
|
return nil, fmt.Errorf("making TLS client config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
serverNameHasPlaceholder := strings.Contains(h.TLS.ServerName, "{")
|
// servername has a placeholder, so we need to replace it
|
||||||
|
if strings.Contains(h.TLS.ServerName, "{") {
|
||||||
// We need to use custom DialTLSContext if:
|
|
||||||
// 1. ServerName has a placeholder that needs to be replaced at request-time, OR
|
|
||||||
// 2. ProxyProtocol is enabled, because req.URL.Host is modified to include
|
|
||||||
// client address info with "->" separator which breaks Go's address parsing
|
|
||||||
if serverNameHasPlaceholder || h.ProxyProtocol != "" {
|
|
||||||
rt.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
rt.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
// reuses the dialer from above to establish a plaintext connection
|
// reuses the dialer from above to establish a plaintext connection
|
||||||
conn, err := dialContext(ctx, network, addr)
|
conn, err := dialContext(ctx, network, addr)
|
||||||
@@ -430,11 +422,9 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// but add our own handshake logic
|
// but add our own handshake logic
|
||||||
|
repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
||||||
tlsConfig := rt.TLSClientConfig.Clone()
|
tlsConfig := rt.TLSClientConfig.Clone()
|
||||||
if serverNameHasPlaceholder {
|
tlsConfig.ServerName = repl.ReplaceAll(tlsConfig.ServerName, "")
|
||||||
repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
|
||||||
tlsConfig.ServerName = repl.ReplaceAll(tlsConfig.ServerName, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// h1 only
|
// h1 only
|
||||||
if caddyhttp.GetVar(ctx, tlsH1OnlyVarKey) == true {
|
if caddyhttp.GetVar(ctx, tlsH1OnlyVarKey) == true {
|
||||||
@@ -448,7 +438,7 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
|
|||||||
// complete the handshake before returning the connection
|
// complete the handshake before returning the connection
|
||||||
if rt.TLSHandshakeTimeout != 0 {
|
if rt.TLSHandshakeTimeout != 0 {
|
||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
ctx, cancel = context.WithTimeoutCause(ctx, rt.TLSHandshakeTimeout, fmt.Errorf("HTTP transport TLS handshake %ds timeout", int(rt.TLSHandshakeTimeout.Seconds())))
|
ctx, cancel = context.WithTimeout(ctx, rt.TLSHandshakeTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
err = tlsConn.HandshakeContext(ctx)
|
err = tlsConn.HandshakeContext(ctx)
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package reverseproxy
|
package reverseproxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -117,81 +115,3 @@ func TestHTTPTransport_RequestHeaderOps_TLS(t *testing.T) {
|
|||||||
t.Fatalf("unexpected Host value; want placeholder, got: %s", got)
|
t.Fatalf("unexpected Host value; want placeholder, got: %s", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestHTTPTransport_DialTLSContext_ProxyProtocol verifies that when TLS and
|
|
||||||
// ProxyProtocol are both enabled, DialTLSContext is set. This is critical because
|
|
||||||
// ProxyProtocol modifies req.URL.Host to include client info with "->" separator
|
|
||||||
// (e.g., "[2001:db8::1]:12345->127.0.0.1:443"), which breaks Go's address parsing.
|
|
||||||
// Without a custom DialTLSContext, Go's HTTP library would fail with
|
|
||||||
// "too many colons in address" when trying to parse the mangled host.
|
|
||||||
func TestHTTPTransport_DialTLSContext_ProxyProtocol(t *testing.T) {
|
|
||||||
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
tls *TLSConfig
|
|
||||||
proxyProtocol string
|
|
||||||
serverNameHasPlaceholder bool
|
|
||||||
expectDialTLSContext bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "no TLS, no proxy protocol",
|
|
||||||
tls: nil,
|
|
||||||
proxyProtocol: "",
|
|
||||||
expectDialTLSContext: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "TLS without proxy protocol",
|
|
||||||
tls: &TLSConfig{},
|
|
||||||
proxyProtocol: "",
|
|
||||||
expectDialTLSContext: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "TLS with proxy protocol v1",
|
|
||||||
tls: &TLSConfig{},
|
|
||||||
proxyProtocol: "v1",
|
|
||||||
expectDialTLSContext: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "TLS with proxy protocol v2",
|
|
||||||
tls: &TLSConfig{},
|
|
||||||
proxyProtocol: "v2",
|
|
||||||
expectDialTLSContext: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "TLS with placeholder ServerName",
|
|
||||||
tls: &TLSConfig{ServerName: "{http.request.host}"},
|
|
||||||
proxyProtocol: "",
|
|
||||||
serverNameHasPlaceholder: true,
|
|
||||||
expectDialTLSContext: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "TLS with placeholder ServerName and proxy protocol",
|
|
||||||
tls: &TLSConfig{ServerName: "{http.request.host}"},
|
|
||||||
proxyProtocol: "v2",
|
|
||||||
serverNameHasPlaceholder: true,
|
|
||||||
expectDialTLSContext: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
ht := &HTTPTransport{
|
|
||||||
TLS: tt.tls,
|
|
||||||
ProxyProtocol: tt.proxyProtocol,
|
|
||||||
}
|
|
||||||
|
|
||||||
rt, err := ht.NewTransport(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewTransport() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hasDialTLSContext := rt.DialTLSContext != nil
|
|
||||||
if hasDialTLSContext != tt.expectDialTLSContext {
|
|
||||||
t.Errorf("DialTLSContext set = %v, want %v", hasDialTLSContext, tt.expectDialTLSContext)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,391 +0,0 @@
|
|||||||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package reverseproxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// newPassiveHandler builds a minimal Handler with passive health checks
|
|
||||||
// configured and a live caddy.Context so the fail-forgetter goroutine can
|
|
||||||
// be cancelled cleanly. The caller must call cancel() when done.
|
|
||||||
func newPassiveHandler(t *testing.T, maxFails int, failDuration time.Duration) (*Handler, context.CancelFunc) {
|
|
||||||
t.Helper()
|
|
||||||
caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
|
|
||||||
h := &Handler{
|
|
||||||
ctx: caddyCtx,
|
|
||||||
HealthChecks: &HealthChecks{
|
|
||||||
Passive: &PassiveHealthChecks{
|
|
||||||
MaxFails: maxFails,
|
|
||||||
FailDuration: caddy.Duration(failDuration),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return h, cancel
|
|
||||||
}
|
|
||||||
|
|
||||||
// provisionedStaticUpstream creates a static upstream, registers it in the
|
|
||||||
// UsagePool, and returns a cleanup func that removes it from the pool.
|
|
||||||
func provisionedStaticUpstream(t *testing.T, h *Handler, addr string) (*Upstream, func()) {
|
|
||||||
t.Helper()
|
|
||||||
u := &Upstream{Dial: addr}
|
|
||||||
h.provisionUpstream(u, false)
|
|
||||||
return u, func() { _, _ = hosts.Delete(addr) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// provisionedDynamicUpstream creates a dynamic upstream, registers it in
|
|
||||||
// dynamicHosts, and returns a cleanup func that removes it.
|
|
||||||
func provisionedDynamicUpstream(t *testing.T, h *Handler, addr string) (*Upstream, func()) {
|
|
||||||
t.Helper()
|
|
||||||
u := &Upstream{Dial: addr}
|
|
||||||
h.provisionUpstream(u, true)
|
|
||||||
return u, func() {
|
|
||||||
dynamicHostsMu.Lock()
|
|
||||||
delete(dynamicHosts, addr)
|
|
||||||
dynamicHostsMu.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- countFailure behaviour ---
|
|
||||||
|
|
||||||
// TestCountFailureNoopWhenNoHealthChecks verifies that countFailure is a no-op
|
|
||||||
// when HealthChecks is nil.
|
|
||||||
func TestCountFailureNoopWhenNoHealthChecks(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
h := &Handler{}
|
|
||||||
u := &Upstream{Dial: "10.1.0.1:80", Host: new(Host)}
|
|
||||||
|
|
||||||
h.countFailure(u)
|
|
||||||
|
|
||||||
if u.Host.Fails() != 0 {
|
|
||||||
t.Errorf("expected 0 fails with no HealthChecks config, got %d", u.Host.Fails())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestCountFailureNoopWhenZeroDuration verifies that countFailure is a no-op
|
|
||||||
// when FailDuration is 0 (the zero value disables passive checks).
|
|
||||||
func TestCountFailureNoopWhenZeroDuration(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
h := &Handler{
|
|
||||||
ctx: caddyCtx,
|
|
||||||
HealthChecks: &HealthChecks{
|
|
||||||
Passive: &PassiveHealthChecks{MaxFails: 1, FailDuration: 0},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
u := &Upstream{Dial: "10.1.0.2:80", Host: new(Host)}
|
|
||||||
|
|
||||||
h.countFailure(u)
|
|
||||||
|
|
||||||
if u.Host.Fails() != 0 {
|
|
||||||
t.Errorf("expected 0 fails with zero FailDuration, got %d", u.Host.Fails())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestCountFailureIncrementsCount verifies that countFailure increments the
|
|
||||||
// fail count on the upstream's Host.
|
|
||||||
func TestCountFailureIncrementsCount(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
h, cancel := newPassiveHandler(t, 2, time.Minute)
|
|
||||||
defer cancel()
|
|
||||||
u := &Upstream{Dial: "10.1.0.3:80", Host: new(Host)}
|
|
||||||
|
|
||||||
h.countFailure(u)
|
|
||||||
|
|
||||||
if u.Host.Fails() != 1 {
|
|
||||||
t.Errorf("expected 1 fail after countFailure, got %d", u.Host.Fails())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestCountFailureDecrementsAfterDuration verifies that the fail count is
|
|
||||||
// decremented back after FailDuration elapses.
|
|
||||||
func TestCountFailureDecrementsAfterDuration(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
const failDuration = 50 * time.Millisecond
|
|
||||||
h, cancel := newPassiveHandler(t, 2, failDuration)
|
|
||||||
defer cancel()
|
|
||||||
u := &Upstream{Dial: "10.1.0.4:80", Host: new(Host)}
|
|
||||||
|
|
||||||
h.countFailure(u)
|
|
||||||
if u.Host.Fails() != 1 {
|
|
||||||
t.Fatalf("expected 1 fail immediately after countFailure, got %d", u.Host.Fails())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait long enough for the forgetter goroutine to fire.
|
|
||||||
time.Sleep(3 * failDuration)
|
|
||||||
|
|
||||||
if u.Host.Fails() != 0 {
|
|
||||||
t.Errorf("expected fail count to return to 0 after FailDuration, got %d", u.Host.Fails())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestCountFailureCancelledContextForgets verifies that cancelling the handler
|
|
||||||
// context (simulating a config unload) also triggers the forgetter to run,
|
|
||||||
// decrementing the fail count.
|
|
||||||
func TestCountFailureCancelledContextForgets(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
h, cancel := newPassiveHandler(t, 2, time.Hour) // very long duration
|
|
||||||
u := &Upstream{Dial: "10.1.0.5:80", Host: new(Host)}
|
|
||||||
|
|
||||||
h.countFailure(u)
|
|
||||||
if u.Host.Fails() != 1 {
|
|
||||||
t.Fatalf("expected 1 fail immediately after countFailure, got %d", u.Host.Fails())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancelling the context should cause the forgetter goroutine to exit and
|
|
||||||
// decrement the count.
|
|
||||||
cancel()
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
|
|
||||||
if u.Host.Fails() != 0 {
|
|
||||||
t.Errorf("expected fail count to be decremented after context cancel, got %d", u.Host.Fails())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- static upstream passive health check ---
|
|
||||||
|
|
||||||
// TestStaticUpstreamHealthyWithNoFailures verifies that a static upstream with
|
|
||||||
// no recorded failures is considered healthy.
|
|
||||||
func TestStaticUpstreamHealthyWithNoFailures(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
h, cancel := newPassiveHandler(t, 2, time.Minute)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
u, cleanup := provisionedStaticUpstream(t, h, "10.2.0.1:80")
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
if !u.Healthy() {
|
|
||||||
t.Error("upstream with no failures should be healthy")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestStaticUpstreamUnhealthyAtMaxFails verifies that a static upstream is
|
|
||||||
// marked unhealthy once its fail count reaches MaxFails.
|
|
||||||
func TestStaticUpstreamUnhealthyAtMaxFails(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
h, cancel := newPassiveHandler(t, 2, time.Minute)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
u, cleanup := provisionedStaticUpstream(t, h, "10.2.0.2:80")
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
h.countFailure(u)
|
|
||||||
if !u.Healthy() {
|
|
||||||
t.Error("upstream should still be healthy after 1 of 2 allowed failures")
|
|
||||||
}
|
|
||||||
|
|
||||||
h.countFailure(u)
|
|
||||||
if u.Healthy() {
|
|
||||||
t.Error("upstream should be unhealthy after reaching MaxFails=2")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestStaticUpstreamRecoversAfterFailDuration verifies that a static upstream
|
|
||||||
// returns to healthy once its failures expire.
|
|
||||||
func TestStaticUpstreamRecoversAfterFailDuration(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
const failDuration = 50 * time.Millisecond
|
|
||||||
h, cancel := newPassiveHandler(t, 1, failDuration)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
u, cleanup := provisionedStaticUpstream(t, h, "10.2.0.3:80")
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
h.countFailure(u)
|
|
||||||
if u.Healthy() {
|
|
||||||
t.Fatal("upstream should be unhealthy immediately after MaxFails failure")
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(3 * failDuration)
|
|
||||||
|
|
||||||
if !u.Healthy() {
|
|
||||||
t.Errorf("upstream should recover to healthy after FailDuration, Fails=%d", u.Host.Fails())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestStaticUpstreamHealthPersistedAcrossReprovisioning verifies that static
|
|
||||||
// upstreams share a Host via the UsagePool, so a second call to provisionUpstream
|
|
||||||
// for the same address (as happens on config reload) sees the accumulated state.
|
|
||||||
func TestStaticUpstreamHealthPersistedAcrossReprovisioning(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
h, cancel := newPassiveHandler(t, 2, time.Minute)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
u1, cleanup1 := provisionedStaticUpstream(t, h, "10.2.0.4:80")
|
|
||||||
defer cleanup1()
|
|
||||||
|
|
||||||
h.countFailure(u1)
|
|
||||||
h.countFailure(u1)
|
|
||||||
|
|
||||||
// Simulate a second handler instance referencing the same upstream
|
|
||||||
// (e.g. after a config reload that keeps the same backend address).
|
|
||||||
u2, cleanup2 := provisionedStaticUpstream(t, h, "10.2.0.4:80")
|
|
||||||
defer cleanup2()
|
|
||||||
|
|
||||||
if u1.Host != u2.Host {
|
|
||||||
t.Fatal("expected both Upstream structs to share the same *Host via UsagePool")
|
|
||||||
}
|
|
||||||
if u2.Healthy() {
|
|
||||||
t.Error("re-provisioned upstream should still see the prior fail count and be unhealthy")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- dynamic upstream passive health check ---
|
|
||||||
|
|
||||||
// TestDynamicUpstreamHealthyWithNoFailures verifies that a freshly provisioned
|
|
||||||
// dynamic upstream is healthy.
|
|
||||||
func TestDynamicUpstreamHealthyWithNoFailures(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
h, cancel := newPassiveHandler(t, 2, time.Minute)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
u, cleanup := provisionedDynamicUpstream(t, h, "10.3.0.1:80")
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
if !u.Healthy() {
|
|
||||||
t.Error("dynamic upstream with no failures should be healthy")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDynamicUpstreamUnhealthyAtMaxFails verifies that a dynamic upstream is
|
|
||||||
// marked unhealthy once its fail count reaches MaxFails.
|
|
||||||
func TestDynamicUpstreamUnhealthyAtMaxFails(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
h, cancel := newPassiveHandler(t, 2, time.Minute)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
u, cleanup := provisionedDynamicUpstream(t, h, "10.3.0.2:80")
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
h.countFailure(u)
|
|
||||||
if !u.Healthy() {
|
|
||||||
t.Error("dynamic upstream should still be healthy after 1 of 2 allowed failures")
|
|
||||||
}
|
|
||||||
|
|
||||||
h.countFailure(u)
|
|
||||||
if u.Healthy() {
|
|
||||||
t.Error("dynamic upstream should be unhealthy after reaching MaxFails=2")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDynamicUpstreamFailCountPersistedBetweenRequests is the core regression
|
|
||||||
// test: it simulates two sequential (non-concurrent) requests to the same
|
|
||||||
// dynamic upstream. Before the fix, the UsagePool entry would be deleted
|
|
||||||
// between requests, wiping the fail count. Now it should survive.
|
|
||||||
func TestDynamicUpstreamFailCountPersistedBetweenRequests(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
h, cancel := newPassiveHandler(t, 2, time.Minute)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// --- first request ---
|
|
||||||
u1 := &Upstream{Dial: "10.3.0.3:80"}
|
|
||||||
h.provisionUpstream(u1, true)
|
|
||||||
h.countFailure(u1)
|
|
||||||
|
|
||||||
if u1.Host.Fails() != 1 {
|
|
||||||
t.Fatalf("expected 1 fail after first request, got %d", u1.Host.Fails())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simulate end of first request: no delete from any pool (key difference
|
|
||||||
// vs. the old behaviour where hosts.Delete was deferred).
|
|
||||||
|
|
||||||
// --- second request: brand-new *Upstream struct, same dial address ---
|
|
||||||
u2 := &Upstream{Dial: "10.3.0.3:80"}
|
|
||||||
h.provisionUpstream(u2, true)
|
|
||||||
|
|
||||||
if u1.Host != u2.Host {
|
|
||||||
t.Fatal("expected both requests to share the same *Host pointer from dynamicHosts")
|
|
||||||
}
|
|
||||||
if u2.Host.Fails() != 1 {
|
|
||||||
t.Errorf("expected fail count to persist across requests, got %d", u2.Host.Fails())
|
|
||||||
}
|
|
||||||
|
|
||||||
// A second failure now tips it over MaxFails=2.
|
|
||||||
h.countFailure(u2)
|
|
||||||
if u2.Healthy() {
|
|
||||||
t.Error("upstream should be unhealthy after accumulated failures across requests")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup.
|
|
||||||
dynamicHostsMu.Lock()
|
|
||||||
delete(dynamicHosts, "10.3.0.3:80")
|
|
||||||
dynamicHostsMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDynamicUpstreamRecoveryAfterFailDuration verifies that a dynamic
|
|
||||||
// upstream's fail count expires and it returns to healthy.
|
|
||||||
func TestDynamicUpstreamRecoveryAfterFailDuration(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
const failDuration = 50 * time.Millisecond
|
|
||||||
h, cancel := newPassiveHandler(t, 1, failDuration)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
u, cleanup := provisionedDynamicUpstream(t, h, "10.3.0.4:80")
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
h.countFailure(u)
|
|
||||||
if u.Healthy() {
|
|
||||||
t.Fatal("upstream should be unhealthy immediately after MaxFails failure")
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(3 * failDuration)
|
|
||||||
|
|
||||||
// Re-provision (as a new request would) to get fresh *Upstream with policy set.
|
|
||||||
u2 := &Upstream{Dial: "10.3.0.4:80"}
|
|
||||||
h.provisionUpstream(u2, true)
|
|
||||||
|
|
||||||
if !u2.Healthy() {
|
|
||||||
t.Errorf("dynamic upstream should recover to healthy after FailDuration, Fails=%d", u2.Host.Fails())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDynamicUpstreamMaxRequestsFromUnhealthyRequestCount verifies that
|
|
||||||
// UnhealthyRequestCount is copied into MaxRequests so Full() works correctly.
|
|
||||||
func TestDynamicUpstreamMaxRequestsFromUnhealthyRequestCount(t *testing.T) {
|
|
||||||
resetDynamicHosts()
|
|
||||||
caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
|
|
||||||
defer cancel()
|
|
||||||
h := &Handler{
|
|
||||||
ctx: caddyCtx,
|
|
||||||
HealthChecks: &HealthChecks{
|
|
||||||
Passive: &PassiveHealthChecks{
|
|
||||||
UnhealthyRequestCount: 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
u, cleanup := provisionedDynamicUpstream(t, h, "10.3.0.5:80")
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
if u.MaxRequests != 3 {
|
|
||||||
t.Errorf("expected MaxRequests=3 from UnhealthyRequestCount, got %d", u.MaxRequests)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should not be full with fewer requests than the limit.
|
|
||||||
_ = u.Host.countRequest(2)
|
|
||||||
if u.Full() {
|
|
||||||
t.Error("upstream should not be full with 2 of 3 allowed requests")
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = u.Host.countRequest(1)
|
|
||||||
if !u.Full() {
|
|
||||||
t.Error("upstream should be full at UnhealthyRequestCount concurrent requests")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
package reverseproxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// prepareTestRequest injects the context values that ServeHTTP and
|
|
||||||
// proxyLoopIteration require (caddy.ReplacerCtxKey, VarsCtxKey, etc.) using
|
|
||||||
// the same helper that the real HTTP server uses.
|
|
||||||
//
|
|
||||||
// A zero-value Server is passed so that caddyhttp.ServerCtxKey is set to a
|
|
||||||
// non-nil pointer; reverseProxy dereferences it to check ShouldLogCredentials.
|
|
||||||
func prepareTestRequest(req *http.Request) *http.Request {
|
|
||||||
repl := caddy.NewReplacer()
|
|
||||||
return caddyhttp.PrepareRequest(req, repl, nil, &caddyhttp.Server{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// closeOnCloseReader is an io.ReadCloser whose Close method actually makes
|
|
||||||
// subsequent reads fail, mimicking the behaviour of a real HTTP request body
|
|
||||||
// (as opposed to io.NopCloser, whose Close is a no-op and would mask the bug
|
|
||||||
// we are testing).
|
|
||||||
type closeOnCloseReader struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
r *strings.Reader
|
|
||||||
closed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCloseOnCloseReader(s string) *closeOnCloseReader {
|
|
||||||
return &closeOnCloseReader{r: strings.NewReader(s)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *closeOnCloseReader) Read(p []byte) (int, error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
if c.closed {
|
|
||||||
return 0, errors.New("http: invalid Read on closed Body")
|
|
||||||
}
|
|
||||||
return c.r.Read(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *closeOnCloseReader) Close() error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
c.closed = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// deadUpstreamAddr returns a TCP address that is guaranteed to refuse
|
|
||||||
// connections: we bind a listener, note its address, close it immediately,
|
|
||||||
// and return the address. Any dial to that address will get ECONNREFUSED.
|
|
||||||
func deadUpstreamAddr(t *testing.T) string {
|
|
||||||
t.Helper()
|
|
||||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create dead upstream listener: %v", err)
|
|
||||||
}
|
|
||||||
addr := ln.Addr().String()
|
|
||||||
ln.Close()
|
|
||||||
return addr
|
|
||||||
}
|
|
||||||
|
|
||||||
// testTransport wraps http.Transport to:
|
|
||||||
// 1. Set the URL scheme to "http" when it is empty (matching what
|
|
||||||
// HTTPTransport.SetScheme does in production; cloneRequest strips the
|
|
||||||
// scheme intentionally so a plain *http.Transport would fail with
|
|
||||||
// "unsupported protocol scheme").
|
|
||||||
// 2. Wrap dial errors as DialError so that tryAgain correctly identifies them
|
|
||||||
// as safe-to-retry regardless of request method (as HTTPTransport does in
|
|
||||||
// production via its custom dialer).
|
|
||||||
type testTransport struct{ *http.Transport }
|
|
||||||
|
|
||||||
func (t testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
||||||
if req.URL.Scheme == "" {
|
|
||||||
req.URL.Scheme = "http"
|
|
||||||
}
|
|
||||||
resp, err := t.Transport.RoundTrip(req)
|
|
||||||
if err != nil {
|
|
||||||
// Wrap dial errors as DialError to match production behaviour.
|
|
||||||
// Without this wrapping, tryAgain treats ECONNREFUSED on a POST
|
|
||||||
// request as non-retryable (only GET is retried by default when
|
|
||||||
// the error is not a DialError).
|
|
||||||
var opErr *net.OpError
|
|
||||||
if errors.As(err, &opErr) && opErr.Op == "dial" {
|
|
||||||
return nil, DialError{err}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return resp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// minimalHandler returns a Handler with only the fields required by ServeHTTP
|
|
||||||
// set directly, bypassing Provision (which requires a full Caddy runtime).
|
|
||||||
// RoundRobinSelection is used so that successive iterations of the proxy loop
|
|
||||||
// advance through the upstream pool in a predictable order.
|
|
||||||
func minimalHandler(retries int, upstreams ...*Upstream) *Handler {
|
|
||||||
return &Handler{
|
|
||||||
logger: zap.NewNop(),
|
|
||||||
Transport: testTransport{&http.Transport{}},
|
|
||||||
Upstreams: upstreams,
|
|
||||||
LoadBalancing: &LoadBalancing{
|
|
||||||
Retries: retries,
|
|
||||||
SelectionPolicy: &RoundRobinSelection{},
|
|
||||||
// RetryMatch intentionally nil: dial errors are always retried
|
|
||||||
// regardless of RetryMatch or request method.
|
|
||||||
},
|
|
||||||
// ctx, connections, connectionsMu, events: zero/nil values are safe
|
|
||||||
// for the code paths exercised by these tests (TryInterval=0 so
|
|
||||||
// ctx.Done() is never consulted; no WebSocket hijacking; no passive
|
|
||||||
// health-check event emission).
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDialErrorBodyRetry verifies that a POST request whose body has NOT been
|
|
||||||
// pre-buffered via request_buffers can still be retried after a dial error.
|
|
||||||
//
|
|
||||||
// Before the fix, a dial error caused Go's transport to close the shared body
|
|
||||||
// (via cloneRequest's shallow copy), so the retry attempt would read from an
|
|
||||||
// already-closed io.ReadCloser and produce:
|
|
||||||
//
|
|
||||||
// http: invalid Read on closed Body → HTTP 502
|
|
||||||
//
|
|
||||||
// After the fix the handler wraps the body in noCloseBody when retries are
|
|
||||||
// configured, preventing the transport's Close() from propagating to the
|
|
||||||
// shared body. Since dial errors never read any bytes, the body remains at
|
|
||||||
// position 0 for the retry.
|
|
||||||
func TestDialErrorBodyRetry(t *testing.T) {
|
|
||||||
// Good upstream: echoes the request body with 200 OK.
|
|
||||||
goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "read body: "+err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
_, _ = w.Write(body)
|
|
||||||
}))
|
|
||||||
t.Cleanup(goodServer.Close)
|
|
||||||
|
|
||||||
const requestBody = "hello, retry"
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
method string
|
|
||||||
body string
|
|
||||||
retries int
|
|
||||||
wantStatus int
|
|
||||||
wantBody string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
// Core regression case: POST with a body, no request_buffers,
|
|
||||||
// dial error on first upstream → retry to second upstream succeeds.
|
|
||||||
name: "POST body retried after dial error",
|
|
||||||
method: http.MethodPost,
|
|
||||||
body: requestBody,
|
|
||||||
retries: 1,
|
|
||||||
wantStatus: http.StatusOK,
|
|
||||||
wantBody: requestBody,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Dial errors are always retried regardless of method, but there
|
|
||||||
// is no body to re-read, so GET has always worked. Keep it as a
|
|
||||||
// sanity check that we did not break the no-body path.
|
|
||||||
name: "GET without body retried after dial error",
|
|
||||||
method: http.MethodGet,
|
|
||||||
body: "",
|
|
||||||
retries: 1,
|
|
||||||
wantStatus: http.StatusOK,
|
|
||||||
wantBody: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Without any retry configuration the handler must give up on the
|
|
||||||
// first dial error and return a 502. Confirms no wrapping occurs
|
|
||||||
// in the no-retry path.
|
|
||||||
name: "no retries configured returns 502 on dial error",
|
|
||||||
method: http.MethodPost,
|
|
||||||
body: requestBody,
|
|
||||||
retries: 0,
|
|
||||||
wantStatus: http.StatusBadGateway,
|
|
||||||
wantBody: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
dead := deadUpstreamAddr(t)
|
|
||||||
|
|
||||||
// Build the upstream pool. RoundRobinSelection starts its
|
|
||||||
// counter at 0 and increments before returning, so with a
|
|
||||||
// two-element pool it picks index 1 first, then index 0.
|
|
||||||
// Put the good upstream at index 0 and the dead one at
|
|
||||||
// index 1 so that:
|
|
||||||
// attempt 1 → pool[1] = dead → DialError (ECONNREFUSED)
|
|
||||||
// attempt 2 → pool[0] = good → 200
|
|
||||||
upstreams := []*Upstream{
|
|
||||||
{Host: new(Host), Dial: goodServer.Listener.Addr().String()},
|
|
||||||
{Host: new(Host), Dial: dead},
|
|
||||||
}
|
|
||||||
if tc.retries == 0 {
|
|
||||||
// For the "no retries" case use only the dead upstream so
|
|
||||||
// there is nowhere to retry to.
|
|
||||||
upstreams = []*Upstream{
|
|
||||||
{Host: new(Host), Dial: dead},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h := minimalHandler(tc.retries, upstreams...)
|
|
||||||
|
|
||||||
// Use closeOnCloseReader so that Close() truly prevents further
|
|
||||||
// reads, matching real http.body semantics. io.NopCloser would
|
|
||||||
// mask the bug because its Close is a no-op.
|
|
||||||
var bodyReader io.ReadCloser
|
|
||||||
if tc.body != "" {
|
|
||||||
bodyReader = newCloseOnCloseReader(tc.body)
|
|
||||||
}
|
|
||||||
req := httptest.NewRequest(tc.method, "http://example.com/", bodyReader)
|
|
||||||
if bodyReader != nil {
|
|
||||||
// httptest.NewRequest wraps the reader in NopCloser; replace
|
|
||||||
// it with our close-aware reader so Close() is propagated.
|
|
||||||
req.Body = bodyReader
|
|
||||||
req.ContentLength = int64(len(tc.body))
|
|
||||||
}
|
|
||||||
req = prepareTestRequest(req)
|
|
||||||
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
return nil
|
|
||||||
}))
|
|
||||||
|
|
||||||
// For error cases (e.g. 502) ServeHTTP returns a HandlerError
|
|
||||||
// rather than writing the status itself.
|
|
||||||
gotStatus := rec.Code
|
|
||||||
if err != nil {
|
|
||||||
if herr, ok := err.(caddyhttp.HandlerError); ok {
|
|
||||||
gotStatus = herr.StatusCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if gotStatus != tc.wantStatus {
|
|
||||||
t.Errorf("status: got %d, want %d (err=%v)", gotStatus, tc.wantStatus, err)
|
|
||||||
}
|
|
||||||
if tc.wantBody != "" && rec.Body.String() != tc.wantBody {
|
|
||||||
t.Errorf("body: got %q, want %q", rec.Body.String(), tc.wantBody)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -32,7 +32,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -47,31 +46,6 @@ import (
|
|||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
|
||||||
)
|
)
|
||||||
|
|
||||||
// inFlightRequests uses sync.Map with atomic.Int64 for lock-free updates on the hot path
|
|
||||||
var inFlightRequests sync.Map
|
|
||||||
|
|
||||||
func incInFlightRequest(address string) {
|
|
||||||
v, _ := inFlightRequests.LoadOrStore(address, new(atomic.Int64))
|
|
||||||
v.(*atomic.Int64).Add(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func decInFlightRequest(address string) {
|
|
||||||
if v, ok := inFlightRequests.Load(address); ok {
|
|
||||||
if v.(*atomic.Int64).Add(-1) <= 0 {
|
|
||||||
inFlightRequests.Delete(address)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getInFlightRequests() map[string]int64 {
|
|
||||||
copyMap := make(map[string]int64)
|
|
||||||
inFlightRequests.Range(func(key, value any) bool {
|
|
||||||
copyMap[key.(string)] = value.(*atomic.Int64).Load()
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
return copyMap
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(Handler{})
|
caddy.RegisterModule(Handler{})
|
||||||
}
|
}
|
||||||
@@ -392,7 +366,7 @@ func (h *Handler) Provision(ctx caddy.Context) error {
|
|||||||
|
|
||||||
// set up upstreams
|
// set up upstreams
|
||||||
for _, u := range h.Upstreams {
|
for _, u := range h.Upstreams {
|
||||||
h.provisionUpstream(u, false)
|
h.provisionUpstream(u)
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.HealthChecks != nil {
|
if h.HealthChecks != nil {
|
||||||
@@ -482,31 +456,18 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
|
|||||||
reqHost := clonedReq.Host
|
reqHost := clonedReq.Host
|
||||||
reqHeader := clonedReq.Header
|
reqHeader := clonedReq.Header
|
||||||
|
|
||||||
// When retries are configured and there is a body, wrap it in
|
// If the cloned request body was fully buffered, keep a reference to its
|
||||||
// io.NopCloser to prevent Go's transport from closing it on dial
|
// buffer so we can reuse it across retries and return it to the pool
|
||||||
// errors. cloneRequest does a shallow copy, so clonedReq.Body and
|
// once we’re done.
|
||||||
// r.Body share the same io.ReadCloser — a dial-failure Close()
|
|
||||||
// would kill the original body for all subsequent retry attempts.
|
|
||||||
// The real body is closed by the HTTP server when the handler
|
|
||||||
// returns.
|
|
||||||
//
|
|
||||||
// If the body was already fully buffered (via request_buffers),
|
|
||||||
// we also extract the buffer so the retry loop can replay it
|
|
||||||
// from the beginning on each attempt. (see #6259, #7546)
|
|
||||||
var bufferedReqBody *bytes.Buffer
|
var bufferedReqBody *bytes.Buffer
|
||||||
if clonedReq.Body != nil && h.LoadBalancing != nil &&
|
if reqBodyBuf, ok := clonedReq.Body.(bodyReadCloser); ok && reqBodyBuf.body == nil && reqBodyBuf.buf != nil {
|
||||||
(h.LoadBalancing.Retries > 0 || h.LoadBalancing.TryDuration > 0) {
|
bufferedReqBody = reqBodyBuf.buf
|
||||||
if reqBodyBuf, ok := clonedReq.Body.(bodyReadCloser); ok && reqBodyBuf.body == nil && reqBodyBuf.buf != nil {
|
reqBodyBuf.buf = nil
|
||||||
bufferedReqBody = reqBodyBuf.buf
|
|
||||||
reqBodyBuf.buf = nil
|
defer func() {
|
||||||
clonedReq.Body = io.NopCloser(bytes.NewReader(bufferedReqBody.Bytes()))
|
bufferedReqBody.Reset()
|
||||||
defer func() {
|
bufPool.Put(bufferedReqBody)
|
||||||
bufferedReqBody.Reset()
|
}()
|
||||||
bufPool.Put(bufferedReqBody)
|
|
||||||
}()
|
|
||||||
} else {
|
|
||||||
clonedReq.Body = io.NopCloser(clonedReq.Body)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
@@ -576,11 +537,18 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
|
|||||||
} else {
|
} else {
|
||||||
upstreams = dUpstreams
|
upstreams = dUpstreams
|
||||||
for _, dUp := range dUpstreams {
|
for _, dUp := range dUpstreams {
|
||||||
h.provisionUpstream(dUp, true)
|
h.provisionUpstream(dUp)
|
||||||
}
|
}
|
||||||
if c := h.logger.Check(zapcore.DebugLevel, "provisioned dynamic upstreams"); c != nil {
|
if c := h.logger.Check(zapcore.DebugLevel, "provisioned dynamic upstreams"); c != nil {
|
||||||
c.Write(zap.Int("count", len(dUpstreams)))
|
c.Write(zap.Int("count", len(dUpstreams)))
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
// these upstreams are dynamic, so they are only used for this iteration
|
||||||
|
// of the proxy loop; be sure to let them go away when we're done with them
|
||||||
|
for _, upstream := range dUpstreams {
|
||||||
|
_, _ = hosts.Delete(upstream.String())
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -936,16 +904,8 @@ func (h Handler) addForwardedHeaders(req *http.Request) error {
|
|||||||
// Go standard library which was used as the foundation.)
|
// Go standard library which was used as the foundation.)
|
||||||
func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origReq *http.Request, repl *caddy.Replacer, di DialInfo, next caddyhttp.Handler) error {
|
func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origReq *http.Request, repl *caddy.Replacer, di DialInfo, next caddyhttp.Handler) error {
|
||||||
_ = di.Upstream.Host.countRequest(1)
|
_ = di.Upstream.Host.countRequest(1)
|
||||||
|
|
||||||
// Increment the in-flight request count
|
|
||||||
incInFlightRequest(di.Address)
|
|
||||||
|
|
||||||
//nolint:errcheck
|
//nolint:errcheck
|
||||||
defer func() {
|
defer di.Upstream.Host.countRequest(-1)
|
||||||
di.Upstream.Host.countRequest(-1)
|
|
||||||
// Decrement the in-flight request count
|
|
||||||
decInFlightRequest(di.Address)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// point the request to this upstream
|
// point the request to this upstream
|
||||||
h.directRequest(req, di)
|
h.directRequest(req, di)
|
||||||
@@ -1318,28 +1278,16 @@ func (h *Handler) directRequest(req *http.Request, di DialInfo) {
|
|||||||
// add client address to the host to let transport differentiate requests from different clients
|
// add client address to the host to let transport differentiate requests from different clients
|
||||||
if ppt, ok := h.Transport.(ProxyProtocolTransport); ok && ppt.ProxyProtocolEnabled() {
|
if ppt, ok := h.Transport.(ProxyProtocolTransport); ok && ppt.ProxyProtocolEnabled() {
|
||||||
if proxyProtocolInfo, ok := caddyhttp.GetVar(req.Context(), proxyProtocolInfoVarKey).(ProxyProtocolInfo); ok {
|
if proxyProtocolInfo, ok := caddyhttp.GetVar(req.Context(), proxyProtocolInfoVarKey).(ProxyProtocolInfo); ok {
|
||||||
// encode the request so it plays well with h2 transport, it's unnecessary for h1 but anyway
|
reqHost = proxyProtocolInfo.AddrPort.String() + "->" + reqHost
|
||||||
// The issue is that h2 transport will use the address to determine if new connections are needed
|
|
||||||
// to roundtrip requests but the without escaping, new connections are constantly created and closed until
|
|
||||||
// file descriptors are exhausted.
|
|
||||||
// see: https://github.com/caddyserver/caddy/issues/7529
|
|
||||||
reqHost = url.QueryEscape(proxyProtocolInfo.AddrPort.String() + "->" + reqHost)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req.URL.Host = reqHost
|
req.URL.Host = reqHost
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h Handler) provisionUpstream(upstream *Upstream, dynamic bool) {
|
func (h Handler) provisionUpstream(upstream *Upstream) {
|
||||||
// create or get the host representation for this upstream;
|
// create or get the host representation for this upstream
|
||||||
// dynamic upstreams are tracked in a separate map with last-seen
|
upstream.fillHost()
|
||||||
// timestamps so their health state persists across requests without
|
|
||||||
// being reference-counted (and thus discarded between requests).
|
|
||||||
if dynamic {
|
|
||||||
upstream.fillDynamicHost()
|
|
||||||
} else {
|
|
||||||
upstream.fillHost()
|
|
||||||
}
|
|
||||||
|
|
||||||
// give it the circuit breaker, if any
|
// give it the circuit breaker, if any
|
||||||
upstream.cb = h.CB
|
upstream.cb = h.CB
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ func (r *RoundRobinSelection) Select(pool UpstreamPool, _ *http.Request, _ http.
|
|||||||
if n == 0 {
|
if n == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for range n {
|
for i := uint32(0); i < n; i++ {
|
||||||
robin := atomic.AddUint32(&r.robin, 1)
|
robin := atomic.AddUint32(&r.robin, 1)
|
||||||
host := pool[robin%n]
|
host := pool[robin%n]
|
||||||
if host.Available() {
|
if host.Available() {
|
||||||
|
|||||||
@@ -536,7 +536,7 @@ func maskBytes(key [4]byte, pos int, b []byte) int {
|
|||||||
// Mask one word at a time.
|
// Mask one word at a time.
|
||||||
n := (len(b) / wordSize) * wordSize
|
n := (len(b) / wordSize) * wordSize
|
||||||
for i := 0; i < n; i += wordSize {
|
for i := 0; i < n; i += wordSize {
|
||||||
*(*uintptr)(unsafe.Add(unsafe.Pointer(&b[0]), i)) ^= kw
|
*(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(i))) ^= kw
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mask one byte at a time for remaining bytes.
|
// Mask one byte at a time for remaining bytes.
|
||||||
|
|||||||
@@ -247,7 +247,6 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
|
|||||||
} else {
|
} else {
|
||||||
r.URL.Path = path
|
r.URL.Path = path
|
||||||
}
|
}
|
||||||
r.URL.RawPath = "" // force recomputing when EscapedPath() is called
|
|
||||||
}
|
}
|
||||||
if qsStart >= 0 {
|
if qsStart >= 0 {
|
||||||
r.URL.RawQuery = newQuery
|
r.URL.RawQuery = newQuery
|
||||||
|
|||||||
@@ -224,11 +224,6 @@ func TestRewrite(t *testing.T) {
|
|||||||
input: newRequest(t, "GET", "/foo#fragFirst?c=d"),
|
input: newRequest(t, "GET", "/foo#fragFirst?c=d"),
|
||||||
expect: newRequest(t, "GET", "/bar#fragFirst?c=d"),
|
expect: newRequest(t, "GET", "/bar#fragFirst?c=d"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
rule: Rewrite{URI: "/api/admin/panel"},
|
|
||||||
input: newRequest(t, "GET", "/api/admin%2Fpanel"),
|
|
||||||
expect: newRequest(t, "GET", "/api/admin/panel"),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
rule: Rewrite{StripPathPrefix: "/prefix"},
|
rule: Rewrite{StripPathPrefix: "/prefix"},
|
||||||
|
|||||||
+11
-26
@@ -97,10 +97,7 @@ type Route struct {
|
|||||||
MatcherSets MatcherSets `json:"-"`
|
MatcherSets MatcherSets `json:"-"`
|
||||||
Handlers []MiddlewareHandler `json:"-"`
|
Handlers []MiddlewareHandler `json:"-"`
|
||||||
|
|
||||||
middleware []Middleware
|
middleware []Middleware
|
||||||
metrics *Metrics
|
|
||||||
metricsCtx caddy.Context
|
|
||||||
handlerName string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty returns true if the route has all zero/default values.
|
// Empty returns true if the route has all zero/default values.
|
||||||
@@ -165,20 +162,12 @@ func (r *Route) ProvisionHandlers(ctx caddy.Context, metrics *Metrics) error {
|
|||||||
r.Handlers = append(r.Handlers, handler.(MiddlewareHandler))
|
r.Handlers = append(r.Handlers, handler.(MiddlewareHandler))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store metrics info for route-level instrumentation (applied once
|
|
||||||
// per route in wrapRoute, instead of per-handler which was redundant).
|
|
||||||
r.metrics = metrics
|
|
||||||
r.metricsCtx = ctx
|
|
||||||
if len(r.Handlers) > 0 {
|
|
||||||
r.handlerName = caddy.GetModuleName(r.Handlers[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make ProvisionHandlers idempotent by clearing the middleware field
|
// Make ProvisionHandlers idempotent by clearing the middleware field
|
||||||
r.middleware = []Middleware{}
|
r.middleware = []Middleware{}
|
||||||
|
|
||||||
// pre-compile the middleware handler chain
|
// pre-compile the middleware handler chain
|
||||||
for _, midhandler := range r.Handlers {
|
for _, midhandler := range r.Handlers {
|
||||||
r.middleware = append(r.middleware, wrapMiddleware(ctx, midhandler))
|
r.middleware = append(r.middleware, wrapMiddleware(ctx, midhandler, metrics))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -309,16 +298,6 @@ func wrapRoute(route Route) Middleware {
|
|||||||
nextCopy = route.middleware[i](nextCopy)
|
nextCopy = route.middleware[i](nextCopy)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply metrics instrumentation once for the entire route,
|
|
||||||
// rather than wrapping each individual handler. This avoids
|
|
||||||
// redundant metrics collection that caused significant CPU
|
|
||||||
// overhead (see issue #4644).
|
|
||||||
if route.metrics != nil {
|
|
||||||
nextCopy = newMetricsInstrumentedRoute(
|
|
||||||
route.metricsCtx, route.handlerName, nextCopy, route.metrics,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextCopy.ServeHTTP(rw, req)
|
return nextCopy.ServeHTTP(rw, req)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -327,14 +306,20 @@ func wrapRoute(route Route) Middleware {
|
|||||||
// wrapMiddleware wraps mh such that it can be correctly
|
// wrapMiddleware wraps mh such that it can be correctly
|
||||||
// appended to a list of middleware in preparation for
|
// appended to a list of middleware in preparation for
|
||||||
// compiling into a handler chain.
|
// compiling into a handler chain.
|
||||||
func wrapMiddleware(ctx caddy.Context, mh MiddlewareHandler) Middleware {
|
func wrapMiddleware(ctx caddy.Context, mh MiddlewareHandler, metrics *Metrics) Middleware {
|
||||||
|
handlerToUse := mh
|
||||||
|
if metrics != nil {
|
||||||
|
// wrap the middleware with metrics instrumentation
|
||||||
|
handlerToUse = newMetricsInstrumentedHandler(ctx, caddy.GetModuleName(mh), mh, metrics)
|
||||||
|
}
|
||||||
|
|
||||||
return func(next Handler) Handler {
|
return func(next Handler) Handler {
|
||||||
return HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
return HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
// EXPERIMENTAL: Trace each module that gets invoked
|
// EXPERIMENTAL: Trace each module that gets invoked
|
||||||
if server, ok := r.Context().Value(ServerCtxKey).(*Server); ok && server != nil {
|
if server, ok := r.Context().Value(ServerCtxKey).(*Server); ok && server != nil {
|
||||||
server.logTrace(mh)
|
server.logTrace(handlerToUse)
|
||||||
}
|
}
|
||||||
return mh.ServeHTTP(w, r, next)
|
return handlerToUse.ServeHTTP(w, r, next)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -486,7 +486,7 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) error {
|
|||||||
// code to any HTTP/1.1 request message that lacks a Host header field and to any
|
// code to any HTTP/1.1 request message that lacks a Host header field and to any
|
||||||
// request message that contains more than one Host header field line or a Host
|
// request message that contains more than one Host header field line or a Host
|
||||||
// header field with an invalid field value."
|
// header field with an invalid field value."
|
||||||
if r.ProtoMajor == 1 && r.ProtoMinor == 1 && r.Host == "" {
|
if r.Host == "" {
|
||||||
return HandlerError{
|
return HandlerError{
|
||||||
Err: errors.New("rfc9112 forbids empty Host"),
|
Err: errors.New("rfc9112 forbids empty Host"),
|
||||||
StatusCode: http.StatusBadRequest,
|
StatusCode: http.StatusBadRequest,
|
||||||
|
|||||||
@@ -312,12 +312,10 @@ func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) {
|
|||||||
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
|
||||||
for key, val := range m {
|
for key, val := range m {
|
||||||
var varValue any
|
var varValue any
|
||||||
var fromPlaceholder bool
|
|
||||||
if strings.HasPrefix(key, "{") &&
|
if strings.HasPrefix(key, "{") &&
|
||||||
strings.HasSuffix(key, "}") &&
|
strings.HasSuffix(key, "}") &&
|
||||||
strings.Count(key, "{") == 1 {
|
strings.Count(key, "{") == 1 {
|
||||||
varValue, _ = repl.Get(strings.Trim(key, "{}"))
|
varValue, _ = repl.Get(strings.Trim(key, "{}"))
|
||||||
fromPlaceholder = true
|
|
||||||
} else {
|
} else {
|
||||||
varValue = vars[key]
|
varValue = vars[key]
|
||||||
}
|
}
|
||||||
@@ -336,14 +334,7 @@ func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) {
|
|||||||
varStr = fmt.Sprintf("%v", vv)
|
varStr = fmt.Sprintf("%v", vv)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only expand placeholders in values from literal variable names
|
valExpanded := repl.ReplaceAll(varStr, "")
|
||||||
// (e.g. map outputs). Values resolved from placeholder keys are
|
|
||||||
// already final and must not be re-expanded, as that would allow
|
|
||||||
// user input like {env.SECRET} to be evaluated.
|
|
||||||
valExpanded := varStr
|
|
||||||
if !fromPlaceholder {
|
|
||||||
valExpanded = repl.ReplaceAll(varStr, "")
|
|
||||||
}
|
|
||||||
if match := val.Match(valExpanded, repl); match {
|
if match := val.Match(valExpanded, repl); match {
|
||||||
return match, nil
|
return match, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import (
|
|||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddypki"
|
"github.com/caddyserver/caddy/v2/modules/caddypki"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -305,19 +304,7 @@ func (ash Handler) openDatabase() (*db.AuthDB, error) {
|
|||||||
// makeClient creates an ACME client which will use a custom
|
// makeClient creates an ACME client which will use a custom
|
||||||
// resolver instead of net.DefaultResolver.
|
// resolver instead of net.DefaultResolver.
|
||||||
func (ash Handler) makeClient() (acme.Client, error) {
|
func (ash Handler) makeClient() (acme.Client, error) {
|
||||||
// If no local resolvers are configured, check for global resolvers from TLS app
|
for _, v := range ash.Resolvers {
|
||||||
resolversToUse := ash.Resolvers
|
|
||||||
if len(resolversToUse) == 0 {
|
|
||||||
tlsAppIface, err := ash.ctx.App("tls")
|
|
||||||
if err == nil {
|
|
||||||
tlsApp := tlsAppIface.(*caddytls.TLS)
|
|
||||||
if len(tlsApp.Resolvers) > 0 {
|
|
||||||
resolversToUse = tlsApp.Resolvers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range resolversToUse {
|
|
||||||
addr, err := caddy.ParseNetworkAddressWithDefaults(v, "udp", 53)
|
addr, err := caddy.ParseNetworkAddressWithDefaults(v, "udp", 53)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -243,49 +243,22 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// build certmagic.Config and attach it to the policy
|
|
||||||
storage := ap.storage
|
|
||||||
if storage == nil {
|
|
||||||
storage = tlsApp.ctx.Storage()
|
|
||||||
}
|
|
||||||
cfg, err := ap.makeCertMagicConfig(tlsApp, issuers, storage)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
certCacheMu.RLock()
|
|
||||||
ap.magic = certmagic.New(certCache, cfg)
|
|
||||||
certCacheMu.RUnlock()
|
|
||||||
|
|
||||||
// give issuers a chance to see the config pointer
|
|
||||||
for _, issuer := range ap.magic.Issuers {
|
|
||||||
if annoying, ok := issuer.(ConfigSetter); ok {
|
|
||||||
annoying.SetConfig(ap.magic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// makeCertMagicConfig constructs a certmagic.Config for this policy using the
|
|
||||||
// provided issuers and storage. It encapsulates common logic shared between
|
|
||||||
// Provision and RebuildCertMagic so we don't duplicate code.
|
|
||||||
func (ap *AutomationPolicy) makeCertMagicConfig(tlsApp *TLS, issuers []certmagic.Issuer, storage certmagic.Storage) (certmagic.Config, error) {
|
|
||||||
// key source
|
|
||||||
keyType := ap.KeyType
|
keyType := ap.KeyType
|
||||||
if keyType != "" {
|
if keyType != "" {
|
||||||
var err error
|
var err error
|
||||||
keyType, err = caddy.NewReplacer().ReplaceOrErr(ap.KeyType, true, true)
|
keyType, err = caddy.NewReplacer().ReplaceOrErr(ap.KeyType, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return certmagic.Config{}, fmt.Errorf("invalid key type %s: %s", ap.KeyType, err)
|
return fmt.Errorf("invalid key type %s: %s", ap.KeyType, err)
|
||||||
}
|
}
|
||||||
if _, ok := supportedCertKeyTypes[keyType]; !ok {
|
if _, ok := supportedCertKeyTypes[keyType]; !ok {
|
||||||
return certmagic.Config{}, fmt.Errorf("unrecognized key type: %s", keyType)
|
return fmt.Errorf("unrecognized key type: %s", keyType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keySource := certmagic.StandardKeyGenerator{
|
keySource := certmagic.StandardKeyGenerator{
|
||||||
KeyType: supportedCertKeyTypes[keyType],
|
KeyType: supportedCertKeyTypes[keyType],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
storage := ap.storage
|
||||||
if storage == nil {
|
if storage == nil {
|
||||||
storage = tlsApp.ctx.Storage()
|
storage = tlsApp.ctx.Storage()
|
||||||
}
|
}
|
||||||
@@ -304,7 +277,7 @@ func (ap *AutomationPolicy) makeCertMagicConfig(tlsApp *TLS, issuers []certmagic
|
|||||||
if noProtections {
|
if noProtections {
|
||||||
if !ap.hadExplicitManagers {
|
if !ap.hadExplicitManagers {
|
||||||
// no managers, no explicitly-configured permission module, this is a config error
|
// no managers, no explicitly-configured permission module, this is a config error
|
||||||
return certmagic.Config{}, fmt.Errorf("on-demand TLS cannot be enabled without a permission module to prevent abuse; please refer to documentation for details")
|
return fmt.Errorf("on-demand TLS cannot be enabled without a permission module to prevent abuse; please refer to documentation for details")
|
||||||
}
|
}
|
||||||
// allow on-demand to be enabled but only for the purpose of the Managers; issuance won't be allowed from Issuers
|
// allow on-demand to be enabled but only for the purpose of the Managers; issuance won't be allowed from Issuers
|
||||||
tlsApp.logger.Warn("on-demand TLS can only get certificates from the configured external manager(s) because no ask endpoint / permission module is specified")
|
tlsApp.logger.Warn("on-demand TLS can only get certificates from the configured external manager(s) because no ask endpoint / permission module is specified")
|
||||||
@@ -361,7 +334,7 @@ func (ap *AutomationPolicy) makeCertMagicConfig(tlsApp *TLS, issuers []certmagic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := certmagic.Config{
|
template := certmagic.Config{
|
||||||
MustStaple: ap.MustStaple,
|
MustStaple: ap.MustStaple,
|
||||||
RenewalWindowRatio: ap.RenewalWindowRatio,
|
RenewalWindowRatio: ap.RenewalWindowRatio,
|
||||||
KeySource: keySource,
|
KeySource: keySource,
|
||||||
@@ -376,31 +349,8 @@ func (ap *AutomationPolicy) makeCertMagicConfig(tlsApp *TLS, issuers []certmagic
|
|||||||
Issuers: issuers,
|
Issuers: issuers,
|
||||||
Logger: tlsApp.logger,
|
Logger: tlsApp.logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsProvisioned reports whether the automation policy has been
|
|
||||||
// provisioned. A provisioned policy has an initialized CertMagic
|
|
||||||
// instance (i.e. ap.magic != nil).
|
|
||||||
func (ap *AutomationPolicy) IsProvisioned() bool { return ap.magic != nil }
|
|
||||||
|
|
||||||
// RebuildCertMagic rebuilds the policy's CertMagic configuration from the
|
|
||||||
// policy's already-populated fields (Issuers, Managers, storage, etc.) and
|
|
||||||
// replaces the internal CertMagic instance. This is a lightweight
|
|
||||||
// alternative to calling Provision because it does not re-provision
|
|
||||||
// modules or re-run module Provision; instead, it constructs a new
|
|
||||||
// certmagic.Config and calls SetConfig on issuers so they receive updated
|
|
||||||
// templates (for example, alternate HTTP/TLS ports supplied by the HTTP
|
|
||||||
// app). RebuildCertMagic should only be called when the policy's required
|
|
||||||
// fields are already populated.
|
|
||||||
func (ap *AutomationPolicy) RebuildCertMagic(tlsApp *TLS) error {
|
|
||||||
cfg, err := ap.makeCertMagicConfig(tlsApp, ap.Issuers, ap.storage)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
certCacheMu.RLock()
|
certCacheMu.RLock()
|
||||||
ap.magic = certmagic.New(certCache, cfg)
|
ap.magic = certmagic.New(certCache, template)
|
||||||
certCacheMu.RUnlock()
|
certCacheMu.RUnlock()
|
||||||
|
|
||||||
// sometimes issuers may need the parent certmagic.Config in
|
// sometimes issuers may need the parent certmagic.Config in
|
||||||
|
|||||||
@@ -167,6 +167,12 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
|
|||||||
}
|
}
|
||||||
tlsApp.RegisterServerNames(echNames)
|
tlsApp.RegisterServerNames(echNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tlsCfg.GetEncryptedClientHelloKeys = func(chi *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
|
||||||
|
tlsApp.EncryptedClientHello.configsMu.RLock()
|
||||||
|
defer tlsApp.EncryptedClientHello.configsMu.RUnlock()
|
||||||
|
return tlsApp.EncryptedClientHello.stdlibReady, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,19 +376,6 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
|
|||||||
cfg.MaxVersion = SupportedProtocols[p.ProtocolMax]
|
cfg.MaxVersion = SupportedProtocols[p.ProtocolMax]
|
||||||
}
|
}
|
||||||
|
|
||||||
// enable ECH (Encrypted ClientHello) if configured
|
|
||||||
if tlsApp.EncryptedClientHello != nil {
|
|
||||||
cfg.GetEncryptedClientHelloKeys = func(_ *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
|
|
||||||
tlsApp.EncryptedClientHello.configsMu.RLock()
|
|
||||||
defer tlsApp.EncryptedClientHello.configsMu.RUnlock()
|
|
||||||
return tlsApp.EncryptedClientHello.stdlibReady, nil
|
|
||||||
}
|
|
||||||
// TLS 1.3 is the first version that supports ECH
|
|
||||||
if cfg.MinVersion < tls.VersionTLS13 {
|
|
||||||
cfg.MaxVersion = tls.VersionTLS13
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// client authentication
|
// client authentication
|
||||||
if p.ClientAuthentication != nil {
|
if p.ClientAuthentication != nil {
|
||||||
if err := p.ClientAuthentication.provision(ctx); err != nil {
|
if err := p.ClientAuthentication.provision(ctx); err != nil {
|
||||||
@@ -892,29 +885,24 @@ func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) erro
|
|||||||
|
|
||||||
// if a custom verification function already exists, wrap it
|
// if a custom verification function already exists, wrap it
|
||||||
clientauth.existingVerifyPeerCert = cfg.VerifyPeerCertificate
|
clientauth.existingVerifyPeerCert = cfg.VerifyPeerCertificate
|
||||||
cfg.VerifyConnection = clientauth.verifyConnection
|
cfg.VerifyPeerCertificate = clientauth.verifyPeerCertificate
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// verifyConnection is for use as a tls.Config.VerifyConnection callback
|
// verifyPeerCertificate is for use as a tls.Config.VerifyPeerCertificate
|
||||||
// to do custom client certificate verification. It is intended for
|
// callback to do custom client certificate verification. It is intended
|
||||||
// installation only by clientauth.ConfigureTLSConfig().
|
// for installation only by clientauth.ConfigureTLSConfig().
|
||||||
//
|
func (clientauth *ClientAuthentication) verifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
||||||
// Unlike VerifyPeerCertificate, VerifyConnection is called on every
|
|
||||||
// connection including resumed sessions, preventing session-resumption bypass.
|
|
||||||
func (clientauth *ClientAuthentication) verifyConnection(cs tls.ConnectionState) error {
|
|
||||||
// first use any pre-existing custom verification function
|
// first use any pre-existing custom verification function
|
||||||
if clientauth.existingVerifyPeerCert != nil {
|
if clientauth.existingVerifyPeerCert != nil {
|
||||||
rawCerts := make([][]byte, len(cs.PeerCertificates))
|
err := clientauth.existingVerifyPeerCert(rawCerts, verifiedChains)
|
||||||
for i, cert := range cs.PeerCertificates {
|
if err != nil {
|
||||||
rawCerts[i] = cert.Raw
|
|
||||||
}
|
|
||||||
if err := clientauth.existingVerifyPeerCert(rawCerts, cs.VerifiedChains); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, verifier := range clientauth.verifiers {
|
for _, verifier := range clientauth.verifiers {
|
||||||
if err := verifier.VerifyClientCertificate(nil, cs.VerifiedChains); err != nil {
|
err := verifier.VerifyClientCertificate(rawCerts, verifiedChains)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -63,27 +62,18 @@ func (fl FolderLoader) Provision(ctx caddy.Context) error {
|
|||||||
func (fl FolderLoader) LoadCertificates() ([]Certificate, error) {
|
func (fl FolderLoader) LoadCertificates() ([]Certificate, error) {
|
||||||
var certs []Certificate
|
var certs []Certificate
|
||||||
for _, dir := range fl {
|
for _, dir := range fl {
|
||||||
root, err := os.OpenRoot(dir)
|
err := filepath.Walk(dir, func(fpath string, info os.FileInfo, err error) error {
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to open root directory %s: %w", dir, err)
|
|
||||||
}
|
|
||||||
err = filepath.WalkDir(dir, func(fpath string, d fs.DirEntry, err error) error {
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to traverse into path: %s", fpath)
|
return fmt.Errorf("unable to traverse into path: %s", fpath)
|
||||||
}
|
}
|
||||||
if d.IsDir() {
|
if info.IsDir() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if !strings.HasSuffix(strings.ToLower(d.Name()), ".pem") {
|
if !strings.HasSuffix(strings.ToLower(info.Name()), ".pem") {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rel, err := filepath.Rel(dir, fpath)
|
bundle, err := os.ReadFile(fpath)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to get relative path for %s: %w", fpath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bundle, err := root.ReadFile(rel)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -93,11 +83,11 @@ func (fl FolderLoader) LoadCertificates() ([]Certificate, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
certs = append(certs, Certificate{Certificate: cert})
|
certs = append(certs, Certificate{Certificate: cert})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
_ = root.Close()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("walking certificates directory %s: %w", dir, err)
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return certs, nil
|
return certs, nil
|
||||||
|
|||||||
@@ -123,15 +123,8 @@ type TLS struct {
|
|||||||
//
|
//
|
||||||
// EXPERIMENTAL: Subject to change.
|
// EXPERIMENTAL: Subject to change.
|
||||||
DNSRaw json.RawMessage `json:"dns,omitempty" caddy:"namespace=dns.providers inline_key=name"`
|
DNSRaw json.RawMessage `json:"dns,omitempty" caddy:"namespace=dns.providers inline_key=name"`
|
||||||
|
dns any // technically, it should be any/all of the libdns interfaces (RecordSetter, RecordAppender, etc.)
|
||||||
|
|
||||||
// The default DNS resolvers to use for TLS-related DNS operations, specifically
|
|
||||||
// for ACME DNS challenges and ACME server DNS validations.
|
|
||||||
// If not specified, the system default resolvers will be used.
|
|
||||||
//
|
|
||||||
// EXPERIMENTAL: Subject to change.
|
|
||||||
Resolvers []string `json:"resolvers,omitempty"`
|
|
||||||
|
|
||||||
dns any // technically, it should be any/all of the libdns interfaces (RecordSetter, RecordAppender, etc.)
|
|
||||||
certificateLoaders []CertificateLoader
|
certificateLoaders []CertificateLoader
|
||||||
automateNames map[string]struct{}
|
automateNames map[string]struct{}
|
||||||
ctx caddy.Context
|
ctx caddy.Context
|
||||||
|
|||||||
+23
-184
@@ -63,7 +63,7 @@ func (m *fileMode) UnmarshalJSON(b []byte) error {
|
|||||||
|
|
||||||
// MarshalJSON satisfies json.Marshaler.
|
// MarshalJSON satisfies json.Marshaler.
|
||||||
func (m *fileMode) MarshalJSON() ([]byte, error) {
|
func (m *fileMode) MarshalJSON() ([]byte, error) {
|
||||||
return fmt.Appendf(nil, "\"%04o\"", *m), nil
|
return []byte(fmt.Sprintf("\"%04o\"", *m)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseFileMode parses a file mode string,
|
// parseFileMode parses a file mode string,
|
||||||
@@ -90,15 +90,6 @@ type FileWriter struct {
|
|||||||
// 0600 by default.
|
// 0600 by default.
|
||||||
Mode fileMode `json:"mode,omitempty"`
|
Mode fileMode `json:"mode,omitempty"`
|
||||||
|
|
||||||
// DirMode controls permissions for any directories created to reach Filename.
|
|
||||||
// Default: 0700 (current behavior).
|
|
||||||
//
|
|
||||||
// Special values:
|
|
||||||
// - "inherit" → copy the nearest existing parent directory's perms (with r→x normalization)
|
|
||||||
// - "from_file" → derive from the file Mode (with r→x), e.g. 0644 → 0755, 0600 → 0700
|
|
||||||
// Numeric octal strings (e.g. "0755") are also accepted. Subject to process umask.
|
|
||||||
DirMode string `json:"dir_mode,omitempty"`
|
|
||||||
|
|
||||||
// Roll toggles log rolling or rotation, which is
|
// Roll toggles log rolling or rotation, which is
|
||||||
// enabled by default.
|
// enabled by default.
|
||||||
Roll *bool `json:"roll,omitempty"`
|
Roll *bool `json:"roll,omitempty"`
|
||||||
@@ -122,16 +113,9 @@ type FileWriter struct {
|
|||||||
// See https://github.com/DeRuina/timberjack#%EF%B8%8F-rotation-notes--warnings for caveats
|
// See https://github.com/DeRuina/timberjack#%EF%B8%8F-rotation-notes--warnings for caveats
|
||||||
RollAt []string `json:"roll_at,omitempty"`
|
RollAt []string `json:"roll_at,omitempty"`
|
||||||
|
|
||||||
// Whether to compress rolled files.
|
// Whether to compress rolled files. Default: true
|
||||||
// Default: true.
|
|
||||||
// Deprecated: Use RollCompression instead, setting it to "none".
|
|
||||||
RollCompress *bool `json:"roll_gzip,omitempty"`
|
RollCompress *bool `json:"roll_gzip,omitempty"`
|
||||||
|
|
||||||
// RollCompression selects the compression algorithm for rolled files.
|
|
||||||
// Accepted values: "none", "gzip", "zstd".
|
|
||||||
// Default: gzip
|
|
||||||
RollCompression string `json:"roll_compression,omitempty"`
|
|
||||||
|
|
||||||
// Whether to use local timestamps in rolled filenames.
|
// Whether to use local timestamps in rolled filenames.
|
||||||
// Default: false
|
// Default: false
|
||||||
RollLocalTime bool `json:"roll_local_time,omitempty"`
|
RollLocalTime bool `json:"roll_local_time,omitempty"`
|
||||||
@@ -193,33 +177,11 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
|
|||||||
// roll log files as a sensible default to avoid disk space exhaustion
|
// roll log files as a sensible default to avoid disk space exhaustion
|
||||||
roll := fw.Roll == nil || *fw.Roll
|
roll := fw.Roll == nil || *fw.Roll
|
||||||
|
|
||||||
// Ensure directory exists before opening the file.
|
// create the file if it does not exist; create with the configured mode, or default
|
||||||
dirPath := filepath.Dir(fw.Filename)
|
// to restrictive if not set. (timberjack will reuse the file mode across log rotation)
|
||||||
switch strings.ToLower(strings.TrimSpace(fw.DirMode)) {
|
if err := os.MkdirAll(filepath.Dir(fw.Filename), 0o700); err != nil {
|
||||||
case "", "0":
|
return nil, err
|
||||||
// Preserve current behavior: locked-down directories by default.
|
|
||||||
if err := os.MkdirAll(dirPath, 0o700); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
case "inherit":
|
|
||||||
if err := mkdirAllInherit(dirPath); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
case "from_file":
|
|
||||||
if err := mkdirAllFromFile(dirPath, os.FileMode(fw.Mode)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
dm, err := parseFileMode(fw.DirMode)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("dir_mode: %w", err)
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(dirPath, dm); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// create/open the file
|
|
||||||
file, err := os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, modeIfCreating)
|
file, err := os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, modeIfCreating)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -261,104 +223,27 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) {
|
|||||||
if fw.RollKeepDays == 0 {
|
if fw.RollKeepDays == 0 {
|
||||||
fw.RollKeepDays = 90
|
fw.RollKeepDays = 90
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine compression algorithm to use. Priority:
|
|
||||||
// 1) explicit RollCompression (none|gzip|zstd)
|
|
||||||
// 2) if RollCompress is unset or true -> gzip
|
|
||||||
// 3) if RollCompress is false -> none
|
|
||||||
var compression string
|
|
||||||
if fw.RollCompression != "" {
|
|
||||||
compression = strings.ToLower(strings.TrimSpace(fw.RollCompression))
|
|
||||||
if compression != "none" && compression != "gzip" && compression != "zstd" {
|
|
||||||
return nil, fmt.Errorf("invalid roll_compression: %s", fw.RollCompression)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if fw.RollCompress == nil || *fw.RollCompress {
|
|
||||||
compression = "gzip"
|
|
||||||
} else {
|
|
||||||
compression = "none"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &timberjack.Logger{
|
return &timberjack.Logger{
|
||||||
Filename: fw.Filename,
|
Filename: fw.Filename,
|
||||||
MaxSize: fw.RollSizeMB,
|
MaxSize: fw.RollSizeMB,
|
||||||
MaxAge: fw.RollKeepDays,
|
MaxAge: fw.RollKeepDays,
|
||||||
MaxBackups: fw.RollKeep,
|
MaxBackups: fw.RollKeep,
|
||||||
LocalTime: fw.RollLocalTime,
|
LocalTime: fw.RollLocalTime,
|
||||||
Compression: compression,
|
Compress: *fw.RollCompress,
|
||||||
RotationInterval: fw.RollInterval,
|
RotationInterval: fw.RollInterval,
|
||||||
RotateAtMinutes: fw.RollAtMinutes,
|
RotateAtMinutes: fw.RollAtMinutes,
|
||||||
RotateAt: fw.RollAt,
|
RotateAt: fw.RollAt,
|
||||||
BackupTimeFormat: fw.BackupTimeFormat,
|
BackupTimeFormat: fw.BackupTimeFormat,
|
||||||
FileMode: os.FileMode(fw.Mode),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// normalizeDirPerm ensures that read bits also have execute bits set.
|
|
||||||
func normalizeDirPerm(p os.FileMode) os.FileMode {
|
|
||||||
if p&0o400 != 0 {
|
|
||||||
p |= 0o100
|
|
||||||
}
|
|
||||||
if p&0o040 != 0 {
|
|
||||||
p |= 0o010
|
|
||||||
}
|
|
||||||
if p&0o004 != 0 {
|
|
||||||
p |= 0o001
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// mkdirAllInherit creates missing dirs using the nearest existing parent's
|
|
||||||
// permissions, normalized with r→x.
|
|
||||||
func mkdirAllInherit(dir string) error {
|
|
||||||
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
cur := dir
|
|
||||||
var parent string
|
|
||||||
for {
|
|
||||||
next := filepath.Dir(cur)
|
|
||||||
if next == cur {
|
|
||||||
parent = next
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if fi, err := os.Stat(next); err == nil {
|
|
||||||
if !fi.IsDir() {
|
|
||||||
return fmt.Errorf("path component %s exists and is not a directory", next)
|
|
||||||
}
|
|
||||||
parent = next
|
|
||||||
break
|
|
||||||
}
|
|
||||||
cur = next
|
|
||||||
}
|
|
||||||
perm := os.FileMode(0o700)
|
|
||||||
if fi, err := os.Stat(parent); err == nil && fi.IsDir() {
|
|
||||||
perm = fi.Mode().Perm()
|
|
||||||
}
|
|
||||||
perm = normalizeDirPerm(perm)
|
|
||||||
return os.MkdirAll(dir, perm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// mkdirAllFromFile creates missing dirs using the file's mode (with r→x) so
|
|
||||||
// 0644 → 0755, 0600 → 0700, etc.
|
|
||||||
func mkdirAllFromFile(dir string, fileMode os.FileMode) error {
|
|
||||||
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
perm := normalizeDirPerm(fileMode.Perm()) | 0o200 // ensure owner write on dir so files can be created
|
|
||||||
return os.MkdirAll(dir, perm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
|
// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
|
||||||
//
|
//
|
||||||
// file <filename> {
|
// file <filename> {
|
||||||
// mode <mode>
|
// mode <mode>
|
||||||
// dir_mode <mode|inherit|from_file>
|
|
||||||
// roll_disabled
|
// roll_disabled
|
||||||
// roll_size <size>
|
// roll_size <size>
|
||||||
// roll_uncompressed
|
// roll_uncompressed
|
||||||
// roll_compression <none|gzip|zstd>
|
|
||||||
// roll_local_time
|
// roll_local_time
|
||||||
// roll_keep <num>
|
// roll_keep <num>
|
||||||
// roll_keep_for <days>
|
// roll_keep_for <days>
|
||||||
@@ -399,22 +284,6 @@ func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|||||||
}
|
}
|
||||||
fw.Mode = fileMode(mode)
|
fw.Mode = fileMode(mode)
|
||||||
|
|
||||||
case "dir_mode":
|
|
||||||
var val string
|
|
||||||
if !d.AllArgs(&val) {
|
|
||||||
return d.ArgErr()
|
|
||||||
}
|
|
||||||
val = strings.TrimSpace(val)
|
|
||||||
switch strings.ToLower(val) {
|
|
||||||
case "inherit", "from_file":
|
|
||||||
fw.DirMode = val
|
|
||||||
default:
|
|
||||||
if _, err := parseFileMode(val); err != nil {
|
|
||||||
return d.Errf("parsing dir_mode: %v", err)
|
|
||||||
}
|
|
||||||
fw.DirMode = val
|
|
||||||
}
|
|
||||||
|
|
||||||
case "roll_disabled":
|
case "roll_disabled":
|
||||||
var f bool
|
var f bool
|
||||||
fw.Roll = &f
|
fw.Roll = &f
|
||||||
@@ -440,19 +309,6 @@ func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|||||||
return d.ArgErr()
|
return d.ArgErr()
|
||||||
}
|
}
|
||||||
|
|
||||||
case "roll_compression":
|
|
||||||
var comp string
|
|
||||||
if !d.AllArgs(&comp) {
|
|
||||||
return d.ArgErr()
|
|
||||||
}
|
|
||||||
comp = strings.ToLower(strings.TrimSpace(comp))
|
|
||||||
switch comp {
|
|
||||||
case "none", "gzip", "zstd":
|
|
||||||
fw.RollCompression = comp
|
|
||||||
default:
|
|
||||||
return d.Errf("parsing roll_compression: must be 'none', 'gzip' or 'zstd'")
|
|
||||||
}
|
|
||||||
|
|
||||||
case "roll_local_time":
|
case "roll_local_time":
|
||||||
fw.RollLocalTime = true
|
fw.RollLocalTime = true
|
||||||
if d.NextArg() {
|
if d.NextArg() {
|
||||||
@@ -496,48 +352,31 @@ func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|||||||
fw.RollInterval = duration
|
fw.RollInterval = duration
|
||||||
|
|
||||||
case "roll_minutes":
|
case "roll_minutes":
|
||||||
// Accept either a single comma-separated argument or
|
var minutesArrayStr string
|
||||||
// multiple space-separated arguments. Collect all
|
if !d.AllArgs(&minutesArrayStr) {
|
||||||
// remaining args on the line and split on commas.
|
|
||||||
args := d.RemainingArgs()
|
|
||||||
if len(args) == 0 {
|
|
||||||
return d.ArgErr()
|
return d.ArgErr()
|
||||||
}
|
}
|
||||||
var minutes []int
|
minutesStr := strings.Split(minutesArrayStr, ",")
|
||||||
for _, arg := range args {
|
minutes := make([]int, len(minutesStr))
|
||||||
parts := strings.SplitSeq(arg, ",")
|
for i := range minutesStr {
|
||||||
for p := range parts {
|
ms := strings.Trim(minutesStr[i], " ")
|
||||||
ms := strings.TrimSpace(p)
|
m, err := strconv.Atoi(ms)
|
||||||
if ms == "" {
|
if err != nil {
|
||||||
return d.Errf("parsing roll_minutes: empty value")
|
return d.Errf("parsing roll_minutes number: %v", err)
|
||||||
}
|
|
||||||
m, err := strconv.Atoi(ms)
|
|
||||||
if err != nil {
|
|
||||||
return d.Errf("parsing roll_minutes number: %v", err)
|
|
||||||
}
|
|
||||||
minutes = append(minutes, m)
|
|
||||||
}
|
}
|
||||||
|
minutes[i] = m
|
||||||
}
|
}
|
||||||
fw.RollAtMinutes = minutes
|
fw.RollAtMinutes = minutes
|
||||||
|
|
||||||
case "roll_at":
|
case "roll_at":
|
||||||
// Accept either a single comma-separated argument or
|
var timeArrayStr string
|
||||||
// multiple space-separated arguments. Collect all
|
if !d.AllArgs(&timeArrayStr) {
|
||||||
// remaining args on the line and split on commas.
|
|
||||||
args := d.RemainingArgs()
|
|
||||||
if len(args) == 0 {
|
|
||||||
return d.ArgErr()
|
return d.ArgErr()
|
||||||
}
|
}
|
||||||
var times []string
|
timeStr := strings.Split(timeArrayStr, ",")
|
||||||
for _, arg := range args {
|
times := make([]string, len(timeStr))
|
||||||
parts := strings.SplitSeq(arg, ",")
|
for i := range timeStr {
|
||||||
for p := range parts {
|
times[i] = strings.Trim(timeStr[i], " ")
|
||||||
ts := strings.TrimSpace(p)
|
|
||||||
if ts == "" {
|
|
||||||
return d.Errf("parsing roll_at: empty value")
|
|
||||||
}
|
|
||||||
times = append(times, ts)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
fw.RollAt = times
|
fw.RollAt = times
|
||||||
|
|
||||||
|
|||||||
@@ -385,225 +385,3 @@ func TestFileModeModification(t *testing.T) {
|
|||||||
t.Errorf("file mode is %v, want %v", st.Mode(), want)
|
t.Errorf("file mode is %v, want %v", st.Mode(), want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDirMode_Inherit(t *testing.T) {
|
|
||||||
m := syscall.Umask(0)
|
|
||||||
defer syscall.Umask(m)
|
|
||||||
|
|
||||||
parent := t.TempDir()
|
|
||||||
if err := os.Chmod(parent, 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
targetDir := filepath.Join(parent, "a", "b")
|
|
||||||
fw := &FileWriter{
|
|
||||||
Filename: filepath.Join(targetDir, "test.log"),
|
|
||||||
DirMode: "inherit",
|
|
||||||
Mode: 0o640,
|
|
||||||
Roll: func() *bool { f := false; return &f }(),
|
|
||||||
}
|
|
||||||
w, err := fw.OpenWriter()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
_ = w.Close()
|
|
||||||
|
|
||||||
st, err := os.Stat(targetDir)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if got := st.Mode().Perm(); got != 0o755 {
|
|
||||||
t.Fatalf("dir perm = %o, want 0755", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDirMode_FromFile(t *testing.T) {
|
|
||||||
m := syscall.Umask(0)
|
|
||||||
defer syscall.Umask(m)
|
|
||||||
|
|
||||||
base := t.TempDir()
|
|
||||||
|
|
||||||
dir1 := filepath.Join(base, "logs1")
|
|
||||||
fw1 := &FileWriter{
|
|
||||||
Filename: filepath.Join(dir1, "app.log"),
|
|
||||||
DirMode: "from_file",
|
|
||||||
Mode: 0o644, // => dir 0755
|
|
||||||
Roll: func() *bool { f := false; return &f }(),
|
|
||||||
}
|
|
||||||
w1, err := fw1.OpenWriter()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
_ = w1.Close()
|
|
||||||
|
|
||||||
st1, err := os.Stat(dir1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if got := st1.Mode().Perm(); got != 0o755 {
|
|
||||||
t.Fatalf("dir perm = %o, want 0755", got)
|
|
||||||
}
|
|
||||||
|
|
||||||
dir2 := filepath.Join(base, "logs2")
|
|
||||||
fw2 := &FileWriter{
|
|
||||||
Filename: filepath.Join(dir2, "app.log"),
|
|
||||||
DirMode: "from_file",
|
|
||||||
Mode: 0o600, // => dir 0700
|
|
||||||
Roll: func() *bool { f := false; return &f }(),
|
|
||||||
}
|
|
||||||
w2, err := fw2.OpenWriter()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
_ = w2.Close()
|
|
||||||
|
|
||||||
st2, err := os.Stat(dir2)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if got := st2.Mode().Perm(); got != 0o700 {
|
|
||||||
t.Fatalf("dir perm = %o, want 0700", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDirMode_ExplicitOctal(t *testing.T) {
|
|
||||||
m := syscall.Umask(0)
|
|
||||||
defer syscall.Umask(m)
|
|
||||||
|
|
||||||
base := t.TempDir()
|
|
||||||
dest := filepath.Join(base, "logs3")
|
|
||||||
fw := &FileWriter{
|
|
||||||
Filename: filepath.Join(dest, "app.log"),
|
|
||||||
DirMode: "0750",
|
|
||||||
Mode: 0o640,
|
|
||||||
Roll: func() *bool { f := false; return &f }(),
|
|
||||||
}
|
|
||||||
w, err := fw.OpenWriter()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
_ = w.Close()
|
|
||||||
|
|
||||||
st, err := os.Stat(dest)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if got := st.Mode().Perm(); got != 0o750 {
|
|
||||||
t.Fatalf("dir perm = %o, want 0750", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDirMode_Default0700(t *testing.T) {
|
|
||||||
m := syscall.Umask(0)
|
|
||||||
defer syscall.Umask(m)
|
|
||||||
|
|
||||||
base := t.TempDir()
|
|
||||||
dest := filepath.Join(base, "logs4")
|
|
||||||
fw := &FileWriter{
|
|
||||||
Filename: filepath.Join(dest, "app.log"),
|
|
||||||
Mode: 0o640,
|
|
||||||
Roll: func() *bool { f := false; return &f }(),
|
|
||||||
}
|
|
||||||
w, err := fw.OpenWriter()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
_ = w.Close()
|
|
||||||
|
|
||||||
st, err := os.Stat(dest)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if got := st.Mode().Perm(); got != 0o700 {
|
|
||||||
t.Fatalf("dir perm = %o, want 0700", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDirMode_UmaskInteraction(t *testing.T) {
|
|
||||||
_ = syscall.Umask(0o022) // typical umask; restore after
|
|
||||||
defer syscall.Umask(0)
|
|
||||||
|
|
||||||
base := t.TempDir()
|
|
||||||
dest := filepath.Join(base, "logs5")
|
|
||||||
fw := &FileWriter{
|
|
||||||
Filename: filepath.Join(dest, "app.log"),
|
|
||||||
DirMode: "0755",
|
|
||||||
Mode: 0o644,
|
|
||||||
Roll: func() *bool { f := false; return &f }(),
|
|
||||||
}
|
|
||||||
w, err := fw.OpenWriter()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
_ = w.Close()
|
|
||||||
|
|
||||||
st, err := os.Stat(dest)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
// 0755 &^ 0022 still 0755 for dirs; this just sanity-checks we didn't get stricter unexpectedly
|
|
||||||
if got := st.Mode().Perm(); got != 0o755 {
|
|
||||||
t.Fatalf("dir perm = %o, want 0755 (considering umask)", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCaddyfile_DirMode_Inherit(t *testing.T) {
|
|
||||||
d := caddyfile.NewTestDispenser(`
|
|
||||||
file /var/log/app.log {
|
|
||||||
dir_mode inherit
|
|
||||||
mode 0640
|
|
||||||
}`)
|
|
||||||
var fw FileWriter
|
|
||||||
if err := fw.UnmarshalCaddyfile(d); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if fw.DirMode != "inherit" {
|
|
||||||
t.Fatalf("got %q", fw.DirMode)
|
|
||||||
}
|
|
||||||
if fw.Mode != 0o640 {
|
|
||||||
t.Fatalf("mode = %o", fw.Mode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCaddyfile_DirMode_FromFile(t *testing.T) {
|
|
||||||
d := caddyfile.NewTestDispenser(`
|
|
||||||
file /var/log/app.log {
|
|
||||||
dir_mode from_file
|
|
||||||
mode 0600
|
|
||||||
}`)
|
|
||||||
var fw FileWriter
|
|
||||||
if err := fw.UnmarshalCaddyfile(d); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if fw.DirMode != "from_file" {
|
|
||||||
t.Fatalf("got %q", fw.DirMode)
|
|
||||||
}
|
|
||||||
if fw.Mode != 0o600 {
|
|
||||||
t.Fatalf("mode = %o", fw.Mode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCaddyfile_DirMode_Octal(t *testing.T) {
|
|
||||||
d := caddyfile.NewTestDispenser(`
|
|
||||||
file /var/log/app.log {
|
|
||||||
dir_mode 0755
|
|
||||||
}`)
|
|
||||||
var fw FileWriter
|
|
||||||
if err := fw.UnmarshalCaddyfile(d); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if fw.DirMode != "0755" {
|
|
||||||
t.Fatalf("got %q", fw.DirMode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCaddyfile_DirMode_Invalid(t *testing.T) {
|
|
||||||
d := caddyfile.NewTestDispenser(`
|
|
||||||
file /var/log/app.log {
|
|
||||||
dir_mode nope
|
|
||||||
}`)
|
|
||||||
var fw FileWriter
|
|
||||||
if err := fw.UnmarshalCaddyfile(d); err == nil {
|
|
||||||
t.Fatal("expected error for invalid dir_mode")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -53,41 +53,3 @@ func TestFileCreationMode(t *testing.T) {
|
|||||||
t.Fatalf("file mode is %v, want rw for user", st.Mode().Perm())
|
t.Fatalf("file mode is %v, want rw for user", st.Mode().Perm())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDirMode_Windows_CreateSucceeds(t *testing.T) {
|
|
||||||
dir, err := os.MkdirTemp("", "caddytest")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create tempdir: %v", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(dir)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
dirMode string
|
|
||||||
}{
|
|
||||||
{"inherit", "inherit"},
|
|
||||||
{"from_file", "from_file"},
|
|
||||||
{"octal", "0755"},
|
|
||||||
{"default", ""},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
subdir := path.Join(dir, "logs-"+tt.name)
|
|
||||||
fw := &FileWriter{
|
|
||||||
Filename: path.Join(subdir, "test.log"),
|
|
||||||
DirMode: tt.dirMode,
|
|
||||||
Mode: 0o600,
|
|
||||||
}
|
|
||||||
w, err := fw.OpenWriter()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to open writer: %v", err)
|
|
||||||
}
|
|
||||||
defer w.Close()
|
|
||||||
|
|
||||||
if _, err := os.Stat(fw.Filename); err != nil {
|
|
||||||
t.Fatalf("expected file to exist: %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user