Compare commits

...

59 Commits

Author SHA1 Message Date
dependabot[bot] 935f19f014 build(deps): bump the actions-deps group across 1 directory with 11 updates
Bumps the actions-deps group with 11 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [actions/checkout](https://github.com/actions/checkout) | `6.0.2` | `6.0.3` |
| [actions/github-script](https://github.com/actions/github-script) | `8.0.0` | `9.0.0` |
| [step-security/harden-runner](https://github.com/step-security/harden-runner) | `2.15.0` | `2.19.4` |
| [actions/setup-go](https://github.com/actions/setup-go) | `6.3.0` | `6.4.0` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `7.0.0` | `7.0.1` |
| [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) | `7.0.0` | `7.2.2` |
| [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) | `9.2.0` | `9.2.1` |
| [actions/dependency-review-action](https://github.com/actions/dependency-review-action) | `4.8.3` | `5.0.0` |
| [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) | `3.10.0` | `4.1.2` |
| [anchore/sbom-action](https://github.com/anchore/sbom-action) | `0.23.0` | `0.24.0` |
| [github/codeql-action](https://github.com/github/codeql-action) | `4.32.4` | `4.36.1` |



Updates `actions/checkout` from 6.0.2 to 6.0.3
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10)

Updates `actions/github-script` from 8.0.0 to 9.0.0
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/ed597411d8f924073f98dfc5c65a23a2325f34cd...3a2844b7e9c422d3c10d287c895573f7108da1b3)

Updates `step-security/harden-runner` from 2.15.0 to 2.19.4
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](https://github.com/step-security/harden-runner/compare/a90bcbc6539c36a85cdfeb73f7e2f433735f215b...9af89fc71515a100421586dfdb3dc9c984fbf411)

Updates `actions/setup-go` from 6.3.0 to 6.4.0
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/4b73464bb391d4059bd26b0524d20df3927bd417...4a3601121dd01d1626a1e23e37211e3254c1c06c)

Updates `actions/upload-artifact` from 7.0.0 to 7.0.1
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/bbbca2ddaa5d8feaa63e36b76fdaad77386f024f...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a)

Updates `goreleaser/goreleaser-action` from 7.0.0 to 7.2.2
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/ec59f474b9834571250b370d4735c50f8e2d1e29...5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89)

Updates `golangci/golangci-lint-action` from 9.2.0 to 9.2.1
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/1e7e51e771db61008b38414a730f564565cf7c20...82606bf257cbaff209d206a39f5134f0cfbfd2ee)

Updates `actions/dependency-review-action` from 4.8.3 to 5.0.0
- [Release notes](https://github.com/actions/dependency-review-action/releases)
- [Commits](https://github.com/actions/dependency-review-action/compare/05fe4576374b728f0c523d6a13d64c25081e0803...a1d282b36b6f3519aa1f3fc636f609c47dddb294)

Updates `sigstore/cosign-installer` from 3.10.0 to 4.1.2
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/d7543c93d881b35a8faa02e8e3605f69b7a1ce62...6f9f17788090df1f26f669e9d70d6ae9567deba6)

Updates `anchore/sbom-action` from 0.23.0 to 0.24.0
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md)
- [Commits](https://github.com/anchore/sbom-action/compare/17ae1740179002c89186b61233e0f892c3118b11...e22c389904149dbc22b58101806040fa8d37a610)

Updates `github/codeql-action` from 4.32.4 to 4.36.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/89a39a4e59826350b863aa6b6252a07ad50cf83e...87557b9c84dde89fdd9b10e88954ac2f4248e463)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-deps
- dependency-name: actions/github-script
  dependency-version: 9.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-deps
- dependency-name: step-security/harden-runner
  dependency-version: 2.19.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-deps
- dependency-name: actions/setup-go
  dependency-version: 6.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-deps
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-deps
- dependency-name: goreleaser/goreleaser-action
  dependency-version: 7.2.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-deps
- dependency-name: golangci/golangci-lint-action
  dependency-version: 9.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-deps
- dependency-name: actions/dependency-review-action
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-deps
- dependency-name: sigstore/cosign-installer
  dependency-version: 4.1.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-deps
- dependency-name: anchore/sbom-action
  dependency-version: 0.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-deps
- dependency-name: github/codeql-action
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-03 17:34:17 +00:00
WeidiDeng fcc7860d03 reverseproxy: replace placeholders specified for sni while using http3 (#7737)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 1m49s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m52s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 2m48s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m35s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m45s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m15s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m34s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m44s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 2m24s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m33s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m48s
Lint / dependency-review (push) Failing after 32s
Lint / govulncheck (push) Failing after 1m4s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m28s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 6m56s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
* reverseproxy: replace placeholders specified for sni while using http3

* add test for placeholder

* reverseproxy: replace placeholders specified for sni while using http3

* add test for placeholder

* reverseproxy: test HTTP/3 SNI host placeholder

---------

Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-06-02 21:49:00 -06:00
JM Sanchez e2eee6a7fc templates: Patch for GHSA-vcc4-2c75-vc9v (#7785)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 2m48s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 2m47s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 3m14s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m31s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m44s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m58s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m31s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m40s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 2m5s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m31s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m46s
Lint / govulncheck (push) Successful in 1m33s
Lint / dependency-review (push) Failing after 1m2s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m54s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 5m49s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
* Patch GHSA-vcc4-2c75-vc9v in stripHTML

templates: fix funcStripHTML bypass via depth counter

The previous false-start approach allowed XSS bypass via inputs like <<>img src=x onerror=alert(1)> and failed on stacked angle brackets.

Replace the tagStart/inTag state machine with a depth counter that mirrors PHP strip_tags behaviour: each '<' increments depth, each '>' decrements it, and text is only emitted at depth zero. Quoted attribute values (both single and double) are tracked so '>' inside href values does not prematurely close a tag.

Signed-off-by: JM Sanchez <77505889+jmrcsnchz@users.noreply.github.com>

* Update tplcontext_test.go

Templates: expand TestStripHTML with attack path coverage

Signed-off-by: JM Sanchez <77505889+jmrcsnchz@users.noreply.github.com>

---------

Signed-off-by: JM Sanchez <77505889+jmrcsnchz@users.noreply.github.com>
2026-06-01 13:35:02 -06:00
Bruno Teixeira Lopes 0e8eb41b87 httpcaddyfile: fix incorrect error message on duplicate matchers (#7780)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m47s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 3m18s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m42s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m30s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m43s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m45s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 2m40s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m58s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 2m46s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m55s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m37s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 3m15s
Lint / dependency-review (push) Failing after 33s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 41s
Lint / govulncheck (push) Successful in 1m40s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
Parse each matcher segment individually using NewDispenser(segment) instead
of DispenseDirective(dir), which coalesced all same-name segments into one
token stream. This caused the second definition name to be misinterpreted
as a matcher module name, producing 'module not registered: http.matchers.@name'
instead of the correct 'matcher is defined more than once' error.

By parsing segments individually, the existing duplicate check in
parseMatcherDefinitions naturally catches the duplicate on the second pass.

Signed-off-by: Brunotlps <brunoteixlps@gmail.com>
2026-05-30 07:05:41 +10:00
Kévin Dunglas 3eb8e48ff0 Merge commit from fork
* feat: drop headers with underscore in their names

* feat: Caddyfile binding and tests for underscore-in-header drop

Add the `allow_underscore_in_headers` global server option, refine the
doc comment, and cover the filter end-to-end: server-level unit tests
(drop, opt-out, debug log, RFC-7230 space rejection), a fastcgi unit
test for the trimmed header name replacer, and forward_auth integration
tests for both the default-drop and opt-out paths.

* remove allow_underscore_in_headers option for now
2026-05-29 11:37:17 -06:00
Muhammad Syafri, S.Kom 03e08ee6a9 encode: prioritize zstd and br over gzip in content negotiation (#7772)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 2m10s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 3m19s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m48s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m30s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m49s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m46s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 3m3s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m48s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 2m0s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m7s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 3m6s
Lint / dependency-review (push) Failing after 36s
Lint / govulncheck (push) Successful in 1m41s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m42s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 6m31s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
* fix(encode): prioritize zstd and br over gzip in content negotiation

* test(encode): update unit tests to reflect new default priority ties

* fix(encode): move default preferences to dynamic encode handler and restore generic negotiation helper

* test(encode): call real Provision function in served-response test

* test(encode): rename served-response test to TestServeHTTPDefaultEncodingPreference

* refactor(encode): use slices.SortStableFunc and httptest.NewRecorder as recommended

* refactor(encode): simplify sorting with cmp.Compare and check request error in test

* test(encode): fix variable redeclaration in TestServeHTTPDefaultEncodingPreference

Fix 'no new variables on left side of :=' error by changing 'err :=' to 'err ='
on line 347, since err was already declared on line 332.

This fixes the build failure in the encode module tests.
2026-05-29 05:26:19 +10:00
gelsomino 86121c860f caddytls: skip idna.ToASCII for pure ASCII SNI values (#7770)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 1m45s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 3m4s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m29s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 4m41s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 2m34s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m36s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 3m17s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m45s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 2m35s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m39s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 2m37s
Lint / govulncheck (push) Successful in 1m54s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 40s
Lint / lint (ubuntu-latest, linux) (push) Successful in 3m19s
Lint / dependency-review (push) Failing after 1m1s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
SNI is always ASCII on the wire (RFC 6066), and most config
patterns are also ASCII. For pure ASCII input, idna.ToASCII
only validates and lowercases, which is equivalent to a simple
strings.ToLower. Add a fast path to avoid the overhead of
idna.ToASCII in the common case.
2026-05-28 11:18:09 +10:00
Muhammad Syafri, S.Kom 4d60d936ed perf(replacer): optimize memory allocation for file placeholders (#7773)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 1m58s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 4m1s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 2m5s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m52s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 2m44s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m45s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m36s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m56s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m50s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 2m34s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 2m51s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m41s
Lint / govulncheck (push) Successful in 2m25s
Lint / dependency-review (push) Failing after 1m4s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 6m43s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
Co-authored-by: jalikajalika5 <105954036+jalikajalika5@users.noreply.github.com>
2026-05-27 14:20:33 +00:00
Lohit 176b043b01 rewrite: prevent placeholder re-expansion in injected query (#7761)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 1m57s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 2m33s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Failing after 3m38s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m56s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 4m39s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m29s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m54s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 2m6s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 7m13s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 2m15s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 3m54s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m26s
Lint / dependency-review (push) Failing after 1m22s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 12m36s
Lint / govulncheck (push) Failing after 13m9s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
When the rewrite URI template ends with a literal '?' and contains a placeholder that expands to client-controlled bytes (e.g. {http.request.header.X-Fwd}), those bytes flow into buildQueryString which runs a second Replacer pass. If the bytes contain placeholder syntax such as {env.SECRET}, that placeholder is evaluated, allowing disclosure of environment variables, files (via {file./path}), or internal request vars through the rewritten request URI.

Escape '{' and '}' in the injected query before assigning it to the query variable, so the second pass cannot find any placeholder syntax to evaluate. Operator-written placeholders in the rewrite template are already expanded by the first pass on the path component, so the only '{' or '}' surviving into the injected query must have come from replacement values.

Fixes GHSA-j8px-rmrx-76h9.

Includes three regression tests mirroring the 'is not re-expanded' tests in modules/caddyhttp/vars_test.go.

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2026-05-26 16:51:18 -06:00
Matthew Holt 4c04143261 Clarify policies for agents / LLM use
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Failing after 8m15s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m35s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m46s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 9m53s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m7s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m41s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 2m1s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 12m0s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 3m54s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 2m7s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m47s
Lint / dependency-review (push) Failing after 1m19s
Lint / govulncheck (push) Successful in 2m20s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m32s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 5m48s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
2026-05-26 14:03:39 -06:00
Zen Dodd 94fcea08f4 go.mod: update x/net (#7767)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m47s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 2m53s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 3m1s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 2m41s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 2m35s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m54s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m44s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 2m48s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 2m45s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 2m45s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m53s
Lint / dependency-review (push) Failing after 1m9s
Lint / govulncheck (push) Successful in 2m1s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m27s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 6m16s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
2026-05-25 12:24:44 -04:00
Matthew Holt 44b667a79f go.mod: Update x/crypto
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m45s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 2m45s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 2m49s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m45s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 3m46s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 4m41s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m41s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m35s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 2m37s
Lint / dependency-review (push) Failing after 1m7s
Lint / govulncheck (push) Failing after 1m34s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 3m29s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 45s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m21s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 3m47s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
2026-05-22 09:25:04 -06:00
Vincent Yang 217a785824 caddyhttp: normalize Windows backslashes in path matcher (#7763)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m33s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 2m37s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 2m37s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 2m56s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m30s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m25s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m56s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m35s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 4m59s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 2m19s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 53s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m51s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
2026-05-21 11:28:40 -06:00
dependabot[bot] b5898c3f32 build(deps): bump the all-updates group across 1 directory with 9 updates (#7752)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m43s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 2m27s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m33s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Failing after 2m2s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 2m47s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m46s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m39s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 5m17s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m27s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 2m35s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 2m36s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 3m39s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 51s
Lint / dependency-review (push) Failing after 57s
Lint / govulncheck (push) Successful in 2m18s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
Bumps the all-updates group with 9 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [github.com/alecthomas/chroma/v2](https://github.com/alecthomas/chroma) | `2.23.1` | `2.24.1` |
| [github.com/google/cel-go](https://github.com/google/cel-go) | `0.28.0` | `0.28.1` |
| [github.com/klauspost/compress](https://github.com/klauspost/compress) | `1.18.5` | `1.18.6` |
| [go.opentelemetry.io/contrib/exporters/autoexport](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.65.0` | `0.68.0` |
| [go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.67.0` | `0.68.0` |
| [go.opentelemetry.io/contrib/propagators/autoprop](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.65.0` | `0.68.0` |
| [go.uber.org/zap](https://github.com/uber-go/zap) | `1.27.1` | `1.28.0` |
| [golang.org/x/net](https://github.com/golang/net) | `0.53.0` | `0.54.0` |
| [github.com/pires/go-proxyproto](https://github.com/pires/go-proxyproto) | `0.11.0` | `0.12.0` |



Updates `github.com/alecthomas/chroma/v2` from 2.23.1 to 2.24.1
- [Release notes](https://github.com/alecthomas/chroma/releases)
- [Commits](https://github.com/alecthomas/chroma/compare/v2.23.1...v2.24.1)

Updates `github.com/google/cel-go` from 0.28.0 to 0.28.1
- [Release notes](https://github.com/google/cel-go/releases)
- [Commits](https://github.com/google/cel-go/compare/v0.28.0...v0.28.1)

Updates `github.com/klauspost/compress` from 1.18.5 to 1.18.6
- [Release notes](https://github.com/klauspost/compress/releases)
- [Commits](https://github.com/klauspost/compress/compare/v1.18.5...v1.18.6)

Updates `go.opentelemetry.io/contrib/exporters/autoexport` from 0.65.0 to 0.68.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.65.0...zpages/v0.68.0)

Updates `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp` from 0.67.0 to 0.68.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.67.0...zpages/v0.68.0)

Updates `go.opentelemetry.io/contrib/propagators/autoprop` from 0.65.0 to 0.68.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go-contrib/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go-contrib/compare/zpages/v0.65.0...zpages/v0.68.0)

Updates `go.uber.org/zap` from 1.27.1 to 1.28.0
- [Release notes](https://github.com/uber-go/zap/releases)
- [Changelog](https://github.com/uber-go/zap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/uber-go/zap/compare/v1.27.1...v1.28.0)

Updates `golang.org/x/net` from 0.53.0 to 0.54.0
- [Commits](https://github.com/golang/net/compare/v0.53.0...v0.54.0)

Updates `github.com/pires/go-proxyproto` from 0.11.0 to 0.12.0
- [Release notes](https://github.com/pires/go-proxyproto/releases)
- [Commits](https://github.com/pires/go-proxyproto/compare/v0.11.0...v0.12.0)

---
updated-dependencies:
- dependency-name: github.com/alecthomas/chroma/v2
  dependency-version: 2.24.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: github.com/google/cel-go
  dependency-version: 0.28.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: github.com/klauspost/compress
  dependency-version: 1.18.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: go.opentelemetry.io/contrib/exporters/autoexport
  dependency-version: 0.68.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
  dependency-version: 0.68.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: go.opentelemetry.io/contrib/propagators/autoprop
  dependency-version: 0.68.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: go.uber.org/zap
  dependency-version: 1.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: golang.org/x/net
  dependency-version: 0.54.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: github.com/pires/go-proxyproto
  dependency-version: 0.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-05-20 12:17:10 -06:00
Zen Dodd 9505c0baa0 caddytls: match IDN SNI in connection policies (#7742)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, aix) (push) Failing after 1m41s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 3m29s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 2m12s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 4m17s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 2m12s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 2m20s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m51s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 2m20s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 2m19s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m43s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m40s
Lint / govulncheck (push) Successful in 2m41s
Lint / dependency-review (push) Failing after 1m29s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 6m12s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Failing after 25m52s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
2026-05-20 13:52:28 -04:00
WeidiDeng ad912569b5 reverseproxy: wraps request body to prevent closing if not read (#7719)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2026-05-20 17:35:40 +00:00
Zen Dodd 6a210e96ee caddyfile: preserve implicit TLS issuer semantics (#7743) 2026-05-20 12:48:37 -04:00
Zen Dodd 6628c4a9de cmd: support caddy start on IPv6-only hosts (#7744) 2026-05-20 10:17:34 -04:00
Zen Dodd 408d20a0e5 caddyauth: add candidate placeholders for rejected identities (#7698) 2026-05-20 13:51:54 +00:00
Eyüp Can Akman 0b265eb845 reverseproxy: Add regression test for DialInfo network override (#7758) 2026-05-20 09:43:58 -04:00
Zen Dodd 88037f1666 chore: clean up wording and typo fixes (#7745)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m28s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 2m34s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 2m35s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m33s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 3m39s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 3m55s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m30s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 2m37s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m37s
Lint / govulncheck (push) Successful in 56s
Lint / dependency-review (push) Failing after 1m9s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 3m8s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 41s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m22s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 3m11s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
* chore: clean up wording and typo fixes
* chore: ASCII -> alphanumeric in lexer for heredoc marker
2026-05-20 16:36:30 +10:00
cbro 325c244ea7 caddytls: fix TLS state races and ECH rotation retry (#7756)
* caddytls: fix data race in session ticket key rotation

stayUpdated copies the map header (configs := s.configs) under the
lock, then iterates the original map after releasing it. Concurrent
calls to register/unregister mutate the same map.

Hold the lock for the entire iteration instead.

* caddytls: fix data race in AllMatchingCertificates

AllMatchingCertificates reads the package-level certCache without
acquiring certCacheMu, while Cleanup sets certCache to nil under
the write lock. The adjacent HasCertificateForSubject correctly
acquires certCacheMu.RLock.

Add the missing RLock/RUnlock to match.

* caddytls: fix ECH key rotation stopping permanently on error

When rotateECHKeys returns an error, the rotation goroutine returns
immediately, stopping all future key rotation for the lifetime of
the process.

Change return to continue, matching the error handling for
publishECHConfigs two lines below.
2026-05-20 16:35:40 +10:00
Brett Bethke 0125ae39cc caddyhttp: omit Last-Modified for unusable mod times (#7740)
See #5548 and #7730
2026-05-20 16:19:11 +10:00
Mohammed Al Sahaf 704394d9d1 chore: deps upgrade (#7751)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m40s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 1m51s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m38s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 2m28s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m25s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m43s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m24s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 2m20s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m49s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m24s
Lint / dependency-review (push) Failing after 52s
Lint / govulncheck (push) Successful in 2m11s
Lint / lint (ubuntu-latest, linux) (push) Successful in 3m2s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 5m45s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2026-05-20 02:42:19 +03:00
Matt Holt 6c675e29f8 caddytls: Fix client auth (fix #7724) (#7727)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Failing after 1m38s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 2m16s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 2m54s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 3m25s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m28s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 2m20s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m44s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m46s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 5m32s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 2m3s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 3m19s
Lint / dependency-review (push) Failing after 1m17s
Lint / govulncheck (push) Successful in 1m55s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m31s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 5m45s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
The peer certificates should be loaded even if existingVerifyPeerCert is nil.

Patched with the assistance of Copilot, as an experiment.
2026-05-14 10:05:57 -06:00
James Hartig 77e9ce7404 reverseproxy: further prevent body closes from dial errors (#7715)
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m28s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 3m19s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 3m55s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 3m56s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m28s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 2m50s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 2m54s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 5m14s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 6m20s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 4m41s
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Lint / lint (ubuntu-latest, linux) (push) Successful in 4m47s
Lint / govulncheck (push) Successful in 1m16s
Lint / dependency-review (push) Failing after 1m9s
2026-05-12 12:05:50 -06:00
Matthew Holt cc58caa109 go.mod: Upgrade quic-go to v0.59.1
Tests / goreleaser-check (push) Has been skipped
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m32s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 2m47s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 2m48s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 3m24s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 3m25s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m23s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m50s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 2m53s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m21s
Lint / dependency-review (push) Failing after 1m14s
Lint / govulncheck (push) Successful in 1m44s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 4m26s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 4m27s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 7m5s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
2026-05-11 17:33:42 -06:00
Br1an d80774cb3f metrics: Add nil check for metricsHandler in AdminMetrics.serveHTTP (#7553)
* metrics: Add nil check for metricsHandler in AdminMetrics.serveHTTP

Prevents panic when the admin metrics endpoint is accessed before
the module is fully provisioned. Returns a proper API error instead
of crashing.

* admin: provision router modules before registering routes

Instead of adding a nil check for metricsHandler, address the root
cause by provisioning admin router modules before calling Routes().
This ensures all handler state is initialized before routes are
registered on the mux.

Merge newAdminHandler and provisionAdminRouters into a single step,
removing the two-phase setup where routes were registered first and
modules provisioned later. The AdminConfig.routers field is no longer
needed since provisioning happens inline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: go fmt admin.go

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-11 17:27:03 -06:00
Rayan Salhab a4a38c3e88 rewrite: escape file matcher paths before rewriting (#7683)
* fix: escape file matcher paths in rewrites

Preserve matched file paths containing literal '?' or '%' when try_files rewrites to http.matchers.file.relative.

* test: cover nested escaped try_files rewrite paths

* test: cover encoded slash try_files rewrite paths

* fix: assert file matcher placeholder as string

---------

Co-authored-by: cyphercodes <cyphercodes@users.noreply.github.com>
2026-05-11 17:16:33 -06:00
Matthew Holt 761347aa63 templates: Explicitly warn about misconfigurations 2026-05-11 16:45:49 -06:00
Steffen Busch 4ba16fe82c docs: add documentation for fileExists and fileStat template functions (#7700)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Failing after 1m42s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 2m37s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 3m36s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 3m44s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 3m55s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m24s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 2m44s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m20s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 2m35s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 2m51s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 2m53s
Lint / govulncheck (push) Successful in 1m41s
Lint / lint (ubuntu-latest, linux) (push) Successful in 3m4s
Lint / dependency-review (push) Failing after 1m4s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 6m4s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
2026-05-12 04:23:58 +10:00
Rijul 0fab9f0f7d caddytls: avoid duplicate automation for wildcard-covered hosts (#7697)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 1m24s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m39s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m48s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 2m32s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m24s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 3m26s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m10s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m58s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m23s
Lint / dependency-review (push) Failing after 24s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 27s
Lint / govulncheck (push) Successful in 1m20s
Lint / lint (ubuntu-latest, linux) (push) Successful in 1m44s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 3m21s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 2m41s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
* caddytls: Fix wildcard race in auto-HTTPS launch

When evaluating whether to skip managing an individual subdomain
due to an existing wildcard configuration, we now explicitly consult
the automate loader.

Because Caddy apps can start in any order, relying strictly on the
TLS app's internal management state was non-deterministic if the
HTTP app started first. Checking the automate loader guarantees
predictable behavior since it is fully populated during the
Provision phase, well before any apps are started.

* respond to review comments

1. update requested comment
2. remove personal domain from test
3. add regression test

* remove unnecessary mutex lock

* refactor: -integration test, +explicit cases

* refactor: remove redundant test, add comment

* rename file and add header

* update copyright year
2026-05-11 00:08:40 +10:00
Zen Dodd 5e76b5ee43 tls: add alpn to managed HTTPS records (#7653)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 1m28s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m56s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 2m18s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 2m55s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 3m3s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m50s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m24s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 2m7s
Lint / govulncheck (push) Successful in 1m14s
Lint / dependency-review (push) Failing after 1m14s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 29s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m43s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 3m54s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 3m48s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
* tls: add alpn to managed HTTPS records

* tls: centralise HTTPS RR ALPN defaults and registration

Reuse shared protocol defaults instead of repeating the default HTTP protocol list, unify server name registration to carry ALPN in one experimental API and reuse the TLS default ALPN ordering for HTTPS RR publication

* http: centralise effective protocol resolution for HTTPS RR ALPN
2026-05-10 13:10:29 +10:00
Matthew Holt 9c78b97f9e fastcgi: Fix lint
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Failing after 42s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Failing after 51s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m28s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 2m42s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m24s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 2m59s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 2m21s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m32s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m24s
Lint / dependency-review (push) Failing after 24s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m58s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 2m15s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 31s
Lint / govulncheck (push) Successful in 1m55s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m41s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
2026-05-08 10:46:28 -06:00
Kévin Dunglas fb324331f4 Merge commit from fork
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m15s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Failing after 1m39s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m25s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 3m42s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 3m42s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 3m45s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m26s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m25s
Lint / govulncheck (push) Successful in 1m14s
Lint / dependency-review (push) Failing after 24s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 2m11s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 2m48s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 2m48s
Lint / lint (ubuntu-latest, linux) (push) Failing after 2m36s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 26s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
Both fallbacks in splitPos relied on golang.org/x/text/search with
search.IgnoreCase, which performs Unicode equivalence matching far beyond
ASCII case folding. Combined with the validated-ASCII guarantee on every
SplitPath entry, that fallback turned non-PHP filenames into PHP scripts:

- when the inner loop hit a non-ASCII byte and the IndexString fallback
  returned -1, the loop broke without resetting match=false, so a stale
  match=true caused a non-existent .php to be reported (PoC:
  "/name.<U+00A1>.txt").
- search.IgnoreCase folded fullwidth, mathematical and circled letters
  onto ASCII, so "/shell.<math sans-serif php>",
  "/shell.<fullwidth p>hp", "/shell.<circled php>" were all detected as
  ".php" files.

Replace the fallback with strict byte-level ASCII case-insensitive
matching: any byte >= utf8.RuneSelf in the path can never be part of a
match, since SplitPath entries are validated ASCII-only and lower-cased
in Provision(). This keeps the hot path branch-light and removes the
x/text/search dependency from the main module.

Reported against FrankenPHP as GHSA-3g8v-8r37-cgjm and
GHSA-v4h7-cj44-8fc8. The vulnerable function in this module was adapted
from the same FrankenPHP code.
2026-05-07 13:59:42 -06:00
tomholford 0780d4489c httpcaddyfile: accept duration strings for log sampling interval (#7694)
Co-authored-by: tomholford <tomholford@users.noreply.github.com>
2026-05-07 18:32:20 +00:00
Zen Dodd d2172bea61 chore: Fix golangci-lint 2.12.1 findings (#7690)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m25s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Failing after 1m41s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m24s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 3m23s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 3m43s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 3m46s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m18s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m24s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m41s
Lint / govulncheck (push) Successful in 1m14s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 2m36s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 2m36s
Lint / dependency-review (push) Failing after 1m14s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m43s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 7m3s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
2026-05-07 03:40:26 -04:00
Zen Dodd c7c9f3108a caddyauth: Revert "set user placeholders before auth rejection (#7685)" (#7688)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m27s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Failing after 1m42s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m23s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 3m33s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 3m38s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 3m42s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m13s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m23s
Lint / govulncheck (push) Successful in 1m41s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 2m22s
Lint / dependency-review (push) Failing after 24s
Lint / lint (ubuntu-latest, linux) (push) Failing after 2m37s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 3m6s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 3m7s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 6m14s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
This reverts commit 7e77eec0ae.
2026-05-05 09:12:46 -06:00
Rayan Salhab 7e77eec0ae caddyauth: set user placeholders before auth rejection (#7685)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m19s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Failing after 2m12s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m32s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m22s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 4m16s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 2m32s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m23s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 2m11s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m23s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 2m37s
Lint / lint (ubuntu-latest, linux) (push) Failing after 1m45s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 2m27s
Lint / dependency-review (push) Failing after 56s
Lint / govulncheck (push) Successful in 2m26s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 6m31s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
* caddyauth: set user placeholders before auth rejection
* docs: update auth placeholder comment
2026-05-03 13:40:11 +10:00
Felix Eckhofer ef496e58ef caddytls: Expand ACME credentials (#7554)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, aix) (push) Failing after 1m41s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Failing after 1m44s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 2m45s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 4m1s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m38s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m28s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 6m2s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 4m33s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 2m31s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 2m39s
Lint / dependency-review (push) Failing after 1m22s
Lint / lint (ubuntu-latest, linux) (push) Failing after 2m13s
Lint / govulncheck (push) Successful in 2m53s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 6m41s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
* caddytls: Expand ACME credentials

This allows using global placeholders such as {file./run/secrets/key_id}
when setting up the tls configuration.

* chore(formatting): gofmt on acmeissuer_test
2026-05-03 07:13:57 +10:00
Amemoyoi 18ab0f955f admin: reject non-canonical config array indices (#7592)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m31s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Failing after 1m47s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m33s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 3m31s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 3m35s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 3m34s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m20s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m28s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m50s
Lint / govulncheck (push) Successful in 1m17s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 2m37s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 2m36s
Lint / dependency-review (push) Failing after 1m16s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m46s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 7m10s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
* admin: reject non-canonical config array indices

* admin: expand canonical array index test coverage

* Update admin.go

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

* Update admin.go

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

* admin: improve canonical array index test diagnostics

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2026-04-30 09:39:57 -06:00
mfrischknecht 6a64bb2ce5 listeners: clean up stale Unix socket files on Windows (#7676)
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m35s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Failing after 1m52s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 3m33s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 3m49s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 4m0s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m6s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Failing after 2m36s
Lint / lint (ubuntu-latest, linux) (push) Failing after 2m5s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 2m41s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 2m31s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 2m26s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 2m31s
Lint / dependency-review (push) Failing after 1m2s
Lint / govulncheck (push) Failing after 2m35s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 6m54s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
* Delete old unix domain socket files on Windows

While Windows doesn't have the need to reuse a socket file descriptor
by dup()ing it on config reloads, there still is a valid need for an
equivalent to the `syscall.Unlink()` call in listen_unix.go (also in
`reuseUnixSocket`).

If a previous Caddy instance didn't terminate properly, the chances it
will leave behind a socket file are very high, breaking all subsequent
starting attempts. Other than for regular files, Windows seemingly has
no way for a process to flag a UNIX domain socket file with `FILE_DELETE_ON_CLOSE`,
which means this scenario can never be avoided entirely (e.g. in the case
of crashes).

For the long comment on `isAbstractUnixSocket`: the logic itself is likely
of dubious value, but I thought it better to explicitly reference the
issue, as I have just spent half an hour searching the web to figure out
whether abstract names will work or not on Windows. At least, the logic
as-is should now do the sensible thing if these are ever implemented
properly (and it matches what the Golang standard library does internally).

* Add a dial attempt to check for active server processes

As @steadytao pointed out (thanks!), the previous code didn't have
solid proof that an existing unix socket file had really been orphaned,
as it's also possible that there's another server process (still running).

This would still give the Windows implementation parity with the unix
one (as that one also unlinks the socket file without further checks),
but I've performed a couple of small tests and found this way of handling
socket files still problematic at least problematic if Caddy is used as
a reverse proxy in real world scenarios.

In tests with a simple Caddyfile that only declares an admin socket,
starting two caddy instances with the same Caddyfile works and behaves
like one would expect: the second instance removes the first instance's
socket file and "wins" the race.

When Caddy is used as a reverse proxy, though, what'll happen is more
complicated: While the second instance wins the race for the admin
socket, as long as the Caddyfile specifies a TCP downstream socket,
the second process will not be able to take this one over from the first
(also to be expected, that's how socket binding usually works).

This results in a rather broken state: The first process still holds on
to its TCP listening sockets, the second process fails to start because
of the error in its listening attempt, leaving an orphaned admin socket
file in the file system. Afterwards, the second process won't be running
and the first _will_ be running but unable to be controlled because its
admin socket has been replaced. This leaves the system in another state
that is bad from an ops perspective.

With this new change, we try first to connect to any unix socket that
isn't already covered by our current process (with a very low timeout)
and can easily decide if the socket is still in use by another process:

- If the connection is accepted, there's obviously a server process.

- If Windows returns WSACONNREFUSED [^1], there is either no active
  server process for the socket file anymore, or the socket file does
  not exist.

- Any other errors are likely a sign that there still is a server process
  (e.g. a timeout would indicate that it's just slow in accepting new
  connection attempts).

[^1]: https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2#wsaeconnrefused

* chore: tidy Windows unix socket reuse helper

---------

Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-04-29 21:52:04 +10:00
Matt Holt 4d6945769d reverseproxy: Add ability to clear dynamic upstreams cache during retries (#7662)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 2m41s
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 1m27s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m27s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m24s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m21s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m25s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m43s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m27s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m27s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m27s
Lint / govulncheck (push) Successful in 1m49s
Lint / dependency-review (push) Failing after 1m3s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 7m12s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
* reverseproxy: Add ability to clear dynamic upstreams cache during retries

This is an optional interface for dynamic upstream modules to implement if they cache results.

TODO: More documentation; this is an experiment.

* Add some godoc

* Export interface; update godoc
2026-04-28 09:16:18 -06:00
Amemoyoi 2d33271482 admin: require path segment boundary in remote access control (#7673)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m21s
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 1m24s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m28s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m25s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m28s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m27s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m21s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m26s
Lint / lint (ubuntu-latest, linux) (push) Successful in 1m59s
Lint / govulncheck (push) Successful in 1m16s
Lint / dependency-review (push) Failing after 24s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 27s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
2026-04-27 08:43:39 -06:00
dependabot[bot] c653e7d61a build(deps): bump github.com/jackc/pgx/v5 from 5.9.0 to 5.9.2 (#7668)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m21s
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m28s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m31s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m24s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m25s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m28s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m25s
Lint / lint (ubuntu-latest, linux) (push) Successful in 1m59s
Lint / govulncheck (push) Successful in 1m12s
Lint / dependency-review (push) Failing after 22s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 28s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
Bumps [github.com/jackc/pgx/v5](https://github.com/jackc/pgx) from 5.9.0 to 5.9.2.
- [Changelog](https://github.com/jackc/pgx/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jackc/pgx/compare/v5.9.0...v5.9.2)

---
updated-dependencies:
- dependency-name: github.com/jackc/pgx/v5
  dependency-version: 5.9.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-26 23:51:26 +10:00
Zen Dodd c1918ff1ad httpcaddyfile: inherit global ACME issuer settings in tls shortcuts (#7617) 2026-04-26 23:39:57 +10:00
Zen Dodd fdbef2a6ef logging: add regression coverage for rotated file mode (#7620) 2026-04-26 23:30:44 +10:00
Kévin Dunglas 2a3ed96f8c metrics: Implement pushing via OLTP (#7664)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m23s
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 1m30s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m25s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m23s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m25s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m23s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m24s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m24s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m26s
Lint / lint (ubuntu-latest, linux) (push) Successful in 1m58s
Lint / govulncheck (push) Successful in 1m13s
Lint / dependency-review (push) Failing after 24s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 29s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
2026-04-25 06:52:08 -04:00
Francis Lavoie 355c178213 chore: Use atomics where appropriate (#7648)
* chore: Use atomics where appropriate

* Use atomic for shutdownAt
2026-04-25 03:47:54 -04:00
Matthew Holt f6ee80be1b go.mod: Upgrade dependencies including CertMagic
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 2m32s
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 1m24s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m28s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m25s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m22s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m23s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m24s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m23s
Lint / govulncheck (push) Successful in 1m43s
Lint / dependency-review (push) Failing after 1m5s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 7m6s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
2026-04-24 11:40:54 -06:00
Matthew Holt 48c08e3890 admin: Limit config size (by @omercnet)
GitHub was giving me errors related to merge status so we are doing this instead
2026-04-24 11:28:40 -06:00
Matthew Holt cf42f61566 Typo fix in security policy 2026-04-24 09:50:06 -06:00
Zen Dodd 41aee97386 core: propagate ECH keys to the QUIC listener (#7670)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m19s
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 1m27s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m25s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m23s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m25s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m22s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m23s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m22s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m24s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m34s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m2s
Lint / govulncheck (push) Successful in 1m20s
Lint / dependency-review (push) Failing after 22s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 24s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
2026-04-23 13:33:41 -06:00
Matt Holt 441d5eb062 caddyhttp: prefer port 443 in auto-HTTPS and add tests (#7666)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m17s
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 1m24s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m27s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m23s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m22s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m21s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m22s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m22s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m27s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m25s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m27s
Lint / lint (ubuntu-latest, linux) (push) Successful in 1m59s
Lint / govulncheck (push) Successful in 1m46s
Lint / dependency-review (push) Failing after 24s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 6m45s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
2026-04-23 17:29:03 +10:00
Daniil Sivak aed1af5976 reverseproxy: add lb_retry_match condition on response status (#7569)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 15m2s
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, aix) (push) Failing after 11m36s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m32s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Failing after 2m3s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Failing after 2m2s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Failing after 11m12s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 6m45s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Failing after 2m3s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m57s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Failing after 56s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Failing after 51s
Lint / lint (ubuntu-latest, linux) (push) Failing after 1m24s
Lint / govulncheck (push) Failing after 1m51s
Lint / dependency-review (push) Failing after 3m55s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 20s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
2026-04-21 14:59:31 -04:00
Zen Dodd 4430756d5c admin: Redact sensitive request headers in API logs (#7578)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m51s
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 1m41s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m37s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m30s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m44s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m36s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m36s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m31s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m42s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m47s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m56s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m49s
Lint / govulncheck (push) Successful in 2m5s
Lint / dependency-review (push) Failing after 1m0s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 42s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
* admin: Redact sensitive request headers in API logs

* Fix govulncheck and typed atomic lint failures

* Sync Go module metadata after dependency downgrade
2026-04-17 14:56:42 -06:00
dependabot[bot] af89c5ab02 build(deps): bump github.com/jackc/pgx/v5 from 5.8.0 to 5.9.0 (#7655)
Bumps [github.com/jackc/pgx/v5](https://github.com/jackc/pgx) from 5.8.0 to 5.9.0.
- [Changelog](https://github.com/jackc/pgx/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jackc/pgx/compare/v5.8.0...v5.9.0)

---
updated-dependencies:
- dependency-name: github.com/jackc/pgx/v5
  dependency-version: 5.9.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-17 14:50:53 -06:00
Mohammed Al Sahaf bd9f145321 chore: add AGENTS.md (#7652)
* chore: add `AGENTS.md`

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

* Apply suggestions from code review

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

* review feedback

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>

---------

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2026-04-17 14:49:58 -06:00
Steffen Busch 24bebd0a07 caddyhttp: Document missing placeholders for escaped URI and prefixed query (#7659) 2026-04-17 16:13:15 -04:00
113 changed files with 4993 additions and 666 deletions
+4
View File
@@ -7,6 +7,7 @@ For starters, we invite you to join [the Caddy forum](https://caddy.community) w
## Common Tasks ## Common Tasks
- [Commenting](#commenting)
- [Contributing code](#contributing-code) - [Contributing code](#contributing-code)
- [Writing a Caddy module](#writing-a-caddy-module) - [Writing a Caddy module](#writing-a-caddy-module)
- [Asking or answering questions for help using Caddy](#getting-help-using-caddy) - [Asking or answering questions for help using Caddy](#getting-help-using-caddy)
@@ -20,6 +21,9 @@ Other menu items:
- [Coordinated Disclosure](#coordinated-disclosure) - [Coordinated Disclosure](#coordinated-disclosure)
- [Thank You](#thank-you) - [Thank You](#thank-you)
### All contributions
All accounts posting, contributing code, or commenting in our repositories MUST disclose the use of assistance such as LLMs ("AI") as a courtesy and an integrity signal or risk being banned.
### Contributing code ### Contributing code
+3 -1
View File
@@ -8,7 +8,7 @@ The Caddy project would like to make sure that it stays on top of all relevant a
| Version | Supported | | Version | Supported |
| ----------- | ----------| | ----------- | ----------|
| 2.latest | ✔️ | | 2.latest | ✔️ |
| <= 2.latest | :x: | | < 2.latest | :x: |
## Acceptable Scope ## Acceptable Scope
@@ -25,6 +25,8 @@ Client-side exploits are out of scope. In other words, it is not a bug in Caddy
Security bugs in code dependencies (including Go's standard library) are out of scope. Instead, if a dependency has patched a relevant security bug, please feel free to open a public issue or pull request to update that dependency in our code. Security bugs in code dependencies (including Go's standard library) are out of scope. Instead, if a dependency has patched a relevant security bug, please feel free to open a public issue or pull request to update that dependency in our code.
Many reports are not security bugs and can be addressed by updating the documentation.
We accept security reports and patches, but do not assign CVEs, for code that has not been released with a non-prerelease tag. We accept security reports and patches, but do not assign CVEs, for code that has not been released with a non-prerelease tag.
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
models: read models: read
contents: read contents: read
steps: steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10
- uses: github/ai-moderator@81159c370785e295c97461ade67d7c33576e9319 - uses: github/ai-moderator@81159c370785e295c97461ade67d7c33576e9319
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
+2 -2
View File
@@ -20,7 +20,7 @@ jobs:
steps: steps:
- name: Check approvals and update PR - name: Check approvals and update PR
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env: env:
MAINTAINER_LOGINS: ${{ secrets.MAINTAINER_LOGINS }} MAINTAINER_LOGINS: ${{ secrets.MAINTAINER_LOGINS }}
with: with:
@@ -165,7 +165,7 @@ jobs:
steps: steps:
- name: Add cancelled label and comment - name: Add cancelled label and comment
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with: with:
script: | script: |
const pr = context.payload.pull_request; const pr = context.payload.pull_request;
+11 -11
View File
@@ -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@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with: with:
egress-policy: audit egress-policy: audit
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Install Go - name: Install Go
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- 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@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with: with:
egress-policy: audit egress-policy: audit
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0 - uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
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@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.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@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
with: with:
version: latest version: latest
args: build --single-target --snapshot args: build --single-target --snapshot
+3 -3
View File
@@ -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@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with: with:
egress-policy: audit egress-policy: audit
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Install Go - name: Install Go
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with: with:
go-version: ${{ matrix.GO_SEMVER }} go-version: ${{ matrix.GO_SEMVER }}
check-latest: true check-latest: true
+8 -8
View File
@@ -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@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with: with:
egress-policy: audit egress-policy: audit
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.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@82606bf257cbaff209d206a39f5134f0cfbfd2ee # v9.2.1
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@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
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@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with: with:
egress-policy: audit egress-policy: audit
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3 uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.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
+4 -4
View File
@@ -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@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with: with:
egress-policy: audit egress-policy: audit
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -73,7 +73,7 @@ jobs:
- name: Check for existing proposal PR - name: Check for existing proposal PR
id: check_existing id: check_existing
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with: with:
script: | script: |
const version = '${{ steps.inputs.outputs.version }}'; const version = '${{ steps.inputs.outputs.version }}';
@@ -177,7 +177,7 @@ jobs:
- name: Create release proposal PR - name: Create release proposal PR
id: create_pr id: create_pr
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with: with:
script: | script: |
const changelog = `${{ steps.setup.outputs.changelog }}`; const changelog = `${{ steps.setup.outputs.changelog }}`;
+11 -11
View File
@@ -28,7 +28,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: with:
fetch-depth: 0 fetch-depth: 0
# Force fetch upstream tags -- because 65 minutes # Force fetch upstream tags -- because 65 minutes
@@ -144,7 +144,7 @@ jobs:
- name: Find related release proposal - name: Find related release proposal
id: find_proposal id: find_proposal
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with: with:
script: | script: |
const version = '${{ steps.vars.outputs.version_tag }}'; const version = '${{ steps.vars.outputs.version_tag }}';
@@ -262,7 +262,7 @@ jobs:
- name: Update release proposal PR - name: Update release proposal PR
if: fromJson(steps.find_proposal.outputs.result).number != null if: fromJson(steps.find_proposal.outputs.result).number != null
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with: with:
script: | script: |
const result = ${{ steps.find_proposal.outputs.result }}; const result = ${{ steps.find_proposal.outputs.result }};
@@ -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@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with: with:
egress-policy: audit egress-policy: audit
- name: Checkout code - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
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@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.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@df4cb1c069e1874edd31b4311f1884172cec0e10 # 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
@@ -415,11 +415,11 @@ jobs:
run: pip install --upgrade cloudsmith-cli run: pip install --upgrade cloudsmith-cli
- name: Install Cosign - name: Install Cosign
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # main uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # main
- 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@e22c389904149dbc22b58101806040fa8d37a610 # 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@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
with: with:
version: latest version: latest
args: release --clean --timeout 60m args: release --clean --timeout 60m
@@ -497,7 +497,7 @@ jobs:
- name: Update release proposal PR - name: Update release proposal PR
if: needs.verify-tag.outputs.proposal_issue_number != '' if: needs.verify-tag.outputs.proposal_issue_number != ''
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with: with:
script: | script: |
const prNumber = parseInt('${{ needs.verify-tag.outputs.proposal_issue_number }}'); const prNumber = parseInt('${{ needs.verify-tag.outputs.proposal_issue_number }}');
+1 -1
View File
@@ -24,7 +24,7 @@ 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@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with: with:
egress-policy: audit egress-policy: audit
+4 -4
View File
@@ -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@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with: with:
egress-policy: audit egress-policy: audit
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v3.29.5
with: with:
sarif_file: results.sarif sarif_file: results.sarif
+221
View File
@@ -0,0 +1,221 @@
# Caddy Project Guidelines
## Mission
**Every site on HTTPS.** Caddy is a security-first, modular, extensible server platform.
## Code Style
### Go Idioms
Follow [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments):
- **Error flow**: Early return, indent error handling—not else blocks
```go
if err != nil {
return err
}
// normal code
```
- **Naming**: initialisms (`URL`, `HTTP`, `ID`—not `Url`, `Http`, `Id`)
- **Receiver names**: 12 letters reflecting type (`c` for `Client`, `h` for `Handler`)
- **Error strings**: Lowercase, no trailing punctuation (`"something failed"` not `"Something failed."`)
- **Doc comments**: Full sentences starting with the name being documented
```go
// Handler serves HTTP requests for the file server.
type Handler struct { ... }
```
- **Empty slices**: `var t []string` (nil slice), not `t := []string{}` (non-nil zero-length)
- **Don't panic**: Use error returns for normal error handling
### Caddy Patterns
**Module registration**:
```go
func init() {
caddy.RegisterModule(MyModule{})
}
func (MyModule) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "namespace.category.name",
New: func() caddy.Module { return new(MyModule) },
}
}
```
**Module lifecycle**: `New()` → JSON unmarshal → `Provision()` → `Validate()` → use → `Cleanup()`
**Interface guards** — compile-time verification that modules implement required interfaces:
```go
var (
_ caddy.Provisioner = (*MyModule)(nil)
_ caddy.Validator = (*MyModule)(nil)
_ caddyfile.Unmarshaler = (*MyModule)(nil)
)
```
**Structured logging** — use the module-scoped logger from context:
```go
func (m *MyModule) Provision(ctx caddy.Context) error {
m.logger = ctx.Logger()
m.logger.Debug("provisioning", zap.String("field", m.Field))
return nil
}
```
**Caddyfile support** — implement `UnmarshalCaddyfile(*caddyfile.Dispenser)` using the `Dispenser` API:
```go
// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax:
//
// directive [arg1] [arg2] {
// subdir value
// }
func (m *MyModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume directive name
for d.NextArg() {
// handle inline arguments
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "subdir":
if !d.NextArg() {
return d.ArgErr()
}
m.Field = d.Val()
default:
return d.Errf("unrecognized subdirective: %s", d.Val())
}
}
return nil
}
```
**Admin API**: Implement `caddy.AdminRouter` for custom endpoints.
**Context**: Use `caddy.Context` for accessing other apps/modules and logging—don't store contexts in structs.
## Architecture
Caddy is built around a **module system** where everything is a module registered via `caddy.RegisterModule()`:
- **Apps** (`caddy.App`): Top-level modules like `http`, `tls`, `pki` that Caddy loads and runs
- **Modules** (`caddy.Module`): Extensible components with namespaced IDs (e.g., `http.handlers.file_server`)
- **Configuration**: Native JSON with adapters (Caddyfile → JSON via `caddyconfig/httpcaddyfile`)
| Directory | Purpose |
|-----------|---------|
| `modules/` | All standard modules (HTTP, TLS, PKI, etc.) |
| `modules/standard/imports.go` | Standard module registry |
| `caddyconfig/httpcaddyfile/` | Caddyfile → JSON adapter for HTTP |
| `caddytest/` | Test utilities and integration tests |
| `cmd/caddy/` | CLI entry point with module imports |
### Critical Packages
`caddyhttp` and `caddytls` require **extra scrutiny** in code review—these are security-critical.
Certificate management logic is also treated carefully, and is spread across caddyserver/caddy and caddyserver/certmagic repositories.
## Quality Gates
**All required before PR is merge-ready:**
| Gate | Command | Notes |
|------|---------|-------|
| Tests pass | `go test -race -short ./...` | Race detection enabled |
| Lint clean | `golangci-lint run --timeout 10m` | No warnings in changed files |
| Builds | `go build ./...` | Must compile |
| Benchmarks | `go test -bench=. -benchmem` | Required for optimizations |
CI runs tests on **Linux, macOS, and Windows**—ensure cross-platform compatibility.
### Build & Test
```bash
# Build
cd cmd/caddy && go build
# Tests with race detection (matches CI)
go test -race -short ./...
# Integration tests
go test ./caddytest/integration/...
# Lint (matches CI)
golangci-lint run --timeout 10m
```
## Testing Conventions
**Table-driven tests** (preferred pattern):
```go
func TestFeature(t *testing.T) {
for i, tc := range []struct {
input string
expected string
wantErr bool
}{
{input: "valid", expected: "result", wantErr: false},
{input: "invalid", expected: "", wantErr: true},
} {
actual, err := Function(tc.input)
if tc.wantErr && err == nil {
t.Errorf("Test %d: expected error but got none", i)
}
if !tc.wantErr && err != nil {
t.Errorf("Test %d: unexpected error: %v", i, err)
}
if actual != tc.expected {
t.Errorf("Test %d: expected %q, got %q", i, tc.expected, actual)
}
}
}
```
**Integration tests** use `caddytest.Tester`:
```go
func TestHTTPFeature(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
admin localhost:2999
http_port 9080
}
localhost:9080 {
respond "hello"
}`, "caddyfile")
tester.AssertGetResponse("http://localhost:9080/", 200, "hello")
}
```
Use non-standard ports (9080, 9443, 2999) to avoid conflicts with running servers.
## AI Contribution Policy
Per [CONTRIBUTING.md](.github/CONTRIBUTING.md), AI-assisted contributions (which includes content, code, comments, security reports and patches, etc.) **MUST** be:
1. **Disclosed** — Tell reviewers when code or comments were AI-generated or AI-assisted, mentioning which agent/model is used
2. **Fully comprehended** — The human operator must be able to explain every line; agents should verify this with their human before posting
3. **Tested** — Automated tests when feasible, thorough manual tests otherwise
4. **Licensed** — Verify AI output doesn't include plagiarized or incompatibly-licensed code
In addition, the **Contributor License Agreement (CLA)** must be signed by the human user, NOT a bot or bot on behalf of the user.
**Do NOT submit code you and the human user cannot fully explain.** Human operators are ultimately responsible for their submissions.
## Other Guidelines
- **Avoid new dependencies** — Justify any additions; tiny deps can be inlined
- **No exported dependency types** — Caddy must not export types defined by external packages
- Use Go modules; check with `go mod tidy`
- Do not implement features or patches that solve specific cases only; design proper, generalized solutions
## Further Reading
- [CONTRIBUTING.md](.github/CONTRIBUTING.md) — Full PR process and expectations
- [Extending Caddy](https://caddyserver.com/docs/extending-caddy) — Module development guide
- [JSON Config](https://caddyserver.com/docs/json/) — Native configuration reference
- [Caddyfile](https://caddyserver.com/docs/caddyfile/concepts) — Caddyfile syntax guide
+52 -44
View File
@@ -45,6 +45,8 @@ import (
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
"github.com/caddyserver/caddy/v2/internal"
) )
// testCertMagicStorageOverride is a package-level test hook. Tests may set // testCertMagicStorageOverride is a package-level test hook. Tests may set
@@ -118,10 +120,6 @@ type AdminConfig struct {
// //
// EXPERIMENTAL: This feature is subject to change. // EXPERIMENTAL: This feature is subject to change.
Remote *RemoteAdmin `json:"remote,omitempty"` Remote *RemoteAdmin `json:"remote,omitempty"`
// Holds onto the routers so that we can later provision them
// if they require provisioning.
routers []AdminRouter
} }
// ConfigSettings configures the management of configuration. // ConfigSettings configures the management of configuration.
@@ -210,8 +208,8 @@ type AdminAccess struct {
// AdminPermissions specifies what kinds of requests are allowed // AdminPermissions specifies what kinds of requests are allowed
// to be made to the admin endpoint. // to be made to the admin endpoint.
type AdminPermissions struct { type AdminPermissions struct {
// The API paths allowed. Paths are simple prefix matches. // The API paths allowed. A request path must either equal an
// Any subpath of the specified paths will be allowed. // allowed path or be a subpath with a path-segment boundary.
Paths []string `json:"paths,omitempty"` Paths []string `json:"paths,omitempty"`
// The HTTP methods allowed for the given paths. // The HTTP methods allowed for the given paths.
@@ -220,7 +218,7 @@ type AdminPermissions struct {
// newAdminHandler reads admin's config and returns an http.Handler suitable // newAdminHandler reads admin's config and returns an http.Handler suitable
// for use in an admin endpoint server, which will be listening on listenAddr. // for use in an admin endpoint server, which will be listening on listenAddr.
func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Context) adminHandler { func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, ctx Context) (adminHandler, error) {
muxWrap := adminHandler{mux: http.NewServeMux()} muxWrap := adminHandler{mux: http.NewServeMux()}
// secure the local or remote endpoint respectively // secure the local or remote endpoint respectively
@@ -277,34 +275,21 @@ func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Co
// register third-party module endpoints // register third-party module endpoints
for _, m := range GetModules("admin.api") { for _, m := range GetModules("admin.api") {
router := m.New().(AdminRouter) router := m.New().(AdminRouter)
// provision the router before registering its routes, so
// handlers have access to all provisioned state
if provisioner, ok := router.(Provisioner); ok {
if err := provisioner.Provision(ctx); err != nil {
return adminHandler{}, fmt.Errorf("provisioning admin router module %s: %v", m.ID, err)
}
}
for _, route := range router.Routes() { for _, route := range router.Routes() {
addRoute(route.Pattern, handlerLabel, route.Handler) addRoute(route.Pattern, handlerLabel, route.Handler)
} }
admin.routers = append(admin.routers, router)
} }
return muxWrap return muxWrap, nil
}
// provisionAdminRouters provisions all the router modules
// in the admin.api namespace that need provisioning.
func (admin *AdminConfig) provisionAdminRouters(ctx Context) error {
for _, router := range admin.routers {
provisioner, ok := router.(Provisioner)
if !ok {
continue
}
err := provisioner.Provision(ctx)
if err != nil {
return err
}
}
// We no longer need the routers once provisioned, allow for GC
admin.routers = nil
return nil
} }
// allowedOrigins returns a list of origins that are allowed. // allowedOrigins returns a list of origins that are allowed.
@@ -428,11 +413,7 @@ func replaceLocalAdminServer(cfg *Config, ctx Context) error {
return err return err
} }
handler := cfg.Admin.newAdminHandler(addr, false, ctx) handler, err := cfg.Admin.newAdminHandler(addr, false, ctx)
// run the provisioners for loaded modules to make sure local
// state is properly re-initialized in the new admin server
err = cfg.Admin.provisionAdminRouters(ctx)
if err != nil { if err != nil {
return err return err
} }
@@ -556,11 +537,7 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
// make the HTTP handler but disable Host/Origin enforcement // make the HTTP handler but disable Host/Origin enforcement
// because we are using TLS authentication instead // because we are using TLS authentication instead
handler := cfg.Admin.newAdminHandler(addr, true, ctx) handler, err := cfg.Admin.newAdminHandler(addr, true, ctx)
// run the provisioners for loaded modules to make sure local
// state is properly re-initialized in the new admin server
err = cfg.Admin.provisionAdminRouters(ctx)
if err != nil { if err != nil {
return err return err
} }
@@ -716,7 +693,7 @@ func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error {
// verify path // verify path
pathFound := accessPerm.Paths == nil pathFound := accessPerm.Paths == nil
for _, allowedPath := range accessPerm.Paths { for _, allowedPath := range accessPerm.Paths {
if strings.HasPrefix(r.URL.Path, allowedPath) { if adminPathAllowed(r.URL.Path, allowedPath) {
pathFound = true pathFound = true
break break
} }
@@ -745,6 +722,19 @@ func (remote RemoteAdmin) enforceAccessControls(r *http.Request) error {
} }
} }
func adminPathAllowed(reqPath, allowedPath string) bool {
if allowedPath == "" || allowedPath == "/" {
return strings.HasPrefix(reqPath, allowedPath)
}
if reqPath == allowedPath {
return true
}
if strings.HasSuffix(allowedPath, "/") {
return strings.HasPrefix(reqPath, allowedPath)
}
return strings.HasPrefix(reqPath, allowedPath+"/")
}
func stopAdminServer(srv *http.Server) error { func stopAdminServer(srv *http.Server) error {
if srv == nil { if srv == nil {
return fmt.Errorf("no admin server") return fmt.Errorf("no admin server")
@@ -800,7 +790,7 @@ func (h adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
zap.String("uri", r.RequestURI), zap.String("uri", r.RequestURI),
zap.String("remote_ip", ip), zap.String("remote_ip", ip),
zap.String("remote_port", port), zap.String("remote_port", port),
zap.Reflect("headers", r.Header), zap.Object("headers", internal.LoggableHTTPHeader{Header: r.Header}),
) )
if r.TLS != nil { if r.TLS != nil {
log = log.With( log = log.With(
@@ -1061,6 +1051,9 @@ func handleConfig(w http.ResponseWriter, r *http.Request) error {
buf.Reset() buf.Reset()
defer bufPool.Put(buf) defer bufPool.Put(buf)
const maxConfigSize = 100 * 1024 * 1024 // 100 MB
r.Body = http.MaxBytesReader(w, r.Body, maxConfigSize)
_, err := io.Copy(buf, r.Body) _, err := io.Copy(buf, r.Body)
if err != nil { if err != nil {
return APIError{ return APIError{
@@ -1143,6 +1136,20 @@ func handleStop(w http.ResponseWriter, r *http.Request) error {
return nil return nil
} }
func parseCanonicalArrayIndex(idx string) (int, error) {
if idx == "" {
return 0, fmt.Errorf("empty index")
}
i, err := strconv.Atoi(idx)
if err != nil {
return 0, err
}
if strconv.Itoa(i) != idx {
return 0, fmt.Errorf("non-canonical array index")
}
return i, nil
}
// unsyncedConfigAccess traverses into the current config and performs // unsyncedConfigAccess traverses into the current config and performs
// the operation at path according to method, using body and out as // the operation at path according to method, using body and out as
// needed. This is a low-level, unsynchronized function; most callers // needed. This is a low-level, unsynchronized function; most callers
@@ -1204,11 +1211,12 @@ traverseLoop:
var idx int var idx int
if method != http.MethodPost { if method != http.MethodPost {
idxStr := parts[len(parts)-1] idxStr := parts[len(parts)-1]
idx, err = strconv.Atoi(idxStr) idx, err = parseCanonicalArrayIndex(idxStr)
if err != nil { if err != nil {
return fmt.Errorf("[%s] invalid array index '%s': %v", return fmt.Errorf("[%s] invalid array index '%s': %v",
path, idxStr, err) path, idxStr, err)
} }
if idx < 0 || (method != http.MethodPut && idx >= len(arr)) || idx > len(arr) { if idx < 0 || (method != http.MethodPut && idx >= len(arr)) || idx > len(arr) {
return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr) return fmt.Errorf("[%s] array index out of bounds: %s", path, idxStr)
} }
@@ -1308,7 +1316,7 @@ traverseLoop:
} }
case []any: case []any:
partInt, err := strconv.Atoi(part) partInt, err := parseCanonicalArrayIndex(part)
if err != nil { if err != nil {
return fmt.Errorf("[/%s] invalid array index '%s': %v", return fmt.Errorf("[/%s] invalid array index '%s': %v",
strings.Join(parts[:i+1], "/"), part, err) strings.Join(parts[:i+1], "/"), part, err)
+204 -15
View File
@@ -15,9 +15,13 @@
package caddy package caddy
import ( import (
"bytes"
"context" "context"
"crypto"
"crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"maps" "maps"
"net/http" "net/http"
@@ -31,6 +35,8 @@ import (
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go" dto "github.com/prometheus/client_model/go"
"go.uber.org/zap"
"go.uber.org/zap/zaptest/observer"
) )
var testCfg = []byte(`{ var testCfg = []byte(`{
@@ -51,6 +57,13 @@ var testCfg = []byte(`{
} }
`) `)
type testAdminPublicKey string
func (k testAdminPublicKey) Equal(x crypto.PublicKey) bool {
other, ok := x.(testAdminPublicKey)
return ok && k == other
}
func TestUnsyncedConfigAccess(t *testing.T) { func TestUnsyncedConfigAccess(t *testing.T) {
// each test is performed in sequence, so // each test is performed in sequence, so
// each change builds on the previous ones; // each change builds on the previous ones;
@@ -242,6 +255,51 @@ func TestAdminHandlerErrorHandling(t *testing.T) {
} }
} }
func TestAdminHandlerServeHTTPRedactsSensitiveHeadersInLogs(t *testing.T) {
core, logs := observer.New(zap.InfoLevel)
defaultLoggerMu.Lock()
origLogger := defaultLogger.logger
defaultLogger.logger = zap.New(core)
defaultLoggerMu.Unlock()
t.Cleanup(func() {
defaultLoggerMu.Lock()
defaultLogger.logger = origLogger
defaultLoggerMu.Unlock()
})
handler := adminHandler{
mux: http.NewServeMux(),
}
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "Bearer secret")
req.Header.Set("Cookie", "session=secret")
req.Header.Set("X-Test", "ok")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if logs.Len() == 0 {
t.Fatal("expected request log entry")
}
ctx := logs.All()[0].ContextMap()
headers, ok := ctx["headers"].(map[string]any)
if !ok {
t.Fatalf("expected headers field in log context, got %T", ctx["headers"])
}
if got := headers["Authorization"]; !reflect.DeepEqual(got, []any{"REDACTED"}) {
t.Fatalf("expected redacted Authorization header, got %#v", got)
}
if got := headers["Cookie"]; !reflect.DeepEqual(got, []any{"REDACTED"}) {
t.Fatalf("expected redacted Cookie header, got %#v", got)
}
if got := headers["X-Test"]; !reflect.DeepEqual(got, []any{"ok"}) {
t.Fatalf("expected X-Test header to remain visible, got %#v", got)
}
}
func initAdminMetrics() { func initAdminMetrics() {
if adminMetrics.requestErrors != nil { if adminMetrics.requestErrors != nil {
prometheus.Unregister(adminMetrics.requestErrors) prometheus.Unregister(adminMetrics.requestErrors)
@@ -282,7 +340,10 @@ func TestAdminHandlerBuiltinRouteErrors(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Failed to parse address: %v", err) t.Fatalf("Failed to parse address: %v", err)
} }
handler := cfg.Admin.newAdminHandler(addr, false, Context{}) handler, err := cfg.Admin.newAdminHandler(addr, false, Context{})
if err != nil {
t.Fatalf("Failed to create admin handler: %v", err)
}
tests := []struct { tests := []struct {
name string name string
@@ -403,7 +464,10 @@ func TestNewAdminHandlerRouterRegistration(t *testing.T) {
admin := &AdminConfig{ admin := &AdminConfig{
EnforceOrigin: false, EnforceOrigin: false,
} }
handler := admin.newAdminHandler(addr, false, Context{}) handler, err := admin.newAdminHandler(addr, false, Context{})
if err != nil {
t.Fatalf("Failed to create admin handler: %v", err)
}
req := httptest.NewRequest("GET", "/mock", nil) req := httptest.NewRequest("GET", "/mock", nil)
req.Host = "localhost:2019" req.Host = "localhost:2019"
@@ -415,10 +479,6 @@ func TestNewAdminHandlerRouterRegistration(t *testing.T) {
t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code) t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code)
t.Logf("Response body: %s", rr.Body.String()) t.Logf("Response body: %s", rr.Body.String())
} }
if len(admin.routers) != 1 {
t.Errorf("Expected 1 router to be stored, got %d", len(admin.routers))
}
} }
type mockProvisionableRouter struct { type mockProvisionableRouter struct {
@@ -456,19 +516,16 @@ func TestAdminRouterProvisioning(t *testing.T) {
name string name string
provisionErr error provisionErr error
wantErr bool wantErr bool
routersAfter int // expected number of routers after provisioning
}{ }{
{ {
name: "successful provisioning", name: "successful provisioning",
provisionErr: nil, provisionErr: nil,
wantErr: false, wantErr: false,
routersAfter: 0,
}, },
{ {
name: "provisioning error", name: "provisioning error",
provisionErr: fmt.Errorf("provision failed"), provisionErr: fmt.Errorf("provision failed"),
wantErr: true, wantErr: true,
routersAfter: 1,
}, },
} }
@@ -504,8 +561,7 @@ func TestAdminRouterProvisioning(t *testing.T) {
t.Fatalf("Failed to parse address: %v", err) t.Fatalf("Failed to parse address: %v", err)
} }
_ = admin.newAdminHandler(addr, false, Context{}) _, err = admin.newAdminHandler(addr, false, Context{})
err = admin.provisionAdminRouters(Context{})
if test.wantErr { if test.wantErr {
if err == nil { if err == nil {
@@ -516,10 +572,6 @@ func TestAdminRouterProvisioning(t *testing.T) {
t.Errorf("Expected no error but got: %v", err) t.Errorf("Expected no error but got: %v", err)
} }
} }
if len(admin.routers) != test.routersAfter {
t.Errorf("Expected %d routers after provisioning, got %d", test.routersAfter, len(admin.routers))
}
}) })
} }
} }
@@ -604,6 +656,99 @@ func TestAllowedOriginsUnixSocket(t *testing.T) {
} }
} }
func TestRemoteAdminAccessControlPathSegmentMatching(t *testing.T) {
const authorizedKey testAdminPublicKey = "authorized"
peerCert := &x509.Certificate{PublicKey: authorizedKey}
tests := []struct {
name string
allowedPath string
requestPath string
wantErr bool
}{
{
name: "exact path",
allowedPath: "/pki/ca/prod",
requestPath: "/pki/ca/prod",
wantErr: false,
},
{
name: "subpath",
allowedPath: "/pki/ca/prod",
requestPath: "/pki/ca/prod/certificates",
wantErr: false,
},
{
name: "trailing slash subpath",
allowedPath: "/pki/ca/prod/",
requestPath: "/pki/ca/prod/certificates",
wantErr: false,
},
{
name: "sibling with shared prefix",
allowedPath: "/pki/ca/prod",
requestPath: "/pki/ca/prod-backup",
wantErr: true,
},
{
name: "same segment plus digit",
allowedPath: "/pki/ca/prod",
requestPath: "/pki/ca/prod1",
wantErr: true,
},
{
name: "root path",
allowedPath: "/",
requestPath: "/pki/ca/prod",
wantErr: false,
},
}
for i, test := range tests {
t.Run(test.name, func(t *testing.T) {
remote := RemoteAdmin{
AccessControl: []*AdminAccess{
{
Permissions: []AdminPermissions{
{
Methods: []string{http.MethodGet},
Paths: []string{test.allowedPath},
},
},
publicKeys: []crypto.PublicKey{authorizedKey},
},
},
}
req := httptest.NewRequest(http.MethodGet, "https://localhost:2021"+test.requestPath, nil)
req.TLS = &tls.ConnectionState{
VerifiedChains: [][]*x509.Certificate{{peerCert}},
}
err := remote.enforceAccessControls(req)
if test.wantErr {
if err == nil {
t.Errorf("test %d (%s): allowed path %q, request path %q: expected forbidden error, got nil", i, test.name, test.allowedPath, test.requestPath)
return
}
var apiErr APIError
if !errors.As(err, &apiErr) {
t.Errorf("test %d (%s): allowed path %q, request path %q: expected APIError with HTTP status %d, got %T: %v", i, test.name, test.allowedPath, test.requestPath, http.StatusForbidden, err, err)
return
}
if apiErr.HTTPStatus != http.StatusForbidden {
t.Errorf("test %d (%s): allowed path %q, request path %q: expected HTTP status %d, got %d", i, test.name, test.allowedPath, test.requestPath, http.StatusForbidden, apiErr.HTTPStatus)
}
return
}
if err != nil {
t.Errorf("test %d (%s): allowed path %q, request path %q: expected no error, got %v", i, test.name, test.allowedPath, test.requestPath, err)
}
})
}
}
func TestReplaceRemoteAdminServer(t *testing.T) { func TestReplaceRemoteAdminServer(t *testing.T) {
const testCert = `MIIDCTCCAfGgAwIBAgIUXsqJ1mY8pKlHQtI3HJ23x2eZPqwwDQYJKoZIhvcNAQEL const testCert = `MIIDCTCCAfGgAwIBAgIUXsqJ1mY8pKlHQtI3HJ23x2eZPqwwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwMTAwMDAwMFoXDTI0MDEw BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwMTAwMDAwMFoXDTI0MDEw
@@ -956,3 +1101,47 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP
}) })
} }
} }
func TestUnsyncedConfigAccessCanonicalArrayIndices(t *testing.T) {
rawCfg = map[string]any{
rawConfigKey: map[string]any{
"list": []any{"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"},
},
}
tests := []struct {
name string
path string
wantOutput string
wantErr bool
}{
{name: "allow zero", path: "/" + rawConfigKey + "/list/0", wantOutput: "\"zero\"\n"},
{name: "allow one", path: "/" + rawConfigKey + "/list/1", wantOutput: "\"one\"\n"},
{name: "allow ten", path: "/" + rawConfigKey + "/list/10", wantOutput: "\"ten\"\n"},
{name: "reject leading zero", path: "/" + rawConfigKey + "/list/01", wantErr: true},
{name: "reject multiple leading zeros", path: "/" + rawConfigKey + "/list/002", wantErr: true},
{name: "reject plus sign", path: "/" + rawConfigKey + "/list/+1", wantErr: true},
{name: "reject negative zero", path: "/" + rawConfigKey + "/list/-0", wantErr: true},
}
for i, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var gotOutput bytes.Buffer
err := unsyncedConfigAccess(http.MethodGet, tc.path, nil, &gotOutput)
if tc.wantErr {
if err == nil {
t.Errorf("test %d (%s): input path %q: expected error, got nil with output %q", i, tc.name, tc.path, gotOutput.String())
}
return
}
if err != nil {
t.Errorf("test %d (%s): input path %q: expected no error with output %q, got error %v with output %q", i, tc.name, tc.path, tc.wantOutput, err, gotOutput.String())
}
if gotOutput.String() != tc.wantOutput {
t.Errorf("test %d (%s): input path %q: expected output %q, got %q", i, tc.name, tc.path, tc.wantOutput, gotOutput.String())
}
})
}
}
+3 -10
View File
@@ -440,13 +440,6 @@ func run(newCfg *Config, start bool) (Context, error) {
} }
}() }()
// Provision any admin routers which may need to access
// some of the other apps at runtime
err = ctx.cfg.Admin.provisionAdminRouters(ctx)
if err != nil {
return ctx, err
}
// Start // Start
err = func() error { err = func() error {
started := make([]string, 0, len(ctx.cfg.apps)) started := make([]string, 0, len(ctx.cfg.apps))
@@ -766,7 +759,7 @@ func Validate(cfg *Config) error {
// code is emitted. // code is emitted.
func exitProcess(ctx context.Context, logger *zap.Logger) { func exitProcess(ctx context.Context, logger *zap.Logger) {
// let the rest of the program know we're quitting; only do it once // let the rest of the program know we're quitting; only do it once
if !atomic.CompareAndSwapInt32(exiting, 0, 1) { if !exiting.CompareAndSwap(false, true) {
return return
} }
@@ -845,11 +838,11 @@ func exitProcess(ctx context.Context, logger *zap.Logger) {
}() }()
} }
var exiting = new(int32) // accessed atomically var exiting atomic.Bool
// Exiting returns true if the process is exiting. // Exiting returns true if the process is exiting.
// EXPERIMENTAL API: subject to change or removal. // EXPERIMENTAL API: subject to change or removal.
func Exiting() bool { return atomic.LoadInt32(exiting) == 1 } func Exiting() bool { return exiting.Load() }
// OnExit registers a callback to invoke during process exit. // OnExit registers a callback to invoke during process exit.
// This registration is PROCESS-GLOBAL, meaning that each // This registration is PROCESS-GLOBAL, meaning that each
+2 -2
View File
@@ -155,7 +155,7 @@ func (l *lexer) next() (bool, error) {
// want to keep. // want to keep.
if ch == '\n' { if ch == '\n' {
if len(val) == 2 { if len(val) == 2 {
return false, fmt.Errorf("missing opening heredoc marker on line #%d; must contain only alpha-numeric characters, dashes and underscores; got empty string", l.line) return false, fmt.Errorf("missing opening heredoc marker on line #%d; must contain only alphanumeric characters, dashes and underscores; got empty string", l.line)
} }
// check if there's too many < // check if there's too many <
@@ -165,7 +165,7 @@ func (l *lexer) next() (bool, error) {
heredocMarker = string(val[2:]) heredocMarker = string(val[2:])
if !heredocMarkerRegexp.Match([]byte(heredocMarker)) { if !heredocMarkerRegexp.Match([]byte(heredocMarker)) {
return false, fmt.Errorf("heredoc marker on line #%d must contain only alpha-numeric characters, dashes and underscores; got '%s'", l.line, heredocMarker) return false, fmt.Errorf("heredoc marker on line #%d must contain only alphanumeric characters, dashes and underscores; got '%s'", l.line, heredocMarker)
} }
inHeredoc = true inHeredoc = true
+1 -1
View File
@@ -424,7 +424,7 @@ EOF
{ {
input: []byte("not-a-heredoc <<\n"), input: []byte("not-a-heredoc <<\n"),
expectErr: true, expectErr: true,
errorMessage: "missing opening heredoc marker on line #1; must contain only alpha-numeric characters, dashes and underscores; got empty string", errorMessage: "missing opening heredoc marker on line #1; must contain only alphanumeric characters, dashes and underscores; got empty string",
}, },
{ {
input: []byte(`heredoc <<<EOF input: []byte(`heredoc <<<EOF
+1 -1
View File
@@ -683,7 +683,7 @@ func (p *parser) directive() error {
// openCurlyBrace expects the current token to be an // openCurlyBrace expects the current token to be an
// opening curly brace. This acts like an assertion // opening curly brace. This acts like an assertion
// because it returns an error if the token is not // because it returns an error if the token is not
// a opening curly brace. It does NOT advance the token. // an opening curly brace. It does NOT advance the token.
func (p *parser) openCurlyBrace() error { func (p *parser) openCurlyBrace() error {
if p.Val() != "{" { if p.Val() != "{" {
if p.valLooksLikeGlobalOptionsAfterImportedSnippets() { if p.valLooksLikeGlobalOptionsAfterImportedSnippets() {
+5 -20
View File
@@ -550,26 +550,11 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
} }
case acmeIssuer != nil: case acmeIssuer != nil:
// implicit ACME issuers (from various subdirectives) - use defaults; there might be more than one // implicit ACME issuers (from various subdirectives) should inherit from
defaultIssuers := caddytls.DefaultIssuers(acmeIssuer.Email) // any globally-configured ACME issuer templates, then apply the local
// shortcut settings as overrides.
// if an ACME CA endpoint was set, the user expects to use that specific one, defaultIssuers := implicitACMEIssuers(h, acmeIssuer)
// not any others that may be defaults, so replace all defaults with that ACME CA
if acmeIssuer.CA != "" {
defaultIssuers = []certmagic.Issuer{acmeIssuer}
}
for _, issuer := range defaultIssuers { for _, issuer := range defaultIssuers {
// apply settings from the implicitly-configured ACMEIssuer to any
// default ACMEIssuers, but preserve each default issuer's CA endpoint,
// because, for example, if you configure the DNS challenge, it should
// apply to any of the default ACMEIssuers, but you don't want to trample
// out their unique CA endpoints
if iss, ok := issuer.(*caddytls.ACMEIssuer); ok && iss != nil {
acmeCopy := *acmeIssuer
acmeCopy.CA = iss.CA
issuer = &acmeCopy
}
configVals = append(configVals, ConfigValue{ configVals = append(configVals, ConfigValue{
Class: "tls.cert_issuer", Class: "tls.cert_issuer",
Value: issuer, Value: issuer,
@@ -1068,7 +1053,7 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
if !d.NextArg() { if !d.NextArg() {
return nil, d.ArgErr() return nil, d.ArgErr()
} }
interval, err := time.ParseDuration(d.Val() + "ns") interval, err := caddy.ParseDuration(d.Val())
if err != nil { if err != nil {
return nil, d.Errf("failed to parse interval: %v", err) return nil, d.Errf("failed to parse interval: %v", err)
} }
+2 -2
View File
@@ -66,14 +66,14 @@ func TestLogDirectiveSyntax(t *testing.T) {
input: `:8080 { input: `:8080 {
log { log {
sampling { sampling {
interval 2 interval 2s
first 3 first 3
thereafter 4 thereafter 4
} }
} }
} }
`, `,
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"sampling":{"interval":2,"first":3,"thereafter":4},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`, output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"sampling":{"interval":2000000000,"first":3,"thereafter":4},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
expectError: false, expectError: false,
}, },
} { } {
+1 -1
View File
@@ -108,7 +108,7 @@ func (st ServerType) Setup(
matcherDefs := make(map[string]caddy.ModuleMap) matcherDefs := make(map[string]caddy.ModuleMap)
for _, segment := range sb.block.Segments { for _, segment := range sb.block.Segments {
if dir := segment.Directive(); strings.HasPrefix(dir, matcherPrefix) { if dir := segment.Directive(); strings.HasPrefix(dir, matcherPrefix) {
d := sb.block.DispenseDirective(dir) d := caddyfile.NewDispenser(segment)
err := parseMatcherDefinitions(d, matcherDefs) err := parseMatcherDefinitions(d, matcherDefs)
if err != nil { if err != nil {
return nil, warnings, err return nil, warnings, err
+39 -2
View File
@@ -2,6 +2,7 @@ package httpcaddyfile
import ( import (
"encoding/json" "encoding/json"
"strings"
"testing" "testing"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -10,8 +11,9 @@ import (
func TestMatcherSyntax(t *testing.T) { func TestMatcherSyntax(t *testing.T) {
for i, tc := range []struct { for i, tc := range []struct {
input string input string
expectError bool expectError bool
expectContains string
}{ }{
{ {
input: `http://localhost input: `http://localhost
@@ -53,6 +55,34 @@ func TestMatcherSyntax(t *testing.T) {
`, `,
expectError: false, expectError: false,
}, },
{
input: `http://localhost {
@test {
path /test
}
@test {
path /other
}
respond @test "hello"
}
`,
expectError: true,
expectContains: "is defined more than once",
},
{
input: `(snippet) {
@{args[0]} {
path /{args[0]}
}
respond @{args[0]} "hello"
}
http://localhost {
import snippet foo
import snippet bar
}
`,
expectError: false,
},
{ {
input: `@matcher { input: `@matcher {
path /matcher-not-allowed/outside-of-site-block/* path /matcher-not-allowed/outside-of-site-block/*
@@ -73,6 +103,13 @@ func TestMatcherSyntax(t *testing.T) {
t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err) t.Errorf("Test %d error expectation failed Expected: %v, got %s", i, tc.expectError, err)
continue continue
} }
if err != nil && tc.expectContains != "" {
if !strings.Contains(err.Error(), tc.expectContains) {
t.Errorf("Test %d error message mismatch: expected to contain %q, got %q",
i, tc.expectContains, err.Error())
}
}
} }
} }
+2
View File
@@ -484,6 +484,8 @@ func unmarshalCaddyfileMetricsOptions(d *caddyfile.Dispenser) (any, error) {
metrics.PerHost = true metrics.PerHost = true
case "observe_catchall_hosts": case "observe_catchall_hosts":
metrics.ObserveCatchallHosts = true metrics.ObserveCatchallHosts = true
case "otlp":
metrics.OTLP = true
default: default:
return nil, d.Errf("unrecognized servers option '%s'", d.Val()) return nil, d.Errf("unrecognized servers option '%s'", d.Val())
} }
+125
View File
@@ -3,7 +3,9 @@ package httpcaddyfile
import ( import (
"encoding/json" "encoding/json"
"testing" "testing"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
_ "github.com/caddyserver/caddy/v2/modules/logging" _ "github.com/caddyserver/caddy/v2/modules/logging"
@@ -166,3 +168,126 @@ func TestGlobalResolversOption(t *testing.T) {
}) })
} }
} }
func TestGlobalCertIssuerAppliesToImplicitACMEIssuer(t *testing.T) {
adapter := caddyfile.Adapter{
ServerType: ServerType{},
}
input := `{
cert_issuer acme {
disable_tlsalpn_challenge
}
}
report.company.intern {
tls {
ca https://deglacme01.company.intern/acme/acme/directory
ca_root /etc/certs/company_root2.crt
}
respond "ok"
}`
out, _, err := adapter.Adapt([]byte(input), nil)
if err != nil {
t.Fatalf("adapting caddyfile: %v", err)
}
var config struct {
Apps struct {
TLS *caddytls.TLS `json:"tls"`
} `json:"apps"`
}
if err := json.Unmarshal(out, &config); err != nil {
t.Fatalf("unmarshaling adapted config: %v", err)
}
if config.Apps.TLS == nil || config.Apps.TLS.Automation == nil {
t.Fatal("expected tls automation config")
}
var subjectPolicy *caddytls.AutomationPolicy
for _, ap := range config.Apps.TLS.Automation.Policies {
if len(ap.SubjectsRaw) == 1 && ap.SubjectsRaw[0] == "report.company.intern" {
subjectPolicy = ap
break
}
}
if subjectPolicy == nil {
t.Fatal("expected subject-specific automation policy")
}
if len(subjectPolicy.IssuersRaw) != 1 {
t.Fatalf("expected one issuer for subject-specific policy, got %d", len(subjectPolicy.IssuersRaw))
}
var issuer caddytls.ACMEIssuer
if err := json.Unmarshal(subjectPolicy.IssuersRaw[0], &issuer); err != nil {
t.Fatalf("unmarshaling issuer: %v", err)
}
if issuer.CA != "https://deglacme01.company.intern/acme/acme/directory" {
t.Fatalf("expected custom ACME CA, got %q", issuer.CA)
}
if len(issuer.TrustedRootsPEMFiles) != 1 || issuer.TrustedRootsPEMFiles[0] != "/etc/certs/company_root2.crt" {
t.Fatalf("expected trusted roots to include site CA root, got %v", issuer.TrustedRootsPEMFiles)
}
if issuer.Challenges == nil || issuer.Challenges.TLSALPN == nil || !issuer.Challenges.TLSALPN.Disabled {
t.Fatalf("expected tls-alpn challenge to be disabled, got %#v", issuer.Challenges)
}
}
func TestMergeACMEIssuers(t *testing.T) {
base := &caddytls.ACMEIssuer{
Email: "ops@example.com",
Challenges: &caddytls.ChallengesConfig{
HTTP: &caddytls.HTTPChallengeConfig{
AlternatePort: 8080,
},
TLSALPN: &caddytls.TLSALPNChallengeConfig{
Disabled: true,
AlternatePort: 8443,
},
DNS: &caddytls.DNSChallengeConfig{
Resolvers: []string{"1.1.1.1"},
OverrideDomain: "_acme-challenge.example.net",
},
},
TrustedRootsPEMFiles: []string{"global.pem"},
}
overrides := &caddytls.ACMEIssuer{
CA: "https://deglacme01.company.intern/acme/acme/directory",
Challenges: &caddytls.ChallengesConfig{
HTTP: &caddytls.HTTPChallengeConfig{
Disabled: true,
},
DNS: &caddytls.DNSChallengeConfig{
PropagationTimeout: caddy.Duration(time.Minute),
},
},
TrustedRootsPEMFiles: []string{"site.pem"},
}
merged := mergeACMEIssuers(base, overrides)
if merged.CA != overrides.CA {
t.Fatalf("expected merged CA %q, got %q", overrides.CA, merged.CA)
}
if merged.Email != base.Email {
t.Fatalf("expected merged email %q, got %q", base.Email, merged.Email)
}
if len(merged.TrustedRootsPEMFiles) != 2 || merged.TrustedRootsPEMFiles[0] != "global.pem" || merged.TrustedRootsPEMFiles[1] != "site.pem" {
t.Fatalf("expected merged roots [global.pem site.pem], got %v", merged.TrustedRootsPEMFiles)
}
if merged.Challenges == nil || merged.Challenges.HTTP == nil || !merged.Challenges.HTTP.Disabled || merged.Challenges.HTTP.AlternatePort != 8080 {
t.Fatalf("expected merged HTTP challenge config to preserve alternate port and apply disable flag, got %#v", merged.Challenges)
}
if merged.Challenges.TLSALPN == nil || !merged.Challenges.TLSALPN.Disabled || merged.Challenges.TLSALPN.AlternatePort != 8443 {
t.Fatalf("expected merged TLS-ALPN challenge config to preserve global settings, got %#v", merged.Challenges)
}
if merged.Challenges.DNS == nil || merged.Challenges.DNS.PropagationTimeout != caddy.Duration(time.Minute) || len(merged.Challenges.DNS.Resolvers) != 1 || merged.Challenges.DNS.Resolvers[0] != "1.1.1.1" || merged.Challenges.DNS.OverrideDomain != "_acme-challenge.example.net" {
t.Fatalf("expected merged DNS challenge config to preserve global values and apply overrides, got %#v", merged.Challenges)
}
if base.CA != "" {
t.Fatalf("expected base issuer to remain unchanged, got CA %q", base.CA)
}
if len(base.TrustedRootsPEMFiles) != 1 || base.TrustedRootsPEMFiles[0] != "global.pem" {
t.Fatalf("expected base roots to remain unchanged, got %v", base.TrustedRootsPEMFiles)
}
}
+336 -1
View File
@@ -612,6 +612,289 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
return nil return nil
} }
// implicitACMEIssuers returns the issuers to use for ACME-related tls
// shortcuts such as ca, ca_root, and dns. If any global cert_issuer options
// configure ACME issuers, those become the templates for the local shortcut
// configuration; otherwise, default ACME issuers are used.
func implicitACMEIssuers(h Helper, acmeIssuer *caddytls.ACMEIssuer) []certmagic.Issuer {
globalIssuers, _ := h.Option("cert_issuer").([]certmagic.Issuer)
var implicitIssuers []certmagic.Issuer
for _, issuer := range globalIssuers {
acmeWrapper, ok := issuer.(acmeCapable)
if !ok {
continue
}
baseIssuer := acmeWrapper.GetACMEIssuer()
if baseIssuer == nil {
continue
}
implicitIssuers = append(implicitIssuers, mergeACMEIssuers(baseIssuer, acmeIssuer))
}
if len(implicitIssuers) > 0 {
return implicitIssuers
}
// If an ACME CA endpoint was set locally, the user expects to use only that
// CA rather than the usual default fallback issuers.
defaultIssuers := caddytls.DefaultIssuers(acmeIssuer.Email)
if acmeIssuer.CA != "" {
defaultIssuers = []certmagic.Issuer{new(caddytls.ACMEIssuer)}
}
implicitIssuers = make([]certmagic.Issuer, 0, len(defaultIssuers))
for _, issuer := range defaultIssuers {
acmeWrapper, ok := issuer.(acmeCapable)
if !ok {
implicitIssuers = append(implicitIssuers, issuer)
continue
}
baseIssuer := acmeWrapper.GetACMEIssuer()
if baseIssuer == nil {
implicitIssuers = append(implicitIssuers, issuer)
continue
}
implicitIssuers = append(implicitIssuers, mergeACMEIssuers(baseIssuer, acmeIssuer))
}
return implicitIssuers
}
func mergeACMEIssuers(base, overrides *caddytls.ACMEIssuer) *caddytls.ACMEIssuer {
if base == nil {
return cloneACMEIssuer(overrides)
}
merged := cloneACMEIssuer(base)
if overrides == nil {
return merged
}
if overrides.CA != "" {
merged.CA = overrides.CA
}
if overrides.TestCA != "" {
merged.TestCA = overrides.TestCA
}
if overrides.Email != "" {
merged.Email = overrides.Email
}
if overrides.Profile != "" {
merged.Profile = overrides.Profile
}
if overrides.AccountKey != "" {
merged.AccountKey = overrides.AccountKey
}
if overrides.ExternalAccount != nil {
merged.ExternalAccount = cloneACMEEAB(overrides.ExternalAccount)
}
if overrides.ACMETimeout != 0 {
merged.ACMETimeout = overrides.ACMETimeout
}
if len(overrides.TrustedRootsPEMFiles) > 0 {
merged.TrustedRootsPEMFiles = appendUniqueStrings(merged.TrustedRootsPEMFiles, overrides.TrustedRootsPEMFiles...)
}
if overrides.PreferredChains != nil {
merged.PreferredChains = cloneChainPreference(overrides.PreferredChains)
}
if overrides.CertificateLifetime != 0 {
merged.CertificateLifetime = overrides.CertificateLifetime
}
if len(overrides.NetworkProxyRaw) > 0 {
merged.NetworkProxyRaw = slices.Clone(overrides.NetworkProxyRaw)
}
merged.Challenges = mergeChallengesConfig(merged.Challenges, overrides.Challenges)
return merged
}
func mergeChallengesConfig(base, overrides *caddytls.ChallengesConfig) *caddytls.ChallengesConfig {
if base == nil {
return cloneChallengesConfig(overrides)
}
merged := cloneChallengesConfig(base)
if overrides == nil {
return merged
}
merged.HTTP = mergeHTTPChallengeConfig(merged.HTTP, overrides.HTTP)
merged.TLSALPN = mergeTLSALPNChallengeConfig(merged.TLSALPN, overrides.TLSALPN)
merged.DNS = mergeDNSChallengeConfig(merged.DNS, overrides.DNS)
if overrides.BindHost != "" {
merged.BindHost = overrides.BindHost
}
if overrides.Distributed != nil {
value := *overrides.Distributed
merged.Distributed = &value
}
return merged
}
func mergeHTTPChallengeConfig(base, overrides *caddytls.HTTPChallengeConfig) *caddytls.HTTPChallengeConfig {
if base == nil {
return cloneHTTPChallengeConfig(overrides)
}
merged := cloneHTTPChallengeConfig(base)
if overrides == nil {
return merged
}
if overrides.Disabled {
merged.Disabled = true
}
if overrides.AlternatePort != 0 {
merged.AlternatePort = overrides.AlternatePort
}
return merged
}
func mergeTLSALPNChallengeConfig(base, overrides *caddytls.TLSALPNChallengeConfig) *caddytls.TLSALPNChallengeConfig {
if base == nil {
return cloneTLSALPNChallengeConfig(overrides)
}
merged := cloneTLSALPNChallengeConfig(base)
if overrides == nil {
return merged
}
if overrides.Disabled {
merged.Disabled = true
}
if overrides.AlternatePort != 0 {
merged.AlternatePort = overrides.AlternatePort
}
return merged
}
func mergeDNSChallengeConfig(base, overrides *caddytls.DNSChallengeConfig) *caddytls.DNSChallengeConfig {
if base == nil {
return cloneDNSChallengeConfig(overrides)
}
merged := cloneDNSChallengeConfig(base)
if overrides == nil {
return merged
}
if len(overrides.ProviderRaw) > 0 {
merged.ProviderRaw = slices.Clone(overrides.ProviderRaw)
}
if overrides.PropagationDelay != 0 {
merged.PropagationDelay = overrides.PropagationDelay
}
if overrides.PropagationTimeout != 0 {
merged.PropagationTimeout = overrides.PropagationTimeout
}
if overrides.Resolvers != nil {
merged.Resolvers = slices.Clone(overrides.Resolvers)
}
if overrides.OverrideDomain != "" {
merged.OverrideDomain = overrides.OverrideDomain
}
if overrides.TTL != 0 {
merged.TTL = overrides.TTL
}
return merged
}
func cloneACMEIssuer(iss *caddytls.ACMEIssuer) *caddytls.ACMEIssuer {
if iss == nil {
return nil
}
cloned := *iss
cloned.Challenges = cloneChallengesConfig(iss.Challenges)
cloned.ExternalAccount = cloneACMEEAB(iss.ExternalAccount)
cloned.TrustedRootsPEMFiles = slices.Clone(iss.TrustedRootsPEMFiles)
cloned.PreferredChains = cloneChainPreference(iss.PreferredChains)
cloned.NetworkProxyRaw = slices.Clone(iss.NetworkProxyRaw)
return &cloned
}
func cloneChallengesConfig(cfg *caddytls.ChallengesConfig) *caddytls.ChallengesConfig {
if cfg == nil {
return nil
}
cloned := *cfg
cloned.HTTP = cloneHTTPChallengeConfig(cfg.HTTP)
cloned.TLSALPN = cloneTLSALPNChallengeConfig(cfg.TLSALPN)
cloned.DNS = cloneDNSChallengeConfig(cfg.DNS)
if cfg.Distributed != nil {
value := *cfg.Distributed
cloned.Distributed = &value
}
return &cloned
}
func cloneHTTPChallengeConfig(cfg *caddytls.HTTPChallengeConfig) *caddytls.HTTPChallengeConfig {
if cfg == nil {
return nil
}
cloned := *cfg
return &cloned
}
func cloneTLSALPNChallengeConfig(cfg *caddytls.TLSALPNChallengeConfig) *caddytls.TLSALPNChallengeConfig {
if cfg == nil {
return nil
}
cloned := *cfg
return &cloned
}
func cloneDNSChallengeConfig(cfg *caddytls.DNSChallengeConfig) *caddytls.DNSChallengeConfig {
if cfg == nil {
return nil
}
cloned := *cfg
cloned.ProviderRaw = slices.Clone(cfg.ProviderRaw)
cloned.Resolvers = slices.Clone(cfg.Resolvers)
return &cloned
}
func cloneACMEEAB(eab *acme.EAB) *acme.EAB {
if eab == nil {
return nil
}
cloned := *eab
return &cloned
}
func cloneChainPreference(pref *caddytls.ChainPreference) *caddytls.ChainPreference {
if pref == nil {
return nil
}
cloned := *pref
cloned.RootCommonName = slices.Clone(pref.RootCommonName)
cloned.AnyCommonName = slices.Clone(pref.AnyCommonName)
if pref.Smallest != nil {
value := *pref.Smallest
cloned.Smallest = &value
}
return &cloned
}
func appendUniqueStrings(existing []string, additions ...string) []string {
for _, value := range additions {
if !slices.Contains(existing, value) {
existing = append(existing, value)
}
}
return existing
}
// newBaseAutomationPolicy returns a new TLS automation policy that gets // newBaseAutomationPolicy returns a new TLS automation policy that gets
// its values from the global options map. It should be used as the base // its values from the global options map. It should be used as the base
// for any other automation policies. A nil policy (and no error) will be // for any other automation policies. A nil policy (and no error) will be
@@ -753,7 +1036,7 @@ outer:
// otherwise the one without any subjects (a catch-all) would be // otherwise the one without any subjects (a catch-all) would be
// eaten up by the one with subjects; and if both have subjects, we // eaten up by the one with subjects; and if both have subjects, we
// need to combine their lists // need to combine their lists
if reflect.DeepEqual(aps[i].IssuersRaw, aps[j].IssuersRaw) && if automationPoliciesHaveSameIssuers(aps[i], aps[j]) &&
reflect.DeepEqual(aps[i].ManagersRaw, aps[j].ManagersRaw) && reflect.DeepEqual(aps[i].ManagersRaw, aps[j].ManagersRaw) &&
bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) && bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
aps[i].MustStaple == aps[j].MustStaple && aps[i].MustStaple == aps[j].MustStaple &&
@@ -845,6 +1128,58 @@ func subjectQualifiesForPublicCert(ap *caddytls.AutomationPolicy, subj string) b
(strings.Count(subj, "*.") < 2 || ap.OnDemand) (strings.Count(subj, "*.") < 2 || ap.OnDemand)
} }
func automationPoliciesHaveSameIssuers(a, b *caddytls.AutomationPolicy) bool {
if reflect.DeepEqual(a.IssuersRaw, b.IssuersRaw) {
return automationPoliciesHaveCompatibleImplicitIssuers(a, b)
}
return automationPolicyUsesDefaultInternalIssuer(a) && automationPolicyUsesDefaultInternalIssuer(b)
}
func automationPolicyUsesDefaultInternalIssuer(ap *caddytls.AutomationPolicy) bool {
if len(ap.IssuersRaw) == 0 && len(ap.Issuers) == 0 {
return automationPolicyImplicitIssuerClass(ap) == "internal"
}
return len(ap.IssuersRaw) == 1 &&
len(ap.Issuers) == 0 &&
string(bytes.TrimSpace(ap.IssuersRaw[0])) == `{"module":"internal"}`
}
// automationPoliciesHaveCompatibleImplicitIssuers returns whether two policies
// without explicit issuers can be consolidated without changing default issuer
// selection for their subjects.
func automationPoliciesHaveCompatibleImplicitIssuers(a, b *caddytls.AutomationPolicy) bool {
if len(a.IssuersRaw) > 0 || len(a.Issuers) > 0 ||
len(b.IssuersRaw) > 0 || len(b.Issuers) > 0 {
return true
}
aClass := automationPolicyImplicitIssuerClass(a)
bClass := automationPolicyImplicitIssuerClass(b)
return aClass == "catch-all" || bClass == "catch-all" || aClass == bClass
}
func automationPolicyImplicitIssuerClass(ap *caddytls.AutomationPolicy) string {
if len(ap.SubjectsRaw) == 0 {
return "catch-all"
}
hasPublic := slices.ContainsFunc(ap.SubjectsRaw, func(subj string) bool {
return subjectQualifiesForPublicCert(ap, subj)
})
hasInternal := slices.ContainsFunc(ap.SubjectsRaw, func(subj string) bool {
return !subjectQualifiesForPublicCert(ap, subj)
})
switch {
case hasPublic && hasInternal:
return "mixed"
case hasPublic:
return "public"
default:
return "internal"
}
}
// automationPolicyHasAllPublicNames returns true if all the names on the policy // automationPolicyHasAllPublicNames returns true if all the names on the policy
// do NOT qualify for public certs OR are tailscale domains. // do NOT qualify for public certs OR are tailscale domains.
func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool { func automationPolicyHasAllPublicNames(ap *caddytls.AutomationPolicy) bool {
+18
View File
@@ -3,6 +3,7 @@ package httpcaddyfile
import ( import (
"testing" "testing"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
) )
@@ -54,3 +55,20 @@ func TestAutomationPolicyIsSubset(t *testing.T) {
} }
} }
} }
func TestAutomationPoliciesAllowSameHostOnDifferentPorts(t *testing.T) {
input := `https://example.com:5000 localhost:5000 {
respond "one"
}
https://example.net localhost:8080 {
respond "two"
}
`
adapter := caddyfile.Adapter{ServerType: ServerType{}}
_, _, err := adapter.Adapt([]byte(input), nil)
if err != nil {
t.Fatalf("adapting Caddyfile: %v", err)
}
}
+6 -6
View File
@@ -518,7 +518,7 @@ func (tc *Tester) AssertResponseCode(req *http.Request, expectedStatusCode int)
return resp return resp
} }
// AssertResponse request a URI and assert the status code and the body contains a string // AssertResponse requests a URI and asserts the status code and body.
func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) { func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expectedBody string) (*http.Response, string) {
tc.t.Helper() tc.t.Helper()
@@ -541,7 +541,7 @@ func (tc *Tester) AssertResponse(req *http.Request, expectedStatusCode int, expe
// Verb specific test functions // Verb specific test functions
// AssertGetResponse GET a URI and expect a statusCode and body text // AssertGetResponse requests a URI with GET and expects a status code and body text.
func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) { func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
tc.t.Helper() tc.t.Helper()
@@ -553,7 +553,7 @@ func (tc *Tester) AssertGetResponse(requestURI string, expectedStatusCode int, e
return tc.AssertResponse(req, expectedStatusCode, expectedBody) return tc.AssertResponse(req, expectedStatusCode, expectedBody)
} }
// AssertDeleteResponse request a URI and expect a statusCode and body text // AssertDeleteResponse requests a URI with DELETE and expects a status code and body text.
func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) { func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int, expectedBody string) (*http.Response, string) {
tc.t.Helper() tc.t.Helper()
@@ -565,7 +565,7 @@ func (tc *Tester) AssertDeleteResponse(requestURI string, expectedStatusCode int
return tc.AssertResponse(req, expectedStatusCode, expectedBody) return tc.AssertResponse(req, expectedStatusCode, expectedBody)
} }
// AssertPostResponseBody POST to a URI and assert the response code and body // AssertPostResponseBody requests a URI with POST and asserts the response code and body.
func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) { func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
tc.t.Helper() tc.t.Helper()
@@ -580,7 +580,7 @@ func (tc *Tester) AssertPostResponseBody(requestURI string, requestHeaders []str
return tc.AssertResponse(req, expectedStatusCode, expectedBody) return tc.AssertResponse(req, expectedStatusCode, expectedBody)
} }
// AssertPutResponseBody PUT to a URI and assert the response code and body // AssertPutResponseBody requests a URI with PUT and asserts the response code and body.
func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) { func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
tc.t.Helper() tc.t.Helper()
@@ -595,7 +595,7 @@ func (tc *Tester) AssertPutResponseBody(requestURI string, requestHeaders []stri
return tc.AssertResponse(req, expectedStatusCode, expectedBody) return tc.AssertResponse(req, expectedStatusCode, expectedBody)
} }
// AssertPatchResponseBody PATCH to a URI and assert the response code and body // AssertPatchResponseBody requests a URI with PATCH and asserts the response code and body.
func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) { func (tc *Tester) AssertPatchResponseBody(requestURI string, requestHeaders []string, requestBody *bytes.Buffer, expectedStatusCode int, expectedBody string) (*http.Response, string) {
tc.t.Helper() tc.t.Helper()
+22
View File
@@ -55,6 +55,28 @@ func TestAutoHTTPtoHTTPSRedirectsExplicitPortDifferentFromHTTPSPort(t *testing.T
tester.AssertRedirect("http://localhost:9080/", "https://localhost:1234/", http.StatusPermanentRedirect) tester.AssertRedirect("http://localhost:9080/", "https://localhost:1234/", http.StatusPermanentRedirect)
} }
func TestAutoHTTPtoHTTPSRedirectsPreferHTTPSPortOverAlternatePort(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
local_certs
}
localhost {
respond "Canonical"
}
localhost:10443 {
respond "Alternate"
}
`, "caddyfile")
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
}
func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) { func TestAutoHTTPRedirectsWithHTTPListenerFirstInAddresses(t *testing.T) {
tester := caddytest.NewTester(t) tester := caddytest.NewTester(t)
tester.InitServer(` tester.InitServer(`
@@ -1,7 +1,7 @@
{ {
log { log {
sampling { sampling {
interval 300 interval 5m
first 50 first 50
thereafter 40 thereafter 40
} }
@@ -13,7 +13,7 @@
"logs": { "logs": {
"default": { "default": {
"sampling": { "sampling": {
"interval": 300, "interval": 300000000000,
"first": 50, "first": 50,
"thereafter": 40 "thereafter": 40
} }
@@ -6,4 +6,4 @@ handle {
END! END!
} }
---------- ----------
heredoc marker on line #4 must contain only alpha-numeric characters, dashes and underscores; got 'END!' heredoc marker on line #4 must contain only alphanumeric characters, dashes and underscores; got 'END!'
@@ -1,7 +1,7 @@
:80 { :80 {
log { log {
sampling { sampling {
interval 300 interval 5m
first 50 first 50
thereafter 40 thereafter 40
} }
@@ -18,7 +18,7 @@
}, },
"log0": { "log0": {
"sampling": { "sampling": {
"interval": 300, "interval": 300000000000,
"first": 50, "first": 50,
"thereafter": 40 "thereafter": 40
}, },
@@ -0,0 +1,35 @@
{
metrics {
otlp
}
}
:80 {
respond "Hello"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"body": "Hello",
"handler": "static_response"
}
]
}
]
}
},
"metrics": {
"otlp": true
}
}
}
}
@@ -0,0 +1,58 @@
:8884
reverse_proxy 127.0.0.1:65535 {
lb_retries 3
lb_retry_match expression `{rp.status_code} in [502, 503]`
lb_retry_match expression `{rp.is_transport_error} || {rp.status_code} == 502`
lb_retry_match expression `method('POST') && {rp.status_code} == 503`
lb_retry_match `{rp.status_code} == 504`
lb_retry_match `{rp.is_transport_error} && method('PUT')`
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"load_balancing": {
"retries": 3,
"retry_match": [
{
"expression": "{http.reverse_proxy.status_code} in [502, 503]"
},
{
"expression": "{http.reverse_proxy.is_transport_error} || {http.reverse_proxy.status_code} == 502"
},
{
"expression": "method('POST') \u0026\u0026 {http.reverse_proxy.status_code} == 503"
},
{
"expression": "{http.reverse_proxy.status_code} == 504"
},
{
"expression": "{http.reverse_proxy.is_transport_error} \u0026\u0026 method('PUT')"
}
]
},
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
}
]
}
]
}
}
}
}
}
@@ -0,0 +1,147 @@
:8884
reverse_proxy 127.0.0.1:65535 {
lb_retries 5
# request matchers (backward-compatible, non-expression)
lb_retry_match {
method POST PUT
}
lb_retry_match {
path /foo*
}
lb_retry_match {
header X-Idempotency-Key *
}
# response status code via expression
lb_retry_match {
expression `{rp.status_code} in [502, 503, 504]`
}
# response header via expression
lb_retry_match {
expression `{rp.header.X-Retry} == "true"`
}
# CEL request functions combined with response placeholders
lb_retry_match {
expression `method('POST') && {rp.status_code} >= 500`
}
lb_retry_match {
expression `path('/api*') && {rp.status_code} in [502, 503]`
}
lb_retry_match {
expression `host('example.com') && {rp.status_code} == 503`
}
lb_retry_match {
expression `query({'retry': 'true'}) && {rp.status_code} >= 500`
}
lb_retry_match {
expression `header({'X-Idempotency-Key': '*'}) && {rp.status_code} in [502, 503]`
}
lb_retry_match {
expression `protocol('https') && {rp.status_code} == 502`
}
lb_retry_match {
expression `path_regexp('^/api/v[0-9]+/') && {rp.status_code} >= 500`
}
lb_retry_match {
expression `header_regexp('Content-Type', '^application/json') && {rp.status_code} == 502`
}
# transport error handling via placeholder
lb_retry_match {
expression `{rp.is_transport_error} || {rp.status_code} in [502, 503]`
}
lb_retry_match {
expression `{rp.is_transport_error} && method('POST')`
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"load_balancing": {
"retries": 5,
"retry_match": [
{
"method": [
"POST",
"PUT"
]
},
{
"path": [
"/foo*"
]
},
{
"header": {
"X-Idempotency-Key": [
"*"
]
}
},
{
"expression": "{http.reverse_proxy.status_code} in [502, 503, 504]"
},
{
"expression": "{http.reverse_proxy.header.X-Retry} == \"true\""
},
{
"expression": "method('POST') \u0026\u0026 {http.reverse_proxy.status_code} \u003e= 500"
},
{
"expression": "path('/api*') \u0026\u0026 {http.reverse_proxy.status_code} in [502, 503]"
},
{
"expression": "host('example.com') \u0026\u0026 {http.reverse_proxy.status_code} == 503"
},
{
"expression": "query({'retry': 'true'}) \u0026\u0026 {http.reverse_proxy.status_code} \u003e= 500"
},
{
"expression": "header({'X-Idempotency-Key': '*'}) \u0026\u0026 {http.reverse_proxy.status_code} in [502, 503]"
},
{
"expression": "protocol('https') \u0026\u0026 {http.reverse_proxy.status_code} == 502"
},
{
"expression": "path_regexp('^/api/v[0-9]+/') \u0026\u0026 {http.reverse_proxy.status_code} \u003e= 500"
},
{
"expression": "header_regexp('Content-Type', '^application/json') \u0026\u0026 {http.reverse_proxy.status_code} == 502"
},
{
"expression": "{http.reverse_proxy.is_transport_error} || {http.reverse_proxy.status_code} in [502, 503]"
},
{
"expression": "{http.reverse_proxy.is_transport_error} \u0026\u0026 method('POST')"
}
]
},
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
}
]
}
]
}
}
}
}
}
+65
View File
@@ -22,6 +22,8 @@ import (
"sync" "sync"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/caddyserver/caddy/v2/caddytest" "github.com/caddyserver/caddy/v2/caddytest"
) )
@@ -204,3 +206,66 @@ func TestForwardAuthCopyHeadersAuthResponseWins(t *testing.T) {
t.Errorf("X-User-Role: want %q, got %q", wantUserRole, gotRole) t.Errorf("X-User-Role: want %q, got %q", wantUserRole, gotRole)
} }
} }
// TestForwardAuthCopyHeadersUnderscoreAlias guards GHSA-f59h-q822-g45g:
// a client-supplied `Remote_user` alias of the copy_headers target
// `Remote-User` must be stripped before the auth route runs, otherwise
// a downstream CGI/FastCGI backend would fold both names into the same
// HTTP_REMOTE_USER variable and the attacker would override the trusted
// identity.
func TestForwardAuthCopyHeadersUnderscoreAlias(t *testing.T) {
const wantRemoteUser = "alice"
authSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Remote-User", wantRemoteUser)
w.WriteHeader(http.StatusOK)
}))
t.Cleanup(authSrv.Close)
type received struct {
remoteUserHyphen, remoteUserUnderscore string
}
var (
mu sync.Mutex
last received
)
backendSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
last = received{
remoteUserHyphen: r.Header.Get("Remote-User"),
remoteUserUnderscore: strings.Join(r.Header["Remote_user"], ","),
}
mu.Unlock()
fmt.Fprint(w, "ok")
}))
t.Cleanup(backendSrv.Close)
tester := caddytest.NewTester(t)
tester.InitServer(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
forward_auth %s {
uri /
copy_headers Remote-User
}
reverse_proxy %s
}
`, strings.TrimPrefix(authSrv.URL, "http://"), strings.TrimPrefix(backendSrv.URL, "http://")), "caddyfile")
req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
// Set the underscore alias via raw map access to bypass http.Header
// canonicalization, as an attacker would on the wire.
req.Header["Remote_user"] = []string{"attacker"}
tester.AssertResponse(req, http.StatusOK, "ok")
mu.Lock()
defer mu.Unlock()
assert.Equal(t, wantRemoteUser, last.remoteUserHyphen, "trusted Remote-User must reach the backend")
assert.Empty(t, last.remoteUserUnderscore, "underscore alias must be dropped")
}
+269
View File
@@ -7,6 +7,7 @@ import (
"os" "os"
"runtime" "runtime"
"strings" "strings"
"sync/atomic"
"testing" "testing"
"github.com/caddyserver/caddy/v2/caddytest" "github.com/caddyserver/caddy/v2/caddytest"
@@ -562,3 +563,271 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) {
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!") tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
} }
// TestReverseProxyRetryMatchStatusCode verifies that lb_retry_match with a
// CEL expression matching on {rp.status_code} causes the request to be
// retried on the next upstream when the first upstream returns a matching
// status code
func TestReverseProxyRetryMatchStatusCode(t *testing.T) {
// Bad upstream: returns 502
badSrv := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadGateway)
}),
}
badLn, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
go badSrv.Serve(badLn)
t.Cleanup(func() { badSrv.Close(); badLn.Close() })
// Good upstream: returns 200
goodSrv := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}),
}
goodLn, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
go goodSrv.Serve(goodLn)
t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
tester := caddytest.NewTester(t)
tester.InitServer(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
reverse_proxy %s %s {
lb_policy round_robin
lb_retries 1
lb_retry_match {
expression `+"`{rp.status_code} in [502, 503]`"+`
}
}
}
`, goodLn.Addr().String(), badLn.Addr().String()), "caddyfile")
tester.AssertGetResponse("http://localhost:9080/", 200, "ok")
}
// TestReverseProxyRetryMatchHeader verifies that lb_retry_match with a CEL
// expression matching on {rp.header.*} causes the request to be retried when
// the upstream sets a matching response header
func TestReverseProxyRetryMatchHeader(t *testing.T) {
var badHits atomic.Int32
// Bad upstream: returns 200 but signals retry via header
badSrv := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
badHits.Add(1)
w.Header().Set("X-Upstream-Retry", "true")
w.Write([]byte("bad"))
}),
}
badLn, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
go badSrv.Serve(badLn)
t.Cleanup(func() { badSrv.Close(); badLn.Close() })
// Good upstream: returns 200 without retry header
goodSrv := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("good"))
}),
}
goodLn, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
go goodSrv.Serve(goodLn)
t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
tester := caddytest.NewTester(t)
tester.InitServer(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
reverse_proxy %s %s {
lb_policy round_robin
lb_retries 1
lb_retry_match {
expression `+"`{rp.header.X-Upstream-Retry} == \"true\"`"+`
}
}
}
`, goodLn.Addr().String(), badLn.Addr().String()), "caddyfile")
tester.AssertGetResponse("http://localhost:9080/", 200, "good")
if badHits.Load() != 1 {
t.Errorf("bad upstream hits: got %d, want 1", badHits.Load())
}
}
// TestReverseProxyRetryMatchCombined verifies that a CEL expression combining
// request path matching with response status code matching works correctly -
// only retrying when both conditions are met
func TestReverseProxyRetryMatchCombined(t *testing.T) {
// Upstream: returns 502 for all requests
srv := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadGateway)
}),
}
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
go srv.Serve(ln)
t.Cleanup(func() { srv.Close(); ln.Close() })
// Good upstream
goodSrv := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}),
}
goodLn, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
go goodSrv.Serve(goodLn)
t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
tester := caddytest.NewTester(t)
tester.InitServer(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
reverse_proxy %s %s {
lb_policy round_robin
lb_retries 1
lb_retry_match {
expression `+"`path('/retry*') && {rp.status_code} in [502, 503]`"+`
}
}
}
`, goodLn.Addr().String(), ln.Addr().String()), "caddyfile")
// /retry path matches the expression - should retry to good upstream
tester.AssertGetResponse("http://localhost:9080/retry", 200, "ok")
// /other path does NOT match - should return the 502
req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/other", nil)
tester.AssertResponse(req, 502, "")
}
// TestReverseProxyRetryMatchIsTransportError verifies that the
// {rp.is_transport_error} == true CEL function correctly identifies transport errors
// and allows retrying them alongside response-based matching
func TestReverseProxyRetryMatchIsTransportError(t *testing.T) {
// Good upstream: returns 200
goodSrv := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}),
}
goodLn, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
go goodSrv.Serve(goodLn)
t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
// Broken upstream: accepts connections but closes immediately
brokenLn, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
t.Cleanup(func() { brokenLn.Close() })
go func() {
for {
conn, err := brokenLn.Accept()
if err != nil {
return
}
conn.Close()
}
}()
tester := caddytest.NewTester(t)
tester.InitServer(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
reverse_proxy %s %s {
lb_policy round_robin
lb_retries 1
lb_retry_match {
expression `+"`{rp.is_transport_error} || {rp.status_code} in [502, 503]`"+`
}
}
}
`, goodLn.Addr().String(), brokenLn.Addr().String()), "caddyfile")
// Transport error on broken upstream should be retried to good upstream
tester.AssertGetResponse("http://localhost:9080/", 200, "ok")
}
func TestReverseProxySNIPlaceHolder(t *testing.T) {
configTemplate := `
{
skip_install_trust
local_certs
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
localhost example.com {
@proxied header X-Transport caddy
respond @proxied {http.request.tls.server_name}
reverse_proxy 127.0.0.1:9443 {
header_up X-Transport caddy
header_up Host {host}
transport http {
versions %s
tls_server_name {header.X-SNI}
tls_insecure_skip_verify
}
}
}
`
for _, versions := range []string{"1.1 2", "3"} {
tester := caddytest.NewTester(t)
tester.InitServer(fmt.Sprintf(configTemplate, versions), "caddyfile")
req, err := http.NewRequest("GET", "https://localhost:9443", nil)
if err != nil {
t.Errorf("failed to create request %s", err)
return
}
req.Header.Set("X-SNI", "example.com")
tester.AssertResponse(req, 200, "example.com")
}
}
+1 -1
View File
@@ -159,7 +159,7 @@ func testH2ToH2CStreamServeH2C(t *testing.T) *http.Server {
} }
// We only accept HTTP/2! // We only accept HTTP/2!
if r.ProtoMajor != 2 { if r.ProtoMajor != 2 {
t.Error("Not a HTTP/2 request, rejected!") t.Error("Not an HTTP/2 request, rejected!")
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
return return
} }
+17 -1
View File
@@ -58,7 +58,7 @@ func cmdStart(fl Flags) (int, error) {
// open a listener to which the child process will connect when // open a listener to which the child process will connect when
// it is ready to confirm that it has successfully started // it is ready to confirm that it has successfully started
ln, err := net.Listen("tcp", "127.0.0.1:0") ln, err := listenTCPForPingback(net.Listen)
if err != nil { if err != nil {
return caddy.ExitCodeFailedStartup, return caddy.ExitCodeFailedStartup,
fmt.Errorf("opening listener for success confirmation: %v", err) fmt.Errorf("opening listener for success confirmation: %v", err)
@@ -169,6 +169,22 @@ func cmdStart(fl Flags) (int, error) {
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
type tcpListenFunc func(network, address string) (net.Listener, error)
func listenTCPForPingback(listen tcpListenFunc) (net.Listener, error) {
ln, ipv4Err := listen("tcp4", "127.0.0.1:0")
if ipv4Err == nil {
return ln, nil
}
ln, ipv6Err := listen("tcp6", "[::1]:0")
if ipv6Err == nil {
return ln, nil
}
return nil, fmt.Errorf("listen on 127.0.0.1:0: %v; listen on [::1]:0: %v", ipv4Err, ipv6Err)
}
func cmdRun(fl Flags) (int, error) { func cmdRun(fl Flags) (int, error) {
caddy.TrapSignals() caddy.TrapSignals()
+1 -1
View File
@@ -566,7 +566,7 @@ argument of --directory. If the directory does not exist, it will be created.
// following format: // following format:
// //
// - lowercase // - lowercase
// - alphanumeric and hyphen characters only // - ASCII lowercase letters, digits and hyphens only
// - cannot start or end with a hyphen // - cannot start or end with a hyphen
// - hyphen cannot be adjacent to another hyphen // - hyphen cannot be adjacent to another hyphen
// //
+76
View File
@@ -1,6 +1,8 @@
package caddycmd package caddycmd
import ( import (
"errors"
"net"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
@@ -169,6 +171,80 @@ here"
} }
} }
func TestListenTCPForPingbackUsesIPv4Loopback(t *testing.T) {
var calls []string
expected := &stubListener{addr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 1234}}
actual, err := listenTCPForPingback(func(network, address string) (net.Listener, error) {
calls = append(calls, network+" "+address)
return expected, nil
})
if err != nil {
t.Fatalf("listenTCPForPingback returned error: %v", err)
}
if actual != expected {
t.Fatalf("expected listener %p, got %p", expected, actual)
}
expectCalls := []string{"tcp4 127.0.0.1:0"}
if !reflect.DeepEqual(calls, expectCalls) {
t.Fatalf("expected calls %v, got %v", expectCalls, calls)
}
}
func TestListenTCPForPingbackFallsBackToIPv6Loopback(t *testing.T) {
var calls []string
expected := &stubListener{addr: &net.TCPAddr{IP: net.ParseIP("::1"), Port: 1234}}
actual, err := listenTCPForPingback(func(network, address string) (net.Listener, error) {
calls = append(calls, network+" "+address)
if len(calls) == 1 {
return nil, errors.New("ipv4 unavailable")
}
return expected, nil
})
if err != nil {
t.Fatalf("listenTCPForPingback returned error: %v", err)
}
if actual != expected {
t.Fatalf("expected listener %p, got %p", expected, actual)
}
expectCalls := []string{"tcp4 127.0.0.1:0", "tcp6 [::1]:0"}
if !reflect.DeepEqual(calls, expectCalls) {
t.Fatalf("expected calls %v, got %v", expectCalls, calls)
}
}
func TestListenTCPForPingbackReportsBothFailures(t *testing.T) {
_, err := listenTCPForPingback(func(network, address string) (net.Listener, error) {
return nil, errors.New(network + " failed")
})
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), "tcp4 failed") ||
!strings.Contains(err.Error(), "tcp6 failed") {
t.Fatalf("expected both listener errors, got: %v", err)
}
}
type stubListener struct {
addr net.Addr
}
func (sl *stubListener) Accept() (net.Conn, error) {
return nil, net.ErrClosed
}
func (sl *stubListener) Close() error {
return nil
}
func (sl *stubListener) Addr() net.Addr {
return sl.addr
}
func Test_isCaddyfile(t *testing.T) { func Test_isCaddyfile(t *testing.T) {
type args struct { type args struct {
configFile string configFile string
+1 -1
View File
@@ -234,7 +234,7 @@ func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
// not sure why), and since New() should return a pointer // not sure why), and since New() should return a pointer
// value, we need to dereference it first // value, we need to dereference it first
iface := any(modInfo.New()) iface := any(modInfo.New())
if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr { if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Pointer {
iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface() iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
} }
modPkgPath := reflect.TypeOf(iface).PkgPath() modPkgPath := reflect.TypeOf(iface).PkgPath()
+1 -1
View File
@@ -378,7 +378,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
// value must be a pointer for unmarshaling into concrete type, even if // value must be a pointer for unmarshaling into concrete type, even if
// the module's concrete type is a slice or map; New() *should* return // the module's concrete type is a slice or map; New() *should* return
// a pointer, otherwise unmarshaling errors or panics will occur // a pointer, otherwise unmarshaling errors or panics will occur
if rv := reflect.ValueOf(val); rv.Kind() != reflect.Ptr { if rv := reflect.ValueOf(val); rv.Kind() != reflect.Pointer {
log.Printf("[WARNING] ModuleInfo.New() for module '%s' did not return a pointer,"+ log.Printf("[WARNING] ModuleInfo.New() for module '%s' did not return a pointer,"+
" so we are using reflection to make a pointer instead; please fix this by"+ " so we are using reflection to make a pointer instead; please fix this by"+
" using new(Type) or &Type notation in your module's New() function.", id) " using new(Type) or &Type notation in your module's New() function.", id)
+29 -29
View File
@@ -1,26 +1,26 @@
module github.com/caddyserver/caddy/v2 module github.com/caddyserver/caddy/v2
go 1.25.0 go 1.25.1
require ( require (
github.com/BurntSushi/toml v1.6.0 github.com/BurntSushi/toml v1.6.0
github.com/DeRuina/timberjack v1.4.1 github.com/DeRuina/timberjack v1.4.2
github.com/KimMachineGun/automemlimit v0.7.5 github.com/KimMachineGun/automemlimit v0.7.5
github.com/Masterminds/sprig/v3 v3.3.0 github.com/Masterminds/sprig/v3 v3.3.0
github.com/alecthomas/chroma/v2 v2.23.1 github.com/alecthomas/chroma/v2 v2.24.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.3
github.com/caddyserver/zerossl v0.1.5 github.com/caddyserver/zerossl v0.1.5
github.com/cloudflare/circl v1.6.3 github.com/cloudflare/circl v1.6.3
github.com/dustin/go-humanize v1.0.1 github.com/dustin/go-humanize v1.0.1
github.com/go-chi/chi/v5 v5.2.5 github.com/go-chi/chi/v5 v5.2.5
github.com/google/cel-go v0.28.0 github.com/google/cel-go v0.28.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/klauspost/compress v1.18.5 github.com/klauspost/compress v1.18.6
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.6
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/quic-go/quic-go v0.59.0 github.com/quic-go/quic-go v0.59.1
github.com/smallstep/certificates v0.30.2 github.com/smallstep/certificates v0.30.2
github.com/smallstep/nosql v0.8.0 github.com/smallstep/nosql v0.8.0
github.com/smallstep/truststore v0.13.0 github.com/smallstep/truststore v0.13.0
@@ -30,27 +30,29 @@ require (
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747 github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747
github.com/yuin/goldmark v1.8.2 github.com/yuin/goldmark v1.8.2
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0
go.opentelemetry.io/contrib/exporters/autoexport v0.68.0 go.opentelemetry.io/contrib/exporters/autoexport v0.68.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0
go.opentelemetry.io/contrib/propagators/autoprop v0.68.0 go.opentelemetry.io/contrib/propagators/autoprop v0.68.0
go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/sdk v1.43.0 go.opentelemetry.io/otel/sdk v1.43.0
go.step.sm/crypto v0.77.2 go.opentelemetry.io/otel/sdk/metric v1.43.0
go.step.sm/crypto v0.81.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.28.0
go.uber.org/zap/exp v0.3.0 go.uber.org/zap/exp v0.3.0
golang.org/x/crypto v0.50.0 golang.org/x/crypto v0.52.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20260323153451-8400f4a93807 golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541
golang.org/x/net v0.53.0 golang.org/x/net v0.55.0
golang.org/x/sync v0.20.0 golang.org/x/sync v0.20.0
golang.org/x/term v0.42.0 golang.org/x/term v0.43.0
golang.org/x/time v0.15.0 golang.org/x/time v0.15.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
cel.dev/expr v0.25.1 // indirect cel.dev/expr v0.25.1 // indirect
cloud.google.com/go/auth v0.18.2 // indirect cloud.google.com/go/auth v0.20.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect
dario.cat/mergo v1.0.2 // indirect dario.cat/mergo v1.0.2 // indirect
@@ -61,16 +63,16 @@ require (
github.com/coreos/go-oidc/v3 v3.17.0 // indirect github.com/coreos/go-oidc/v3 v3.17.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.4 // indirect github.com/go-jose/go-jose/v3 v3.0.5 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect
github.com/google/go-tpm v0.9.8 // indirect github.com/google/go-tpm v0.9.8 // indirect
github.com/google/go-tspi v0.3.0 // indirect github.com/google/go-tspi v0.3.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect
github.com/googleapis/gax-go/v2 v2.19.0 // indirect github.com/googleapis/gax-go/v2 v2.22.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/jackc/pgx/v5 v5.8.0 // indirect github.com/jackc/pgx/v5 v5.9.2 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
@@ -87,7 +89,6 @@ require (
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect github.com/zeebo/blake3 v0.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.43.0 // indirect go.opentelemetry.io/contrib/propagators/aws v1.43.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.43.0 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.43.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.43.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.43.0 // indirect
@@ -104,14 +105,13 @@ require (
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 // indirect
go.opentelemetry.io/otel/log v0.19.0 // indirect go.opentelemetry.io/otel/log v0.19.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v2 v2.4.4 // 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.36.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect
google.golang.org/api v0.272.0 // indirect google.golang.org/api v0.277.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // 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
) )
@@ -129,7 +129,7 @@ require (
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.2.0 // indirect github.com/dgraph-io/ristretto v0.2.0 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dlclark/regexp2 v1.12.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
@@ -149,7 +149,7 @@ require (
github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/pires/go-proxyproto v0.11.0 github.com/pires/go-proxyproto v0.12.0
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.6.2 github.com/prometheus/client_model v0.6.2
github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/common v0.67.5 // indirect
@@ -168,11 +168,11 @@ require (
go.opentelemetry.io/otel/trace v1.43.0 go.opentelemetry.io/otel/trace v1.43.0
go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/mod v0.34.0 // indirect golang.org/x/mod v0.35.0 // indirect
golang.org/x/sys v0.43.0 golang.org/x/sys v0.45.0
golang.org/x/text v0.36.0 golang.org/x/text v0.37.0 // indirect
golang.org/x/tools v0.43.0 // indirect golang.org/x/tools v0.44.0 // indirect
google.golang.org/grpc v1.80.0 // indirect google.golang.org/grpc v1.81.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
) )
+92 -92
View File
@@ -2,18 +2,18 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= cloud.google.com/go/iam v1.7.0 h1:JD3zh0C6LHl16aCn5Akff0+GELdp1+4hmh6ndoFLl8U=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= cloud.google.com/go/iam v1.7.0/go.mod h1:tetWZW1PD/m6vcuY2Zj/aU0eCHNPuxedbnbRTyKXvdY=
cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU= cloud.google.com/go/kms v1.31.0 h1:LS8N92OxFDgOLg5NCo3OmbvjtQAIVT5gUHVLKIDHaFE=
cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58= cloud.google.com/go/kms v1.31.0/go.mod h1:YIyXZym11R5uovJJt4oN5eUL3oPmirF3yKeIh6QAf4U=
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY=
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E=
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE= code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM= 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=
@@ -28,8 +28,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DeRuina/timberjack v1.4.1 h1:JftM5HN/ITKehAXjtdbGqN5XZIS1biHm7VSjU0Qbtqg= github.com/DeRuina/timberjack v1.4.2 h1:4bKlzhKdsR+2oNkgef9mqb4n11ICow8VK88RfzJPzN8=
github.com/DeRuina/timberjack v1.4.1/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE= github.com/DeRuina/timberjack v1.4.2/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=
github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk= github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk=
github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
@@ -43,8 +43,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM=
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
@@ -53,40 +53,40 @@ github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmO
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw= github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8=
github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc=
github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU=
github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= github.com/aws/aws-sdk-go-v2/config v1.32.17/go.mod h1:OXqUMzgXytfoF9JaKkhrOYsyh72t9G+MJH8mMRaexOE=
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= github.com/aws/aws-sdk-go-v2/credentials v1.19.16 h1:r3RJBuU7X9ibt8RHbMjWE6y60QbKBiII6wSrXnapxSU=
github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= github.com/aws/aws-sdk-go-v2/credentials v1.19.16/go.mod h1:6cx7zqDENJDbBIIWX6P8s0h6hqHC8Avbjh9Dseo27ug=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 h1:UuSfcORqNSz/ey3VPRS8TcVH2Ikf0/sC+Hdj400QI6U=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23/go.mod h1:+G/OSGiOFnSOkYloKj/9M35s74LgVAdJBSD5lsFfqKg=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 h1:GpT/TrnBYuE5gan2cZbTtvP+JlHsutdmlV2YfEyNde0=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23/go.mod h1:xYWD6BS9ywC5bS3sz9Xh04whO/hzK2plt2Zkyrp4JuA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 h1:bpd8vxhlQi2r1hiueOw02f/duEPTMK59Q4QMAoTTtTo=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23/go.mod h1:15DfR2nw+CRHIk0tqNyifu3G1YdAOy68RftkhMDDwYk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 h1:OQqn11BtaYv1WLUowvcA30MpzIu8Ti4pcLPIIyoKZrA=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24/go.mod h1:X5ZJyfwVrWA96GzPmUCWFQaEARPR7gCrpq2E92PJwAE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 h1:FLudkZLt5ci0ozzgkVo8BJGwvqNaZbTWb3UcucAateA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9/go.mod h1:w7wZ/s9qK7c8g4al+UyoF1Sp/Z45UwMGcqIzLWVQHWk=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 h1:pbrxO/kuIwgEsOPLkaHu0O+m4fNgLU8B3vxQ+72jTPw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23/go.mod h1:/CMNUqoj46HpS3MNRDEDIwcgEnrtZlKRaHNaHxIFpNA=
github.com/aws/aws-sdk-go-v2/service/kms v1.50.3 h1:s/zDSG/a/Su9aX+v0Ld9cimUCdkr5FWPmBV8owaEbZY= github.com/aws/aws-sdk-go-v2/service/kms v1.51.1 h1:zuSf4olLKZW8cF/W9Y5wvGT+/0raY/3kVp49KsGs0QY=
github.com/aws/aws-sdk-go-v2/service/kms v1.50.3/go.mod h1:/iSgiUor15ZuxFGQSTf3lA2FmKxFsQoc2tADOarQBSw= github.com/aws/aws-sdk-go-v2/service/kms v1.51.1/go.mod h1:Y0+uxvxz6ib4KktRdK0V4X45Vcs/JyYoz8H71pO8xeI=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow= github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 h1:TdJ+HdzOBhU8+iVAOGUTU63VXopcumCOF1paFulHWZc=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE= github.com/aws/aws-sdk-go-v2/service/signin v1.0.11/go.mod h1:R82ZRExE/nheo0N+T8zHPcLRTcH8MGsnR3BiVGX0TwI=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o= github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 h1:7byT8HUWrgoRp6sXjxtZwgOKfhss5fW6SkLBtqzgRoE=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8= github.com/aws/aws-sdk-go-v2/service/sso v1.30.17/go.mod h1:xNWknVi4Ezm1vg1QsB/5EWpAJURq22uqd38U8qKvOJc=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 h1:+1Kl1zx6bWi4X7cKi3VYh29h8BvsCoHQEQ6ST9X8w7w=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21/go.mod h1:4vIRDq+CJB2xFAXZ+YgGUTiEft7oAQlhIs71xcSeuVg=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOItExNM9L1euNuh/fk=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc= github.com/caddyserver/certmagic v0.25.3 h1:mGf5ba8F7xA4c5jfDZZbK2buY1VEkbnwpMDixaju94A=
github.com/caddyserver/certmagic v0.25.2/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg= github.com/caddyserver/certmagic v0.25.3/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA=
github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE= github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0= github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0=
@@ -133,8 +133,8 @@ github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WA
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
@@ -149,8 +149,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -168,8 +168,8 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/cel-go v0.28.0 h1:KjSWstCpz/MN5t4a8gnGJNIYUsJRpdi/r97xWDphIQc= github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM=
github.com/google/cel-go v0.28.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo= github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo=
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k= github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k=
@@ -187,10 +187,10 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE= github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4=
github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA= github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
@@ -205,14 +205,14 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -259,8 +259,8 @@ github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhM
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4= github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -280,8 +280,8 @@ github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEy
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
@@ -377,8 +377,8 @@ go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 h1:w3zlHYETbDwXyWHZlyyR58
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0/go.mod h1:GR/mClR2nn7vE8RLwxKjoBNg+QtgdDhRzxVa93koy5o= go.opentelemetry.io/contrib/bridges/prometheus v0.68.0/go.mod h1:GR/mClR2nn7vE8RLwxKjoBNg+QtgdDhRzxVa93koy5o=
go.opentelemetry.io/contrib/exporters/autoexport v0.68.0 h1:0D3GFvELGIwQGfC6agLsbrEYSGWZTRTxIXxcQUqrOuk= go.opentelemetry.io/contrib/exporters/autoexport v0.68.0 h1:0D3GFvELGIwQGfC6agLsbrEYSGWZTRTxIXxcQUqrOuk=
go.opentelemetry.io/contrib/exporters/autoexport v0.68.0/go.mod h1:DM2NV7Zb8CcGeVPt6glouY0FAiwZQ/iqgcWExhgWeN8= go.opentelemetry.io/contrib/exporters/autoexport v0.68.0/go.mod h1:DM2NV7Zb8CcGeVPt6glouY0FAiwZQ/iqgcWExhgWeN8=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/contrib/propagators/autoprop v0.68.0 h1:wLGFvNBPqQhzBn0QRBZjrriH8lZ9gqtTz8ufHEjLg7k= go.opentelemetry.io/contrib/propagators/autoprop v0.68.0 h1:wLGFvNBPqQhzBn0QRBZjrriH8lZ9gqtTz8ufHEjLg7k=
@@ -431,8 +431,8 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.step.sm/crypto v0.77.2 h1:qFjjei+RHc5kP5R7NW9OUWT7SqWIuAOvOkXqg4fNWj8= go.step.sm/crypto v0.81.0 h1:e+ouzpNt3Xm4dp7HGXhgYB5y4iFik3vh3phHKWmvugU=
go.step.sm/crypto v0.77.2/go.mod h1:W0YJb9onM5l78qgkXIJ2Up6grnwW8EtpCKIza/NCg0o= go.step.sm/crypto v0.81.0/go.mod h1:fsTizqQeASjTXnbv9O00XtRlIuXRkCdoRiJNyXGQujc=
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=
@@ -441,8 +441,8 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
@@ -456,10 +456,10 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260323153451-8400f4a93807 h1:sQVhWLXbNsa8CTzHOX3IHc7C4Q2JyxI5AweuMQZ/5H0= golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 h1:FmKxj9ocLKn45jiR2jQMwCVhDvaK7fKQFzfuT9GvyK8=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260323153451-8400f4a93807/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs= golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -467,8 +467,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -477,8 +477,8 @@ 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.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -506,8 +506,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -517,8 +517,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -528,8 +528,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -538,21 +538,21 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA= google.golang.org/api v0.277.0 h1:HJfyJUiNeBBUMai7ez8u14wkp/gH/I4wpGbbO9o+cSk=
google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA= google.golang.org/api v0.277.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ=
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI= google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI=
google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs= google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA= google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
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=
+54
View File
@@ -0,0 +1,54 @@
package internal
import (
"net/http"
"strings"
"go.uber.org/zap/zapcore"
)
// LoggableHTTPHeader makes an HTTP header loggable with zap.Object().
// Headers with potentially sensitive information (Cookie, Set-Cookie,
// Authorization, and Proxy-Authorization) are logged with empty values.
type LoggableHTTPHeader struct {
http.Header
ShouldLogCredentials bool
}
// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
func (h LoggableHTTPHeader) MarshalLogObject(enc zapcore.ObjectEncoder) error {
if h.Header == nil {
return nil
}
for key, val := range h.Header {
if !h.ShouldLogCredentials {
switch strings.ToLower(key) {
case "cookie", "set-cookie", "authorization", "proxy-authorization":
val = []string{"REDACTED"} // see #5669. I still think ▒▒▒▒ would be cool.
}
}
enc.AddArray(key, LoggableStringArray(val))
}
return nil
}
// LoggableStringArray makes a slice of strings marshalable for logging.
type LoggableStringArray []string
// MarshalLogArray satisfies the zapcore.ArrayMarshaler interface.
func (sa LoggableStringArray) MarshalLogArray(enc zapcore.ArrayEncoder) error {
if sa == nil {
return nil
}
for _, s := range sa {
enc.AppendString(s)
}
return nil
}
// Interface guards
var (
_ zapcore.ObjectMarshaler = (*LoggableHTTPHeader)(nil)
_ zapcore.ArrayMarshaler = (*LoggableStringArray)(nil)
)
+10 -14
View File
@@ -30,10 +30,6 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
func reuseUnixSocket(_, _ string) (any, error) {
return nil, nil
}
func listenReusable(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (any, error) { func listenReusable(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (any, error) {
var socketFile *os.File var socketFile *os.File
@@ -120,8 +116,8 @@ func listenReusable(ctx context.Context, lnKey string, network, address string,
// re-wrapped in a new fakeCloseListener each time the listener // re-wrapped in a new fakeCloseListener each time the listener
// is reused. This type is atomic and values must not be copied. // is reused. This type is atomic and values must not be copied.
type fakeCloseListener struct { type fakeCloseListener struct {
closed int32 // accessed atomically; belongs to this struct only closed atomic.Bool
*sharedListener // embedded, so we also become a net.Listener *sharedListener // embedded, so we also become a net.Listener
keepAliveConfig net.KeepAliveConfig keepAliveConfig net.KeepAliveConfig
} }
@@ -131,7 +127,7 @@ type canSetKeepAliveConfig interface {
func (fcl *fakeCloseListener) Accept() (net.Conn, error) { func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
// if the listener is already "closed", return error // if the listener is already "closed", return error
if atomic.LoadInt32(&fcl.closed) == 1 { if fcl.closed.Load() {
return nil, fakeClosedErr(fcl) return nil, fakeClosedErr(fcl)
} }
@@ -155,7 +151,7 @@ func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
// that we set when Close() was called, and return a non-temporary and // that we set when Close() was called, and return a non-temporary and
// non-timeout error value to the caller, masking the "true" error, so // non-timeout error value to the caller, masking the "true" error, so
// that server loops / goroutines won't retry, linger, and leak // that server loops / goroutines won't retry, linger, and leak
if atomic.LoadInt32(&fcl.closed) == 1 { if fcl.closed.Load() {
// we dereference the sharedListener explicitly even though it's embedded // we dereference the sharedListener explicitly even though it's embedded
// so that it's clear in the code that side-effects are shared with other // so that it's clear in the code that side-effects are shared with other
// users of this listener, not just our own reference to it; we also don't // users of this listener, not just our own reference to it; we also don't
@@ -175,7 +171,7 @@ func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
// underlying listener. The underlying listener is only closed // underlying listener. The underlying listener is only closed
// if the caller is the last known user of the socket. // if the caller is the last known user of the socket.
func (fcl *fakeCloseListener) Close() error { func (fcl *fakeCloseListener) Close() error {
if atomic.CompareAndSwapInt32(&fcl.closed, 0, 1) { if fcl.closed.CompareAndSwap(false, true) {
// There are two ways I know of to get an Accept() // There are two ways I know of to get an Accept()
// function to return to the server loop that called // function to return to the server loop that called
// it: close the listener, or set a deadline in the // it: close the listener, or set a deadline in the
@@ -238,13 +234,13 @@ func (sl *sharedListener) Destruct() error {
// fakeClosePacketConn is like fakeCloseListener, but for PacketConns, // fakeClosePacketConn is like fakeCloseListener, but for PacketConns,
// or more specifically, *net.UDPConn // or more specifically, *net.UDPConn
type fakeClosePacketConn struct { type fakeClosePacketConn struct {
closed int32 // accessed atomically; belongs to this struct only closed atomic.Bool
*sharedPacketConn // embedded, so we also become a net.PacketConn; its key is used in Close *sharedPacketConn // embedded, so we also become a net.PacketConn; its key is used in Close
} }
func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
// if the listener is already "closed", return error // if the listener is already "closed", return error
if atomic.LoadInt32(&fcpc.closed) == 1 { if fcpc.closed.Load() {
return 0, nil, &net.OpError{ return 0, nil, &net.OpError{
Op: "readfrom", Op: "readfrom",
Net: fcpc.LocalAddr().Network(), Net: fcpc.LocalAddr().Network(),
@@ -258,7 +254,7 @@ func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err e
if err != nil { if err != nil {
// this server was stopped, so clear the deadline and let // this server was stopped, so clear the deadline and let
// any new server continue reading; but we will exit // any new server continue reading; but we will exit
if atomic.LoadInt32(&fcpc.closed) == 1 { if fcpc.closed.Load() {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() { if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
if err = fcpc.SetReadDeadline(time.Time{}); err != nil { if err = fcpc.SetReadDeadline(time.Time{}); err != nil {
return n, addr, err return n, addr, err
@@ -273,7 +269,7 @@ func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err e
// Close won't close the underlying socket unless there is no more reference, then listenerPool will close it. // Close won't close the underlying socket unless there is no more reference, then listenerPool will close it.
func (fcpc *fakeClosePacketConn) Close() error { func (fcpc *fakeClosePacketConn) Close() error {
if atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) { if fcpc.closed.CompareAndSwap(false, true) {
_ = fcpc.SetReadDeadline(time.Now()) // unblock ReadFrom() calls to kick old servers out of their loops _ = fcpc.SetReadDeadline(time.Now()) // unblock ReadFrom() calls to kick old servers out of their loops
_, _ = listenerPool.Delete(fcpc.sharedPacketConn.key) _, _ = listenerPool.Delete(fcpc.sharedPacketConn.key)
} }
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build (!unix || solaris) && !windows
package caddy
func reuseUnixSocket(_, _ string) (any, error) {
return nil, nil
}
+89
View File
@@ -0,0 +1,89 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package caddy
import (
"errors"
"fmt"
"io/fs"
"net"
"os"
"strings"
"syscall"
"time"
)
var errUnixSocketAlreadyInUse = errors.New("unix socket is already in use by another process")
func reuseUnixSocket(network, addr string) (any, error) {
if !IsUnixNetwork(network) {
return nil, nil
}
// Note: This is here mainly for proper compatibility, because Unix sockets with abstract names are in an interesting limbo state on Windows:
// Go already translates `@` characters to `\0` for Windows: https://github.com/golang/go/blob/65d5c5f6dd8aa7b221cff6ec3f5101ea2e5f3efa/src/syscall/syscall_windows.go#L910
// ...but there still is an open issue about the fact that this is not properly supported: https://github.com/microsoft/WSL/issues/4240#issuecomment-620805115
// The main issue is that the original announcement proclaimed support for this feature, but it was (apparently) never implemented: https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/
isAbstractUnixSocket := strings.HasPrefix(addr, "@")
if isAbstractUnixSocket {
// Abstract Unix sockets do not require us to remove stale socket files.
return nil, nil
}
// On Windows, we're using the `fakeCloseListener` wrappers around a single, ever-living listener.
// So, if there's an active listener entry in the pool, we're the current owner of the Unix socket file.
_, socketBelongsToCurrentProcess := listenerPool.References(listenerKey(network, addr))
if socketBelongsToCurrentProcess {
// Reuse/cleanup is entirely handled by the refcounting mechanism in `listenerPool`.
return nil, nil
}
// If the socket file does not exist or has no backing server process, this will fail instantly.
connection, err := net.DialTimeout("unix", addr, 10*time.Millisecond)
if err == nil {
connection.Close()
return nil, fmt.Errorf("cannot reuse socket %v: %w", addr, errUnixSocketAlreadyInUse)
}
// Windows returns this error code both if the socket file does not exist and if it isn't backed by a server process anymore.
// See: https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2#wsaeconnrefused
const WSAECONNREFUSED syscall.Errno = 10061
var errno syscall.Errno
hasNoListeningServerProcess := errors.As(err, &errno) && errno == WSAECONNREFUSED
if !hasNoListeningServerProcess {
return nil, fmt.Errorf("cannot reuse socket %v: %w", addr, errUnixSocketAlreadyInUse)
}
// If the socket file exists, it hasn't been created by our process, and it seemingly
// isn't backed by a server process anymore. Try to delete it so we can bind to it later.
err = os.Remove(addr)
if err == nil {
return nil, nil
} else if errors.Is(err, fs.ErrNotExist) {
// Either the file didn't exist in the first place, or it was deleted before we were able to.
return nil, nil
} else {
// We failed to delete the file. Likely, it belongs to another (active) process.
return nil, err
}
}
+12 -10
View File
@@ -63,7 +63,7 @@ func reuseUnixSocket(network, addr string) (any, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
atomic.AddInt32(unixSocket.count, 1) unixSocket.count.Add(1)
unixSockets[socketKey] = &unixListener{ln.(*net.UnixListener), socketKey, unixSocket.count} unixSockets[socketKey] = &unixListener{ln.(*net.UnixListener), socketKey, unixSocket.count}
case *unixConn: case *unixConn:
@@ -71,7 +71,7 @@ func reuseUnixSocket(network, addr string) (any, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
atomic.AddInt32(unixSocket.count, 1) unixSocket.count.Add(1)
unixSockets[socketKey] = &unixConn{pc.(*net.UnixConn), socketKey, unixSocket.count} unixSockets[socketKey] = &unixConn{pc.(*net.UnixConn), socketKey, unixSocket.count}
} }
@@ -165,8 +165,9 @@ func listenReusable(ctx context.Context, lnKey string, network, address string,
if !fd { if !fd {
// TODO: Not 100% sure this is necessary, but we do this for net.UnixListener, so... // TODO: Not 100% sure this is necessary, but we do this for net.UnixListener, so...
if unix, ok := ln.(*net.UnixConn); ok { if unix, ok := ln.(*net.UnixConn); ok {
one := int32(1) cnt := new(atomic.Int32)
ln = &unixConn{unix, lnKey, &one} cnt.Store(1)
ln = &unixConn{unix, lnKey, cnt}
unixSockets[lnKey] = ln.(*unixConn) unixSockets[lnKey] = ln.(*unixConn)
} }
} }
@@ -181,8 +182,9 @@ func listenReusable(ctx context.Context, lnKey string, network, address string,
// (we do our own "unlink on close" -- not required, but more tidy) // (we do our own "unlink on close" -- not required, but more tidy)
if unix, ok := ln.(*net.UnixListener); ok { if unix, ok := ln.(*net.UnixListener); ok {
unix.SetUnlinkOnClose(false) unix.SetUnlinkOnClose(false)
one := int32(1) cnt := new(atomic.Int32)
ln = &unixListener{unix, lnKey, &one} cnt.Store(1)
ln = &unixListener{unix, lnKey, cnt}
unixSockets[lnKey] = ln.(*unixListener) unixSockets[lnKey] = ln.(*unixListener)
} }
} }
@@ -216,11 +218,11 @@ func reusePort(network, address string, conn syscall.RawConn) error {
type unixListener struct { type unixListener struct {
*net.UnixListener *net.UnixListener
mapKey string mapKey string
count *int32 // accessed atomically count *atomic.Int32
} }
func (uln *unixListener) Close() error { func (uln *unixListener) Close() error {
newCount := atomic.AddInt32(uln.count, -1) newCount := uln.count.Add(-1)
if newCount == 0 { if newCount == 0 {
file, err := uln.File() file, err := uln.File()
var name string var name string
@@ -242,11 +244,11 @@ func (uln *unixListener) Close() error {
type unixConn struct { type unixConn struct {
*net.UnixConn *net.UnixConn
mapKey string mapKey string
count *int32 // accessed atomically count *atomic.Int32
} }
func (uc *unixConn) Close() error { func (uc *unixConn) Close() error {
newCount := atomic.AddInt32(uc.count, -1) newCount := uc.count.Add(-1)
if newCount == 0 { if newCount == 0 {
file, err := uc.File() file, err := uc.File()
var name string var name string
+19 -6
View File
@@ -462,7 +462,10 @@ func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config
sqs := newSharedQUICState(tlsConf) sqs := newSharedQUICState(tlsConf)
// http3.ConfigureTLSConfig only uses this field and tls App sets this field as well // http3.ConfigureTLSConfig only uses this field and tls App sets this field as well
//nolint:gosec //nolint:gosec
quicTlsConfig := &tls.Config{GetConfigForClient: sqs.getConfigForClient} quicTlsConfig := &tls.Config{
GetConfigForClient: sqs.getConfigForClient,
GetEncryptedClientHelloKeys: sqs.getEncryptedClientHelloKeys,
}
// Require clients to verify their source address when we're handling more than 1000 handshakes per second. // Require clients to verify their source address when we're handling more than 1000 handshakes per second.
// TODO: make tunable? // TODO: make tunable?
limiter := rate.NewLimiter(1000, 1000) limiter := rate.NewLimiter(1000, 1000)
@@ -540,6 +543,16 @@ func (sqs *sharedQUICState) getConfigForClient(ch *tls.ClientHelloInfo) (*tls.Co
return sqs.activeTlsConf.GetConfigForClient(ch) return sqs.activeTlsConf.GetConfigForClient(ch)
} }
// getEncryptedClientHelloKeys is used as tls.Config's GetEncryptedClientHelloKeys field.
func (sqs *sharedQUICState) getEncryptedClientHelloKeys(ch *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
sqs.rmu.RLock()
defer sqs.rmu.RUnlock()
if sqs.activeTlsConf.GetEncryptedClientHelloKeys == nil {
return nil, nil
}
return sqs.activeTlsConf.GetEncryptedClientHelloKeys(ch)
}
// addState adds tls.Config and activeRequests to the map if not present and returns the corresponding context and its cancelFunc // addState adds tls.Config and activeRequests to the map if not present and returns the corresponding context and its cancelFunc
// so that when cancelled, the active tls.Config will change // so that when cancelled, the active tls.Config will change
func (sqs *sharedQUICState) addState(tlsConfig *tls.Config) (context.Context, context.CancelCauseFunc) { func (sqs *sharedQUICState) addState(tlsConfig *tls.Config) (context.Context, context.CancelCauseFunc) {
@@ -611,8 +624,8 @@ func fakeClosedErr(l interface{ Addr() net.Addr }) error {
var errFakeClosed = fmt.Errorf("QUIC listener 'closed' 😉") var errFakeClosed = fmt.Errorf("QUIC listener 'closed' 😉")
type fakeCloseQuicListener struct { type fakeCloseQuicListener struct {
closed int32 // accessed atomically; belongs to this struct only closed atomic.Int32
*sharedQuicListener // embedded, so we also become a quic.EarlyListener *sharedQuicListener // embedded, so we also become a quic.EarlyListener
context context.Context context context.Context
contextCancel context.CancelCauseFunc contextCancel context.CancelCauseFunc
} }
@@ -629,16 +642,16 @@ func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (*quic.Conn, error)
} }
// if the listener is "closed", return a fake closed error instead // if the listener is "closed", return a fake closed error instead
if atomic.LoadInt32(&fcql.closed) == 1 && errors.Is(err, context.Canceled) { if fcql.closed.Load() == 1 && errors.Is(err, context.Canceled) {
return nil, fakeClosedErr(fcql) return nil, fakeClosedErr(fcql)
} }
return nil, err return nil, err
} }
func (fcql *fakeCloseQuicListener) Close() error { func (fcql *fakeCloseQuicListener) Close() error {
if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) { if fcql.closed.CompareAndSwap(0, 1) {
fcql.contextCancel(errFakeClosed) fcql.contextCancel(errFakeClosed)
} else if atomic.CompareAndSwapInt32(&fcql.closed, 1, 2) { } else if fcql.closed.CompareAndSwap(1, 2) {
_, _ = listenerPool.Delete(fcql.sharedQuicListener.key) _, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
} }
return nil return nil
+58
View File
@@ -15,6 +15,7 @@
package caddy package caddy
import ( import (
"crypto/tls"
"reflect" "reflect"
"testing" "testing"
@@ -175,6 +176,63 @@ func TestJoinNetworkAddress(t *testing.T) {
} }
} }
func TestSharedQUICStateGetEncryptedClientHelloKeys(t *testing.T) {
hello := &tls.ClientHelloInfo{ServerName: "example.com"}
initialKeys := []tls.EncryptedClientHelloKey{{Config: []byte("initial"), PrivateKey: []byte("initial-key")}}
updatedKeys := []tls.EncryptedClientHelloKey{{Config: []byte("updated"), PrivateKey: []byte("updated-key")}}
initialConfig := &tls.Config{
GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
return nil, nil
},
GetEncryptedClientHelloKeys: func(*tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
return initialKeys, nil
},
}
sqs := newSharedQUICState(initialConfig)
keys, err := sqs.getEncryptedClientHelloKeys(hello)
if err != nil {
t.Fatalf("getting initial ECH keys: %v", err)
}
if !reflect.DeepEqual(keys, initialKeys) {
t.Fatalf("unexpected initial ECH keys: got %#v, want %#v", keys, initialKeys)
}
updatedConfig := &tls.Config{
GetConfigForClient: func(*tls.ClientHelloInfo) (*tls.Config, error) {
return nil, nil
},
GetEncryptedClientHelloKeys: func(*tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
return updatedKeys, nil
},
}
_, cancel := sqs.addState(updatedConfig)
sqs.rmu.Lock()
sqs.activeTlsConf = updatedConfig
sqs.rmu.Unlock()
keys, err = sqs.getEncryptedClientHelloKeys(hello)
if err != nil {
t.Fatalf("getting updated ECH keys: %v", err)
}
if !reflect.DeepEqual(keys, updatedKeys) {
t.Fatalf("unexpected updated ECH keys: got %#v, want %#v", keys, updatedKeys)
}
cancel(nil)
keys, err = sqs.getEncryptedClientHelloKeys(hello)
if err != nil {
t.Fatalf("getting restored ECH keys: %v", err)
}
if !reflect.DeepEqual(keys, initialKeys) {
t.Fatalf("unexpected restored ECH keys: got %#v, want %#v", keys, initialKeys)
}
}
func TestParseNetworkAddress(t *testing.T) { func TestParseNetworkAddress(t *testing.T) {
for i, tc := range []struct { for i, tc := range []struct {
input string input string
+17 -37
View File
@@ -20,7 +20,6 @@ import (
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
"maps"
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
@@ -69,6 +68,7 @@ func init() {
// `{http.request.orig_uri.path.dir}` | The request's original directory // `{http.request.orig_uri.path.dir}` | The request's original directory
// `{http.request.orig_uri.path.file}` | The request's original filename // `{http.request.orig_uri.path.file}` | The request's original filename
// `{http.request.orig_uri.query}` | The request's original query string (without `?`) // `{http.request.orig_uri.query}` | The request's original query string (without `?`)
// `{http.request.orig_uri.prefixed_query}` | The request's original query string with a `?` prefix, if non-empty
// `{http.request.port}` | The port part of the request's Host header // `{http.request.port}` | The port part of the request's Host header
// `{http.request.proto}` | The protocol of the request // `{http.request.proto}` | The protocol of the request
// `{http.request.local.host}` | The host (IP) part of the local address the connection arrived on // `{http.request.local.host}` | The host (IP) part of the local address the connection arrived on
@@ -98,11 +98,15 @@ func init() {
// `{http.request.tls.client.san.ips.*}` | SAN IP addresses (index optional) // `{http.request.tls.client.san.ips.*}` | SAN IP addresses (index optional)
// `{http.request.tls.client.san.uris.*}` | SAN URIs (index optional) // `{http.request.tls.client.san.uris.*}` | SAN URIs (index optional)
// `{http.request.uri}` | The full request URI // `{http.request.uri}` | The full request URI
// `{http.request.uri_escaped}` | The full request URI with query-style URL encoding applied (using url.QueryEscape)
// `{http.request.uri.path}` | The path component of the request URI // `{http.request.uri.path}` | The path component of the request URI
// `{http.request.uri.path_escaped}` | The path component of the request URI with query-style URL encoding applied (using url.QueryEscape)
// `{http.request.uri.path.*}` | Parts of the path, split by `/` (0-based from left) // `{http.request.uri.path.*}` | Parts of the path, split by `/` (0-based from left)
// `{http.request.uri.path.dir}` | The directory, excluding leaf filename // `{http.request.uri.path.dir}` | The directory, excluding leaf filename
// `{http.request.uri.path.file}` | The filename of the path, excluding directory // `{http.request.uri.path.file}` | The filename of the path, excluding directory
// `{http.request.uri.query}` | The query string (without `?`) // `{http.request.uri.query}` | The query string (without `?`)
// `{http.request.uri.query_escaped}` | The query string with query-style URL encoding applied (using url.QueryEscape)
// `{http.request.uri.prefixed_query}` | The query string with a `?` prefix, if non-empty
// `{http.request.uri.query.*}` | Individual query string value // `{http.request.uri.query.*}` | Individual query string value
// `{http.response.header.*}` | Specific response header field // `{http.response.header.*}` | Specific response header field
// `{http.vars.*}` | Custom variables in the HTTP handler chain // `{http.vars.*}` | Custom variables in the HTTP handler chain
@@ -203,6 +207,9 @@ func (app *App) Provision(ctx caddy.Context) error {
app.Metrics.httpMetrics = &httpMetrics{} app.Metrics.httpMetrics = &httpMetrics{}
// Scan config for allowed hosts to prevent cardinality explosion // Scan config for allowed hosts to prevent cardinality explosion
app.Metrics.scanConfigForHosts(app) app.Metrics.scanConfigForHosts(app)
if err := app.Metrics.provisionOTLP(ctx); err != nil {
return err
}
} }
// prepare each server // prepare each server
oldContext := ctx.Context oldContext := ctx.Context
@@ -214,8 +221,6 @@ func (app *App) Provision(ctx caddy.Context) error {
srv.ctx = ctx srv.ctx = ctx
srv.logger = app.logger.Named("log") srv.logger = app.logger.Named("log")
srv.errorLogger = app.logger.Named("log.error") srv.errorLogger = app.logger.Named("log.error")
srv.shutdownAtMu = new(sync.RWMutex)
if srv.Metrics != nil { if srv.Metrics != nil {
srv.logger.Warn("per-server 'metrics' is deprecated; use 'metrics' in the root 'http' app instead") srv.logger.Warn("per-server 'metrics' is deprecated; use 'metrics' in the root 'http' app instead")
app.Metrics = cmp.Or(app.Metrics, &Metrics{ app.Metrics = cmp.Or(app.Metrics, &Metrics{
@@ -235,12 +240,7 @@ func (app *App) Provision(ctx caddy.Context) error {
// if no protocols configured explicitly, enable all except h2c // if no protocols configured explicitly, enable all except h2c
if len(srv.Protocols) == 0 { if len(srv.Protocols) == 0 {
srv.Protocols = []string{"h1", "h2", "h3"} srv.Protocols = srv.protocolsWithDefaults()
}
srvProtocolsUnique := map[string]struct{}{}
for _, srvProtocol := range srv.Protocols {
srvProtocolsUnique[srvProtocol] = struct{}{}
} }
if srv.ListenProtocols != nil { if srv.ListenProtocols != nil {
@@ -251,31 +251,7 @@ func (app *App) Provision(ctx caddy.Context) error {
for i, lnProtocols := range srv.ListenProtocols { for i, lnProtocols := range srv.ListenProtocols {
if lnProtocols != nil { if lnProtocols != nil {
// populate empty listen protocols with server protocols srv.ListenProtocols[i] = srv.listenerProtocolsWithDefaults(lnProtocols)
lnProtocolsDefault := false
var lnProtocolsInclude []string
srvProtocolsInclude := maps.Clone(srvProtocolsUnique)
// keep existing listener protocols unless they are empty
for _, lnProtocol := range lnProtocols {
if lnProtocol == "" {
lnProtocolsDefault = true
} else {
lnProtocolsInclude = append(lnProtocolsInclude, lnProtocol)
delete(srvProtocolsInclude, lnProtocol)
}
}
// append server protocols to listener protocols if any listener protocols were empty
if lnProtocolsDefault {
for _, srvProtocol := range srv.Protocols {
if _, ok := srvProtocolsInclude[srvProtocol]; ok {
lnProtocolsInclude = append(lnProtocolsInclude, srvProtocol)
}
}
}
srv.ListenProtocols[i] = lnProtocolsInclude
} }
} }
} }
@@ -689,9 +665,7 @@ func (app *App) Stop() error {
for _, addr := range na.Expand() { for _, addr := range na.Expand() {
if caddy.ListenerUsage(addr.Network, addr.JoinHostPort(0)) < 2 { if caddy.ListenerUsage(addr.Network, addr.JoinHostPort(0)) < 2 {
app.logger.Debug("listener closing and shutdown delay is configured", zap.String("address", addr.String())) app.logger.Debug("listener closing and shutdown delay is configured", zap.String("address", addr.String()))
server.shutdownAtMu.Lock() server.shutdownAt.Store(&scheduledTime)
server.shutdownAt = scheduledTime
server.shutdownAtMu.Unlock()
delay = true delay = true
} else { } else {
app.logger.Debug("shutdown delay configured but listener will remain open", zap.String("address", addr.String())) app.logger.Debug("shutdown delay configured but listener will remain open", zap.String("address", addr.String()))
@@ -816,6 +790,12 @@ func (app *App) Stop() error {
} }
} }
// flush and shut down the OTLP metrics exporter (if configured) so any
// last data point reaches the collector before the process exits
if err := app.Metrics.shutdown(ctx); err != nil {
app.logger.Error("shutting down OTLP metrics", zap.Error(err))
}
app.stopped = true app.stopped = true
return nil return nil
} }
+46 -8
View File
@@ -173,7 +173,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
for d := range serverDomainSet { for d := range serverDomainSet {
echDomains = append(echDomains, d) echDomains = append(echDomains, d)
} }
app.tlsApp.RegisterServerNames(echDomains) app.tlsApp.RegisterServerNames(echDomains, httpsRRALPNs(srv))
// nothing more to do here if there are no domains that qualify for // nothing more to do here if there are no domains that qualify for
// automatic HTTPS and there are no explicit TLS connection policies: // automatic HTTPS and there are no explicit TLS connection policies:
@@ -258,18 +258,13 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
// an empty string to indicate a catch-all, which we have to // an empty string to indicate a catch-all, which we have to
// treat special later // treat special later
if len(serverDomainSet) == 0 { if len(serverDomainSet) == 0 {
redirDomains[""] = append(redirDomains[""], addr) app.recordAutoHTTPSRedirectAddress(redirDomains, "", addr)
continue continue
} }
// ...and associate it with each domain in this server // ...and associate it with each domain in this server
for d := range serverDomainSet { for d := range serverDomainSet {
// if this domain is used on more than one HTTPS-enabled app.recordAutoHTTPSRedirectAddress(redirDomains, d, addr)
// port, we'll have to choose one, so prefer the HTTPS port
if _, ok := redirDomains[d]; !ok ||
addr.StartPort == uint(app.httpsPort()) {
redirDomains[d] = append(redirDomains[d], addr)
}
} }
} }
} }
@@ -517,6 +512,35 @@ redirServersLoop:
return nil return nil
} }
// recordAutoHTTPSRedirectAddress stores redirect destinations for one domain
// using a single winning port while keeping all bind addresses on that port.
//
// This is needed to avoid two opposite regressions in auto-HTTPS redirects:
// preserve all listener addresses when a site binds multiple addresses on the
// same HTTPS port, but do not mix in alternate HTTPS ports when the canonical
// app HTTPS port is also available.
func (app *App) recordAutoHTTPSRedirectAddress(redirDomains map[string][]caddy.NetworkAddress, domain string, addr caddy.NetworkAddress) {
existing := redirDomains[domain]
if len(existing) == 0 {
redirDomains[domain] = []caddy.NetworkAddress{addr}
return
}
existingPort := existing[0].StartPort
if addr.StartPort != existingPort {
if addr.StartPort == uint(app.httpsPort()) && existingPort != uint(app.httpsPort()) {
redirDomains[domain] = []caddy.NetworkAddress{addr}
}
return
}
if slices.Contains(existing, addr) {
return
}
redirDomains[domain] = append(existing, addr)
}
func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route { func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route {
redirTo := "https://{http.request.host}" redirTo := "https://{http.request.host}"
@@ -550,6 +574,20 @@ func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route {
} }
} }
func httpsRRALPNs(srv *Server) []string {
alpn := make(map[string]struct{}, 3)
if srv.protocol("h3") {
alpn["h3"] = struct{}{}
}
if srv.protocol("h2") {
alpn["h2"] = struct{}{}
}
if srv.protocol("h1") {
alpn["http/1.1"] = struct{}{}
}
return caddytls.OrderedHTTPSRRALPN(alpn)
}
// createAutomationPolicies ensures that automated certificates for this // createAutomationPolicies ensures that automated certificates for this
// app are managed properly. This adds up to two automation policies: // app are managed properly. This adds up to two automation policies:
// one for the public names, and one for the internal names. If a catch-all // one for the public names, and one for the internal names. If a catch-all
+47
View File
@@ -0,0 +1,47 @@
package caddyhttp
import (
"reflect"
"testing"
)
func TestHTTPSRRALPNsDefaultProtocols(t *testing.T) {
srv := &Server{}
got := httpsRRALPNs(srv)
want := []string{"h3", "h2", "http/1.1"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected ALPN values: got %v want %v", got, want)
}
}
func TestHTTPSRRALPNsListenProtocolOverrides(t *testing.T) {
srv := &Server{
Protocols: []string{"h1", "h2"},
ListenProtocols: [][]string{
{"h1"},
nil,
{},
{"h3", ""},
},
}
got := httpsRRALPNs(srv)
want := []string{"h3", "h2", "http/1.1"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected ALPN values: got %v want %v", got, want)
}
}
func TestHTTPSRRALPNsIgnoresH2COnly(t *testing.T) {
srv := &Server{
Protocols: []string{"h2c"},
}
got := httpsRRALPNs(srv)
if len(got) != 0 {
t.Fatalf("unexpected ALPN values: got %v want none", got)
}
}
+27 -4
View File
@@ -37,6 +37,12 @@ func init() {
// `{http.auth.user.*}` placeholders may be set for any authentication // `{http.auth.user.*}` placeholders may be set for any authentication
// modules that provide user metadata. // modules that provide user metadata.
// //
// If authentication is rejected but a provider returns user information,
// the placeholder `{http.auth.candidate.id}` will be set to the candidate
// username, and also `{http.auth.candidate.*}` placeholders may be set
// for candidate user metadata. Candidate placeholders do not represent a
// successfully authenticated principal.
//
// In case of an error, the placeholder `{http.auth.<provider>.error}` // In case of an error, the placeholder `{http.auth.<provider>.error}`
// will be set to the error message returned by the authentication // will be set to the error message returned by the authentication
// provider. // provider.
@@ -78,6 +84,8 @@ func (a *Authentication) Provision(ctx caddy.Context) error {
func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
var user User var user User
var candidate User
var hasCandidate bool
var authed bool var authed bool
var err error var err error
for provName, prov := range a.Providers { for provName, prov := range a.Providers {
@@ -94,19 +102,34 @@ func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
if authed { if authed {
break break
} }
if userHasInfo(user) {
candidate = user
hasCandidate = true
}
} }
if !authed { if !authed {
if hasCandidate {
setAuthUserPlaceholders(repl, "http.auth.candidate", candidate)
}
return caddyhttp.Error(http.StatusUnauthorized, fmt.Errorf("not authenticated")) return caddyhttp.Error(http.StatusUnauthorized, fmt.Errorf("not authenticated"))
} }
repl.Set("http.auth.user.id", user.ID) setAuthUserPlaceholders(repl, "http.auth.user", user)
for k, v := range user.Metadata {
repl.Set("http.auth.user."+k, v)
}
return next.ServeHTTP(w, r) return next.ServeHTTP(w, r)
} }
func userHasInfo(user User) bool {
return user.ID != "" || len(user.Metadata) > 0
}
func setAuthUserPlaceholders(repl *caddy.Replacer, namespace string, user User) {
repl.Set(namespace+".id", user.ID)
for k, v := range user.Metadata {
repl.Set(namespace+"."+k, v)
}
}
// Authenticator is a type which can authenticate a request. // Authenticator is a type which can authenticate a request.
// If a request was not authenticated, it returns false. An // If a request was not authenticated, it returns false. An
// error is only returned if authenticating the request fails // error is only returned if authenticating the request fails
@@ -0,0 +1,197 @@
// 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 caddyauth
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func TestAuthenticationRejectedUserSetsCandidatePlaceholders(t *testing.T) {
auth := Authentication{
Providers: map[string]Authenticator{
"test": staticAuthenticator{
user: User{
ID: "alice",
Metadata: map[string]string{
"role": "admin",
},
},
},
},
logger: zap.NewNop(),
}
req, repl := newRequestWithReplacer()
nextCalled := false
err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
nextCalled = true
return nil
}))
if err == nil {
t.Fatal("expected authentication error")
}
var handlerErr caddyhttp.HandlerError
if !errors.As(err, &handlerErr) {
t.Fatalf("expected HandlerError, got %T", err)
}
if handlerErr.StatusCode != http.StatusUnauthorized {
t.Fatalf("expected status %d, got %d", http.StatusUnauthorized, handlerErr.StatusCode)
}
if nextCalled {
t.Fatal("next handler was called for rejected authentication")
}
assertPlaceholder(t, repl, "http.auth.candidate.id", "alice")
assertPlaceholder(t, repl, "http.auth.candidate.role", "admin")
assertPlaceholderAbsent(t, repl, "http.auth.user.id")
assertPlaceholderAbsent(t, repl, "http.auth.user.role")
}
func TestAuthenticationSuccessfulUserSetsUserPlaceholdersOnly(t *testing.T) {
auth := Authentication{
Providers: map[string]Authenticator{
"test": staticAuthenticator{
user: User{
ID: "alice",
Metadata: map[string]string{
"role": "admin",
},
},
authed: true,
},
},
logger: zap.NewNop(),
}
req, repl := newRequestWithReplacer()
nextCalled := false
err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
nextCalled = true
return nil
}))
if err != nil {
t.Fatalf("expected no authentication error, got %v", err)
}
if !nextCalled {
t.Fatal("next handler was not called for successful authentication")
}
assertPlaceholder(t, repl, "http.auth.user.id", "alice")
assertPlaceholder(t, repl, "http.auth.user.role", "admin")
assertPlaceholderAbsent(t, repl, "http.auth.candidate.id")
assertPlaceholderAbsent(t, repl, "http.auth.candidate.role")
}
func TestAuthenticationSuccessfulProviderDoesNotExposeEarlierCandidate(t *testing.T) {
auth := Authentication{
Providers: map[string]Authenticator{
"first": staticAuthenticator{
user: User{
ID: "rejected",
Metadata: map[string]string{
"role": "guest",
},
},
},
"second": staticAuthenticator{
user: User{
ID: "accepted",
Metadata: map[string]string{
"role": "admin",
},
},
authed: true,
},
},
logger: zap.NewNop(),
}
req, repl := newRequestWithReplacer()
err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
return nil
}))
if err != nil {
t.Fatalf("expected no authentication error, got %v", err)
}
assertPlaceholder(t, repl, "http.auth.user.id", "accepted")
assertPlaceholder(t, repl, "http.auth.user.role", "admin")
assertPlaceholderAbsent(t, repl, "http.auth.candidate.id")
assertPlaceholderAbsent(t, repl, "http.auth.candidate.role")
}
func TestAuthenticationRejectedEmptyUserDoesNotSetCandidatePlaceholders(t *testing.T) {
auth := Authentication{
Providers: map[string]Authenticator{
"test": staticAuthenticator{},
},
logger: zap.NewNop(),
}
req, repl := newRequestWithReplacer()
err := auth.ServeHTTP(httptest.NewRecorder(), req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
t.Fatal("next handler was called for rejected authentication")
return nil
}))
if err == nil {
t.Fatal("expected authentication error")
}
assertPlaceholderAbsent(t, repl, "http.auth.candidate.id")
}
func newRequestWithReplacer() (*http.Request, *caddy.Replacer) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
return req.WithContext(ctx), repl
}
func assertPlaceholder(t *testing.T, repl *caddy.Replacer, key, expected string) {
t.Helper()
actual, ok := repl.GetString(key)
if !ok {
t.Fatalf("expected placeholder %q to be set", key)
}
if actual != expected {
t.Fatalf("expected placeholder %q to be %q, got %q", key, expected, actual)
}
}
func assertPlaceholderAbsent(t *testing.T, repl *caddy.Replacer, key string) {
t.Helper()
if actual, ok := repl.GetString(key); ok {
t.Fatalf("expected placeholder %q to be absent, got %q", key, actual)
}
}
type staticAuthenticator struct {
user User
authed bool
err error
}
func (a staticAuthenticator) Authenticate(http.ResponseWriter, *http.Request) (User, bool, error) {
return a.user, a.authed, a.err
}
+3 -3
View File
@@ -108,7 +108,7 @@ func (m *MatchExpression) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &m.Expr) return json.Unmarshal(data, &m.Expr)
} }
// otherwise, it's a full object, so unmarshal it, // otherwise, it's a full object, so unmarshal it,
// using an temp map to avoid infinite recursion // using a temp map to avoid infinite recursion
var tmpJson map[string]any var tmpJson map[string]any
err := json.Unmarshal(data, &tmpJson) err := json.Unmarshal(data, &tmpJson)
*m = MatchExpression{ *m = MatchExpression{
@@ -118,7 +118,7 @@ func (m *MatchExpression) UnmarshalJSON(data []byte) error {
return err return err
} }
// Provision sets ups m. // Provision sets up m.
func (m *MatchExpression) Provision(ctx caddy.Context) error { func (m *MatchExpression) Provision(ctx caddy.Context) error {
m.log = ctx.Logger() m.log = ctx.Logger()
@@ -319,7 +319,7 @@ func (cr celHTTPRequest) Value() any { return cr }
var pkixNameCELType = cel.ObjectType("pkix.Name", traits.ReceiverType) var pkixNameCELType = cel.ObjectType("pkix.Name", traits.ReceiverType)
// celPkixName wraps an pkix.Name with // celPkixName wraps a pkix.Name with
// methods to satisfy the ref.Val interface. // methods to satisfy the ref.Val interface.
type celPkixName struct{ *pkix.Name } type celPkixName struct{ *pkix.Name }
+1 -1
View File
@@ -79,7 +79,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
wantResult: true, wantResult: true,
}, },
{ {
name: "header matches an placeholder replaced during the header matcher (MatchHeader)", name: "header matches a placeholder replaced during the header matcher (MatchHeader)",
expression: &MatchExpression{ expression: &MatchExpression{
Expr: `header({'Field': '\{http.request.uri.path}'})`, Expr: `header({'Field': '\{http.request.uri.path}'})`,
}, },
+15 -7
View File
@@ -20,12 +20,12 @@
package encode package encode
import ( import (
"cmp"
"fmt" "fmt"
"io" "io"
"math" "math"
"net/http" "net/http"
"slices" "slices"
"sort"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -127,6 +127,14 @@ func (enc *Encode) Provision(ctx caddy.Context) error {
} }
} }
if len(enc.Prefer) == 0 {
for _, encName := range []string{"zstd", "br", "gzip"} {
if _, ok := enc.writerPools[encName]; ok {
enc.Prefer = append(enc.Prefer, encName)
}
}
}
return nil return nil
} }
@@ -162,7 +170,7 @@ func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh
// to comply with RFC 9110 section 8.8.3(.3), we modify the Etag when encoding // to comply with RFC 9110 section 8.8.3(.3), we modify the Etag when encoding
// by appending a hyphen and the encoder name; the problem is, the client will // by appending a hyphen and the encoder name; the problem is, the client will
// send back that Etag in a If-None-Match header, but upstream handlers that set // send back that Etag in an If-None-Match header, but upstream handlers that set
// the Etag in the first place don't know that we appended to their Etag! so here // the Etag in the first place don't know that we appended to their Etag! so here
// we have to strip our addition so the upstream handlers can still honor client // we have to strip our addition so the upstream handlers can still honor client
// caches without knowing about our changes... // caches without knowing about our changes...
@@ -369,7 +377,7 @@ const sniffLen = 512
// ReadFrom will try to use sendfile to copy from the reader to the response writer. // ReadFrom will try to use sendfile to copy from the reader to the response writer.
// It's only used if the response writer implements io.ReaderFrom and the data can't be compressed. // It's only used if the response writer implements io.ReaderFrom and the data can't be compressed.
// It's based on stdlin http1.1 response writer implementation. // It's based on the standard library HTTP/1.1 response writer implementation.
// https://github.com/golang/go/blob/f4e3ec3dbe3b8e04a058d266adf8e048bab563f2/src/net/http/server.go#L586 // https://github.com/golang/go/blob/f4e3ec3dbe3b8e04a058d266adf8e048bab563f2/src/net/http/server.go#L586
func (rw *responseWriter) ReadFrom(r io.Reader) (int64, error) { func (rw *responseWriter) ReadFrom(r io.Reader) (int64, error) {
rf, ok := rw.ResponseWriter.(io.ReaderFrom) rf, ok := rw.ResponseWriter.(io.ReaderFrom)
@@ -538,11 +546,11 @@ func AcceptedEncodings(r *http.Request, preferredOrder []string) []string {
} }
// sort preferences by descending q-factor first, then by preferOrder // sort preferences by descending q-factor first, then by preferOrder
sort.Slice(prefs, func(i, j int) bool { slices.SortStableFunc(prefs, func(a, b encodingPreference) int {
if math.Abs(prefs[i].q-prefs[j].q) < 0.00001 { if math.Abs(a.q-b.q) < 0.00001 {
return prefs[i].preferOrder > prefs[j].preferOrder return cmp.Compare(b.preferOrder, a.preferOrder)
} }
return prefs[i].q > prefs[j].q return cmp.Compare(b.q, a.q)
}) })
prefEncNames := make([]string, len(prefs)) prefEncNames := make([]string, len(prefs))
+75
View File
@@ -1,10 +1,16 @@
package encode package encode
import ( import (
"context"
"io"
"net/http" "net/http"
"net/http/httptest"
"slices" "slices"
"sync" "sync"
"testing" "testing"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
) )
func BenchmarkOpenResponseWriter(b *testing.B) { func BenchmarkOpenResponseWriter(b *testing.B) {
@@ -295,3 +301,72 @@ func TestIsEncodeAllowed(t *testing.T) {
}) })
} }
} }
type mockEncoder struct{}
func (mockEncoder) Write(p []byte) (n int, err error) { return len(p), nil }
func (mockEncoder) Close() error { return nil }
func (mockEncoder) Reset(w io.Writer) {}
func (mockEncoder) Flush() error { return nil }
func TestServeHTTPDefaultEncodingPreference(t *testing.T) {
enc := new(Encode)
enc.MinLength = 1 // compress everything
enc.writerPools = map[string]*sync.Pool{
"gzip": {
New: func() any { return mockEncoder{} },
},
"zstd": {
New: func() any { return mockEncoder{} },
},
}
// Call Provision() with a valid caddy.Context to exercise the real path
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
defer cancel()
if err := enc.Provision(ctx); err != nil {
t.Fatalf("Provision failed: %v", err)
}
// Test default preference: zstd preferred over gzip
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatalf("error creating request: %v", err)
}
r.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
w := httptest.NewRecorder()
w.Header().Set("Content-Type", "text/plain")
next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte("Hello, world! This is a long enough string to satisfy min length if it wasn't 1."))
return err
})
err = enc.ServeHTTP(w, r, next)
if err != nil {
t.Fatalf("ServeHTTP returned error: %v", err)
}
// ETag suffix or Content-Encoding header should reflect zstd
contentEncoding := w.Header().Get("Content-Encoding")
if contentEncoding != "zstd" {
t.Errorf("Expected Content-Encoding to be 'zstd' by default, got '%s'", contentEncoding)
}
// Test explicit user preference: gzip over zstd
enc.Prefer = []string{"gzip", "zstd"}
w2 := httptest.NewRecorder()
w2.Header().Set("Content-Type", "text/plain")
err = enc.ServeHTTP(w2, r, next)
if err != nil {
t.Fatalf("ServeHTTP returned error: %v", err)
}
contentEncoding2 := w2.Header().Get("Content-Encoding")
if contentEncoding2 != "gzip" {
t.Errorf("Expected Content-Encoding to be 'gzip' when explicitly preferred, got '%s'", contentEncoding2)
}
}
+14 -2
View File
@@ -281,7 +281,13 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re
sortParam = sortCookie.Value sortParam = sortCookie.Value
} }
case sortByName, sortByNameDirFirst, sortBySize, sortByTime: case sortByName, sortByNameDirFirst, sortBySize, sortByTime:
http.SetCookie(w, &http.Cookie{Name: "sort", Value: sortParam, Secure: r.TLS != nil}) http.SetCookie(w, &http.Cookie{ //nolint:gosec // Secure depends on whether the request itself used TLS
Name: "sort",
Value: sortParam,
Secure: r.TLS != nil,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
} }
// then figure out the order // then figure out the order
@@ -292,7 +298,13 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re
orderParam = orderCookie.Value orderParam = orderCookie.Value
} }
case sortOrderAsc, sortOrderDesc: case sortOrderAsc, sortOrderDesc:
http.SetCookie(w, &http.Cookie{Name: "order", Value: orderParam, Secure: r.TLS != nil}) http.SetCookie(w, &http.Cookie{ //nolint:gosec // Secure depends on whether the request itself used TLS
Name: "order",
Value: orderParam,
Secure: r.TLS != nil,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
} }
// finally, apply the sorting and limiting // finally, apply the sorting and limiting
@@ -28,6 +28,7 @@ import (
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/internal/filesystems" "github.com/caddyserver/caddy/v2/internal/filesystems"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
) )
type testCase struct { type testCase struct {
@@ -188,6 +189,105 @@ func fileMatcherTest(t *testing.T, i int, tc testCase) {
} }
} }
func TestTryFilesRewriteEscapesMatchedPath(t *testing.T) {
root := t.TempDir()
tests := []struct {
name string
requestTarget string
filename string
extraFiles []string
wantPath string
wantRequestURI string
skipWindows bool
}{
{
name: "question mark in path",
requestTarget: "/%3F.html",
filename: "?.html",
wantPath: "/?.html",
wantRequestURI: "/%3F.html",
skipWindows: true,
},
{
name: "percent in path",
requestTarget: "/%25.html",
filename: "%.html",
wantPath: "/%.html",
wantRequestURI: "/%25.html",
},
{
name: "encoded question mark remains percent-encoded",
requestTarget: "/%253F.html",
filename: "%3F.html",
wantPath: "/%3F.html",
wantRequestURI: "/%253F.html",
},
{
name: "question mark in nested path",
requestTarget: "/nested/%3F.html",
filename: filepath.Join("nested", "?.html"),
wantPath: "/nested/?.html",
wantRequestURI: "/nested/%3F.html",
skipWindows: true,
},
{
name: "encoded slash in filename does not conflict with nesting",
requestTarget: "/nested%252Ffile.html",
filename: "nested%2Ffile.html",
extraFiles: []string{filepath.Join("nested", "file.html")},
wantPath: "/nested%2Ffile.html",
wantRequestURI: "/nested%252Ffile.html",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if tc.skipWindows && runtime.GOOS == "windows" {
t.Skip("Windows file names cannot contain question marks")
}
for _, name := range append([]string{tc.filename}, tc.extraFiles...) {
filename := filepath.Join(root, name)
if err := os.MkdirAll(filepath.Dir(filename), 0o700); err != nil {
t.Fatalf("creating test file parent directory: %v", err)
}
if err := os.WriteFile(filename, []byte(name), 0o600); err != nil {
t.Fatalf("writing test file: %v", err)
}
}
m := &MatchFile{
fsmap: &filesystems.FileSystemMap{},
Root: root,
TryFiles: []string{"{http.request.uri.path}"},
}
req := httptest.NewRequest(http.MethodGet, "http://example.com"+tc.requestTarget, nil)
repl := caddyhttp.NewTestReplacer(req)
matched, err := m.MatchWithError(req)
if err != nil {
t.Fatalf("matching file: %v", err)
}
if !matched {
t.Fatalf("expected request %s to match %s", tc.requestTarget, tc.filename)
}
rewrite.Rewrite{URI: "{http.matchers.file.relative}"}.Rewrite(req, repl)
if req.URL.Path != tc.wantPath {
t.Errorf("rewritten path = %q, want %q", req.URL.Path, tc.wantPath)
}
if req.RequestURI != tc.wantRequestURI {
t.Errorf("rewritten request URI = %q, want %q", req.RequestURI, tc.wantRequestURI)
}
if req.URL.RawQuery != "" {
t.Errorf("rewritten raw query = %q, want empty", req.URL.RawQuery)
}
})
}
}
func TestPHPFileMatcher(t *testing.T) { func TestPHPFileMatcher(t *testing.T) {
for i, tc := range []struct { for i, tc := range []struct {
path string path string
+24 -5
View File
@@ -29,6 +29,7 @@ import (
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"time"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
@@ -123,7 +124,7 @@ type FileServer struct {
// put "hidden" in the list. To hide only ./hidden, put "./hidden" in the list. // put "hidden" in the list. To hide only ./hidden, put "./hidden" in the list.
// //
// 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 explicitness, 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 // Note that hide comparisons are case-sensitive. On case-insensitive
@@ -579,7 +580,17 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
// that errors generated by ServeContent are written immediately // that errors generated by ServeContent are written immediately
// to the response, so we cannot handle them (but errors there // to the response, so we cannot handle them (but errors there
// are rare) // are rare)
http.ServeContent(w, r, info.Name(), info.ModTime(), file.(io.ReadSeeker)) //
// There are a few file modification times that aren't useful
// to send in Last-Modified headers, but the golang http library only
// omits Last-Modified headers for the Unix epoch time. So, force
// the modification time to the epoch time if it's not useful.
zeroTime := time.Time{}
modTime := info.ModTime()
if !usefulModTime(modTime) {
modTime = zeroTime
}
http.ServeContent(w, r, info.Name(), modTime, file.(io.ReadSeeker))
return nil return nil
} }
@@ -726,6 +737,14 @@ func (fsrv *FileServer) notFound(w http.ResponseWriter, r *http.Request, next ca
return caddyhttp.Error(http.StatusNotFound, nil) return caddyhttp.Error(http.StatusNotFound, nil)
} }
// Indicates whether a file's modification time is useful for validator
// generation purposes (i.e. inclusion in ETag and Last-Modified headers).
// See issues #5548 and #7730.
func usefulModTime(modTime time.Time) bool {
mtimeunix := modTime.Unix()
return mtimeunix != 0 && mtimeunix != 1
}
// calculateEtag computes an entity tag using a strong validator // calculateEtag computes an entity tag using a strong validator
// without consuming the contents of the file. It requires the // without consuming the contents of the file. It requires the
// file info contain the correct size and modification time. // file info contain the correct size and modification time.
@@ -743,8 +762,8 @@ func (fsrv *FileServer) notFound(w http.ResponseWriter, r *http.Request, next ca
// which we consider precise enough to qualify as a strong validator. // which we consider precise enough to qualify as a strong validator.
func calculateEtag(d os.FileInfo) string { func calculateEtag(d os.FileInfo) string {
mtime := d.ModTime() mtime := d.ModTime()
if mtimeUnix := mtime.Unix(); mtimeUnix == 0 || mtimeUnix == 1 { if !usefulModTime(mtime) {
return "" // not useful anyway; see issue #5548 return ""
} }
var sb strings.Builder var sb strings.Builder
sb.WriteRune('"') sb.WriteRune('"')
@@ -785,7 +804,7 @@ func redirect(w http.ResponseWriter, r *http.Request, toPath string) error {
if r.URL.RawQuery != "" { if r.URL.RawQuery != "" {
toPath += "?" + r.URL.RawQuery toPath += "?" + r.URL.RawQuery
} }
http.Redirect(w, r, toPath, http.StatusPermanentRedirect) http.Redirect(w, r, toPath, http.StatusPermanentRedirect) //nolint:gosec // toPath is a same-origin path and leading // is stripped above
return nil return nil
} }
@@ -15,10 +15,17 @@
package fileserver package fileserver
import ( import (
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"testing" "testing"
"time"
"github.com/caddyserver/caddy/v2"
) )
func TestFileHidden(t *testing.T) { func TestFileHidden(t *testing.T) {
@@ -128,3 +135,52 @@ func TestFileHidden(t *testing.T) {
} }
} }
} }
// Check to make sure that we don't serve ETag and Last-Modified headers
// for files with invalid modification times
func TestModTimeHeaders(t *testing.T) {
check_validator_headers(time.Now(), true, t)
check_validator_headers(time.Unix(0, 0), false, t)
check_validator_headers(time.Unix(1, 0), false, t)
check_validator_headers(time.Unix(2, 0), true, t)
}
func check_validator_headers(modTime time.Time, expect_headers bool, t *testing.T) {
f := false
fsrv := FileServer{
Root: "./testdata",
CanonicalURIs: &f,
}
w := httptest.NewRecorder()
r, err := http.NewRequest("GET", "/modtime.txt", nil)
if err != nil {
t.Fatal(err)
}
repl := caddy.NewReplacer()
ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl)
r = r.WithContext(ctx)
ctx2, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) // module will be nil by default
fsrv.Provision(ctx2)
path := "testdata/modtime.txt"
os.Chtimes(path, modTime, modTime)
fsrv.ServeHTTP(w, r, nil)
if expect_headers {
if w.Header().Get("ETag") == "" {
t.Errorf("Didn't get ETag header for file with valid mod time %s", modTime)
}
if w.Header().Get("Last-Modified") == "" {
t.Errorf("Didn't get Last-Modified header for file with valid mod time %s", modTime)
}
} else {
if w.Header().Get("ETag") != "" {
t.Errorf("Got ETag header for file with invalid mod time %s", modTime)
}
if w.Header().Get("Last-Modified") != "" {
t.Errorf("Got Last-Modified header for file with invalid mod time %s", modTime)
}
}
}
View File
+1 -1
View File
@@ -15,7 +15,7 @@ type connectionStater interface {
// http2Listener wraps the listener to solve the following problems: // http2Listener wraps the listener to solve the following problems:
// 1. prevent genuine h2c connections from succeeding if h2c is not enabled // 1. prevent genuine h2c connections from succeeding if h2c is not enabled
// and the connection doesn't implment connectionStater or the resulting NegotiatedProtocol // and the connection doesn't implement connectionStater or the resulting NegotiatedProtocol
// isn't http2. // isn't http2.
// This does allow a connection to pass as tls enabled even if it's not, listener wrappers // This does allow a connection to pass as tls enabled even if it's not, listener wrappers
// can do this. // can do this.
+1 -1
View File
@@ -101,7 +101,7 @@ type httpRedirectConn struct {
// Read tries to peek at the first few bytes of the request, and if we get // Read tries to peek at the first few bytes of the request, and if we get
// an error reading the headers, and that error was due to the bytes looking // an error reading the headers, and that error was due to the bytes looking
// like an HTTP request, then we perform a HTTP->HTTPS redirect on the same // like an HTTP request, then we perform an HTTP->HTTPS redirect on the same
// port as the original connection. // port as the original connection.
func (c *httpRedirectConn) Read(p []byte) (int, error) { func (c *httpRedirectConn) Read(p []byte) (int, error) {
if c.once { if c.once {
+6 -41
View File
@@ -18,9 +18,10 @@ import (
"crypto/tls" "crypto/tls"
"net" "net"
"net/http" "net/http"
"strings"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
"github.com/caddyserver/caddy/v2/internal"
) )
// LoggableHTTPRequest makes an HTTP request loggable with zap.Object(). // LoggableHTTPRequest makes an HTTP request loggable with zap.Object().
@@ -47,12 +48,12 @@ func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error {
enc.AddString("method", r.Method) enc.AddString("method", r.Method)
enc.AddString("host", r.Host) enc.AddString("host", r.Host)
enc.AddString("uri", r.RequestURI) enc.AddString("uri", r.RequestURI)
enc.AddObject("headers", LoggableHTTPHeader{ enc.AddObject("headers", internal.LoggableHTTPHeader{
Header: r.Header, Header: r.Header,
ShouldLogCredentials: r.ShouldLogCredentials, ShouldLogCredentials: r.ShouldLogCredentials,
}) })
if r.TransferEncoding != nil { if r.TransferEncoding != nil {
enc.AddArray("transfer_encoding", LoggableStringArray(r.TransferEncoding)) enc.AddArray("transfer_encoding", internal.LoggableStringArray(r.TransferEncoding))
} }
if r.TLS != nil { if r.TLS != nil {
enc.AddObject("tls", LoggableTLSConnState(*r.TLS)) enc.AddObject("tls", LoggableTLSConnState(*r.TLS))
@@ -61,44 +62,10 @@ func (r LoggableHTTPRequest) MarshalLogObject(enc zapcore.ObjectEncoder) error {
} }
// LoggableHTTPHeader makes an HTTP header loggable with zap.Object(). // LoggableHTTPHeader makes an HTTP header loggable with zap.Object().
// Headers with potentially sensitive information (Cookie, Set-Cookie, type LoggableHTTPHeader = internal.LoggableHTTPHeader
// Authorization, and Proxy-Authorization) are logged with empty values.
type LoggableHTTPHeader struct {
http.Header
ShouldLogCredentials bool
}
// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
func (h LoggableHTTPHeader) MarshalLogObject(enc zapcore.ObjectEncoder) error {
if h.Header == nil {
return nil
}
for key, val := range h.Header {
if !h.ShouldLogCredentials {
switch strings.ToLower(key) {
case "cookie", "set-cookie", "authorization", "proxy-authorization":
val = []string{"REDACTED"} // see #5669. I still think ▒▒▒▒ would be cool.
}
}
enc.AddArray(key, LoggableStringArray(val))
}
return nil
}
// LoggableStringArray makes a slice of strings marshalable for logging. // LoggableStringArray makes a slice of strings marshalable for logging.
type LoggableStringArray []string type LoggableStringArray = internal.LoggableStringArray
// MarshalLogArray satisfies the zapcore.ArrayMarshaler interface.
func (sa LoggableStringArray) MarshalLogArray(enc zapcore.ArrayEncoder) error {
if sa == nil {
return nil
}
for _, s := range sa {
enc.AppendString(s)
}
return nil
}
// LoggableTLSConnState makes a TLS connection state loggable with zap.Object(). // LoggableTLSConnState makes a TLS connection state loggable with zap.Object().
type LoggableTLSConnState tls.ConnectionState type LoggableTLSConnState tls.ConnectionState
@@ -121,7 +88,5 @@ func (t LoggableTLSConnState) MarshalLogObject(enc zapcore.ObjectEncoder) error
// Interface guards // Interface guards
var ( var (
_ zapcore.ObjectMarshaler = (*LoggableHTTPRequest)(nil) _ zapcore.ObjectMarshaler = (*LoggableHTTPRequest)(nil)
_ zapcore.ObjectMarshaler = (*LoggableHTTPHeader)(nil)
_ zapcore.ArrayMarshaler = (*LoggableStringArray)(nil)
_ zapcore.ObjectMarshaler = (*LoggableTLSConnState)(nil) _ zapcore.ObjectMarshaler = (*LoggableTLSConnState)(nil)
) )
+27 -6
View File
@@ -435,12 +435,12 @@ func (m MatchPath) MatchWithError(r *http.Request) (bool, error) {
// can be used instead. // can be used instead.
reqPath := strings.ToLower(r.URL.Path) reqPath := strings.ToLower(r.URL.Path)
// See #2917; Windows ignores trailing dots and spaces
// when accessing files (sigh), potentially causing a
// security risk (cry) if PHP files end up being served
// as static files, exposing the source code, instead of
// being matched by *.php to be treated as PHP scripts.
if runtime.GOOS == "windows" { // issue #5613 if runtime.GOOS == "windows" { // issue #5613
// Windows treats backslashes as path separators and
// ignores trailing dots and spaces when accessing files
// (sigh), potentially causing a security risk (cry) if
// protected files are not matched as intended.
reqPath = strings.ReplaceAll(reqPath, `\`, "/")
reqPath = strings.TrimRight(reqPath, ". ") reqPath = strings.TrimRight(reqPath, ". ")
} }
@@ -478,7 +478,12 @@ func (m MatchPath) MatchWithError(r *http.Request) (bool, error) {
// the intent is to compare that part of the path in raw/escaped // the intent is to compare that part of the path in raw/escaped
// space; i.e. "%40"=="%40", not "@", and "%2F"=="%2F", not "/" // space; i.e. "%40"=="%40", not "@", and "%2F"=="%2F", not "/"
if strings.Contains(matchPattern, "%") { if strings.Contains(matchPattern, "%") {
reqPathForPattern := CleanPath(r.URL.EscapedPath(), mergeSlashes) escapedPath := r.URL.EscapedPath()
if runtime.GOOS == "windows" {
escapedPath = windowsEscapedPathSeparatorRepl.Replace(escapedPath)
matchPattern = windowsEscapedPathSeparatorRepl.Replace(matchPattern)
}
reqPathForPattern := CleanPath(escapedPath, mergeSlashes)
if m.matchPatternWithEscapeSequence(reqPathForPattern, matchPattern) { if m.matchPatternWithEscapeSequence(reqPathForPattern, matchPattern) {
return true, nil return true, nil
} }
@@ -643,6 +648,14 @@ func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) b
return matches return matches
} }
// windowsEscapedPathSeparatorRepl normalizes Windows backslash separators
// while preserving escaped-path matching semantics.
var windowsEscapedPathSeparatorRepl = strings.NewReplacer(
`\`, "%2f",
"%5c", "%2f",
"%5C", "%2f",
)
// CELLibrary produces options that expose this matcher for use in CEL // CELLibrary produces options that expose this matcher for use in CEL
// expression matchers. // expression matchers.
// //
@@ -1562,6 +1575,14 @@ func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, er
// instances of the matcher in this set // instances of the matcher in this set
tokensByMatcherName := make(map[string][]caddyfile.Token) tokensByMatcherName := make(map[string][]caddyfile.Token)
for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); { for nesting := d.Nesting(); d.NextArg() || d.NextBlock(nesting); {
// if the token is quoted (backtick), treat it as a shorthand
// for an expression matcher, same as @named matcher parsing
if d.Token().Quoted() {
expressionToken := d.Token().Clone()
expressionToken.Text = "expression"
tokensByMatcherName["expression"] = append(tokensByMatcherName["expression"], expressionToken, d.Token())
continue
}
matcherName := d.Val() matcherName := d.Val()
tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...) tokensByMatcherName[matcherName] = append(tokensByMatcherName[matcherName], d.NextSegment()...)
} }
+53 -10
View File
@@ -461,18 +461,61 @@ func TestPathMatcherWindows(t *testing.T) {
return return
} }
req := &http.Request{URL: &url.URL{Path: "/index.php . . .."}}
repl := caddy.NewReplacer() repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
match := MatchPath{"*.php"} for _, tc := range []struct {
matched, err := match.MatchWithError(req) name string
if err != nil { path string
t.Errorf("Expected no error, but got: %v", err) requestTarget string
} match MatchPath
if !matched { }{
t.Errorf("Expected to match; should ignore trailing dots and spaces") {
name: "trailing dots and spaces",
path: "/index.php . . ..",
match: MatchPath{"*.php"},
},
{
name: "encoded backslash path separator",
requestTarget: `/private%5csecret.txt`,
match: MatchPath{"/private/*"},
},
{
name: "encoded backslash path separator with escaped wildcard",
requestTarget: `/private%5csecret.txt`,
match: MatchPath{"/private/%*"},
},
{
name: "uppercase encoded backslash path separator with escaped wildcard",
requestTarget: `/private%5Csecret.txt`,
match: MatchPath{"/private/%*"},
},
{
name: "encoded backslash in escaped pattern",
requestTarget: `/private%5csecret.txt`,
match: MatchPath{"/private%5c%*"},
},
} {
t.Run(tc.name, func(t *testing.T) {
u := &url.URL{Path: tc.path}
if tc.requestTarget != "" {
var err error
u, err = url.ParseRequestURI(tc.requestTarget)
if err != nil {
t.Fatalf("Parsing request target: %v", err)
}
}
req := &http.Request{URL: u}
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
matched, err := tc.match.MatchWithError(req)
if err != nil {
t.Errorf("Expected no error, but got: %v", err)
}
if !matched {
t.Errorf("Expected %q to match %v", req.URL.Path, tc.match)
}
})
} }
} }
+84 -4
View File
@@ -3,6 +3,7 @@ package caddyhttp
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"net/http" "net/http"
"strings" "strings"
"sync" "sync"
@@ -10,9 +11,14 @@ import (
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
otelprom "go.opentelemetry.io/contrib/bridges/prometheus"
"go.opentelemetry.io/contrib/exporters/autoexport"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/internal/metrics" caddymetrics "github.com/caddyserver/caddy/v2/internal/metrics"
) )
// Metrics configures metrics observations. // Metrics configures metrics observations.
@@ -67,10 +73,20 @@ type Metrics struct {
// for production environments exposed to the internet). // for production environments exposed to the internet).
ObserveCatchallHosts bool `json:"observe_catchall_hosts,omitempty"` ObserveCatchallHosts bool `json:"observe_catchall_hosts,omitempty"`
// Enable pushing metrics via OTLP in addition to the existing Prometheus
// scrape endpoints. When set, a PeriodicReader is attached to the shared
// Prometheus registry (via a Prometheus -> OpenTelemetry bridge), and the
// exporter is autoconfigured from the standard OTEL_* environment
// variables (OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_PROTOCOL,
// OTEL_METRICS_EXPORTER, ...). Set OTEL_METRICS_EXPORTER=none or simply
// keep this field false to disable OTLP export.
OTLP bool `json:"otlp,omitempty"`
init sync.Once init sync.Once
httpMetrics *httpMetrics httpMetrics *httpMetrics
allowedHosts map[string]struct{} allowedHosts map[string]struct{}
hasHTTPSServer bool hasHTTPSServer bool
meterProvider *sdkmetric.MeterProvider
} }
type httpMetrics struct { type httpMetrics struct {
@@ -147,6 +163,70 @@ func initHTTPMetrics(ctx caddy.Context, metrics *Metrics) {
}, httpLabels) }, httpLabels)
} }
// provisionOTLP wires a MeterProvider that periodically reads the process-wide
// Prometheus registry and pushes the result via OTLP. The exporter and reader
// are autoconfigured from the standard OTEL_* environment variables, matching
// the ergonomics of the existing `tracing` directive. It is a no-op when
// m.OTLP is false, and honors OTEL_METRICS_EXPORTER=none (autoexport
// short-circuits to a no-op reader in that case).
func (m *Metrics) provisionOTLP(ctx caddy.Context) error {
if !m.OTLP {
return nil
}
// Register a Prometheus -> OpenTelemetry bridge against the process-wide
// Prometheus registry as the *default* source the NewMetricReader below
// will read from.
//
// NB: despite the "With*" naming, autoexport.WithFallbackMetricProducer is
// a package-level setter (it returns nothing) — it mutates autoexport's
// internal producer registry and takes effect on the very next call to
// NewMetricReader. It is NOT a MetricOption and must not be passed as one.
// Users can still override the source by setting OTEL_METRICS_PRODUCERS.
reg := ctx.GetMetricsRegistry()
autoexport.WithFallbackMetricProducer(func(context.Context) (sdkmetric.Producer, error) {
return otelprom.NewMetricProducer(otelprom.WithGatherer(reg)), nil
})
reader, err := autoexport.NewMetricReader(ctx)
if err != nil {
return fmt.Errorf("creating OTLP metric reader: %w", err)
}
version, _ := caddy.Version()
res, err := resource.Merge(resource.Default(), resource.NewSchemaless(
semconv.WebEngineName(ServerHeader),
semconv.WebEngineVersion(version),
))
if err != nil {
return fmt.Errorf("building OTLP metrics resource: %w", err)
}
m.meterProvider = sdkmetric.NewMeterProvider(
sdkmetric.WithResource(res),
sdkmetric.WithReader(reader),
)
return nil
}
// shutdown flushes and tears down the OTLP MeterProvider if one was provisioned.
// Both ForceFlush and Shutdown are always attempted so that a flush failure
// does not prevent the reader goroutines from being stopped; errors from both
// are returned joined.
func (m *Metrics) shutdown(ctx context.Context) error {
if m == nil || m.meterProvider == nil {
return nil
}
// ForceFlush gives the final collection a chance to reach the collector
// before the reader goroutine is stopped by Shutdown.
return errors.Join(
m.meterProvider.ForceFlush(ctx),
m.meterProvider.Shutdown(ctx),
)
}
// scanConfigForHosts scans the HTTP app configuration to build a set of allowed hosts // scanConfigForHosts scans the HTTP app configuration to build a set of allowed hosts
// for metrics collection, similar to how auto-HTTPS scans for domain names. // for metrics collection, similar to how auto-HTTPS scans for domain names.
func (m *Metrics) scanConfigForHosts(app *App) { func (m *Metrics) scanConfigForHosts(app *App) {
@@ -234,7 +314,7 @@ func newMetricsInstrumentedRoute(ctx caddy.Context, handler string, next Handler
func (h *metricsInstrumentedRoute) ServeHTTP(w http.ResponseWriter, r *http.Request) error { func (h *metricsInstrumentedRoute) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
server := serverNameFromContext(r.Context()) server := serverNameFromContext(r.Context())
labels := prometheus.Labels{"server": server, "handler": h.handler} labels := prometheus.Labels{"server": server, "handler": h.handler}
method := metrics.SanitizeMethod(r.Method) method := caddymetrics.SanitizeMethod(r.Method)
// the "code" value is set later, but initialized here to eliminate the possibility // the "code" value is set later, but initialized here to eliminate the possibility
// of a panic // of a panic
statusLabels := prometheus.Labels{"server": server, "handler": h.handler, "method": method, "code": ""} statusLabels := prometheus.Labels{"server": server, "handler": h.handler, "method": method, "code": ""}
@@ -264,7 +344,7 @@ func (h *metricsInstrumentedRoute) ServeHTTP(w http.ResponseWriter, r *http.Requ
// being called when the headers are written. // being called when the headers are written.
// Effectively the same behaviour as promhttp.InstrumentHandlerTimeToWriteHeader. // Effectively the same behaviour as promhttp.InstrumentHandlerTimeToWriteHeader.
writeHeaderRecorder := ShouldBufferFunc(func(status int, header http.Header) bool { writeHeaderRecorder := ShouldBufferFunc(func(status int, header http.Header) bool {
statusLabels["code"] = metrics.SanitizeCode(status) statusLabels["code"] = caddymetrics.SanitizeCode(status)
ttfb := time.Since(start).Seconds() ttfb := time.Since(start).Seconds()
h.metrics.httpMetrics.responseDuration.With(statusLabels).Observe(ttfb) h.metrics.httpMetrics.responseDuration.With(statusLabels).Observe(ttfb)
return false return false
@@ -280,7 +360,7 @@ func (h *metricsInstrumentedRoute) ServeHTTP(w http.ResponseWriter, r *http.Requ
if statusLabels["code"] == "" { if statusLabels["code"] == "" {
// we still sanitize it, even though it's likely to be 0. A 200 is // we still sanitize it, even though it's likely to be 0. A 200 is
// returned on fallthrough so we want to reflect that. // returned on fallthrough so we want to reflect that.
statusLabels["code"] = metrics.SanitizeCode(status) statusLabels["code"] = caddymetrics.SanitizeCode(status)
} }
h.metrics.httpMetrics.requestDuration.With(statusLabels).Observe(dur) h.metrics.httpMetrics.requestDuration.With(statusLabels).Observe(dur)
+50
View File
@@ -523,6 +523,56 @@ func TestMetricsInstrumentedRoute(t *testing.T) {
} }
} }
func TestMetricsProvisionOTLPDisabled(t *testing.T) {
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
m := &Metrics{OTLP: false}
if err := m.provisionOTLP(ctx); err != nil {
t.Fatalf("provisionOTLP returned unexpected error: %v", err)
}
if m.meterProvider != nil {
t.Fatalf("meterProvider should remain nil when OTLP is disabled")
}
// shutdown must be safe on a never-provisioned Metrics.
if err := m.shutdown(context.Background()); err != nil {
t.Fatalf("shutdown returned unexpected error: %v", err)
}
}
func TestMetricsProvisionOTLPNoopExporter(t *testing.T) {
// OTEL_METRICS_EXPORTER=none makes autoexport return its built-in
// no-op reader, which avoids any network I/O while still exercising
// the full provisionOTLP -> shutdown lifecycle.
t.Setenv("OTEL_METRICS_EXPORTER", "none")
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
m := &Metrics{OTLP: true}
if err := m.provisionOTLP(ctx); err != nil {
t.Fatalf("provisionOTLP returned unexpected error: %v", err)
}
if m.meterProvider == nil {
t.Fatalf("provisionOTLP did not create a MeterProvider")
}
if err := m.shutdown(context.Background()); err != nil {
t.Fatalf("shutdown returned unexpected error: %v", err)
}
}
// shutdown on a nil receiver is a convenience so App.Stop can call it
// without guarding against app.Metrics being unset.
func TestMetricsShutdownNilReceiver(t *testing.T) {
var m *Metrics
if err := m.shutdown(context.Background()); err != nil {
t.Fatalf("shutdown on nil Metrics returned unexpected error: %v", err)
}
}
func BenchmarkMetricsInstrumentedRoute(b *testing.B) { func BenchmarkMetricsInstrumentedRoute(b *testing.B) {
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()}) ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
m := &Metrics{ m := &Metrics{
+4 -7
View File
@@ -387,17 +387,14 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
switch key { switch key {
case "http.shutting_down": case "http.shutting_down":
server := req.Context().Value(ServerCtxKey).(*Server) server := req.Context().Value(ServerCtxKey).(*Server)
server.shutdownAtMu.RLock() return server.shutdownAt.Load() != nil, true
defer server.shutdownAtMu.RUnlock()
return !server.shutdownAt.IsZero(), true
case "http.time_until_shutdown": case "http.time_until_shutdown":
server := req.Context().Value(ServerCtxKey).(*Server) server := req.Context().Value(ServerCtxKey).(*Server)
server.shutdownAtMu.RLock() t := server.shutdownAt.Load()
defer server.shutdownAtMu.RUnlock() if t == nil {
if server.shutdownAt.IsZero() {
return nil, true return nil, true
} }
return time.Until(server.shutdownAt), true return time.Until(*t), true
} }
return nil, false return nil, false
+1 -1
View File
@@ -67,7 +67,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
// lb_retries <retries> // lb_retries <retries>
// lb_try_duration <duration> // lb_try_duration <duration>
// lb_try_interval <interval> // lb_try_interval <interval>
// lb_retry_match <request-matcher> // lb_retry_match <matcher>
// //
// # active health checking // # active health checking
// health_uri <uri> // health_uri <uri>
@@ -135,8 +135,8 @@ type client struct {
logger *zap.Logger logger *zap.Logger
} }
// Do made the request and returns a io.Reader that translates the data read // Do makes the request and returns an io.Reader that translates the data read
// from fcgi responder out of fcgi packet before returning it. // from the FastCGI responder out of FastCGI packets before returning it.
func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error) { func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error) {
// check for CONTENT_LENGTH, since the lack of it or wrong value will cause the backend to hang // check for CONTENT_LENGTH, since the lack of it or wrong value will cause the backend to hang
if clStr, ok := p["CONTENT_LENGTH"]; !ok { if clStr, ok := p["CONTENT_LENGTH"]; !ok {
@@ -179,7 +179,7 @@ func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error)
return r, err return r, err
} }
// clientCloser is a io.ReadCloser. It wraps a io.Reader with a Closer // clientCloser is an io.ReadCloser. It wraps an io.Reader with a Closer
// that closes the client connection. // that closes the client connection.
type clientCloser struct { type clientCloser struct {
rwc net.Conn rwc net.Conn
@@ -208,8 +208,8 @@ func (f clientCloser) Close() error {
return f.rwc.Close() return f.rwc.Close()
} }
// Request returns a HTTP Response with Header and Body // Request returns an HTTP response with header and body
// from fcgi responder // from the FastCGI responder.
func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Response, err error) { func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Response, err error) {
r, err := c.Do(p, req) r, err := c.Do(p, req)
if err != nil { if err != nil {
@@ -28,8 +28,6 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
"golang.org/x/text/language"
"golang.org/x/text/search"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
@@ -418,14 +416,19 @@ func (t Transport) buildEnv(r *http.Request) (envVars, error) {
return env, nil return env, nil
} }
var splitSearchNonASCII = search.New(language.Und, search.IgnoreCase)
// splitPos returns the index where path should // splitPos returns the index where path should
// be split based on t.SplitPath. // be split based on t.SplitPath.
// //
// example: if splitPath is [".php"] // example: if splitPath is [".php"]
// "/path/to/script.php/some/path": ("/path/to/script.php", "/some/path") // "/path/to/script.php/some/path": ("/path/to/script.php", "/some/path")
// //
// Matching is strictly ASCII case-insensitive. Bytes >= utf8.RuneSelf in path
// never match any split entry: split strings are validated ASCII-only and
// lower-cased in Provision(), so any Unicode equivalence (e.g. fullwidth or
// mathematical letters folding to ASCII) would let an attacker upload a file
// whose name contains such code points and have it served as PHP. See
// FrankenPHP advisories GHSA-3g8v-8r37-cgjm and GHSA-v4h7-cj44-8fc8.
//
// Adapted from FrankenPHP's code (copyright 2026 Kévin Dunglas, MIT license) // Adapted from FrankenPHP's code (copyright 2026 Kévin Dunglas, MIT license)
func (t Transport) splitPos(path string) int { func (t Transport) splitPos(path string) int {
// TODO: from v1... // TODO: from v1...
@@ -438,31 +441,18 @@ func (t Transport) splitPos(path string) int {
pathLen := len(path) pathLen := len(path)
// We are sure that split strings are all ASCII-only and lower-case because of validation and normalization in Provision().
for _, split := range t.SplitPath { for _, split := range t.SplitPath {
splitLen := len(split) splitLen := len(split)
if splitLen == 0 || splitLen > pathLen {
continue
}
for i := range pathLen { for i := 0; i <= pathLen-splitLen; i++ {
if path[i] >= utf8.RuneSelf {
if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 {
return end
}
break
}
if i+splitLen > pathLen {
continue
}
match := true match := true
for j := range splitLen { for j := range splitLen {
c := path[i+j] c := path[i+j]
if c >= utf8.RuneSelf { if c >= utf8.RuneSelf {
if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 { match = false
return end
}
break break
} }
@@ -517,7 +507,7 @@ var tlsProtocolStrings = map[uint16]string{
tls.VersionTLS13: "TLSv1.3", tls.VersionTLS13: "TLSv1.3",
} }
var headerNameReplacer = strings.NewReplacer(" ", "_", "-", "_") var headerNameReplacer = strings.NewReplacer("-", "_")
// Interface guards // Interface guards
var ( var (
@@ -191,6 +191,65 @@ func TestSplitPos(t *testing.T) {
splitPath: []string{".php"}, splitPath: []string{".php"},
wantPos: 9, wantPos: 9,
}, },
// Regression tests adapted from FrankenPHP advisories
// GHSA-3g8v-8r37-cgjm and GHSA-v4h7-cj44-8fc8: search.IgnoreCase
// matched Unicode equivalents of ASCII letters as ".php", and an
// inner non-ASCII byte path could leave the match flag stale.
{
name: "non-ascii byte after dot must not match",
path: "/PoC-match-unset.¡.txt",
splitPath: []string{".php"},
wantPos: -1,
},
{
name: "non-ascii byte mid-extension must not match",
path: "/script.p\xc2\xa1p",
splitPath: []string{".php"},
wantPos: -1,
},
{
name: "small full stop ﹒ in extension must not match",
path: "/shell﹒php",
splitPath: []string{".php"},
wantPos: -1,
},
{
name: "fullwidth full stop in extension must not match",
path: "/shellphp",
splitPath: []string{".php"},
wantPos: -1,
},
{
name: "fullwidth p in extension must not match",
path: "/shell.hp",
splitPath: []string{".php"},
wantPos: -1,
},
{
name: "circled php must not match",
path: "/shell.ⓟⓗⓟ",
splitPath: []string{".php"},
wantPos: -1,
},
{
name: "mathematical sans-serif bold php must not match",
path: "/shell.\U0001D5FD\U0001D5F5\U0001D5FD",
splitPath: []string{".php"},
wantPos: -1,
},
{
name: "mathematical script php must not match",
path: "/shell.\U0001D4C5\U0001D4BD\U0001D4C5",
splitPath: []string{".php"},
wantPos: -1,
},
{
name: "circled php with later real php still picks the real one",
path: "/shell.ⓟⓗⓟ.anything-after-payload.php",
splitPath: []string{".php"},
// "/shell." (7) + "ⓟⓗⓟ" (3*3 bytes) + ".anything-after-payload.php" (27) = 43
wantPos: 43,
},
} }
for _, tt := range tests { for _, tt := range tests {
@@ -244,3 +303,55 @@ func TestSplitPosUnicodeSecurityRegression(t *testing.T) {
assert.Equal(t, ".txt.php", pathInfo, "path info should be the remainder after first .php") assert.Equal(t, ".txt.php", pathInfo, "path info should be the remainder after first .php")
} }
} }
// TestHeaderNameReplacer asserts the CGI header-to-env normalization rule:
// hyphens are mapped to underscores while every other character (including
// spaces) is passed through. Spaces are not RFC 7230 tokens, so they cannot
// reach this function from the wire; the only header names that survive
// untouched at the server layer are sanitized by the underscore filter in
// caddyhttp.Server.serveHTTP (see GHSA-f59h-q822-g45g).
func TestHeaderNameReplacer(t *testing.T) {
tests := []struct {
in, want string
}{
{"X-Forwarded-For", "X_Forwarded_For"},
{"Remote-User", "Remote_User"},
// Underscores are preserved (the server has already dropped any
// underscore-named headers when the filter is on).
{"Remote_User", "Remote_User"},
// Spaces are not rewritten because Go's HTTP parser rejects whitespace in
// header field names.
{"Foo Bar", "Foo Bar"},
}
for _, tt := range tests {
assert.Equal(t, tt.want, headerNameReplacer.Replace(tt.in), "input %q", tt.in)
}
}
// TestSplitPosSecurityRegressionUnicodeBypass guards against the FrankenPHP
// advisories GHSA-3g8v-8r37-cgjm (uninitialized match flag on inner non-ASCII
// byte) and GHSA-v4h7-cj44-8fc8 (Unicode equivalence via search.IgnoreCase
// folding fullwidth/mathematical/circled letters onto ASCII). Every payload
// below produced a false positive in the vulnerable implementation; none
// must match here.
func TestSplitPosSecurityRegressionUnicodeBypass(t *testing.T) {
t.Parallel()
tr := Transport{SplitPath: []string{".php"}}
payloads := []string{
"/PoC-match-unset.¡.txt", // GHSA-3g8v: stale match=true on IndexString fallback
"/shell﹒php", // U+FE52 small full stop
"/shellphp", // U+FF0E fullwidth full stop
"/shell.hp", // U+FF50 fullwidth p
"/shell.pp", // U+FF48 fullwidth h
"/shell.ph", // U+FF50 fullwidth p (trailing)
"/shell.\U0001D5C1\U0001D5B5\U0001D5C1", // mathematical sans-serif p/h
"/shell.\U0001D5FD\U0001D5F5\U0001D5FD", // mathematical sans-serif bold p/h
"/shell.\U0001D4C5\U0001D4BD\U0001D4C5", // mathematical script p/h
"/shell.ⓟⓗⓟ", // circled latin small
}
for _, p := range payloads {
assert.Equalf(t, -1, tr.splitPos(p), "payload %q must not be detected as .php", p)
}
}
@@ -522,7 +522,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ
body = io.LimitReader(body, h.HealthChecks.Active.MaxSize) body = io.LimitReader(body, h.HealthChecks.Active.MaxSize)
} }
defer func() { defer func() {
// drain any remaining body so connection could be re-used // drain any remaining body so connection could be reused
_, _ = io.Copy(io.Discard, body) _, _ = io.Copy(io.Discard, body)
resp.Body.Close() resp.Body.Close()
}() }()
+1 -2
View File
@@ -174,7 +174,7 @@ func (u *Upstream) fillDynamicHost() {
// 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 {
numRequests atomic.Int64 // atomic.Int64 is automatically aligned for us (see https://golang.org/pkg/sync/atomic/#pkg-note-BUG) numRequests atomic.Int64
fails atomic.Int64 fails atomic.Int64
activePasses atomic.Int64 activePasses atomic.Int64
activeFails atomic.Int64 activeFails atomic.Int64
@@ -250,7 +250,6 @@ func (h *Host) resetHealth() {
// (This returns the status only from the "active" health checks.) // (This returns the status only from the "active" health checks.)
func (u *Upstream) healthy() bool { func (u *Upstream) healthy() bool {
return u.unhealthy.Load() == 0 return u.unhealthy.Load() == 0
// return atomic.LoadInt32(&u.unhealthy) == 0
} }
// SetHealthy sets the upstream has healthy or unhealthy // SetHealthy sets the upstream has healthy or unhealthy
@@ -32,6 +32,7 @@ import (
"time" "time"
"github.com/pires/go-proxyproto" "github.com/pires/go-proxyproto"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3" "github.com/quic-go/quic-go/http3"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
@@ -161,7 +162,8 @@ type HTTPTransport struct {
// `HTTPS_PROXY`, and `NO_PROXY` environment variables. // `HTTPS_PROXY`, and `NO_PROXY` environment variables.
NetworkProxyRaw json.RawMessage `json:"network_proxy,omitempty" caddy:"namespace=caddy.network_proxy inline_key=from"` NetworkProxyRaw json.RawMessage `json:"network_proxy,omitempty" caddy:"namespace=caddy.network_proxy inline_key=from"`
h3Transport *http3.Transport // TODO: EXPERIMENTAL (May 2024) h3Transport *http3.Transport // TODO: EXPERIMENTAL (May 2024)
quicTransport *quic.Transport // used by h3Transport if sni placeholder is used, otherwise nil
} }
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
@@ -499,6 +501,25 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
if err != nil { if err != nil {
return nil, fmt.Errorf("making TLS client config for HTTP/3 transport: %v", err) return nil, fmt.Errorf("making TLS client config for HTTP/3 transport: %v", err)
} }
if strings.Contains(h.TLS.ServerName, "{") {
// copied from quic-go
udpConn, err := net.ListenUDP("udp", nil)
if err != nil {
return nil, fmt.Errorf("making udp socket for HTTP/3 transport: %v", err)
}
h.quicTransport = &quic.Transport{Conn: udpConn}
h.h3Transport.Dial = func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
// tlsCfg is already cloned from h3Transport.TLSClientConfig
repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
tlsCfg.ServerName = repl.ReplaceAll(tlsCfg.ServerName, "")
udpAddr, err := resolveUDPAddr(ctx, "udp", addr)
if err != nil {
return nil, err
}
return h.quicTransport.DialEarly(ctx, udpAddr, tlsCfg, cfg)
}
}
} }
} else if len(h.Versions) > 1 && slices.Contains(h.Versions, "3") { } else if len(h.Versions) > 1 && slices.Contains(h.Versions, "3") {
return nil, fmt.Errorf("if HTTP/3 is enabled to the upstream, no other HTTP versions are supported") return nil, fmt.Errorf("if HTTP/3 is enabled to the upstream, no other HTTP versions are supported")
@@ -525,6 +546,71 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
return rt, nil return rt, nil
} }
// TODO: EXPERIMENTAL (May 2025)
// copied from quic-go
func resolveUDPAddr(ctx context.Context, network, addr string) (*net.UDPAddr, error) {
host, portStr, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
port, err := net.LookupPort(network, portStr)
if err != nil {
return nil, err
}
resolver := net.DefaultResolver
ipAddrs, err := resolver.LookupIPAddr(ctx, host)
if err != nil {
return nil, err
}
addrs := addrList(ipAddrs)
ip := addrs.forResolve(network, addr)
return &net.UDPAddr{IP: ip.IP, Port: port, Zone: ip.Zone}, nil
}
// TODO: EXPERIMENTAL (May 2025)
// copied from quic-go
// An addrList represents a list of network endpoint addresses.
// Copy from [net.addrList] and change type from [net.Addr] to [net.IPAddr]
type addrList []net.IPAddr
// isIPv4 reports whether addr contains an IPv4 address.
func isIPv4(addr net.IPAddr) bool {
return addr.IP.To4() != nil
}
// isNotIPv4 reports whether addr does not contain an IPv4 address.
func isNotIPv4(addr net.IPAddr) bool { return !isIPv4(addr) }
// forResolve returns the most appropriate address in address for
// a call to ResolveTCPAddr, ResolveUDPAddr, or ResolveIPAddr.
// IPv4 is preferred, unless addr contains an IPv6 literal.
func (addrs addrList) forResolve(network, addr string) net.IPAddr {
var want6 bool
switch network {
case "ip":
// IPv6 literal (addr does NOT contain a port)
want6 = strings.ContainsRune(addr, ':')
case "tcp", "udp":
// IPv6 literal. (addr contains a port, so look for '[')
want6 = strings.ContainsRune(addr, '[')
}
if want6 {
return addrs.first(isNotIPv4)
}
return addrs.first(isIPv4)
}
// first returns the first address which satisfies strategy, or if
// none do, then the first address of any kind.
func (addrs addrList) first(strategy func(net.IPAddr) bool) net.IPAddr {
for _, addr := range addrs {
if strategy(addr) {
return addr
}
}
return addrs[0]
}
// RequestHeaderOps implements TransportHeaderOpsProvider. It returns header // RequestHeaderOps implements TransportHeaderOpsProvider. It returns header
// operations for requests when the transport's configuration indicates they // operations for requests when the transport's configuration indicates they
// should be applied. In particular, when TLS is enabled for this transport, // should be applied. In particular, when TLS is enabled for this transport,
@@ -623,6 +709,16 @@ func (h HTTPTransport) Cleanup() error {
return nil return nil
} }
h.Transport.CloseIdleConnections() h.Transport.CloseIdleConnections()
// h3 related cleanup, errors are ignored as nothing can be done.
// TODO: log these errors if any
if h.h3Transport != nil {
h.h3Transport.CloseIdleConnections()
_ = h.h3Transport.Close()
if h.quicTransport != nil {
_ = h.quicTransport.Close()
_ = h.quicTransport.Conn.Close()
}
}
return nil return nil
} }
@@ -4,11 +4,14 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net"
"net/url"
"reflect" "reflect"
"testing" "testing"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
) )
func TestHTTPTransportUnmarshalCaddyFileWithCaPools(t *testing.T) { func TestHTTPTransportUnmarshalCaddyFileWithCaPools(t *testing.T) {
@@ -194,3 +197,85 @@ func TestHTTPTransport_DialTLSContext_ProxyProtocol(t *testing.T) {
}) })
} }
} }
// TestHTTPTransport_DialContext_DialInfoOverride is a regression test for
// issue #6447: a `tcp4/`-prefixed upstream silently fell back to plain `tcp`
// because dialContext only honored DialInfo for unix networks. PR #7300 widened
// the condition so DialInfo is honored when no upstream HTTP proxy is in use,
// and skipped (for non-unix networks) when one is. Both halves are pinned here.
func TestHTTPTransport_DialContext_DialInfoOverride(t *testing.T) {
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
defer cancel()
ln, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
t.Cleanup(func() { ln.Close() })
go func() {
for {
c, err := ln.Accept()
if err != nil {
return
}
c.Close()
}
}()
ht := &HTTPTransport{}
rt, err := ht.NewTransport(ctx)
if err != nil {
t.Fatalf("NewTransport: %v", err)
}
proxyURL, err := url.Parse("http://proxy.example:8080")
if err != nil {
t.Fatalf("parse proxy URL: %v", err)
}
tests := []struct {
name string
proxy bool
dialInfo string
defaultAddr string
}{
{
// no proxy: DialInfo should be applied, so the dial lands on
// the live listener despite the bogus default address.
name: "honors DialInfo when no proxy",
proxy: false,
dialInfo: ln.Addr().String(),
defaultAddr: "127.0.0.1:1",
},
{
// proxy active: DialInfo must NOT be applied for non-unix
// networks; the default address (the live listener) is used.
name: "skips DialInfo when proxy active",
proxy: true,
dialInfo: "127.0.0.1:1",
defaultAddr: ln.Addr().String(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dialCtx := context.WithValue(context.Background(), caddyhttp.VarsCtxKey, make(map[string]any))
caddyhttp.SetVar(dialCtx, dialInfoVarKey, DialInfo{
Network: "tcp4",
Address: tt.dialInfo,
})
if tt.proxy {
caddyhttp.SetVar(dialCtx, proxyVarKey, proxyURL)
}
conn, err := rt.DialContext(dialCtx, "tcp", tt.defaultAddr)
if err != nil {
t.Fatalf("DialContext: %v", err)
}
t.Cleanup(func() { conn.Close() })
if got := conn.RemoteAddr().String(); got != ln.Addr().String() {
t.Fatalf("conn.RemoteAddr() = %s, want %s", got, ln.Addr().String())
}
})
}
}
@@ -1,6 +1,7 @@
package reverseproxy package reverseproxy
import ( import (
"context"
"errors" "errors"
"io" "io"
"net" "net"
@@ -8,11 +9,13 @@ import (
"net/http/httptest" "net/http/httptest"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"testing" "testing"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
) )
@@ -255,3 +258,530 @@ func TestDialErrorBodyRetry(t *testing.T) {
}) })
} }
} }
// newExpressionMatcher provisions a MatchExpression for use in tests
func newExpressionMatcher(t *testing.T, expr string) *caddyhttp.MatchExpression {
t.Helper()
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
t.Cleanup(cancel)
m := &caddyhttp.MatchExpression{Expr: expr}
if err := m.Provision(ctx); err != nil {
t.Fatalf("failed to provision expression %q: %v", expr, err)
}
return m
}
// minimalHandlerWithRetryMatch is like minimalHandler but also configures
// RetryMatch so that response-based retry can be tested
func minimalHandlerWithRetryMatch(retries int, retryMatch caddyhttp.MatcherSets, upstreams ...*Upstream) *Handler {
h := minimalHandler(retries, upstreams...)
h.LoadBalancing.RetryMatch = retryMatch
return h
}
// TestResponseRetryStatusCode verifies that when an upstream returns a status
// code matching a retry_match expression, the request is retried on the next
// upstream
func TestResponseRetryStatusCode(t *testing.T) {
// Bad upstream: returns 502
badServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadGateway)
}))
t.Cleanup(badServer.Close)
// Good upstream: returns 200
goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}))
t.Cleanup(goodServer.Close)
retryMatch := caddyhttp.MatcherSets{
caddyhttp.MatcherSet{
newExpressionMatcher(t, "{http.reverse_proxy.status_code} in [502, 503]"),
},
}
// RoundRobin picks index 1 first, then 0
upstreams := []*Upstream{
{Host: new(Host), Dial: goodServer.Listener.Addr().String()},
{Host: new(Host), Dial: badServer.Listener.Addr().String()},
}
h := minimalHandlerWithRetryMatch(1, retryMatch, upstreams...)
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
req = prepareTestRequest(req)
rec := httptest.NewRecorder()
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}))
gotStatus := rec.Code
if err != nil {
if herr, ok := err.(caddyhttp.HandlerError); ok {
gotStatus = herr.StatusCode
}
}
if gotStatus != http.StatusOK {
t.Errorf("status: got %d, want %d (err=%v)", gotStatus, http.StatusOK, err)
}
}
// TestResponseRetryHeader verifies that response header matching triggers
// retries via a CEL expression checking {rp.header.*}
func TestResponseRetryHeader(t *testing.T) {
// Bad upstream: returns 200 but with X-Upstream-Retry header
badServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Upstream-Retry", "true")
w.WriteHeader(http.StatusOK)
w.Write([]byte("bad"))
}))
t.Cleanup(badServer.Close)
// Good upstream: returns 200 without retry header
goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("good"))
}))
t.Cleanup(goodServer.Close)
retryMatch := caddyhttp.MatcherSets{
caddyhttp.MatcherSet{
newExpressionMatcher(t, `{http.reverse_proxy.header.X-Upstream-Retry} == "true"`),
},
}
// RoundRobin picks index 1 first, then 0
upstreams := []*Upstream{
{Host: new(Host), Dial: goodServer.Listener.Addr().String()},
{Host: new(Host), Dial: badServer.Listener.Addr().String()},
}
h := minimalHandlerWithRetryMatch(1, retryMatch, upstreams...)
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
req = prepareTestRequest(req)
rec := httptest.NewRecorder()
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if rec.Code != http.StatusOK {
t.Errorf("status: got %d, want %d", rec.Code, http.StatusOK)
}
if rec.Body.String() != "good" {
t.Errorf("body: got %q, want %q (retried to wrong upstream)", rec.Body.String(), "good")
}
}
// TestResponseRetryNoMatchNoRetry verifies that when no retry_match entries
// match the response, the original response is returned without retrying
func TestResponseRetryNoMatchNoRetry(t *testing.T) {
var hits atomic.Int32
// Server that returns 500 - but retry_match only matches 502/503
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
hits.Add(1)
w.WriteHeader(http.StatusInternalServerError)
}))
t.Cleanup(server.Close)
retryMatch := caddyhttp.MatcherSets{
caddyhttp.MatcherSet{
newExpressionMatcher(t, "{http.reverse_proxy.status_code} in [502, 503]"),
},
}
upstreams := []*Upstream{
{Host: new(Host), Dial: server.Listener.Addr().String()},
{Host: new(Host), Dial: server.Listener.Addr().String()},
}
h := minimalHandlerWithRetryMatch(2, retryMatch, upstreams...)
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
req = prepareTestRequest(req)
rec := httptest.NewRecorder()
_ = h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}))
// Only one hit - no retry since 500 doesn't match [502, 503]
if hits.Load() != 1 {
t.Errorf("upstream hits: got %d, want 1 (should not have retried)", hits.Load())
}
}
// TestResponseRetryExhaustedPreservesStatusCode verifies that when retries
// are exhausted, the actual upstream status code (e.g. 503) is reported
// to the client, not a generic 502
func TestResponseRetryExhaustedPreservesStatusCode(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable) // 503
}))
t.Cleanup(server.Close)
retryMatch := caddyhttp.MatcherSets{
caddyhttp.MatcherSet{
newExpressionMatcher(t, "{http.reverse_proxy.status_code} == 503"),
},
}
upstreams := []*Upstream{
{Host: new(Host), Dial: server.Listener.Addr().String()},
{Host: new(Host), Dial: server.Listener.Addr().String()},
}
h := minimalHandlerWithRetryMatch(1, retryMatch, upstreams...)
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
req = prepareTestRequest(req)
rec := httptest.NewRecorder()
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}))
gotStatus := rec.Code
if err != nil {
if herr, ok := err.(caddyhttp.HandlerError); ok {
gotStatus = herr.StatusCode
}
}
// Must return 503 (actual upstream status), not 502 (generic proxy error)
if gotStatus != http.StatusServiceUnavailable {
t.Errorf("status: got %d, want %d (status code not preserved)", gotStatus, http.StatusServiceUnavailable)
}
}
// TestResponseRetryHeaderCleanup verifies that stale response header
// placeholders from a previous upstream attempt are cleaned up before the
// next retry evaluation. Without cleanup, a header like X-Retry: true from
// upstream A would leak into the retry match for upstream B even if B does
// not set that header
func TestResponseRetryHeaderCleanup(t *testing.T) {
// First upstream: returns 200 with X-Retry header (triggers retry)
firstServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Retry", "true")
w.WriteHeader(http.StatusOK)
w.Write([]byte("first"))
}))
t.Cleanup(firstServer.Close)
// Second upstream: returns 200 WITHOUT X-Retry header (should NOT retry)
secondServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("second"))
}))
t.Cleanup(secondServer.Close)
retryMatch := caddyhttp.MatcherSets{
caddyhttp.MatcherSet{
newExpressionMatcher(t, `{http.reverse_proxy.header.X-Retry} == "true"`),
},
}
// RoundRobin picks index 1 first, then 0
upstreams := []*Upstream{
{Host: new(Host), Dial: secondServer.Listener.Addr().String()},
{Host: new(Host), Dial: firstServer.Listener.Addr().String()},
}
h := minimalHandlerWithRetryMatch(2, retryMatch, upstreams...)
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
req = prepareTestRequest(req)
rec := httptest.NewRecorder()
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Should get "second" - the first upstream's X-Retry header must not
// leak into the second upstream's retry evaluation
if rec.Body.String() != "second" {
t.Errorf("body: got %q, want %q (stale header leaked between retries)", rec.Body.String(), "second")
}
}
// TestRequestOnlyMatcherDoesNotRetryResponses verifies that a pure request
// matcher like method PUT in lb_retry_match does NOT trigger response-based
// retries. Only expression matchers (which can reference response data)
// should trigger response retries
func TestRequestOnlyMatcherDoesNotRetryResponses(t *testing.T) {
var hits atomic.Int32
// Server returns 200 OK for all requests
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
hits.Add(1)
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}))
t.Cleanup(server.Close)
// method PUT matcher - should NOT trigger response retries
retryMatch := caddyhttp.MatcherSets{
caddyhttp.MatcherSet{
caddyhttp.MatchMethod{"PUT"},
},
}
upstreams := []*Upstream{
{Host: new(Host), Dial: server.Listener.Addr().String()},
{Host: new(Host), Dial: server.Listener.Addr().String()},
}
h := minimalHandlerWithRetryMatch(2, retryMatch, upstreams...)
req := httptest.NewRequest(http.MethodPut, "http://example.com/", nil)
req = prepareTestRequest(req)
rec := httptest.NewRecorder()
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Should hit only once - no retry for 200 OK even though method matches
if hits.Load() != 1 {
t.Errorf("upstream hits: got %d, want 1 (should not retry successful responses)", hits.Load())
}
if rec.Code != http.StatusOK {
t.Errorf("status: got %d, want %d", rec.Code, http.StatusOK)
}
}
// brokenUpstreamAddr returns the address of a TCP listener that accepts
// connections but immediately closes them, causing a transport error (not
// a dial error). This simulates an upstream that is reachable but broken
func brokenUpstreamAddr(t *testing.T) string {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
t.Cleanup(func() { ln.Close() })
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
conn.Close()
}
}()
return ln.Addr().String()
}
// TestTransportErrorPlaceholder verifies that the is_transport_error
// placeholder is set to true during transport error evaluation in tryAgain()
// and that expression matchers using {rp.is_transport_error} can match it
func TestTransportErrorPlaceholder(t *testing.T) {
broken := brokenUpstreamAddr(t)
goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}))
t.Cleanup(goodServer.Close)
retryMatch := caddyhttp.MatcherSets{
caddyhttp.MatcherSet{
newExpressionMatcher(t, "{http.reverse_proxy.is_transport_error} == true"),
},
}
// RoundRobin picks index 1 first (broken), then 0 (good)
upstreams := []*Upstream{
{Host: new(Host), Dial: goodServer.Listener.Addr().String()},
{Host: new(Host), Dial: broken},
}
h := minimalHandlerWithRetryMatch(1, retryMatch, upstreams...)
req := httptest.NewRequest(http.MethodPost, "http://example.com/", nil)
req = prepareTestRequest(req)
rec := httptest.NewRecorder()
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}))
gotStatus := rec.Code
if err != nil {
if herr, ok := err.(caddyhttp.HandlerError); ok {
gotStatus = herr.StatusCode
}
}
// POST transport error should be retried because is_transport_error matched
if gotStatus != http.StatusOK {
t.Errorf("status: got %d, want %d (transport error should have been retried)", gotStatus, http.StatusOK)
}
}
// TestTransportErrorPlaceholderNotSetForResponses verifies that the
// is_transport_error placeholder is NOT set when evaluating response
// matchers, so {rp.is_transport_error} is false for response retries
func TestTransportErrorPlaceholderNotSetForResponses(t *testing.T) {
var hits atomic.Int32
// Server returns 502 - but the matcher only checks is_transport_error
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
hits.Add(1)
w.WriteHeader(http.StatusBadGateway)
}))
t.Cleanup(server.Close)
// Only matches transport errors, not response errors
retryMatch := caddyhttp.MatcherSets{
caddyhttp.MatcherSet{
newExpressionMatcher(t, "{http.reverse_proxy.is_transport_error} == true"),
},
}
upstreams := []*Upstream{
{Host: new(Host), Dial: server.Listener.Addr().String()},
{Host: new(Host), Dial: server.Listener.Addr().String()},
}
h := minimalHandlerWithRetryMatch(2, retryMatch, upstreams...)
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
req = prepareTestRequest(req)
rec := httptest.NewRecorder()
_ = h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}))
// Should hit only once - is_transport_error is false during response
// evaluation so the 502 is NOT retried
if hits.Load() != 1 {
t.Errorf("upstream hits: got %d, want 1 (is_transport_error should be false for responses)", hits.Load())
}
}
// TestRetryMatchAllowsExpressionMixedWithOtherMatchers verifies that
// lb_retry_match accepts a block mixing expression with other matchers
func TestRetryMatchAllowsExpressionMixedWithOtherMatchers(t *testing.T) {
tests := []struct {
name string
input string
}{
{
name: "expression alone",
input: `reverse_proxy localhost:9080 {
lb_retry_match {
expression ` + "`{rp.status_code} in [502, 503]`" + `
}
}`,
},
{
name: "method alone",
input: `reverse_proxy localhost:9080 {
lb_retry_match {
method PUT
}
}`,
},
{
name: "expression mixed with method",
input: `reverse_proxy localhost:9080 {
lb_retry_match {
method POST
expression ` + "`{rp.status_code} in [502, 503]`" + `
}
}`,
},
{
name: "expression mixed with path",
input: `reverse_proxy localhost:9080 {
lb_retry_match {
path /api*
expression ` + "`{rp.status_code} == 502`" + `
}
}`,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
h := &Handler{}
d := caddyfile.NewTestDispenser(tc.input)
err := h.UnmarshalCaddyfile(d)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
})
}
}
// TestSubrouteErrorFallbackWithBody is similar to TestDialErrorBodyRetry but
// mimics Subroute's Error handler rather than testing retries specifically
func TestSubrouteErrorFallbackWithBody(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)
_, err = w.Write(body)
if err != nil {
t.Errorf("error writing in good server: %v", err)
}
}))
t.Cleanup(goodServer.Close)
// Handler which will dial error
badProxy := minimalHandler(0, &Upstream{Host: new(Host), Dial: deadUpstreamAddr(t)})
bodyReader := newCloseOnCloseReader("hello world")
req := httptest.NewRequest("POST", "http://localhost/", bodyReader)
// httptest.NewRequest wraps the reader in NopCloser; replace
// it with our close-aware reader so Close() is propagated.
req.Body = bodyReader
req = prepareTestRequest(req)
rec := httptest.NewRecorder()
err := badProxy.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}))
if err == nil {
t.Fatalf("Expected error from badProxy.ServeHTTP")
}
// Simulate the Subroute's Error handler by calling another handler with the
// same request and recorder
goodProxy := minimalHandler(0, &Upstream{Host: new(Host), Dial: goodServer.Listener.Addr().String()})
err = goodProxy.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}))
if err != nil {
t.Fatalf("Expected no error from goodProxy.ServeHTTP, got: %v", err)
}
if rec.Code != http.StatusOK {
t.Errorf("status: got %d, want %d", rec.Code, http.StatusOK)
}
expectedBody := "hello world"
if rec.Body.String() != expectedBody {
t.Errorf("body: got %q, want %q", rec.Body.String(), expectedBody)
}
}
+168 -29
View File
@@ -449,6 +449,39 @@ func (h *Handler) Cleanup() error {
return err return err
} }
// bodyNopCloserIfNotRead wraps a request body to prevent closing if not read, i.e., when
// dialing to upstream fails.
// It will close the body as normal if the body is read.
type bodyNopCloserIfNotRead struct {
io.ReadCloser
read int // tracks the number of bytes read, -1 when first Read returns 0, io.EOF
}
func (b *bodyNopCloserIfNotRead) Read(p []byte) (int, error) {
if b.read == -1 {
return 0, io.EOF
}
n, err := b.ReadCloser.Read(p)
// first Read returns 0, io.EOF
if b.read == 0 && n == 0 && err == io.EOF {
b.read = -1
} else {
b.read += n
}
return n, err
}
func (b *bodyNopCloserIfNotRead) Close() error {
// don't close the body
if b.read == 0 {
return nil
}
// close as usual, when -1, any read will return EOF as the original read will do
// in other cases, the read will fail as body is closed because we do not want partial bodies to be sent to the upstream
// users can buffer the entire request body to allow the request to be resent
return b.ReadCloser.Close()
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
@@ -488,20 +521,19 @@ 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 request contained a body, wrap it in io.NopCloser
// io.NopCloser to prevent Go's transport from closing it on dial // to prevent Go's transport from closing it on dial errors.
// errors. cloneRequest does a shallow copy, so clonedReq.Body and // cloneRequest does a shallow copy, so clonedReq.Body and
// r.Body share the same io.ReadCloser — a dial-failure Close() // r.Body share the same io.ReadCloser — a dial-failure Close()
// would kill the original body for all subsequent retry attempts. // would kill the original body for all subsequent retry
// The real body is closed by the HTTP server when the handler // attempts or subsequent handlers. The real body is closed by
// returns. // the HTTP server when the handler returns.
// //
// If the body was already fully buffered (via request_buffers), // If the body was already fully buffered (via request_buffers),
// we also extract the buffer so the retry loop can replay it // we also extract the buffer so the retry loop can replay it
// from the beginning on each attempt. (see #6259, #7546) // from the beginning on each attempt. (see #6259, #7546, #7713)
var bufferedReqBody *bytes.Buffer var bufferedReqBody *bytes.Buffer
if clonedReq.Body != nil && h.LoadBalancing != nil && if clonedReq.Body != nil {
(h.LoadBalancing.Retries > 0 || h.LoadBalancing.TryDuration > 0) {
if reqBodyBuf, ok := clonedReq.Body.(bodyReadCloser); ok && reqBodyBuf.body == nil && reqBodyBuf.buf != nil { if reqBodyBuf, ok := clonedReq.Body.(bodyReadCloser); ok && reqBodyBuf.body == nil && reqBodyBuf.buf != nil {
bufferedReqBody = reqBodyBuf.buf bufferedReqBody = reqBodyBuf.buf
reqBodyBuf.buf = nil reqBodyBuf.buf = nil
@@ -511,7 +543,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
bufPool.Put(bufferedReqBody) bufPool.Put(bufferedReqBody)
}() }()
} else { } else {
clonedReq.Body = io.NopCloser(clonedReq.Body) clonedReq.Body = &bodyNopCloserIfNotRead{ReadCloser: clonedReq.Body}
} }
} }
@@ -574,6 +606,17 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
// get the updated list of upstreams // get the updated list of upstreams
upstreams := h.Upstreams upstreams := h.Upstreams
if h.DynamicUpstreams != nil { if h.DynamicUpstreams != nil {
if retries > 0 {
// after a failure (and thus during a retry), give dynamic upstream modules an opportunity
// to purge their relevant cache entries so we don't keep retrying bad upstreams
if cachingDynamicUpstreams, ok := h.DynamicUpstreams.(CachingUpstreamSource); ok {
if err := cachingDynamicUpstreams.ResetCache(r); err != nil {
if c := h.logger.Check(zapcore.ErrorLevel, "failed clearing dynamic upstream source's cache"); c != nil {
c.Write(zap.Error(err))
}
}
}
}
dUpstreams, err := h.DynamicUpstreams.GetUpstreams(r) dUpstreams, err := h.DynamicUpstreams.GetUpstreams(r)
if err != nil { if err != nil {
if c := h.logger.Check(zapcore.ErrorLevel, "failed getting dynamic upstreams; falling back to static upstreams"); c != nil { if c := h.logger.Check(zapcore.ErrorLevel, "failed getting dynamic upstreams; falling back to static upstreams"); c != nil {
@@ -670,8 +713,12 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
return true, succ.error return true, succ.error
} }
// remember this failure (if enabled) // remember this failure (if enabled); response-based retries
h.countFailure(upstream) // are not counted as failures since the upstream did respond
// successfully - only the response content triggered a retry
if _, isRetryableResponse := proxyErr.(retryableResponseError); !isRetryableResponse {
h.countFailure(upstream)
}
// if we've tried long enough, break // if we've tried long enough, break
if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r, h.logger) { if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r, h.logger) {
@@ -1055,6 +1102,45 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
res.Body, _ = h.bufferedBody(res.Body, h.ResponseBuffers) res.Body, _ = h.bufferedBody(res.Body, h.ResponseBuffers)
} }
// set response placeholders so they can be used in retry match
// expressions and handle_response routes; clear stale header
// placeholders from a previous attempt first so they don't
// leak into the next retry evaluation
repl.DeleteByPrefix("http.reverse_proxy.header.")
for field, value := range res.Header {
repl.Set("http.reverse_proxy.header."+field, strings.Join(value, ","))
}
repl.Set("http.reverse_proxy.status_code", res.StatusCode)
repl.Set("http.reverse_proxy.status_text", res.Status)
// check if the response matches a retry match entry; if so,
// close the body and return a retryable error so the request
// is retried with the next upstream. Only evaluate matcher sets
// that contain at least one expression matcher, since those are
// the ones that can reference response data ({rp.status_code},
// {rp.header.*}). Pure request-only matchers (method, path, etc.)
// are skipped to avoid retrying every response that matches a
// request condition
if h.LoadBalancing != nil && len(h.LoadBalancing.RetryMatch) > 0 {
for _, matcherSet := range h.LoadBalancing.RetryMatch {
if !matcherSetHasExpressionMatcher(matcherSet) {
continue
}
match, err := matcherSet.MatchWithError(req)
if err != nil {
h.logger.Error("error matching request for retry", zap.Error(err))
break
}
if match {
res.Body.Close()
return retryableResponseError{
error: fmt.Errorf("upstream response matched retry_match (status %d)", res.StatusCode),
statusCode: res.StatusCode,
}
}
}
}
// see if any response handler is configured for this response from the backend // see if any response handler is configured for this response from the backend
for i, rh := range h.HandleResponse { for i, rh := range h.HandleResponse {
if rh.Match != nil && !rh.Match.Match(res.StatusCode, res.Header) { if rh.Match != nil && !rh.Match.Match(res.StatusCode, res.Header) {
@@ -1074,14 +1160,6 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
break break
} }
// set up the replacer so that parts of the original response can be
// used for routing decisions
for field, value := range res.Header {
repl.Set("http.reverse_proxy.header."+field, strings.Join(value, ","))
}
repl.Set("http.reverse_proxy.status_code", res.StatusCode)
repl.Set("http.reverse_proxy.status_text", res.Status)
if c := logger.Check(zapcore.DebugLevel, "handling response"); c != nil { if c := logger.Check(zapcore.DebugLevel, "handling response"); c != nil {
c.Write(zap.Int("handler", i)) c.Write(zap.Int("handler", i))
} }
@@ -1266,18 +1344,29 @@ func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int
// specifically a dialer error, we need to be careful // specifically a dialer error, we need to be careful
if proxyErr != nil { if proxyErr != nil {
_, isDialError := proxyErr.(DialError) _, isDialError := proxyErr.(DialError)
_, isRetryableResponse := proxyErr.(retryableResponseError)
herr, isHandlerError := proxyErr.(caddyhttp.HandlerError) herr, isHandlerError := proxyErr.(caddyhttp.HandlerError)
// if the error occurred after a connection was established, // if the error occurred after a connection was established,
// we have to assume the upstream received the request, and // we have to assume the upstream received the request, and
// retries need to be carefully decided, because some requests // retries need to be carefully decided, because some requests
// are not idempotent // are not idempotent; retryableResponseError is excluded here
if !isDialError && (!isHandlerError || !errors.Is(herr, errNoUpstream)) { // because its retry decision was already made in reverseProxy()
// when the response matchers were evaluated
if !isDialError && !isRetryableResponse && (!isHandlerError || !errors.Is(herr, errNoUpstream)) {
if lb.RetryMatch == nil && req.Method != "GET" { if lb.RetryMatch == nil && req.Method != "GET" {
// by default, don't retry requests if they aren't GET // by default, don't retry requests if they aren't GET
return false return false
} }
// set transport error flag so CEL expressions can use
// {rp.is_transport_error} to decide whether to retry
repl, _ := req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
if repl != nil {
repl.Set("http.reverse_proxy.is_transport_error", true)
defer repl.Delete("http.reverse_proxy.is_transport_error")
}
match, err := lb.RetryMatch.AnyMatchWithError(req) match, err := lb.RetryMatch.AnyMatchWithError(req)
if err != nil { if err != nil {
logger.Error("error matching request for retry", zap.Error(err)) logger.Error("error matching request for retry", zap.Error(err))
@@ -1507,6 +1596,12 @@ func removeConnectionHeaders(h http.Header) {
// statusError returns an error value that has a status code. // statusError returns an error value that has a status code.
func statusError(err error) error { func statusError(err error) error {
// if a response-based retry was exhausted, use the actual upstream
// status code instead of a generic 502
if rre, ok := err.(retryableResponseError); ok {
return caddyhttp.Error(rre.statusCode, err)
}
// errors proxying usually mean there is a problem with the upstream(s) // errors proxying usually mean there is a problem with the upstream(s)
statusCode := http.StatusBadGateway statusCode := http.StatusBadGateway
@@ -1558,13 +1653,15 @@ type LoadBalancing struct {
// to spin if all backends are down and latency is very low. // to spin if all backends are down and latency is very low.
TryInterval caddy.Duration `json:"try_interval,omitempty"` TryInterval caddy.Duration `json:"try_interval,omitempty"`
// A list of matcher sets that restricts with which requests retries are // A list of matcher sets that controls retry behavior. Matcher sets
// allowed. A request must match any of the given matcher sets in order // without expression matchers (e.g. method, path) restrict which
// to be retried if the connection to the upstream succeeded but the // requests are retried on transport errors - if unspecified, only
// subsequent round-trip failed. If the connection to the upstream failed, // GET requests will be retried. Matcher sets with CEL expression
// a retry is always allowed. If unspecified, only GET requests will be // matchers are evaluated against upstream responses and can
// allowed to be retried. Note that a retry is done with the next available // reference {rp.status_code}, {rp.header.*}, and
// host according to the load balancing policy. // {rp.is_transport_error}. Dial errors are always retried
// regardless of this setting. Retries use the next available
// upstream per the load balancing policy
RetryMatchRaw caddyhttp.RawMatcherSets `json:"retry_match,omitempty" caddy:"namespace=http.matchers"` RetryMatchRaw caddyhttp.RawMatcherSets `json:"retry_match,omitempty" caddy:"namespace=http.matchers"`
SelectionPolicy Selector `json:"-"` SelectionPolicy Selector `json:"-"`
@@ -1586,10 +1683,28 @@ type Selector interface {
// may be called during each retry, multiple times per request, and as // may be called during each retry, multiple times per request, and as
// such, needs to be instantaneous. The returned slice will not be // such, needs to be instantaneous. The returned slice will not be
// modified. // modified.
//
// For upstream sources that cache results, implement the
// [CachingUpstreamSource] interface for optimal performance.
type UpstreamSource interface { type UpstreamSource interface {
GetUpstreams(*http.Request) ([]*Upstream, error) GetUpstreams(*http.Request) ([]*Upstream, error)
} }
// CachingUpstreamSource is an upstream source that caches its upstreams.
// The relevant cache entry can be cleared/reset for a given request during
// retries if a request fails. This can help ensure that failing backends
// are not retried.
//
// EXPERIMENTAL: Subject to change.
type CachingUpstreamSource interface {
UpstreamSource
// ResetCache clears any cache entry related to the given request.
// The next time GetUpstreams is called, it should have new upstream
// information for the given request.
ResetCache(*http.Request) error
}
// Hop-by-hop headers. These are removed when sent to the backend. // Hop-by-hop headers. These are removed when sent to the backend.
// As of RFC 7230, hop-by-hop headers are required to appear in the // As of RFC 7230, hop-by-hop headers are required to appear in the
// Connection header field. These are the headers defined by the // Connection header field. These are the headers defined by the
@@ -1662,10 +1777,34 @@ type RequestHeaderOpsTransport interface {
RequestHeaderOps() *headers.HeaderOps RequestHeaderOps() *headers.HeaderOps
} }
// matcherSetHasExpressionMatcher reports whether a matcher set contains
// at least one expression matcher. Expression matchers can reference
// response data via placeholders like {rp.status_code}. Matcher sets
// without expression matchers only test request properties and should
// not be evaluated for response-based retry decisions
func matcherSetHasExpressionMatcher(matcherSet caddyhttp.MatcherSet) bool {
for _, m := range matcherSet {
if _, ok := m.(*caddyhttp.MatchExpression); ok {
return true
}
}
return false
}
// roundtripSucceededError is an error type that is returned if the // roundtripSucceededError is an error type that is returned if the
// roundtrip succeeded, but an error occurred after-the-fact. // roundtrip succeeded, but an error occurred after-the-fact.
type roundtripSucceededError struct{ error } type roundtripSucceededError struct{ error }
// retryableResponseError is returned when the upstream response matched
// a retry_match entry, indicating the request should be retried with the
// next upstream. It preserves the original status code so that if retries
// are exhausted, the actual upstream status is reported instead of a
// generic 502
type retryableResponseError struct {
error
statusCode int
}
// bodyReadCloser is a reader that, upon closing, will return // bodyReadCloser is a reader that, upon closing, will return
// its buffer to the pool and close the underlying body reader. // its buffer to the pool and close the underlying body reader.
type bodyReadCloser struct { type bodyReadCloser struct {
@@ -664,10 +664,12 @@ func (s CookieHashSelection) Select(pool UpstreamPool, req *http.Request, w http
return upstream return upstream
} }
cookie := &http.Cookie{ cookie := &http.Cookie{
Name: s.Name, Name: s.Name,
Value: sha, Value: sha,
Path: "/", Path: "/",
Secure: false, Secure: false,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
} }
isProxyHttps := false isProxyHttps := false
if trusted, ok := caddyhttp.GetVar(req.Context(), caddyhttp.TrustedProxyVarKey).(bool); ok && trusted { if trusted, ok := caddyhttp.GetVar(req.Context(), caddyhttp.TrustedProxyVarKey).(bool); ok && trusted {
@@ -568,7 +568,7 @@ func TestQueryHashPolicy(t *testing.T) {
pool[1].setHealthy(false) pool[1].setHealthy(false)
h = queryPolicy.Select(pool, request, nil) h = queryPolicy.Select(pool, request, nil)
if h != nil { if h != nil {
t.Error("Expected query policy policy host to be nil.") t.Error("Expected query policy host to be nil.")
} }
request = httptest.NewRequest(http.MethodGet, "/?foo=aa11&foo=bb22", nil) request = httptest.NewRequest(http.MethodGet, "/?foo=aa11&foo=bb22", nil)
@@ -630,7 +630,7 @@ func TestURIHashPolicy(t *testing.T) {
pool[1].setHealthy(false) pool[1].setHealthy(false)
h = uriPolicy.Select(pool, request, nil) h = uriPolicy.Select(pool, request, nil)
if h != nil { if h != nil {
t.Error("Expected uri policy policy host to be nil.") t.Error("Expected uri policy host to be nil.")
} }
} }
+17 -4
View File
@@ -119,6 +119,18 @@ func (su *SRVUpstreams) Provision(ctx caddy.Context) error {
return nil return nil
} }
func (su *SRVUpstreams) ResetCache(r *http.Request) error {
srvsMu.Lock()
if r == nil {
srvs = make(map[string]srvLookup)
} else {
suAddr, _, _, _ := su.expandedAddr(r)
delete(srvs, suAddr)
}
srvsMu.Unlock()
return nil
}
func (su SRVUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) { func (su SRVUpstreams) GetUpstreams(r *http.Request) ([]*Upstream, error) {
suAddr, service, proto, name := su.expandedAddr(r) suAddr, service, proto, name := su.expandedAddr(r)
@@ -554,8 +566,9 @@ var (
// Interface guards // Interface guards
var ( var (
_ caddy.Provisioner = (*SRVUpstreams)(nil) _ caddy.Provisioner = (*SRVUpstreams)(nil)
_ UpstreamSource = (*SRVUpstreams)(nil) _ UpstreamSource = (*SRVUpstreams)(nil)
_ caddy.Provisioner = (*AUpstreams)(nil) _ CachingUpstreamSource = (*SRVUpstreams)(nil)
_ UpstreamSource = (*AUpstreams)(nil) _ caddy.Provisioner = (*AUpstreams)(nil)
_ UpstreamSource = (*AUpstreams)(nil)
) )
+35 -6
View File
@@ -211,12 +211,7 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
var newPath, newQuery, newFrag string var newPath, newQuery, newFrag string
if path != "" { if path != "" {
// replace the `path` placeholder to escaped path path = escapePathPlaceholders(path, r, repl)
pathPlaceholder := "{http.request.uri.path}"
if strings.Contains(path, pathPlaceholder) {
path = strings.ReplaceAll(path, pathPlaceholder, r.URL.EscapedPath())
}
newPath = repl.ReplaceAll(path, "") newPath = repl.ReplaceAll(path, "")
} }
@@ -228,6 +223,15 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
newPath, injectedQuery = before, after newPath, injectedQuery = before, after
// don't overwrite explicitly-configured query string // don't overwrite explicitly-configured query string
if query == "" { if query == "" {
// the injected query came from the first-pass placeholder
// expansion above, which means any '{' or '}' bytes in it
// must have come from replacement values (e.g. a request
// header), not from operator-written placeholder syntax.
// escape them so buildQueryString does not re-expand them,
// which would allow attacker input like {env.SECRET} to be
// evaluated (see GHSA-j8px-rmrx-76h9).
injectedQuery = strings.ReplaceAll(injectedQuery, "{", "%7B")
injectedQuery = strings.ReplaceAll(injectedQuery, "}", "%7D")
query = injectedQuery query = injectedQuery
} }
} }
@@ -300,6 +304,31 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
return r.Method != oldMethod || r.RequestURI != oldURI return r.Method != oldMethod || r.RequestURI != oldURI
} }
func escapePathPlaceholders(path string, r *http.Request, repl *caddy.Replacer) string {
// Replace path-valued placeholders in escaped form before the URI is parsed,
// otherwise literal '?' and '%' bytes from the path can be interpreted as URI
// delimiters or percent-escape sequences during the rewrite.
pathPlaceholder := "{http.request.uri.path}"
if strings.Contains(path, pathPlaceholder) {
path = strings.ReplaceAll(path, pathPlaceholder, r.URL.EscapedPath())
}
fileMatchRelativePlaceholder := "{http.matchers.file.relative}"
if strings.Contains(path, fileMatchRelativePlaceholder) {
if val, ok := repl.Get("http.matchers.file.relative"); ok {
if relativePath, ok := val.(string); ok {
path = strings.ReplaceAll(path, fileMatchRelativePlaceholder, escapePathPreservingSlashes(relativePath))
}
}
}
return path
}
func escapePathPreservingSlashes(path string) string {
return strings.ReplaceAll(url.PathEscape(path), "%2F", "/")
}
// buildQueryString takes an input query string and // buildQueryString takes an input query string and
// performs replacements on each component, returning // performs replacements on each component, returning
// the resulting query string. This function appends // the resulting query string. This function appends
+36
View File
@@ -18,6 +18,7 @@ import (
"net/http" "net/http"
"reflect" "reflect"
"regexp" "regexp"
"strings"
"testing" "testing"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
@@ -350,6 +351,32 @@ func TestRewrite(t *testing.T) {
input: newRequest(t, "GET", "/foo//bar///baz?a=b//c"), input: newRequest(t, "GET", "/foo//bar///baz?a=b//c"),
expect: newRequest(t, "GET", "/foo/bar/baz?a=b//c"), expect: newRequest(t, "GET", "/foo/bar/baz?a=b//c"),
}, },
// regression tests for GHSA-j8px-rmrx-76h9: when the rewrite URI
// ends with a literal '?', the first-pass placeholder expansion
// may produce a path containing attacker-controlled bytes that
// then get split at '?' and fed into buildQueryString, which runs
// a SECOND placeholder pass. Bytes injected via a header value (or
// any other client-controlled placeholder) must not be treated as
// placeholder syntax during this second pass.
{
// literal header value containing placeholder syntax is not re-expanded into query
rule: Rewrite{URI: "/serve/{http.request.header.X-Fwd}?"},
input: newRequestWithHeader(t, "GET", "/anything", "X-Fwd", "foo?{env.CADDY_REWRITE_TEST_SECRET}=leak"),
expect: newRequest(t, "GET", "/serve/foo?%7Benv.CADDY_REWRITE_TEST_SECRET%7D=leak"),
},
{
// literal header value with placeholder syntax in query position is not re-expanded
rule: Rewrite{URI: "/serve/{http.request.header.X-Fwd}?"},
input: newRequestWithHeader(t, "GET", "/anything", "X-Fwd", "ok?key={env.CADDY_REWRITE_TEST_SECRET}"),
expect: newRequest(t, "GET", "/serve/ok?key=%7Benv.CADDY_REWRITE_TEST_SECRET%7D"),
},
{
// literal header value with embedded file placeholder is not re-expanded
rule: Rewrite{URI: "/serve/{http.request.header.X-Fwd}?"},
input: newRequestWithHeader(t, "GET", "/anything", "X-Fwd", "ok?path={file./etc/passwd}"),
expect: newRequest(t, "GET", "/serve/ok?path=%7Bfile./etc/passwd%7D"),
},
} { } {
// copy the original input just enough so that we can // copy the original input just enough so that we can
// compare it after the rewrite to see if it changed // compare it after the rewrite to see if it changed
@@ -364,6 +391,9 @@ func TestRewrite(t *testing.T) {
repl.Set("http.request.uri", tc.input.RequestURI) repl.Set("http.request.uri", tc.input.RequestURI)
repl.Set("http.request.uri.path", tc.input.URL.Path) repl.Set("http.request.uri.path", tc.input.URL.Path)
repl.Set("http.request.uri.query", tc.input.URL.RawQuery) repl.Set("http.request.uri.query", tc.input.URL.RawQuery)
for field, vals := range tc.input.Header {
repl.Set("http.request.header."+field, strings.Join(vals, ","))
}
// we can't directly call Provision() without a valid caddy.Context // we can't directly call Provision() without a valid caddy.Context
// (TODO: fix that) so here we ad-hoc compile the regex // (TODO: fix that) so here we ad-hoc compile the regex
@@ -456,6 +486,12 @@ func newRequest(t *testing.T, method, uri string) *http.Request {
return req return req
} }
func newRequestWithHeader(t *testing.T, method, uri, headerKey, headerVal string) *http.Request {
req := newRequest(t, method, uri)
req.Header.Set(headerKey, headerVal)
return req
}
// reqEqual if r1 and r2 are equal enough for our purposes. // reqEqual if r1 and r2 are equal enough for our purposes.
func reqEqual(r1, r2 *http.Request) bool { func reqEqual(r1, r2 *http.Request) bool {
if r1.Method != r2.Method { if r1.Method != r2.Method {
+5 -4
View File
@@ -18,6 +18,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"slices"
"strings" "strings"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
@@ -241,8 +242,8 @@ func (routes RouteList) Compile(next Handler) Handler {
mid = append(mid, wrapRoute(route)) mid = append(mid, wrapRoute(route))
} }
stack := next stack := next
for i := len(mid) - 1; i >= 0; i-- { for _, middleware := range slices.Backward(mid) {
stack = mid[i](stack) stack = middleware(stack)
} }
return stack return stack
} }
@@ -305,8 +306,8 @@ func wrapRoute(route Route) Middleware {
} }
// compile this route's handler stack // compile this route's handler stack
for i := len(route.middleware) - 1; i >= 0; i-- { for _, middleware := range slices.Backward(route.middleware) {
nextCopy = route.middleware[i](nextCopy) nextCopy = middleware(nextCopy)
} }
// Apply metrics instrumentation once for the entire route, // Apply metrics instrumentation once for the entire route,
+65 -15
View File
@@ -28,7 +28,7 @@ import (
"runtime" "runtime"
"slices" "slices"
"strings" "strings"
"sync" "sync/atomic"
"time" "time"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
@@ -291,8 +291,7 @@ type Server struct {
trustedProxies IPRangeSource trustedProxies IPRangeSource
shutdownAt time.Time shutdownAt atomic.Pointer[time.Time]
shutdownAtMu *sync.RWMutex
// registered callback functions // registered callback functions
connStateFuncs []func(net.Conn, http.ConnState) connStateFuncs []func(net.Conn, http.ConnState)
@@ -301,6 +300,8 @@ type Server struct {
onStopFuncs []func(context.Context) error // TODO: Experimental (Nov. 2023) onStopFuncs []func(context.Context) error // TODO: Experimental (Nov. 2023)
} }
var defaultProtocols = []string{"h1", "h2", "h3"}
var ( var (
ServerHeader = "Caddy" ServerHeader = "Caddy"
serverHeader = []string{ServerHeader} serverHeader = []string{ServerHeader}
@@ -493,6 +494,19 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) error {
} }
} }
// Drop headers whose names contain `_`: once FastCGI/CGI/FrankenPHP etc. rewrites `-` to
// `_`, an underscore alias collides with the legitimate hyphenated header
// and can bypass `forward_auth copy_headers` (GHSA-f59h-q822-g45g).
for k := range r.Header {
if strings.ContainsRune(k, '_') {
delete(r.Header, k)
if c := s.logger.Check(zapcore.DebugLevel, "dropping header containing underscore"); c != nil {
c.Write(zap.String("header", k))
}
}
}
// execute the primary handler chain // execute the primary handler chain
return s.primaryHandlerChain.ServeHTTP(w, r) return s.primaryHandlerChain.ServeHTTP(w, r)
} }
@@ -900,22 +914,58 @@ func (s *Server) logRequest(
// protocol returns true if the protocol proto is configured/enabled. // protocol returns true if the protocol proto is configured/enabled.
func (s *Server) protocol(proto string) bool { func (s *Server) protocol(proto string) bool {
if s.ListenProtocols == nil { if s.ListenProtocols == nil {
if slices.Contains(s.Protocols, proto) { return slices.Contains(s.protocolsWithDefaults(), proto)
}
for _, lnProtocols := range s.ListenProtocols {
if slices.Contains(s.listenerProtocolsWithDefaults(lnProtocols), proto) {
return true return true
} }
} else {
for _, lnProtocols := range s.ListenProtocols {
for _, lnProtocol := range lnProtocols {
if lnProtocol == "" && slices.Contains(s.Protocols, proto) || lnProtocol == proto {
return true
}
}
}
} }
return false return false
} }
func (s *Server) protocolsWithDefaults() []string {
if len(s.Protocols) == 0 {
return defaultProtocols
}
return s.Protocols
}
func (s *Server) listenerProtocolsWithDefaults(lnProtocols []string) []string {
serverProtocols := s.protocolsWithDefaults()
if len(lnProtocols) == 0 {
return serverProtocols
}
lnProtocolsDefault := false
lnProtocolsInclude := make([]string, 0, len(lnProtocols)+len(serverProtocols))
srvProtocolsInclude := make(map[string]struct{}, len(serverProtocols))
for _, srvProtocol := range serverProtocols {
srvProtocolsInclude[srvProtocol] = struct{}{}
}
for _, lnProtocol := range lnProtocols {
if lnProtocol == "" {
lnProtocolsDefault = true
continue
}
lnProtocolsInclude = append(lnProtocolsInclude, lnProtocol)
delete(srvProtocolsInclude, lnProtocol)
}
if lnProtocolsDefault {
for _, srvProtocol := range serverProtocols {
if _, ok := srvProtocolsInclude[srvProtocol]; ok {
lnProtocolsInclude = append(lnProtocolsInclude, srvProtocol)
}
}
}
return lnProtocolsInclude
}
// Listeners returns the server's listeners. These are active listeners, // Listeners returns the server's listeners. These are active listeners,
// so calling Accept() or Close() on them will probably break things. // so calling Accept() or Close() on them will probably break things.
// They are made available here for read-only purposes (e.g. Addr()) // They are made available here for read-only purposes (e.g. Addr())
@@ -1086,11 +1136,11 @@ func strictUntrustedClientIp(r *http.Request, headers []string, trusted []netip.
for _, headerName := range headers { for _, headerName := range headers {
parts := strings.Split(strings.Join(r.Header.Values(headerName), ","), ",") parts := strings.Split(strings.Join(r.Header.Values(headerName), ","), ",")
for i := len(parts) - 1; i >= 0; i-- { for _, part := range slices.Backward(parts) {
// Some proxies may retain the port number, so split if possible // Some proxies may retain the port number, so split if possible
host, _, err := net.SplitHostPort(parts[i]) host, _, err := net.SplitHostPort(part)
if err != nil { if err != nil {
host = parts[i] host = part
} }
// Remove any zone identifier from the IP address // Remove any zone identifier from the IP address
+77
View File
@@ -1,16 +1,20 @@
package caddyhttp package caddyhttp
import ( import (
"bufio"
"bytes" "bytes"
"context" "context"
"io" "io"
"net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/netip" "net/netip"
"strings"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
) )
@@ -478,6 +482,79 @@ func TestServer_DetermineTrustedProxy_MatchRightMostUntrustedSkippingTrusted(t *
assert.Equal(t, clientIP, "45.54.45.54") assert.Equal(t, clientIP, "45.54.45.54")
} }
// TestServer_serveHTTP_DropsUnderscoreHeader covers GHSA-f59h-q822-g45g: an
// underscore-named alias (e.g. `Remote_user`) of a hyphenated header must be
// dropped before any handler runs.
func TestServer_serveHTTP_DropsUnderscoreHeader(t *testing.T) {
got := &http.Header{}
s := &Server{
logger: zap.NewNop(),
primaryHandlerChain: HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
*got = r.Header.Clone()
return nil
}),
}
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
req.Header["X-Real-Header"] = []string{"ok"}
req.Header["Remote_user"] = []string{"attacker"}
req.Header["Remote_groups"] = []string{"admin"}
require.NoError(t, s.serveHTTP(httptest.NewRecorder(), req))
assert.NotContains(t, *got, "Remote_user")
assert.NotContains(t, *got, "Remote_groups")
assert.Equal(t, "ok", got.Get("X-Real-Header"))
}
// TestServer_serveHTTP_LogsDroppedUnderscoreHeader verifies each dropped
// header is emitted at debug level so operators can diagnose unexpectedly
// missing headers without spamming the log on adversarial traffic.
func TestServer_serveHTTP_LogsDroppedUnderscoreHeader(t *testing.T) {
var buf bytes.Buffer
s := &Server{
logger: testLogger(buf.Write),
primaryHandlerChain: HandlerFunc(func(http.ResponseWriter, *http.Request) error {
return nil
}),
}
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
req.Header["Remote_user"] = []string{"attacker"}
require.NoError(t, s.serveHTTP(httptest.NewRecorder(), req))
assert.Contains(t, buf.String(), `"level":"debug"`)
assert.Contains(t, buf.String(), `"msg":"dropping header containing underscore"`)
assert.Contains(t, buf.String(), `"header":"Remote_user"`)
}
// TestServer_SpaceInHeaderNameReturnsBadRequest documents why the underscore
// filter does not also strip space-named headers: Go's HTTP parser rejects a
// space in a field name with 400 before any handler runs, so such a request
// can never reach Caddy's pipeline.
func TestServer_SpaceInHeaderNameReturnsBadRequest(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Errorf("handler must not be reached; got headers %v", r.Header)
}))
t.Cleanup(srv.Close)
addr := strings.TrimPrefix(srv.URL, "http://")
conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
require.NoError(t, err)
t.Cleanup(func() { _ = conn.Close() })
require.NoError(t, conn.SetDeadline(time.Now().Add(5*time.Second)))
_, err = conn.Write([]byte("GET / HTTP/1.1\r\n" +
"Host: " + addr + "\r\n" +
"Remote User: attacker\r\n" +
"Connection: close\r\n\r\n"))
require.NoError(t, err)
resp, err := http.ReadResponse(bufio.NewReader(conn), nil)
require.NoError(t, err)
t.Cleanup(func() { _ = resp.Body.Close() })
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
}
func TestServer_DetermineTrustedProxy_MatchRightMostUntrustedFirst(t *testing.T) { func TestServer_DetermineTrustedProxy_MatchRightMostUntrustedFirst(t *testing.T) {
localPrivatePrefix, _ := netip.ParsePrefix("10.0.0.0/8") localPrivatePrefix, _ := netip.ParsePrefix("10.0.0.0/8")
+30 -2
View File
@@ -36,13 +36,22 @@ func init() {
// Templates is a middleware which executes response bodies as Go templates. // Templates is a middleware which executes response bodies as Go templates.
// The syntax is documented in the Go standard library's // The syntax is documented in the Go standard library's
// [text/template package](https://golang.org/pkg/text/template/). // [text/template package](https://golang.org/pkg/text/template/).
// Note that ANY response body that matches and qualifies may be evaluated,
// even if it comes from a proxied backend.
// //
// ⚠️ Template functions/actions are still experimental, so they are subject to change. // ⚠️ Template functions/actions can access the environment, files on disk,
// and make HTTP requests. This is extremely useful, but you need to make
// sure templates are only evaluated on content that you trust, control, or
// at least sanitize properly.
// //
// Custom template functions can be registered by creating a plugin module under the `http.handlers.templates.functions.*` namespace that implements the `CustomFunctions` interface. // ⚠️ Templates are still experimental, so they are subject to change.
// //
// [All Sprig functions](https://masterminds.github.io/sprig/) are supported. // [All Sprig functions](https://masterminds.github.io/sprig/) are supported.
// //
// Custom template functions can be registered by creating a plugin module
// under the `http.handlers.templates.functions.*` namespace that implements
// the `CustomFunctions` interface.
//
// In addition to the standard functions and the Sprig library, Caddy adds // In addition to the standard functions and the Sprig library, Caddy adds
// extra functions and data that are available to a template: // extra functions and data that are available to a template:
// //
@@ -162,6 +171,25 @@ func init() {
// {{listFiles "/mydir"}} // {{listFiles "/mydir"}}
// ``` // ```
// //
// ##### `fileExists`
//
// Returns true if the given file name, relative to the template context's file root,
// can be opened successfully.
//
// ```
// {{fileExists "path/to/file.html"}}
// ```
//
// ##### `fileStat`
//
// Returns [FileInfo](https://pkg.go.dev/io/fs#FileInfo) using [Stat](https://pkg.go.dev/io/fs#Stat)
// on the given file name, relative to the template context's file root.
//
// ```
// {{$css := fileStat "css/style.css" -}}
// <link rel="stylesheet" href="/css/style.css?v={{ $css.ModTime.Unix }}">
// ```
//
// ##### `markdown` // ##### `markdown`
// //
// Renders the given Markdown text as HTML and returns it. This uses the // Renders the given Markdown text as HTML and returns it. This uses the
+22 -25
View File
@@ -312,35 +312,32 @@ func (c TemplateContext) Host() (string, error) {
return host, nil return host, nil
} }
// funcStripHTML returns s without HTML tags. It is fairly naive // funcStripHTML returns s without HTML tags. Similar to PHP's strip_tags()
// but works with most valid HTML inputs.
func (TemplateContext) funcStripHTML(s string) string { func (TemplateContext) funcStripHTML(s string) string {
var buf bytes.Buffer var buf bytes.Buffer
var inTag, inQuotes bool depth := 0
var tagStart int var quoteChar rune
for i, ch := range s { for _, ch := range s {
if inTag { switch {
if ch == '>' && !inQuotes { case depth > 0 && quoteChar == 0 && (ch == '"' || ch == '\''):
inTag = false // entering a quoted attribute value
} else if ch == '<' && !inQuotes { quoteChar = ch
// false start case depth > 0 && ch == quoteChar:
buf.WriteString(s[tagStart:i]) // leaving a quoted attribute value
tagStart = i quoteChar = 0
} else if ch == '"' { case ch == '<' && quoteChar == 0:
inQuotes = !inQuotes depth++
case ch == '>' && quoteChar == 0:
if depth > 0 {
depth--
} else {
buf.WriteRune(ch) // stray '>' with no opening '<', keep it
}
default:
if depth == 0 {
buf.WriteRune(ch)
} }
continue
} }
if ch == '<' {
inTag = true
tagStart = i
continue
}
buf.WriteRune(ch)
}
if inTag {
// false start
buf.WriteString(s[tagStart:])
} }
return buf.String() return buf.String()
} }
+35 -5
View File
@@ -419,14 +419,44 @@ func TestStripHTML(t *testing.T) {
expect: `h1`, expect: `h1`,
}, },
{ {
// tags not closed // unclosed tag — trailing text must be stripped, not emitted
input: `<h1`, input: `<h1`,
expect: `<h1`, expect: ``,
}, },
{ {
// false start // false start — second '<' increments depth, single '>' only closes one level
input: `<h1<b>hi`, input: `<h1<b>hi`,
expect: `<h1hi`, expect: ``,
},
{
// XSS bypass via double opening bracket
input: `<<>img src=x onerror=alert('XSS')>`,
expect: ``,
},
{
// stacked angle brackets (PHP strip_tags parity)
input: `<<<<<>>>>><b>hello</b>`,
expect: `hello`,
},
{
// unclosed tag strips trailing text
input: `hello <world`,
expect: `hello `,
},
{
// '>' inside double-quoted attribute must not close tag early
input: `<a href="foo>bar">text</a>`,
expect: `text`,
},
{
// '>' inside single-quoted attribute must not close tag early
input: `<a href='foo>bar'>text</a>`,
expect: `text`,
},
{
// stray '>' with no opening '<' is preserved
input: `stray > bracket`,
expect: `stray > bracket`,
}, },
} { } {
actual := tplContext.funcStripHTML(test.input) actual := tplContext.funcStripHTML(test.input)
+1 -2
View File
@@ -21,7 +21,6 @@ import (
) )
const ( const (
webEngineName = "Caddy"
defaultSpanName = "handler" defaultSpanName = "handler"
nextCallCtxKey caddy.CtxKey = "nextCall" nextCallCtxKey caddy.CtxKey = "nextCall"
) )
@@ -58,7 +57,7 @@ func newOpenTelemetryWrapper(
} }
version, _ := caddy.Version() version, _ := caddy.Version()
res, err := ot.newResource(webEngineName, version) res, err := ot.newResource(caddyhttp.ServerHeader, version)
if err != nil { if err != nil {
return ot, fmt.Errorf("creating resource error: %w", err) return ot, fmt.Errorf("creating resource error: %w", err)
} }
+36
View File
@@ -140,6 +140,42 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error {
iss.Email = email iss.Email = email
} }
// expand CA endpoint, if non-empty
if iss.CA != "" {
ca, err := repl.ReplaceOrErr(iss.CA, true, true)
if err != nil {
return fmt.Errorf("expanding CA endpoint '%s': %v", iss.CA, err)
}
iss.CA = ca
}
// expand TestCA endpoint, if non-empty
if iss.TestCA != "" {
testca, err := repl.ReplaceOrErr(iss.TestCA, true, true)
if err != nil {
return fmt.Errorf("expanding TestCA endpoint '%s': %v", iss.TestCA, err)
}
iss.TestCA = testca
}
// expand EAB credentials, if non-empty
if iss.ExternalAccount != nil {
if iss.ExternalAccount.KeyID != "" {
keyID, err := repl.ReplaceOrErr(iss.ExternalAccount.KeyID, true, true)
if err != nil {
return fmt.Errorf("expanding EAB key ID '%s': %v", iss.ExternalAccount.KeyID, err)
}
iss.ExternalAccount.KeyID = keyID
}
if iss.ExternalAccount.MACKey != "" {
macKey, err := repl.ReplaceOrErr(iss.ExternalAccount.MACKey, true, true)
if err != nil {
return fmt.Errorf("expanding EAB MAC key (redacted): %v", err)
}
iss.ExternalAccount.MACKey = macKey
}
}
// expand account key, if non-empty // expand account key, if non-empty
if iss.AccountKey != "" { if iss.AccountKey != "" {
accountKey, err := repl.ReplaceOrErr(iss.AccountKey, true, true) accountKey, err := repl.ReplaceOrErr(iss.AccountKey, true, true)
+43
View File
@@ -0,0 +1,43 @@
package caddytls
import (
"github.com/caddyserver/caddy/v2"
"github.com/mholt/acmez/v3/acme"
"testing"
)
func TestACMEIssuerExpandPlaceholders(t *testing.T) {
t.Setenv("CADDY_TEST_CA_URL", "https://acme.example.com/directory")
t.Setenv("CADDY_TEST_TEST_CA_URL", "https://acme2.example.com/directory")
t.Setenv("CADDY_TEST_EAB_KEY_ID", "example-key-id")
t.Setenv("CADDY_TEST_EAB_MAC_KEY", "example-mac-key")
caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: t.Context()})
defer cancel()
iss := &ACMEIssuer{
CA: "{env.CADDY_TEST_CA_URL}",
TestCA: "{env.CADDY_TEST_TEST_CA_URL}",
ExternalAccount: &acme.EAB{
KeyID: "{env.CADDY_TEST_EAB_KEY_ID}",
MACKey: "{env.CADDY_TEST_EAB_MAC_KEY}",
},
}
if err := iss.Provision(caddyCtx); err != nil {
t.Fatalf("Provision() returned unexpected error: %v", err)
}
if want := "https://acme.example.com/directory"; iss.CA != want {
t.Errorf("CA: got %q, want %q", iss.CA, want)
}
if want := "https://acme2.example.com/directory"; iss.TestCA != want {
t.Errorf("TestCA: got %q, want %q", iss.TestCA, want)
}
if want := "example-key-id"; iss.ExternalAccount.KeyID != want {
t.Errorf("ExternalAccount.KeyID: got %q, want %q", iss.ExternalAccount.KeyID, want)
}
if want := "example-mac-key"; iss.ExternalAccount.MACKey != want {
t.Errorf("ExternalAccount.MACKey: got %q, want %q", iss.ExternalAccount.MACKey, want)
}
}
+1 -1
View File
@@ -158,7 +158,7 @@ type AutomationPolicy struct {
DisableOCSPStapling bool `json:"disable_ocsp_stapling,omitempty"` DisableOCSPStapling bool `json:"disable_ocsp_stapling,omitempty"`
// Overrides the URLs of OCSP responders embedded in certificates. // Overrides the URLs of OCSP responders embedded in certificates.
// Each key is a OCSP server URL to override, and its value is the // Each key is an OCSP server URL to override, and its value is the
// replacement. An empty value will disable querying of that server. // replacement. An empty value will disable querying of that server.
// EXPERIMENTAL. Subject to change. // EXPERIMENTAL. Subject to change.
OCSPOverrides map[string]string `json:"ocsp_overrides,omitempty"` OCSPOverrides map[string]string `json:"ocsp_overrides,omitempty"`
+11 -9
View File
@@ -107,7 +107,8 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
if sni, ok := m.(MatchServerName); ok { if sni, ok := m.(MatchServerName); ok {
for _, sniName := range sni { for _, sniName := range sni {
// index for fast lookups during handshakes // index for fast lookups during handshakes
indexedBySNI[sniName] = append(indexedBySNI[sniName], p) indexName := asciiServerNameForMatch(sniName)
indexedBySNI[indexName] = append(indexedBySNI[indexName], p)
} }
} }
} }
@@ -118,7 +119,7 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
// filter policies by SNI first, if possible, to speed things up // filter policies by SNI first, if possible, to speed things up
// when there may be lots of policies // when there may be lots of policies
possiblePolicies := cp possiblePolicies := cp
if indexedPolicies, ok := indexedBySNI[hello.ServerName]; ok { if indexedPolicies, ok := indexedBySNI[asciiServerNameForMatch(hello.ServerName)]; ok {
possiblePolicies = indexedPolicies possiblePolicies = indexedPolicies
} }
@@ -153,9 +154,9 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
// in its config (remember, TLS connection policies are used by *other* apps to // in its config (remember, TLS connection policies are used by *other* apps to
// run TLS servers) -- we skip names with placeholders // run TLS servers) -- we skip names with placeholders
if tlsApp.EncryptedClientHello.Publication == nil { if tlsApp.EncryptedClientHello.Publication == nil {
var echNames []string
repl := caddy.NewReplacer() repl := caddy.NewReplacer()
for _, p := range cp { for _, p := range cp {
var echNames []string
for _, m := range p.matchers { for _, m := range p.matchers {
if sni, ok := m.(MatchServerName); ok { if sni, ok := m.(MatchServerName); ok {
for _, name := range sni { for _, name := range sni {
@@ -164,8 +165,8 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
} }
} }
} }
tlsApp.RegisterServerNames(echNames, p.ALPN)
} }
tlsApp.RegisterServerNames(echNames)
} }
tlsCfg.GetEncryptedClientHelloKeys = func(chi *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) { tlsCfg.GetEncryptedClientHelloKeys = func(chi *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
@@ -896,18 +897,19 @@ func (clientauth *ClientAuthentication) ConfigureTLSConfig(cfg *tls.Config) erro
// Unlike VerifyPeerCertificate, VerifyConnection is called on every // Unlike VerifyPeerCertificate, VerifyConnection is called on every
// connection including resumed sessions, preventing session-resumption bypass. // connection including resumed sessions, preventing session-resumption bypass.
func (clientauth *ClientAuthentication) verifyConnection(cs tls.ConnectionState) error { func (clientauth *ClientAuthentication) verifyConnection(cs tls.ConnectionState) error {
rawCerts := make([][]byte, len(cs.PeerCertificates))
for i, cert := range cs.PeerCertificates {
rawCerts[i] = cert.Raw
}
// 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))
for i, cert := range cs.PeerCertificates {
rawCerts[i] = cert.Raw
}
if err := clientauth.existingVerifyPeerCert(rawCerts, cs.VerifiedChains); err != nil { 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 { if err := verifier.VerifyClientCertificate(rawCerts, cs.VerifiedChains); err != nil {
return err return err
} }
} }
+36
View File
@@ -15,6 +15,8 @@
package caddytls package caddytls
import ( import (
"context"
"crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"reflect" "reflect"
@@ -24,6 +26,40 @@ import (
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
) )
func TestConnectionPolicyIDNSNIMatcherFastPath(t *testing.T) {
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
defer cancel()
targetTLSConfig := &tls.Config{ClientAuth: tls.RequireAnyClientCert}
policies := ConnectionPolicies{
{
matchers: []ConnectionMatcher{MatchServerName{"つ.Localhost"}},
TLSConfig: targetTLSConfig,
},
}
const sniFastPathThreshold = 30
for i := len(policies); i < sniFastPathThreshold; i++ {
policies = append(policies, &ConnectionPolicy{
matchers: []ConnectionMatcher{MatchServerName{fmt.Sprintf("example-%d.localhost", i)}},
TLSConfig: &tls.Config{},
})
}
policies = append(policies, &ConnectionPolicy{
matchers: []ConnectionMatcher{MatchServerName{"xn--k9j.localhost"}},
TLSConfig: &tls.Config{ClientAuth: tls.NoClientCert},
})
tlsConfig := policies.TLSConfig(ctx)
got, err := tlsConfig.GetConfigForClient(&tls.ClientHelloInfo{ServerName: "XN--K9J.LOCALHOST"})
if err != nil {
t.Fatalf("GetConfigForClient() error = %v", err)
}
if got != targetTLSConfig {
t.Fatalf("expected Unicode IDN policy to match before later punycode policy")
}
}
func TestClientAuthenticationUnmarshalCaddyfileWithDirectiveName(t *testing.T) { func TestClientAuthenticationUnmarshalCaddyfileWithDirectiveName(t *testing.T) {
const test_der_1 = `MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==` const test_der_1 = `MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==`
const test_cert_file_1 = "../../caddytest/caddy.ca.cer" const test_cert_file_1 = "../../caddytest/caddy.ca.cer"

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