Compare commits

..

63 Commits

Author SHA1 Message Date
Francis Lavoie e38228b5f1 reverseproxy: Test that WebSockets + unix-sockets works 2026-04-17 11:45:16 -04:00
prettysunflower 7dedd1486c fix(caddyfile): {block} in snippet (#7558)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m47s
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 1m23s
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 1m25s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m44s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m35s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 2m32s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 2m18s
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 1m48s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m34s
Lint / govulncheck (push) Successful in 2m38s
Lint / dependency-review (push) Failing after 1m29s
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
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 7m56s
* fix(caddyfile): {block} in snippet

Resolve issue #7557

So, here is the situation:
- Pull request #7206 included some changes to the doImport's function of
  Caddyfile's parser. What it does is that if there is no token within a
  block that follows the import, and the import contains `{block}`, then
  the `{block}` token is discarded.
- After this pull request:
  - Issue #7518 noticed that in cases that `{block}` was not imported,
    a runtime error was raised due to the assumption that tokens were
    always added to `tokensCopy` on every iteration of `importedTokens`.
    This was fixed by pull request #7543.
  - Issue #7557 notices that {block} can be ignored when imported from a
    certain file. There, it's again an issue with how the import works.
    When `import snippets` is called, this import instruction doesn't
    contains any nested blocks. And when the argument replacer that is
    the `importedTokens` loop is called and finds `{block}`, it uses the
    block from the file's import (which in this case is nothing),
    `{block}` is erased, and unavailable when the import directive is
    called for the imported snippet.

The changed in this commit addresses the second issue by checking before
replacing `{block}` if we're currently in a snippet definition, and
appending the `{block}` token to `tokensCopy` if we are.

With this changes, when importing those snippets, the `{block}` token
will be available to be replaced by the nested blocks in `tokensToAdd`
if needed, or erased if there are no nested blocks and `tokensToAdd` is empty.

Tests added in pull requests #7206 and #7543 passes with this new
implementation, confirming that unused `{block}` are accepted if nothing
is passed to `import`, as well as the other usual tests.
A new test was also added based on issue #7557 reporting, and also passes.

Signed-off-by: prettysunflower <me@prettysunflower.moe>

* caddyfile: add imported snippet block placeholder coverage

---------

Signed-off-by: prettysunflower <me@prettysunflower.moe>
Co-authored-by: Zen Dodd <mail@steadytao.com>
2026-04-15 02:58:53 -04:00
Max Truxa 7586e68e27 fileserver: show symlink targets verbatim (#7579)
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 1m37s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Failing after 2m28s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 2m27s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 52s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 3m9s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 3m16s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 57s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 59s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m43s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m30s
Lint / lint (ubuntu-latest, linux) (push) Successful in 1m43s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 2m7s
Lint / govulncheck (push) Successful in 1m55s
Lint / dependency-review (push) Failing after 1m12s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 19m42s
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
`reveal_symlinks` was exposing symlink targets as fully resolved absolute paths, even if the target is a relative path. With this change the link target is shown as-is, without resolving anything.
2026-04-15 04:49:30 +10:00
Zen Dodd 0c7c91a447 logging: preserve ts for journald-wrapped JSON logs (#7644)
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 4s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Failing after 3s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Failing after 3s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Failing after 3s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Failing after 2s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Failing after 3s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Failing after 2s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Failing after 1s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Failing after 3s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Failing after 1s
Lint / lint (ubuntu-latest, linux) (push) Failing after 4s
Lint / govulncheck (push) Failing after 9s
Lint / dependency-review (push) Failing after 10s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 3s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 2m6s
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-13 17:33:02 -06:00
Dean Ruina 1a3e900b35 chore: bump timberjack to v1.4.1 (#7618)
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 1m23s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Failing after 1m26s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m37s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 26s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 2m20s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 2m31s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m11s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 47s
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 1m45s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m45s
Lint / lint (ubuntu-latest, linux) (push) Successful in 1m40s
Lint / govulncheck (push) Successful in 1m21s
Lint / dependency-review (push) Failing after 54s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 8m57s
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-13 03:31:59 -04:00
tsinglua 0722cf6fd8 chore: replace interface{} with any for modernization (#7571)
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 1m28s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m38s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m39s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 1m50s
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 1m32s
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 1m41s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m23s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m36s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m34s
Lint / dependency-review (push) Failing after 23s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 27s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m5s
Lint / govulncheck (push) Successful in 1m25s
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: tsinglua <tsinglua@outlook.com>
2026-04-11 19:53:12 +03:00
Zen Dodd 8e2dd5079c caddyfile: Improve import/global options UX for imports before global options (#7642)
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 35s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Failing after 32s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m40s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 2m2s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m29s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m29s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m24s
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 1m30s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m31s
Lint / dependency-review (push) Failing after 24s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m20s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 33s
Lint / govulncheck (push) Successful in 1m23s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m17s
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
* caddyfile: improve import/global options UX

Keep standalone global-options braces stable in fmt when they follow import lines.

Also improve validate output for imports before the global options block with a clearer error message.

Add focused formatter and parser regression coverage

* caddyfile: satisfy staticcheck in formatter
2026-04-10 17:17:55 -06:00
Zen Dodd 5f44ea0748 logging: add journald encoder wrapper (#7623) 2026-04-10 17:09:12 -06:00
dependabot[bot] c8e4ac2c8c build(deps): bump github.com/go-jose/go-jose/v4 from 4.1.3 to 4.1.4 (#7621)
Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.1.3 to 4.1.4.
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Commits](https://github.com/go-jose/go-jose/compare/v4.1.3...v4.1.4)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v4
  dependency-version: 4.1.4
  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-10 16:33:07 -06:00
Zen Dodd 7dcc041eec vars: Add matcher placeholder handling tests (#7640)
* vars: add matcher placeholder handling tests

* vars: add query placeholder matcher coverage
2026-04-10 16:27:52 -06:00
Zen Dodd ca0ca67fbd reverseproxy: make stream copy buffer size configurable (#7627) 2026-04-10 14:49:32 -06:00
dependabot[bot] 92b62004eb build(deps): bump the all-updates group across 1 directory with 11 updates (#7641)
Bumps the all-updates group with 8 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [github.com/DeRuina/timberjack](https://github.com/DeRuina/timberjack) | `1.4.0` | `1.4.1` |
| [github.com/google/cel-go](https://github.com/google/cel-go) | `0.27.0` | `0.28.0` |
| [go.opentelemetry.io/contrib/exporters/autoexport](https://github.com/open-telemetry/opentelemetry-go-contrib) | `0.67.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.67.0` | `0.68.0` |
| [go.step.sm/crypto](https://github.com/smallstep/crypto) | `0.77.1` | `0.77.2` |
| [golang.org/x/crypto](https://github.com/golang/crypto) | `0.49.0` | `0.50.0` |
| [golang.org/x/net](https://github.com/golang/net) | `0.52.0` | `0.53.0` |



Updates `github.com/DeRuina/timberjack` from 1.4.0 to 1.4.1
- [Release notes](https://github.com/DeRuina/timberjack/releases)
- [Changelog](https://github.com/DeRuina/timberjack/blob/main/CHANGELOG.md)
- [Commits](https://github.com/DeRuina/timberjack/compare/v1.4.0...v1.4.1)

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

Updates `go.opentelemetry.io/contrib/exporters/autoexport` 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/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.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.step.sm/crypto` from 0.77.1 to 0.77.2
- [Release notes](https://github.com/smallstep/crypto/releases)
- [Commits](https://github.com/smallstep/crypto/compare/v0.77.1...v0.77.2)

Updates `golang.org/x/crypto` from 0.49.0 to 0.50.0
- [Commits](https://github.com/golang/crypto/compare/v0.49.0...v0.50.0)

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

Updates `golang.org/x/term` from 0.41.0 to 0.42.0
- [Commits](https://github.com/golang/term/compare/v0.41.0...v0.42.0)

Updates `golang.org/x/sys` from 0.42.0 to 0.43.0
- [Commits](https://github.com/golang/sys/compare/v0.42.0...v0.43.0)

Updates `golang.org/x/text` from 0.35.0 to 0.36.0
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.35.0...v0.36.0)

---
updated-dependencies:
- dependency-name: github.com/DeRuina/timberjack
  dependency-version: 1.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: github.com/google/cel-go
  dependency-version: 0.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  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.step.sm/crypto
  dependency-version: 0.77.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
- dependency-name: golang.org/x/crypto
  dependency-version: 0.50.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: golang.org/x/net
  dependency-version: 0.53.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: golang.org/x/term
  dependency-version: 0.42.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: golang.org/x/sys
  dependency-version: 0.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-updates
- dependency-name: golang.org/x/text
  dependency-version: 0.36.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>
2026-04-10 20:39:00 +00:00
dependabot[bot] 6c23ec2f3c build(deps): bump go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp (#7637)
Bumps [go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp](https://github.com/open-telemetry/opentelemetry-go) from 1.42.0 to 1.43.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.42.0...v1.43.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp
  dependency-version: 1.43.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-10 14:31:08 -06:00
vnxme 5de1565ff6 vars: Don't expand placeholders in values (#7629) 2026-04-10 09:37:43 -06:00
Harsh Patel d7834676aa tls: add system and combined CA pool modules (#7406)
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 1m59s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m58s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m58s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 3m9s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m37s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m39s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m40s
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 1m39s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m38s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m38s
Lint / dependency-review (push) Failing after 58s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m28s
Lint / govulncheck (push) Successful in 2m11s
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
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 28s
* feat: add system and combined CA pool modules

* fix: combining pools using `CertificateProvider`

* fix: lint issue

* chore: caddyfiletests
* doing it for first time, so not sure if its right.

* fix: use `x509` native addCert

* chore: explicit err handling

* Apply suggestion from @mohammed90

---------

Co-authored-by: Mohammed Al Sahaf <mohammed@caffeinatedwonders.com>
2026-04-06 01:13:34 +03:00
Pieter Berkel 4f50458866 tls: expand placeholders in dns_challenge override_domain (#7609)
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 2m43s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 2m41s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 2m46s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 2m53s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m30s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m38s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m38s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m39s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m24s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m33s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m35s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m12s
Lint / dependency-review (push) Failing after 57s
Lint / govulncheck (push) Successful in 1m47s
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
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 8m11s
2026-03-31 05:46:32 +00:00
yubiuser ea4ee3ae5d reverseproxy: Fix check for header_up Host {upstream_hostport} redundancy (#7564)
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 2m32s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 2m28s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 2m31s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 2m39s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m29s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m46s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m41s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m45s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m29s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m29s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m43s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m12s
Lint / dependency-review (push) Failing after 1m17s
Lint / govulncheck (push) Successful in 1m37s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 8m4s
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 check for header_up

Signed-off-by: yubiuser <github@yubiuser.dev>

* Onyl check in case commonScheme == "https"

Signed-off-by: yubiuser <github@yubiuser.dev>

* Move check after TLS transport is enabled

Signed-off-by: yubiuser <github@yubiuser.dev>

---------

Signed-off-by: yubiuser <github@yubiuser.dev>
2026-03-30 10:56:10 -06:00
Francis Lavoie 30b80bece8 notify: Always send "READY=1" even after an error (#7597)
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 1m39s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m40s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m40s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m49s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m54s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m51s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 2m26s
Lint / dependency-review (push) Failing after 29s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 43s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m27s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m35s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m37s
Lint / govulncheck (push) Successful in 1m26s
Lint / lint (ubuntu-latest, linux) (push) Successful in 1m59s
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-03-28 13:08:34 -06:00
Sam Ottenhoff 7a630f2910 encode: make zstd checksum configurable (#7586)
* http: make zstd checksum configurable

* disable_checksum
2026-03-28 13:07:21 -06:00
Marc 62e9c05264 root: introduce down-propagating Helper.BlockState for other directives/plugins to use (#7594)
* add 'root' key to Helper.State for access in frankenphp's `php_server` directive

* clone state before passing it to child directives, but keep sharing it among sibling directives

* propagate named route state from children to parent

* use BlockState to set "root" instead

* gofmt -w .

* go fmt ./...

* here we go
2026-03-28 17:44:42 +00:00
Tao 6f6771aa1d rewrite: skip query rename when source key is absent (#7599) 2026-03-28 13:10:34 -04:00
Matt Holt acf8d6a1ae caddytls: Consolidate empty APs more smartly (#7567)
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 1m39s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m38s
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 1m45s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m46s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m52s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 1m55s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 2m1s
Lint / dependency-review (push) Failing after 1m4s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m37s
Lint / govulncheck (push) Successful in 1m27s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m35s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m45s
Lint / lint (ubuntu-latest, linux) (push) Successful in 1m59s
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
* caddytls: Consoldate empty APs more smartly (fix #7559)

* Revise consolidation logic
2026-03-26 14:41:34 -06:00
Matt Holt e98ed6232d chore: Resolve recent CI failures (#7593)
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, freebsd) (push) Successful in 1m28s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m37s
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, netbsd) (push) Successful in 1m43s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m46s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m52s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 1m54s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m53s
Lint / dependency-review (push) Failing after 23s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m20s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m18s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m18s
Lint / govulncheck (push) Successful in 2m2s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m11s
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-03-25 23:21:27 -06:00
Matthew Holt c35ba5588d Add missing return to handleError in admin server
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, illumos) (push) Failing after 1m28s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Failing after 1m28s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Failing after 1m38s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 2m27s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 2m27s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 2m30s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m50s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 2m58s
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 / dependency-review (push) Failing after 1m44s
Lint / govulncheck (push) Failing after 1m58s
Lint / lint (ubuntu-latest, linux) (push) Failing after 2m5s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 6m39s
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
Thanks to @Wernerina's LLM for finding this bug
2026-03-25 16:33:24 -06:00
Tao 5d189aff40 caddytls: Avoid default issuers for implicit tailscale policies (#7577)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m37s
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 1m29s
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 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 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 1m25s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m23s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m34s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m28s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m22s
Lint / govulncheck (push) Successful in 1m43s
Lint / dependency-review (push) Failing after 59s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 36s
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-03-20 09:36:03 -06:00
vnxme df65455b1f caddyhttp: Sync placeholder expansion in vars and vars_regexp (#7573)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m26s
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 1m29s
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 1m20s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m40s
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 1m26s
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 1m20s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m30s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m7s
Lint / govulncheck (push) Successful in 1m20s
Lint / dependency-review (push) Failing after 23s
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
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 7m23s
* vars: Expand placeholders in custom variables like in `vars_regexp`

* vars: Reuse variables inside match loops
2026-03-17 13:08:47 -06:00
Matthew Holt 8499e34e10 caddytls: Ensure key list always gets set (fix #7555)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 2m29s
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 1m23s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m38s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m37s
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 1m32s
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 1m27s
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 1m29s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m23s
Lint / govulncheck (push) Successful in 1m44s
Lint / dependency-review (push) Failing after 59s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 6m18s
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-03-16 16:21:47 -06:00
Matthew Holt 1fbb28720b Fix lint errors
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 2m15s
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 2m36s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m14s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m27s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m14s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m27s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m14s
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 1m14s
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 1m13s
Lint / lint (ubuntu-latest, linux) (push) Failing after 1m13s
Lint / govulncheck (push) Successful in 1m31s
Lint / dependency-review (push) Failing after 59s
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
Use VerifyConnection instead of VerifyPeerCertificate; the other 2 fixes are "meh" not really a big deal or an issue at all.
2026-03-11 13:33:59 -06:00
Matthew Holt ffb6ab0644 Revert cosign (see #7536)
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 1m28s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m26s
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 1m31s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m27s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m24s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m21s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m37s
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 1m29s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m2s
Lint / govulncheck (push) Successful in 1m26s
Lint / dependency-review (push) Failing after 27s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 21s
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-03-05 08:41:54 -07:00
dependabot[bot] 9371ee67c6 build(deps): bump the actions-deps group across 1 directory with 12 updates (#7536)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 2m39s
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
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m24s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m22s
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 1m23s
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 1m28s
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 1m27s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m27s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m34s
Lint / govulncheck (push) Successful in 1m26s
Lint / dependency-review (push) Failing after 1m2s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 5m36s
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 actions-deps group with 12 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [actions/checkout](https://github.com/actions/checkout) | `5.0.0` | `6.0.2` |
| [github/ai-moderator](https://github.com/github/ai-moderator) | `1.1.2` | `1.1.4` |
| [step-security/harden-runner](https://github.com/step-security/harden-runner) | `2.13.1` | `2.15.0` |
| [actions/setup-go](https://github.com/actions/setup-go) | `6.0.0` | `6.3.0` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `4.6.2` | `7.0.0` |
| [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) | `6.4.0` | `7.0.0` |
| [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) | `8.0.0` | `9.2.0` |
| [actions/dependency-review-action](https://github.com/actions/dependency-review-action) | `4.8.0` | `4.8.3` |
| [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) | `3.10.0` | `4.0.0` |
| [anchore/sbom-action](https://github.com/anchore/sbom-action) | `0.20.6` | `0.23.0` |
| [peter-evans/repository-dispatch](https://github.com/peter-evans/repository-dispatch) | `4.0.0` | `4.0.1` |
| [github/codeql-action](https://github.com/github/codeql-action) | `3.30.5` | `4.32.4` |



Updates `actions/checkout` from 5.0.0 to 6.0.2
- [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/08c6903cd8c0fde910a37f88322edcfb5dd907a8...de0fac2e4500dabe0009e67214ff5f5447ce83dd)

Updates `github/ai-moderator` from 1.1.2 to 1.1.4
- [Release notes](https://github.com/github/ai-moderator/releases)
- [Commits](https://github.com/github/ai-moderator/compare/6bcdb2a79c2e564db8d76d7d4439d91a044c4eb6...81159c370785e295c97461ade67d7c33576e9319)

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

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

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

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

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

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

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

Updates `anchore/sbom-action` from 0.20.6 to 0.23.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/f8bdd1d8ac5e901a77a92f111440fdb1b593736b...17ae1740179002c89186b61233e0f892c3118b11)

Updates `peter-evans/repository-dispatch` from 4.0.0 to 4.0.1
- [Release notes](https://github.com/peter-evans/repository-dispatch/releases)
- [Commits](https://github.com/peter-evans/repository-dispatch/compare/5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f...28959ce8df70de7be546dd1250a005dd32156697)

Updates `github/codeql-action` from 3.30.5 to 4.32.4
- [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/3599b3baa15b485a2e49ef411a7a4bb2452e7f93...89a39a4e59826350b863aa6b6252a07ad50cf83e)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-deps
- dependency-name: github/ai-moderator
  dependency-version: 1.1.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-deps
- dependency-name: step-security/harden-runner
  dependency-version: 2.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-deps
- dependency-name: actions/setup-go
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-deps
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-deps
- dependency-name: goreleaser/goreleaser-action
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-deps
- dependency-name: golangci/golangci-lint-action
  dependency-version: 9.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-deps
- dependency-name: actions/dependency-review-action
  dependency-version: 4.8.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-deps
- dependency-name: sigstore/cosign-installer
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-deps
- dependency-name: anchore/sbom-action
  dependency-version: 0.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-deps
- dependency-name: peter-evans/repository-dispatch
  dependency-version: 4.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-deps
- dependency-name: github/codeql-action
  dependency-version: 4.32.4
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-04 21:29:44 -07:00
dependabot[bot] 5d20adc7a9 build(deps): bump github.com/smallstep/certificates (#7535)
Bumps the all-updates group with 1 update: [github.com/smallstep/certificates](https://github.com/smallstep/certificates).


Updates `github.com/smallstep/certificates` from 0.30.0-rc2.0.20260211214201-20608299c29c to 0.30.0-rc3
- [Release notes](https://github.com/smallstep/certificates/releases)
- [Changelog](https://github.com/smallstep/certificates/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smallstep/certificates/commits/v0.30.0-rc3)

---
updated-dependencies:
- dependency-name: github.com/smallstep/certificates
  dependency-version: 0.30.0-rc3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-04 21:26:18 -07:00
Francis Lavoie 6e5e08cf58 Wire up Cause for most context cancels (#7538) 2026-03-04 17:14:52 -07:00
Matthew Holt fbfb8fc517 rewrite: Force recomputing path when escaped path matches rewrite target
Thank you for the report by @MaherAzzouzi, and the suggested fix!
2026-03-04 16:18:33 -07:00
Matt Holt e06dfcf6ed Update SECURITY.md
Simplify what versions are supported, clarify our policy for unreleased code (or beta code), and expand our AI policy to require a disclosure in ALL cases, even if AI is not used. As well as an invitation to share in some chocolate milk with us if you're human.
2026-03-04 16:16:24 -07:00
Oleh Konko | semantic verification for trust infra | LLM-augmented operations pipeline (precision-first, claim≤evidence, submit-human) | verify the payload, not the signer 566e710991 fileserver: document hide case-sensitivity (F-CADDY-FILESERVER-HIDE-CASE-001) (#7548) 2026-03-04 17:00:10 -05:00
Tom Paulus a5e7c6e232 reverseproxy: prevent body close on dial-error retries (#7547)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 2m58s
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 1m42s
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 1m27s
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 1m24s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m33s
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 1m29s
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 1m25s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m30s
Lint / govulncheck (push) Successful in 1m50s
Lint / dependency-review (push) Failing after 1m27s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 5m36s
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-03-04 15:17:02 -05:00
Francis Lavoie db2986028f reverseproxy: Track dynamic upstreams, enable passive healthchecking (#7539)
* reverseproxy: Track dynamic upstreams, enable passive healthchecking

* Add tests for dynamic upstream tracking, admin endpoint, health checks
2026-03-04 15:05:26 -05:00
Sam.An 7e83775e3a Merge commit from fork
Only apply repl.ReplaceAll() on values from literal variable names
(e.g. map outputs), not on values resolved from placeholder keys
(e.g. {http.request.header.*}). The placeholder path already resolves
the value via repl.Get(), so a second expansion allows user-controlled
input containing {env.*} or {file.*} to be evaluated, leaking
environment variables and file contents.

Add regression test to verify placeholder-sourced values are not
re-expanded.
2026-03-04 09:08:39 -07:00
newklei 2dbcdefbbe forward_auth: copy_headers does not strip client-supplied identity headers (Fixes GHSA-7r4p-vjf4-gxv4) (#7545)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m48s
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 1m50s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m58s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m58s
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 2m2s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m54s
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 1m41s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m46s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m39s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m31s
Lint / govulncheck (push) Successful in 1m32s
Lint / dependency-review (push) Failing after 28s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 38s
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 using copy_headers in a forward_auth block, client-supplied headers with
the same names were not being removed before being forwarded to the backend.

This happens because PR #6608 added a MatchNot guard that skips the Set
operation when the auth service does not return a given header. That guard
prevents setting headers to empty strings, which is the correct behavior,
but it also means a client can send X-User-Id: admin in their request and
if the auth service validates the token without returning X-User-Id, Caddy
skips the Set and the client value passes through unchanged to the backend.

The fix adds an unconditional delete route for each copy_headers entry,
placed just before the existing conditional set route. The delete always runs
regardless of what the auth service returns. The conditional set still only
runs when the auth service provides that header.

The end result is:
  - Client-supplied headers are always removed
  - When the auth service returns the header, the backend gets that value
  - When the auth service does not return the header, the backend sees nothing

Existing behavior is unchanged for any deployment where the auth service
returns all of the configured copy_headers entries.

Fixes GHSA-7r4p-vjf4-gxv4
2026-03-03 23:30:49 -05:00
Varun Chawla dc36082859 caddyhttp: Collect metrics once per route instead of per handler (#7492)
* perf: collect metrics once per route instead of per handler (#4644)

Move Prometheus metrics instrumentation from the per-handler level to
the per-route level. Previously, every middleware handler in a route was
individually wrapped with metricsInstrumentedHandler, causing metrics to
be collected N times per request (once per handler in the chain). Since
all handlers in a route see the same request, these per-handler metrics
were redundant and added significant CPU overhead (73% of request
handling time per the original profiling).

The fix introduces metricsInstrumentedRoute which wraps the entire
compiled handler chain once in wrapRoute, collecting metrics only when
the route actually matches. The handler label uses the first handler's
module name, which is the most meaningful identifier for the route.

Benchmark results (5 handlers per route):
  Old (per-handler):  ~4650 ns/op, 4400 B/op, 45 allocs/op
  New (per-route):    ~940 ns/op,  816 B/op,   8 allocs/op
  Improvement:        ~5x faster, ~5.4x less memory, ~5.6x fewer allocs

Signed-off-by: Varun Chawla <varun_6april@hotmail.com>

* Remove unused metricsInstrumentedHandler code

Delete the metricsInstrumentedHandler type, its constructor, and
ServeHTTP method since they are no longer used after switching to
route-level metrics collection via metricsInstrumentedRoute. Also
remove the unused metrics parameter from wrapMiddleware and the
middlewareHandlerFunc test helper, and convert existing tests to
use the new route-level API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address review feedback: restore comments, move function to bottom

- Move computeApproximateRequestSize back to bottom of file to minimize diff
- Restore all useful comments that were accidentally dropped
- Old metricsInstrumentedHandler already removed in previous commit

---------

Signed-off-by: Varun Chawla <varun_6april@hotmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:15:55 -07:00
Paulo Henrique 88616e86e6 api: Add all in-flight requests /reverse_proxy/upstreams (Fixes #7277) (#7517)
This refactors the initial approach in PR #7281, replacing the UsagePool
with a dedicated package-level sync.Map and atomic.Int64 to track
in-flight requests without global lock contention.

It also introduces a lookup map in the admin API to fix a potential
O(n^2) iteration over upstreams, ensuring that draining upstreams
are correctly exposed across config reloads without leaking memory.

Co-authored-by: Y.Horie <u5.horie@gmail.com>

reverseproxy: optimize in-flight tracking and admin API

- Replaced sync.RWMutex with sync.Map and atomic.Int64 to avoid lock contention under high RPS.
- Introduced a lookup map in the admin API to fix a potential O(n^2) iteration over upstreams.
2026-03-03 15:14:55 -07:00
Salent Olivick 7b34e3107e core: Check whether @id is unique (#7002)
* caddy.go: Check whether @id is unique(#6991)

* Alternate implementation, using Gemini 3.1

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2026-03-03 15:09:49 -07:00
Francis Lavoie a6acb3902c proxyproto: Generated test coverage (#7540) 2026-03-03 15:08:09 -07:00
Francis Lavoie 45cf61b127 logging: Ensure slog error level logs don't print stack traces (#7512) 2026-03-03 14:44:42 -07:00
Francis Lavoie d935a6956c autohttps: Ensure CertMagic config is recreated after autohttps runs (#7510) 2026-03-03 14:44:06 -07:00
prettysunflower 2dd3852416 fix(caddyfile): Prevent parser to panic when no token were added by empty {block} (#7543)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m24s
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 1m35s
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 1m33s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m30s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m37s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m40s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 2m8s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m34s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m35s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m33s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m18s
Lint / govulncheck (push) Successful in 1m17s
Lint / dependency-review (push) Failing after 24s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 36s
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-03-03 13:16:21 -05:00
Akın Demirci 11b56c6cfc reverseproxy: Fix health_port being ignored in health checks (#7533) 2026-03-03 13:10:54 -05:00
Alexandre Daubois f283062d37 cmd: Custom binary names through CustomBinaryName and CustomLongDescription (#7513)
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 1m36s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m39s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m27s
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 1m37s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m50s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m39s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m46s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m30s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m30s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m15s
Lint / govulncheck (push) Successful in 1m20s
Lint / dependency-review (push) Failing after 24s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 35s
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-03-02 06:04:28 -05:00
WeidiDeng 2ab043b890 reverseproxy: query escape request urls when proxy protocol is enabled (#7537) 2026-03-02 02:04:06 -05:00
Pavel Siomachkin f145bce553 tls: Add tls_resolvers global option for DNS challenge configuration (#7297)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m37s
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 1m53s
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 1m42s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m47s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m51s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m12s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m49s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m39s
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 1m33s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m22s
Lint / govulncheck (push) Successful in 1m24s
Lint / dependency-review (push) Failing after 25s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 37s
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: Francis Lavoie <lavofr@gmail.com>
2026-03-01 15:32:04 -05:00
Matt Holt 174fa2ddb9 caddyhttp: Evaluate tls.client placeholders more accurately (fix #7530) (#7534)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 2m13s
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 1m33s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m40s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m33s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m38s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m43s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m48s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m51s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m37s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m35s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m35s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m29s
Lint / govulncheck (push) Successful in 1m24s
Lint / dependency-review (push) Failing after 25s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 34s
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-02-28 22:03:18 -07:00
Matt Holt cd9e1660aa cmd: Pass configFile, not configFlag, for reload command (#7532)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m41s
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 1m40s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m46s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m36s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m31s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m47s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m4s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m38s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m52s
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 1m36s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m55s
Lint / govulncheck (push) Successful in 2m5s
Lint / dependency-review (push) Failing after 44s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 1m9s
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
* cmd: Pass configFile, not configFlag, for reload command

This *should* fix #7528.

* Remove debug log line

---------

Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2026-02-27 15:24:05 -07:00
Matthew Holt 06a05e383c Revert "encode: Implement Flush for legacy compatibility"
This reverts commit bdcdaf77ba.
2026-02-27 14:14:19 -07:00
Matthew Holt ce203aa9e1 go.mod: Upgrade x/net
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m38s
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
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m34s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m36s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m53s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m37s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m58s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m49s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m46s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m45s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 2m19s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m35s
Lint / govulncheck (push) Successful in 2m4s
Lint / dependency-review (push) Failing after 30s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 1m4s
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-02-27 10:35:24 -07:00
Matthew Holt eac02ee98f caddyhttp: Limit empty Host check to HTTP/1.1 2026-02-27 10:22:39 -07:00
Oleksandr Redko 72eaf2583a chore: Enable modernize linter (#7519)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m39s
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 1m32s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m34s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m45s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m30s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m42s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m12s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m33s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m28s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m50s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m26s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m20s
Lint / govulncheck (push) Failing after 1m40s
Lint / dependency-review (push) Failing after 32s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 48s
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-02-26 14:01:35 -07:00
Fardjad Davari 9798f6964d caddyhttp: Avoid nil pointer dereference in proxyWrapper (#7521)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m25s
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 1m29s
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 1m34s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m39s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m27s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m37s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m59s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m30s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m28s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m3s
Lint / govulncheck (push) Successful in 1m22s
Lint / dependency-review (push) Failing after 23s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 55s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Failing after 10m20s
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-02-25 04:08:41 -05:00
Francis Lavoie 9873752978 logging: Support zstd roll compression (#7515)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m20s
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 1m22s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m34s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m28s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m31s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m27s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 2m0s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m25s
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 1m23s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m0s
Lint / govulncheck (push) Successful in 1m18s
Lint / dependency-review (push) Failing after 25s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 33s
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-02-23 16:04:45 -07:00
Dean Ruina 294dfff443 logging: add DirMode options and propagate FileMode to rotations (#7335)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m20s
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
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m20s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 1m22s
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 1m26s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m30s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m30s
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 1m27s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m21s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m10s
Lint / govulncheck (push) Successful in 1m20s
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
Co-authored-by: Francis Lavoie <lavofr@gmail.com>
2026-02-23 07:27:27 +00:00
Paulo Henrique 76b198f586 http: Sort auto-HTTPS redirect routes by host specificity (fixes #7390) (#7502)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m49s
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 1m46s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 1m31s
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 1m31s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m38s
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 2m22s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m40s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 1m57s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 1m27s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m13s
Lint / govulncheck (push) Successful in 2m1s
Lint / dependency-review (push) Failing after 1m12s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 50s
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-02-21 21:42:40 -05:00
Paulo Henrique 7ffb640a4d httpcaddyfile: Fix missing TLS connection policies when auto_https is default (#7325) (#7507) 2026-02-21 21:42:03 -05:00
Mohammed Al Sahaf d7b21c6104 reverseproxy: fix tls dialing w/ proxy protocol (#7508) 2026-02-21 21:37:10 -05:00
Francis Lavoie 6610e2f1bd chore: Disable windows/arm build target (Go 1.26 disabled) (#7503)
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m45s
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 1m25s
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 1m28s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 1m24s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m27s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 1m27s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 1m36s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m37s
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 1m26s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m3s
Lint / govulncheck (push) Successful in 1m35s
Lint / dependency-review (push) Failing after 23s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 41s
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-02-20 22:47:21 +00:00
110 changed files with 6979 additions and 594 deletions
+8 -7
View File
@@ -1,15 +1,14 @@
# Security Policy
The Caddy project would like to make sure that it stays on top of all practically-exploitable vulnerabilities.
The Caddy project would like to make sure that it stays on top of all relevant and practically-exploitable vulnerabilities.
## Supported Versions
| Version | Supported |
| -------- | ----------|
| 2.latest | ✔️ |
| 1.x | :x: |
| < 1.x | :x: |
| Version | Supported |
| ----------- | ----------|
| 2.latest | ✔️ |
| <= 2.latest | :x: |
## Acceptable Scope
@@ -26,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.
We accept security reports and patches, but do not assign CVEs, for code that has not been released with a non-prerelease tag.
## Reporting a Vulnerability
@@ -33,7 +34,7 @@ We get a lot of difficult reports that turn out to be invalid. Clear, obvious re
First please ensure your report falls within the accepted scope of security bugs (above).
**YOU MUST DISCLOSE THE USE OF LLMs ("AI") INVOLVED IN ANY WAY.** Whether you are using AI for discovery, as part of writing the report or its replies, and/or testing or validating proofs and changes, we require you to mention the extent of it. **FAILURE TO INCLUDE A DISCLOSURE MAY LEAD TO IMMEDIATE DISMISSAL OF YOUR REPORT AND POTENTIAL BLOCKLISTING.**
:warning: **YOU MUST DISCLOSE WHETHER YOU USED LLMs ("AI") IN ANY WAY.** Whether you are using AI for discovery, as part of writing the report or its replies, and/or testing or validating proofs and changes, we require you to mention the extent of it. **FAILURE TO INCLUDE A DISCLOSURE EVEN IF YOU DO NOT USE AI MAY LEAD TO IMMEDIATE DISMISSAL OF YOUR REPORT AND POTENTIAL BLOCKLISTING.** We will not waste our time chatting with bots. But if you're a human, pull up a chair and we'll drink some chocolate milk.
We'll need enough information to verify the bug and make a patch. To speed things up, please include:
+2 -2
View File
@@ -16,8 +16,8 @@ jobs:
models: read
contents: read
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- uses: github/ai-moderator@6bcdb2a79c2e564db8d76d7d4439d91a044c4eb6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- uses: github/ai-moderator@81159c370785e295c97461ade67d7c33576e9319
with:
token: ${{ secrets.GITHUB_TOKEN }}
spam-label: 'spam'
+11 -11
View File
@@ -65,15 +65,15 @@ jobs:
actions: write # to allow uploading artifacts and cache
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
with:
egress-policy: audit
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version: ${{ matrix.GO_SEMVER }}
check-latest: true
@@ -120,7 +120,7 @@ jobs:
./caddy stop
- name: Publish Build Artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: caddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }}
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
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
with:
egress-policy: audit
allowed-endpoints: ci-s390x.caddyserver.com:22
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Run Tests
run: |
set +e
@@ -221,19 +221,19 @@ jobs:
if: github.event.pull_request.head.repo.full_name == 'caddyserver/caddy' && github.actor != 'dependabot[bot]'
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
with:
egress-policy: audit
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
- uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
with:
version: latest
args: check
- name: Install Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version: "~1.26"
check-latest: true
@@ -241,7 +241,7 @@ jobs:
run: |
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
xcaddy version
- uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
- uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
with:
version: latest
args: build --single-target --snapshot
+3 -3
View File
@@ -51,15 +51,15 @@ jobs:
continue-on-error: true
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
with:
egress-policy: audit
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version: ${{ matrix.GO_SEMVER }}
check-latest: true
+8 -8
View File
@@ -45,18 +45,18 @@ jobs:
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
with:
egress-policy: audit
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version: '~1.26'
check-latest: true
- name: golangci-lint
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
with:
version: latest
@@ -73,7 +73,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
with:
egress-policy: audit
@@ -90,14 +90,14 @@ jobs:
pull-requests: write
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
with:
egress-policy: audit
- name: 'Checkout Repository'
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: 'Dependency Review'
uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0
uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3
with:
comment-summary-in-pr: on-failure
# https://github.com/actions/dependency-review-action/issues/430#issuecomment-1468975566
+2 -2
View File
@@ -28,11 +28,11 @@ jobs:
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
with:
egress-policy: audit
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
+7 -7
View File
@@ -28,7 +28,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
# Force fetch upstream tags -- because 65 minutes
@@ -355,23 +355,23 @@ jobs:
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
with:
egress-policy: audit
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Install Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version: ${{ matrix.GO_SEMVER }}
check-latest: true
# Force fetch upstream tags -- because 65 minutes
# tl;dr: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.2.2 runs this line:
# tl;dr: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 runs this line:
# git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +ebc278ec98bb24f2852b61fde2a9bf2e3d83818b:refs/tags/
# which makes its own local lightweight tag, losing all the annotations in the process. Our earlier script ran:
# git fetch --prune --unshallow
@@ -419,7 +419,7 @@ jobs:
- name: Cosign version
run: cosign version
- name: Install Syft
uses: anchore/sbom-action/download-syft@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # main
uses: anchore/sbom-action/download-syft@17ae1740179002c89186b61233e0f892c3118b11 # main
- name: Syft version
run: syft version
- name: Install xcaddy
@@ -428,7 +428,7 @@ jobs:
xcaddy version
# GoReleaser will take care of publishing those artifacts into the release
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
with:
version: latest
args: release --clean --timeout 60m
+3 -3
View File
@@ -24,12 +24,12 @@ jobs:
# See https://github.com/peter-evans/repository-dispatch
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
with:
egress-policy: audit
- name: Trigger event on caddyserver/dist
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
with:
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: caddyserver/dist
@@ -37,7 +37,7 @@ jobs:
client-payload: '{"tag": "${{ github.event.release.tag_name }}"}'
- name: Trigger event on caddyserver/caddy-docker
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4.0.0
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
with:
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
repository: caddyserver/caddy-docker
+4 -4
View File
@@ -37,12 +37,12 @@ jobs:
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0
with:
egress-policy: audit
- name: "Checkout code"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -72,7 +72,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: SARIF file
path: results.sarif
@@ -81,6 +81,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard (optional).
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.29.5
uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v3.29.5
with:
sarif_file: results.sarif
+1
View File
@@ -32,6 +32,7 @@ linters:
- importas
- ineffassign
- misspell
- modernize
- prealloc
- promlinter
- sloglint
+3 -1
View File
@@ -13,7 +13,7 @@ before:
- cp cmd/caddy/main.go caddy-build/main.go
- /bin/sh -c 'cd ./caddy-build && go mod init caddy'
# prepare syso files for windows embedding
- /bin/sh -c 'for a in amd64 arm arm64; do XCADDY_SKIP_BUILD=1 GOOS=windows GOARCH=$a xcaddy build {{.Env.TAG}}; done'
- /bin/sh -c 'for a in amd64 arm64; do XCADDY_SKIP_BUILD=1 GOOS=windows GOARCH=$a xcaddy build {{.Env.TAG}}; done'
- /bin/sh -c 'mv /tmp/buildenv_*/*.syso caddy-build'
# GoReleaser doesn't seem to offer {{.Tag}} at this stage, so we have to embed it into the env
# so we run: TAG=$(git describe --abbrev=0) goreleaser release --rm-dist --skip-publish --skip-validate
@@ -67,6 +67,8 @@ builds:
goarch: s390x
- goos: windows
goarch: riscv64
- goos: windows
goarch: arm
- goos: freebsd
goarch: ppc64le
- goos: freebsd
+6 -1
View File
@@ -749,10 +749,14 @@ func stopAdminServer(srv *http.Server) error {
if srv == nil {
return fmt.Errorf("no admin server")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
timeout := 10 * time.Second
ctx, cancel := context.WithTimeoutCause(context.Background(), timeout, fmt.Errorf("stopping admin server: %ds timeout", int(timeout.Seconds())))
defer cancel()
err := srv.Shutdown(ctx)
if err != nil {
if cause := context.Cause(ctx); cause != nil && errors.Is(err, context.DeadlineExceeded) {
err = cause
}
return fmt.Errorf("shutting down admin server: %v", err)
}
Log().Named("admin").Info("stopped previous server", zap.String("address", srv.Addr))
@@ -855,6 +859,7 @@ func (h adminHandler) serveHTTP(w http.ResponseWriter, r *http.Request) {
Err: errors.New("invalid origin 'null'"),
Message: "Buggy browser is sending null Origin header.",
})
return
}
if h.enforceHost {
+57 -13
View File
@@ -88,7 +88,7 @@ type Config struct {
storage certmagic.Storage
eventEmitter eventEmitter
cancelFunc context.CancelFunc
cancelFunc context.CancelCauseFunc
// fileSystems is a dict of fileSystems that will later be loaded from and added to.
fileSystems FileSystems
@@ -127,10 +127,9 @@ func Load(cfgJSON []byte, forceReload bool) error {
zap.Error(notifyErr),
zap.String("reload_err", err.Error()))
}
return
}
if err := notify.Ready(); err != nil {
Log().Error("unable to notify to service manager of ready state", zap.Error(err))
if notifyErr := notify.Ready(); notifyErr != nil {
Log().Error("unable to notify to service manager of ready state", zap.Error(notifyErr))
}
}()
@@ -227,8 +226,18 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
idx := make(map[string]string)
err = indexConfigObjects(rawCfg[rawConfigKey], "/"+rawConfigKey, idx)
if err != nil {
if len(rawCfgJSON) > 0 {
var oldCfg any
err2 := json.Unmarshal(rawCfgJSON, &oldCfg)
if err2 != nil {
err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
}
rawCfg[rawConfigKey] = oldCfg
} else {
rawCfg[rawConfigKey] = nil
}
return APIError{
HTTPStatus: http.StatusInternalServerError,
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("indexing config: %v", err),
}
}
@@ -248,6 +257,8 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
}
rawCfg[rawConfigKey] = oldCfg
} else {
rawCfg[rawConfigKey] = nil
}
return fmt.Errorf("loading new config: %v", err)
@@ -281,14 +292,19 @@ func indexConfigObjects(ptr any, configPath string, index map[string]string) err
case map[string]any:
for k, v := range val {
if k == idKey {
var idStr string
switch idVal := v.(type) {
case string:
index[idVal] = configPath
idStr = idVal
case float64: // all JSON numbers decode as float64
index[fmt.Sprintf("%v", idVal)] = configPath
idStr = fmt.Sprintf("%v", idVal)
default:
return fmt.Errorf("%s: %s field must be a string or number", configPath, idKey)
}
if existingPath, ok := index[idStr]; ok {
return fmt.Errorf("duplicate ID '%s' found at %s and %s", idStr, existingPath, configPath)
}
index[idStr] = configPath
continue
}
// traverse this object property recursively
@@ -416,7 +432,7 @@ func run(newCfg *Config, start bool) (Context, error) {
// partially copied from provisionContext
if err != nil {
globalMetrics.configSuccess.Set(0)
ctx.cfg.cancelFunc()
ctx.cfg.cancelFunc(fmt.Errorf("configuration start error: %w", err))
if currentCtx.cfg != nil {
certmagic.Default.Storage = currentCtx.cfg.storage
@@ -492,7 +508,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
// cleanup occurs when we return if there
// was an error; if no error, it will get
// cleaned up on next config cycle
ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg})
ctx, cancelCause := NewContextWithCause(Context{Context: context.Background(), cfg: newCfg})
defer func() {
if err != nil {
globalMetrics.configSuccess.Set(0)
@@ -501,7 +517,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
// since the associated config won't be used;
// this will cause all modules that were newly
// provisioned to clean themselves up
cancel()
cancelCause(fmt.Errorf("configuration error: %w", err))
// also undo any other state changes we made
if currentCtx.cfg != nil {
@@ -509,7 +525,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
}
}
}()
newCfg.cancelFunc = cancel // clean up later
newCfg.cancelFunc = cancelCause // clean up later
// set up logging before anything bad happens
if newCfg.Logging == nil {
@@ -729,7 +745,7 @@ func unsyncedStop(ctx Context) {
}
// clean up all modules
ctx.cfg.cancelFunc()
ctx.cfg.cancelFunc(fmt.Errorf("stopping apps"))
}
// Validate loads, provisions, and validates
@@ -737,7 +753,7 @@ func unsyncedStop(ctx Context) {
func Validate(cfg *Config) error {
_, err := run(cfg, false)
if err == nil {
cfg.cancelFunc() // call Cleanup on all modules
cfg.cancelFunc(fmt.Errorf("validation complete")) // call Cleanup on all modules
}
return err
}
@@ -945,6 +961,34 @@ func InstanceID() (uuid.UUID, error) {
// for example.
var CustomVersion string
// CustomBinaryName is an optional string that overrides the root
// command name from the default of "caddy". This is useful for
// downstream projects that embed Caddy but use a different binary
// name. Shell completions and help text will use this name instead
// of "caddy".
//
// Set this variable during `go build` with `-ldflags`:
//
// -ldflags '-X github.com/caddyserver/caddy/v2.CustomBinaryName=my_custom_caddy'
//
// for example.
var CustomBinaryName string
// CustomLongDescription is an optional string that overrides the
// long description of the root Cobra command. This is useful for
// downstream projects that embed Caddy but want different help
// output.
//
// Set this variable in an init() function of a package that is
// imported by your main:
//
// func init() {
// caddy.CustomLongDescription = "My custom server based on Caddy..."
// }
//
// for example.
var CustomLongDescription string
// Version returns the Caddy version in a simple/short form, and
// a full version string. The short form will not have spaces and
// is intended for User-Agent strings and similar, but may be
+1 -1
View File
@@ -270,7 +270,7 @@ func (d *Dispenser) File() string {
// targets are left unchanged. If all the targets are filled,
// then true is returned.
func (d *Dispenser) Args(targets ...*string) bool {
for i := 0; i < len(targets); i++ {
for i := range targets {
if !d.NextArg() {
return false
}
+39 -1
View File
@@ -63,8 +63,33 @@ func Format(input []byte) []byte {
heredocClosingMarker []rune
nesting int // indentation level
currentToken strings.Builder
currentLineFirstToken string
previousLineWasTopLevelImport bool
openBraceOwnLine bool
)
finishToken := func() {
if currentToken.Len() == 0 {
return
}
if currentLineFirstToken == "" {
currentLineFirstToken = currentToken.String()
}
currentToken.Reset()
}
finishLine := func() {
finishToken()
if currentLineFirstToken != "" {
previousLineWasTopLevelImport = nesting == 0 && currentLineFirstToken == "import"
} else if !openBrace || !openBraceOwnLine || openBraceWritten {
previousLineWasTopLevelImport = false
}
currentLineFirstToken = ""
}
write := func(ch rune) {
out.WriteRune(ch)
last = ch
@@ -220,9 +245,11 @@ func Format(input []byte) []byte {
}
if unicode.IsSpace(ch) {
finishToken()
space = true
heredocEscaped = false
if ch == '\n' {
finishLine()
newLines++
}
continue
@@ -249,13 +276,19 @@ func Format(input []byte) []byte {
}
openBrace = false
if beginningOfLine {
if openBraceOwnLine && previousLineWasTopLevelImport {
if last != '\n' {
nextLine()
}
indent()
} else if beginningOfLine {
indent()
} else if !openBraceSpace || !unicode.IsSpace(last) {
write(' ')
}
write('{')
openBraceWritten = true
openBraceOwnLine = false
nextLine()
newLines = 0
// prevent infinite nesting from ridiculous inputs (issue #4169)
@@ -266,8 +299,10 @@ func Format(input []byte) []byte {
switch {
case ch == '{':
finishToken()
openBrace = true
openBraceSpace = spacePrior && !beginningOfLine
openBraceOwnLine = newLines > 0
if openBraceSpace && newLines == 0 {
write(' ')
}
@@ -275,11 +310,13 @@ func Format(input []byte) []byte {
if quotes == "`" {
write('{')
openBraceWritten = true
openBraceOwnLine = false
continue
}
continue
case ch == '}' && (spacePrior || !openBrace):
finishToken()
if quotes == "`" {
write('}')
continue
@@ -324,6 +361,7 @@ func Format(input []byte) []byte {
space = true
}
currentToken.WriteRune(ch)
write(ch)
beginningOfLine = false
+15
View File
@@ -475,6 +475,21 @@ Hope this helps.` + "`" + `
}`,
expect: "https://localhost:8953 {\n\trespond `Here are some random numbers:\n\n{{randNumeric 16}}\n\nHope this helps.`\n}",
},
{
description: "imports before global options block keep standalone brace",
input: `import ./conf.d/matcher_my_subnet.caddy
import ./conf.d/matcher_not_my_subnet.caddy
{
order crowdsec first
order appsec after crowdsec
}`,
expect: `import ./conf.d/matcher_my_subnet.caddy
import ./conf.d/matcher_not_my_subnet.caddy
{
order crowdsec first
order appsec after crowdsec
}`,
},
} {
// the formatter should output a trailing newline,
// even if the tests aren't written to expect that
+24 -3
View File
@@ -507,7 +507,7 @@ func (p *parser) doImport(nesting int) error {
// format, won't check for nesting correctness or any other error, that's what parser does.
if !maybeSnippet && nesting == 0 {
// first of the line
if i == 0 || isNextOnNewLine(tokensCopy[i-1], token) {
if i == 0 || isNextOnNewLine(tokensCopy[len(tokensCopy)-1], token) {
index = 0
} else {
index++
@@ -550,7 +550,11 @@ func (p *parser) doImport(nesting int) error {
}
if foundBlockDirective {
tokensCopy = append(tokensCopy, tokensToAdd...)
if maybeSnippet {
tokensCopy = append(tokensCopy, token)
} else {
tokensCopy = append(tokensCopy, tokensToAdd...)
}
continue
}
@@ -616,7 +620,7 @@ func (p *parser) doSingleImport(importFile string) ([]Token, error) {
if err != nil {
return nil, p.Errf("Failed to get absolute path of file: %s: %v", importFile, err)
}
for i := 0; i < len(importedTokens); i++ {
for i := range importedTokens {
importedTokens[i].File = filename
}
@@ -682,11 +686,28 @@ func (p *parser) directive() error {
// a opening curly brace. It does NOT advance the token.
func (p *parser) openCurlyBrace() error {
if p.Val() != "{" {
if p.valLooksLikeGlobalOptionsAfterImportedSnippets() {
return p.Err("global options block must appear before import directives; move the global options block to the top of the Caddyfile")
}
return p.SyntaxErr("{")
}
return nil
}
func (p *parser) valLooksLikeGlobalOptionsAfterImportedSnippets() bool {
if p.Val() != "import" || len(p.block.Keys) == 0 {
return false
}
for _, key := range p.block.Keys {
if !strings.HasPrefix(key.Text, "(") || !strings.HasSuffix(key.Text, ")") {
return false
}
}
return true
}
// closeCurlyBrace expects the current token to be
// a closing curly brace. This acts like an assertion
// because it returns an error if the token is not
+101
View File
@@ -930,6 +930,107 @@ func TestAcceptSiteImportWithBraces(t *testing.T) {
}
}
func TestGlobalOptionsAfterImportedSnippetsGivesHelpfulError(t *testing.T) {
tempDir := t.TempDir()
importFile1 := filepath.Join(tempDir, "matcher_snippet_1.caddy")
importFile2 := filepath.Join(tempDir, "matcher_snippet_2.caddy")
err := os.WriteFile(importFile1, []byte(`(matcher1)`), 0o644)
if err != nil {
t.Fatalf("writing first import file: %v", err)
}
err = os.WriteFile(importFile2, []byte(`(matcher2)`), 0o644)
if err != nil {
t.Fatalf("writing second import file: %v", err)
}
_, err = Parse("Testfile", []byte(`import `+importFile1+`
import `+importFile2+`
{
debug
}`))
if err == nil {
t.Fatal("Expected an error, but got nil")
}
expected := "global options block must appear before import directives; move the global options block to the top of the Caddyfile"
if !strings.HasPrefix(err.Error(), expected) {
t.Errorf("Expected error to start with '%s' but got '%v'", expected, err)
}
}
func TestImportedSnippetDefinitionRetainsBlockPlaceholder(t *testing.T) {
tempDir := t.TempDir()
importFile := filepath.Join(tempDir, "snippets.caddy")
err := os.WriteFile(importFile, []byte(`
(site) {
http://{args[0]} {
respond "before"
{block}
respond "after"
}
}
`), 0o644)
if err != nil {
t.Fatalf("writing imported snippet file: %v", err)
}
for _, tc := range []struct {
name string
input string
expectedDirectives []string
}{
{
name: "with nested block",
input: `
import ` + importFile + `
import site example.com {
redir https://example.net
}
`,
expectedDirectives: []string{"respond", "redir", "respond"},
},
{
name: "without nested block",
input: `
import ` + importFile + `
import site example.com
`,
expectedDirectives: []string{"respond", "respond"},
},
} {
t.Run(tc.name, func(t *testing.T) {
p := testParser(tc.input)
blocks, err := p.parseAll()
if err != nil {
t.Fatalf("parseAll: %v", err)
}
if len(blocks) != 1 {
t.Fatalf("expected exactly one server block, got %d", len(blocks))
}
if actual := blocks[0].GetKeysText(); len(actual) != 1 || actual[0] != "http://example.com" {
t.Fatalf("expected server block key http://example.com, got %v", actual)
}
if len(blocks[0].Segments) != len(tc.expectedDirectives) {
t.Fatalf("expected %d segments, got %d", len(tc.expectedDirectives), len(blocks[0].Segments))
}
for i, directive := range tc.expectedDirectives {
if actual := blocks[0].Segments[i].Directive(); actual != directive {
t.Fatalf("segment %d: expected directive %q, got %q", i, directive, actual)
}
}
})
}
}
func testParser(input string) parser {
return parser{Dispenser: NewTestDispenser(input)}
}
+6
View File
@@ -668,6 +668,8 @@ func parseRoot(h Helper) ([]ConfigValue, error) {
if !h.NextArg() {
return nil, h.ArgErr()
}
// store the unmatched root in block state so sibling directives can access it
h.BlockState["root"] = h.Val()
return h.NewRoute(nil, caddyhttp.VarsMiddleware{"root": h.Val()}), nil
}
@@ -682,6 +684,10 @@ func parseRoot(h Helper) ([]ConfigValue, error) {
if !h.NextArg() {
return nil, h.ArgErr()
}
// store the unmatched root in state so sibling/child directives can access it
if userMatcherSet == nil {
h.BlockState["root"] = h.Val()
}
// make the route with the matcher
return h.NewRoute(userMatcherSet, caddyhttp.VarsMiddleware{"root": h.Val()}), nil
}
+10 -1
View File
@@ -202,7 +202,10 @@ func RegisterGlobalOption(opt string, setupFunc UnmarshalGlobalFunc) {
type Helper struct {
*caddyfile.Dispenser
// State stores intermediate variables during caddyfile adaptation.
State map[string]any
State map[string]any
// BlockState stores intermediate variables scoped to the current block.
// It propagates down, but unlike state not back up from child to parent.
BlockState map[string]any
options map[string]any
warnings *[]caddyconfig.Warning
matcherDefs map[string]caddy.ModuleMap
@@ -385,6 +388,11 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
}
}
// clone BlockState once for the entire block so sibling directives
// can share state, but changes don't leak to the parent scope
subBlockState := make(map[string]any, len(h.BlockState))
maps.Copy(subBlockState, h.BlockState)
// with matchers ready to go, evaluate each directive's segment
for _, seg := range segments {
dir := seg.Directive()
@@ -396,6 +404,7 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
subHelper := h
subHelper.Dispenser = caddyfile.NewDispenser(seg)
subHelper.matcherDefs = matcherDefs
subHelper.BlockState = subBlockState
results, err := dirFunc(subHelper)
if err != nil {
+3 -1
View File
@@ -143,6 +143,7 @@ func (st ServerType) Setup(
parentBlock: sb.block,
groupCounter: gc,
State: state,
BlockState: state,
}
results, err := dirFunc(h)
@@ -504,6 +505,7 @@ func (ServerType) extractNamedRoutes(
parentBlock: sb.block,
groupCounter: gc,
State: state,
BlockState: state,
}
handler, err := ParseSegmentAsSubroute(h)
@@ -822,7 +824,7 @@ func (st *ServerType) serversFromPairings(
// https://caddy.community/t/making-sense-of-auto-https-and-why-disabling-it-still-serves-https-instead-of-http/9761
createdTLSConnPolicies, ok := sblock.pile["tls.connection_policy"]
hasTLSEnabled := (ok && len(createdTLSConnPolicies) > 0) ||
(addr.Host != "" && srv.AutoHTTPS != nil && !slices.Contains(srv.AutoHTTPS.Skip, addr.Host))
(addr.Host != "" && (srv.AutoHTTPS == nil || !slices.Contains(srv.AutoHTTPS.Skip, addr.Host)))
// we'll need to remember if the address qualifies for auto-HTTPS, so we
// can add a TLS conn policy if necessary
@@ -1,9 +1,11 @@
package httpcaddyfile
import (
"encoding/json"
"testing"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func TestMatcherSyntax(t *testing.T) {
@@ -209,3 +211,53 @@ func TestGlobalOptions(t *testing.T) {
}
}
}
func TestDefaultSNIWithoutHTTPS(t *testing.T) {
caddyfileStr := `{
default_sni my-sni.com
}
example.com {
}`
adapter := caddyfile.Adapter{
ServerType: ServerType{},
}
result, _, err := adapter.Adapt([]byte(caddyfileStr), nil)
if err != nil {
t.Fatalf("Failed to adapt Caddyfile: %v", err)
}
var config struct {
Apps struct {
HTTP struct {
Servers map[string]*caddyhttp.Server `json:"servers"`
} `json:"http"`
} `json:"apps"`
}
if err := json.Unmarshal(result, &config); err != nil {
t.Fatalf("Failed to unmarshal JSON config: %v", err)
}
server, ok := config.Apps.HTTP.Servers["srv0"]
if !ok {
t.Fatalf("Expected server 'srv0' to be created")
}
if len(server.TLSConnPolicies) == 0 {
t.Fatalf("Expected TLS connection policies to be generated, got none")
}
found := false
for _, policy := range server.TLSConnPolicies {
if policy.DefaultSNI == "my-sni.com" {
found = true
break
}
}
if !found {
t.Errorf("Expected default_sni 'my-sni.com' in TLS connection policies, but it was missing. Generated JSON: %s", string(result))
}
}
+10
View File
@@ -64,6 +64,7 @@ func init() {
RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
RegisterGlobalOption("persist_config", parseOptPersistConfig)
RegisterGlobalOption("dns", parseOptDNS)
RegisterGlobalOption("tls_resolvers", parseOptTLSResolvers)
RegisterGlobalOption("ech", parseOptECH)
RegisterGlobalOption("renewal_window_ratio", parseOptRenewalWindowRatio)
}
@@ -306,6 +307,15 @@ func parseOptSingleString(d *caddyfile.Dispenser, _ any) (any, error) {
return val, nil
}
func parseOptTLSResolvers(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
resolvers := d.RemainingArgs()
if len(resolvers) == 0 {
return nil, d.ArgErr()
}
return resolvers, nil
}
func parseOptDefaultBind(d *caddyfile.Dispenser, _ any) (any, error) {
d.Next() // consume option name
+104
View File
@@ -1,9 +1,11 @@
package httpcaddyfile
import (
"encoding/json"
"testing"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/modules/caddytls"
_ "github.com/caddyserver/caddy/v2/modules/logging"
)
@@ -62,3 +64,105 @@ func TestGlobalLogOptionSyntax(t *testing.T) {
}
}
}
func TestGlobalResolversOption(t *testing.T) {
tests := []struct {
name string
input string
expectResolvers []string
expectError bool
}{
{
name: "single resolver",
input: `{
tls_resolvers 1.1.1.1
}
example.com {
}`,
expectResolvers: []string{"1.1.1.1"},
expectError: false,
},
{
name: "two resolvers",
input: `{
tls_resolvers 1.1.1.1 8.8.8.8
}
example.com {
}`,
expectResolvers: []string{"1.1.1.1", "8.8.8.8"},
expectError: false,
},
{
name: "multiple resolvers",
input: `{
tls_resolvers 1.1.1.1 8.8.8.8 9.9.9.9
}
example.com {
}`,
expectResolvers: []string{"1.1.1.1", "8.8.8.8", "9.9.9.9"},
expectError: false,
},
{
name: "no resolvers specified",
input: `{
}
example.com {
}`,
expectResolvers: nil,
expectError: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
adapter := caddyfile.Adapter{
ServerType: ServerType{},
}
out, _, err := adapter.Adapt([]byte(tc.input), nil)
if (err != nil) != tc.expectError {
t.Errorf("error expectation failed. Expected error: %v, got: %v", tc.expectError, err)
return
}
if tc.expectError {
return
}
// Parse the output JSON to check resolvers
var config struct {
Apps struct {
TLS *caddytls.TLS `json:"tls"`
} `json:"apps"`
}
if err := json.Unmarshal(out, &config); err != nil {
t.Errorf("failed to unmarshal output: %v", err)
return
}
// Check if resolvers match expected
if config.Apps.TLS == nil {
if tc.expectResolvers != nil {
t.Errorf("Expected TLS config with resolvers %v, but TLS config is nil", tc.expectResolvers)
}
return
}
actualResolvers := config.Apps.TLS.Resolvers
if len(tc.expectResolvers) == 0 && len(actualResolvers) == 0 {
return // Both empty, ok
}
if len(actualResolvers) != len(tc.expectResolvers) {
t.Errorf("Expected %d resolvers, got %d. Expected: %v, got: %v", len(tc.expectResolvers), len(actualResolvers), tc.expectResolvers, actualResolvers)
return
}
for j, expected := range tc.expectResolvers {
if actualResolvers[j] != expected {
t.Errorf("Resolver %d mismatch. Expected: %s, got: %s", j, expected, actualResolvers[j])
}
}
})
}
}
+34 -3
View File
@@ -334,6 +334,11 @@ func (st ServerType) buildTLSApp(
tlsApp.DNSRaw = caddyconfig.JSONModuleObject(globalDNS, "name", globalDNS.(caddy.Module).CaddyModule().ID.Name(), nil)
}
// set up "global" (to the TLS app) DNS resolvers config
if globalResolvers, ok := options["tls_resolvers"]; ok && globalResolvers != nil {
tlsApp.Resolvers = globalResolvers.([]string)
}
// set up ECH from Caddyfile options
if ech, ok := options["ech"].(*caddytls.ECH); ok {
tlsApp.EncryptedClientHello = ech
@@ -595,6 +600,15 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
if globalCertLifetime != nil && acmeIssuer.CertificateLifetime == 0 {
acmeIssuer.CertificateLifetime = globalCertLifetime.(caddy.Duration)
}
// apply global resolvers if DNS challenge is configured and resolvers are not already set
globalResolvers := options["tls_resolvers"]
if globalResolvers != nil && acmeIssuer.Challenges != nil && acmeIssuer.Challenges.DNS != nil {
// Check if DNS challenge is actually configured
hasDNSChallenge := globalACMEDNSok || acmeIssuer.Challenges.DNS.ProviderRaw != nil
if hasDNSChallenge && len(acmeIssuer.Challenges.DNS.Resolvers) == 0 {
acmeIssuer.Challenges.DNS.Resolvers = globalResolvers.([]string)
}
}
return nil
}
@@ -684,14 +698,31 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
emptyAPCount := 0
origLenAPs := len(aps)
// compute the number of empty policies (disregarding subjects) - see #4128
// while we're at it,
emptyAP := new(caddytls.AutomationPolicy)
for i := 0; i < len(aps); i++ {
emptyAP.SubjectsRaw = aps[i].SubjectsRaw
emptyAP.ManagersRaw = nil
if reflect.DeepEqual(aps[i], emptyAP) {
// AP is empty
emptyAPCount++
if !automationPolicyHasAllPublicNames(aps[i]) {
// if this automation policy has internal names, we might as well remove it
// so auto-https can implicitly use the internal issuer
// see if this AP shadows something later
shadowIdx := automationPolicyShadows(i, aps)
emptyAP.SubjectsRaw = nil
if shadowIdx >= 0 {
emptyAP.SubjectsRaw = aps[shadowIdx].SubjectsRaw
// allow the later policy, which is likely for a wildcard, to have cert
// managers ("get_certificate"), since wildcards now cover specific
// subdomains by default, when configured (see discussion in #7559)
emptyAP.ManagersRaw = aps[shadowIdx].ManagersRaw
}
// if this is the last AP, we can delete it, since auto-https should
// pick it up; if it shadows something later that is also empty, we
// can similarly delete this; but if it shadows something that is NOT
// empty, we must not delete it since the shadowing has a purpose
if i == len(aps)-1 || (shadowIdx >= 0 && reflect.DeepEqual(aps[shadowIdx], emptyAP)) {
aps = slices.Delete(aps, i, i+1)
i--
}
+1 -1
View File
@@ -151,7 +151,7 @@ func doHttpCallWithRetries(ctx caddy.Context, client *http.Client, request *http
var err error
const maxAttempts = 10
for i := 0; i < maxAttempts; i++ {
for i := range maxAttempts {
resp, err = attemptHttpCall(client, request)
if err != nil && i < maxAttempts-1 {
select {
+116
View File
@@ -1,6 +1,7 @@
package caddytest
import (
"bytes"
"net/http"
"strings"
"testing"
@@ -126,3 +127,118 @@ func TestLoadUnorderedJSON(t *testing.T) {
}
tester.AssertResponseCode(req, 200)
}
func TestCheckID(t *testing.T) {
tester := NewTester(t)
tester.InitServer(`{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"http": {
"http_port": 9080,
"servers": {
"s_server": {
"@id": "s_server",
"listen": [
":9080"
],
"routes": [
{
"handle": [
{
"handler": "static_response",
"body": "Hello"
}
]
}
]
}
}
}
}
}
`, "json")
headers := []string{"Content-Type:application/json"}
sServer1 := []byte(`{"@id":"s_server","listen":[":9080"],"routes":[{"@id":"route1","handle":[{"handler":"static_response","body":"Hello 2"}]}]}`)
// PUT to an existing ID should fail with a 409 conflict
tester.AssertPutResponseBody(
"http://localhost:2999/id/s_server",
headers,
bytes.NewBuffer(sServer1),
409,
`{"error":"[/config/apps/http/servers/s_server] key already exists: s_server"}`+"\n")
// POST replaces the object fully
tester.AssertPostResponseBody(
"http://localhost:2999/id/s_server",
headers,
bytes.NewBuffer(sServer1),
200,
"")
// Verify the server is running the new route
tester.AssertGetResponse(
"http://localhost:9080/",
200,
"Hello 2")
// Update the existing route to ensure IDs are handled correctly when replaced
tester.AssertPostResponseBody(
"http://localhost:2999/id/s_server",
headers,
bytes.NewBuffer([]byte(`{"@id":"s_server","listen":[":9080"],"routes":[{"@id":"route1","handle":[{"handler":"static_response","body":"Hello2"}],"match":[{"path":["/route_1/*"]}]}]}`)),
200,
"")
sServer2 := []byte(`{"@id":"s_server","listen":[":9080"],"routes":[{"@id":"route1","handle":[{"handler":"static_response","body":"Hello2"}],"match":[{"path":["/route_1/*"]}]}]}`)
// Identical patch should succeed and return 200 (config is unchanged branch)
tester.AssertPatchResponseBody(
"http://localhost:2999/id/s_server",
headers,
bytes.NewBuffer(sServer2),
200,
"")
route2 := []byte(`{"@id":"route2","handle": [{"handler": "static_response","body": "route2"}],"match":[{"path":["/route_2/*"]}]}`)
// Put a new route2 object before the route1 object due to the path of /id/route1
// Being translated to: /config/apps/http/servers/s_server/routes/0
tester.AssertPutResponseBody(
"http://localhost:2999/id/route1",
headers,
bytes.NewBuffer(route2),
200,
"")
// Verify that the whole config looks correct, now containing both route1 and route2
tester.AssertGetResponse(
"http://localhost:2999/config/",
200,
`{"admin":{"listen":"localhost:2999"},"apps":{"http":{"http_port":9080,"servers":{"s_server":{"@id":"s_server","listen":[":9080"],"routes":[{"@id":"route2","handle":[{"body":"route2","handler":"static_response"}],"match":[{"path":["/route_2/*"]}]},{"@id":"route1","handle":[{"body":"Hello2","handler":"static_response"}],"match":[{"path":["/route_1/*"]}]}]}}}}}`+"\n")
// Try to add another copy of route2 using POST to test duplicate ID handling
// Since the first route2 ended up at array index 0, and we are appending to the array, the index for the new element would be 2
tester.AssertPostResponseBody(
"http://localhost:2999/id/route2",
headers,
bytes.NewBuffer(route2),
400,
`{"error":"indexing config: duplicate ID 'route2' found at /config/apps/http/servers/s_server/routes/0 and /config/apps/http/servers/s_server/routes/2"}`+"\n")
// Use PATCH to modify an existing object successfully
tester.AssertPatchResponseBody(
"http://localhost:2999/id/route1",
headers,
bytes.NewBuffer([]byte(`{"@id":"route1","handle":[{"handler":"static_response","body":"route1"}],"match":[{"path":["/route_1/*"]}]}`)),
200,
"")
// Verify the PATCH updated the server state
tester.AssertGetResponse(
"http://localhost:9080/route_1/",
200,
"route1")
}
+2 -2
View File
@@ -51,7 +51,7 @@ func TestACMEServerWithDefaults(t *testing.T) {
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
Logger: slog.New(zapslog.NewHandler(logger.Core())),
Logger: slog.New(zapslog.NewHandler(logger.Core(), zapslog.WithName("acmez"))),
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
@@ -120,7 +120,7 @@ func TestACMEServerWithMismatchedChallenges(t *testing.T) {
Client: &acme.Client{
Directory: "https://acme.localhost:9443/acme/local/directory",
HTTPClient: tester.Client,
Logger: slog.New(zapslog.NewHandler(logger.Core())),
Logger: slog.New(zapslog.NewHandler(logger.Core(), zapslog.WithName("acmez"))),
},
ChallengeSolvers: map[string]acmez.Solver{
acme.ChallengeTypeHTTP01: &naiveHTTPSolver{logger: logger},
+23
View File
@@ -143,3 +143,26 @@ func TestAutoHTTPRedirectsInsertedBeforeUserDefinedCatchAllWithNoExplicitHTTPSit
tester.AssertGetResponse("http://foo.localhost:9080/", 200, "Foo")
tester.AssertGetResponse("http://baz.localhost:9080/", 200, "Foo")
}
func TestAutoHTTPSRedirectSortingExactMatchOverWildcard(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
local_certs
}
*.localhost:10443 {
respond "Wildcard"
}
dev.localhost {
respond "Exact"
}
`, "caddyfile")
tester.AssertRedirect("http://dev.localhost:9080/", "https://dev.localhost/", http.StatusPermanentRedirect)
tester.AssertRedirect("http://foo.localhost:9080/", "https://foo.localhost:10443/", http.StatusPermanentRedirect)
}
@@ -18,7 +18,9 @@ encode gzip zstd {
# Long way with a block for each encoding
encode {
zstd
zstd {
disable_checksum
}
gzip 5
}
@@ -71,7 +73,9 @@ encode
"gzip": {
"level": 5
},
"zstd": {}
"zstd": {
"checksum": false
}
},
"handler": "encode",
"prefer": [
@@ -46,6 +46,18 @@ app.example.com {
}
]
},
{
"handle": [
{
"handler": "headers",
"request": {
"delete": [
"Remote-Email"
]
}
}
]
},
{
"handle": [
{
@@ -73,6 +85,18 @@ app.example.com {
}
]
},
{
"handle": [
{
"handler": "headers",
"request": {
"delete": [
"Remote-Groups"
]
}
}
]
},
{
"handle": [
{
@@ -100,6 +124,18 @@ app.example.com {
}
]
},
{
"handle": [
{
"handler": "headers",
"request": {
"delete": [
"Remote-Name"
]
}
}
]
},
{
"handle": [
{
@@ -127,6 +163,18 @@ app.example.com {
}
]
},
{
"handle": [
{
"handler": "headers",
"request": {
"delete": [
"Remote-User"
]
}
}
]
},
{
"handle": [
{
@@ -200,4 +248,4 @@ app.example.com {
}
}
}
}
}
@@ -0,0 +1,146 @@
:8080
forward_auth 127.0.0.1:9091 {
uri /
copy_headers X-User-Id X-User-Role
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8080"
],
"routes": [
{
"handle": [
{
"handle_response": [
{
"match": {
"status_code": [
2
]
},
"routes": [
{
"handle": [
{
"handler": "vars"
}
]
},
{
"handle": [
{
"handler": "headers",
"request": {
"delete": [
"X-User-Id"
]
}
}
]
},
{
"handle": [
{
"handler": "headers",
"request": {
"set": {
"X-User-Id": [
"{http.reverse_proxy.header.X-User-Id}"
]
}
}
}
],
"match": [
{
"not": [
{
"vars": {
"{http.reverse_proxy.header.X-User-Id}": [
""
]
}
}
]
}
]
},
{
"handle": [
{
"handler": "headers",
"request": {
"delete": [
"X-User-Role"
]
}
}
]
},
{
"handle": [
{
"handler": "headers",
"request": {
"set": {
"X-User-Role": [
"{http.reverse_proxy.header.X-User-Role}"
]
}
}
}
],
"match": [
{
"not": [
{
"vars": {
"{http.reverse_proxy.header.X-User-Role}": [
""
]
}
}
]
}
]
}
]
}
],
"handler": "reverse_proxy",
"headers": {
"request": {
"set": {
"X-Forwarded-Method": [
"{http.request.method}"
],
"X-Forwarded-Uri": [
"{http.request.uri}"
]
}
}
},
"rewrite": {
"method": "GET",
"uri": "/"
},
"upstreams": [
{
"dial": "127.0.0.1:9091"
}
]
}
]
}
]
}
}
}
}
}
@@ -35,6 +35,18 @@ forward_auth localhost:9000 {
}
]
},
{
"handle": [
{
"handler": "headers",
"request": {
"delete": [
"1"
]
}
}
]
},
{
"handle": [
{
@@ -62,6 +74,18 @@ forward_auth localhost:9000 {
}
]
},
{
"handle": [
{
"handler": "headers",
"request": {
"delete": [
"B"
]
}
}
]
},
{
"handle": [
{
@@ -89,6 +113,18 @@ forward_auth localhost:9000 {
}
]
},
{
"handle": [
{
"handler": "headers",
"request": {
"delete": [
"3"
]
}
}
]
},
{
"handle": [
{
@@ -116,6 +152,18 @@ forward_auth localhost:9000 {
}
]
},
{
"handle": [
{
"handler": "headers",
"request": {
"delete": [
"D"
]
}
}
]
},
{
"handle": [
{
@@ -143,6 +191,18 @@ forward_auth localhost:9000 {
}
]
},
{
"handle": [
{
"handler": "headers",
"request": {
"delete": [
"5"
]
}
}
]
},
{
"handle": [
{
@@ -203,4 +263,4 @@ forward_auth localhost:9000 {
}
}
}
}
}
@@ -0,0 +1,77 @@
{
email test@example.com
dns mock
tls_resolvers 1.1.1.1 8.8.8.8
acme_dns
}
example.com {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"issuers": [
{
"challenges": {
"dns": {
"resolvers": [
"1.1.1.1",
"8.8.8.8"
]
}
},
"email": "test@example.com",
"module": "acme"
},
{
"ca": "https://acme.zerossl.com/v2/DV90",
"challenges": {
"dns": {
"resolvers": [
"1.1.1.1",
"8.8.8.8"
]
}
},
"email": "test@example.com",
"module": "acme"
}
]
}
]
},
"dns": {
"name": "mock"
},
"resolvers": [
"1.1.1.1",
"8.8.8.8"
]
}
}
}
@@ -0,0 +1,38 @@
{
tls_resolvers 1.1.1.1 8.8.8.8
}
example.com {
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"resolvers": [
"1.1.1.1",
"8.8.8.8"
]
}
}
}
@@ -0,0 +1,72 @@
{
email test@example.com
dns mock
tls_resolvers 1.1.1.1 8.8.8.8
}
example.com {
tls {
dns mock
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"example.com"
],
"issuers": [
{
"challenges": {
"dns": {
"provider": {
"name": "mock"
},
"resolvers": [
"1.1.1.1",
"8.8.8.8"
]
}
},
"email": "test@example.com",
"module": "acme"
}
]
}
]
},
"dns": {
"name": "mock"
},
"resolvers": [
"1.1.1.1",
"8.8.8.8"
]
}
}
}
@@ -0,0 +1,98 @@
{
email test@example.com
dns mock
tls_resolvers 1.1.1.1 8.8.8.8
acme_dns
}
example.com {
tls {
resolvers 9.9.9.9
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"example.com"
],
"issuers": [
{
"challenges": {
"dns": {
"resolvers": [
"9.9.9.9"
]
}
},
"email": "test@example.com",
"module": "acme"
}
]
},
{
"issuers": [
{
"challenges": {
"dns": {
"resolvers": [
"1.1.1.1",
"8.8.8.8"
]
}
},
"email": "test@example.com",
"module": "acme"
},
{
"ca": "https://acme.zerossl.com/v2/DV90",
"challenges": {
"dns": {
"resolvers": [
"1.1.1.1",
"8.8.8.8"
]
}
},
"email": "test@example.com",
"module": "acme"
}
]
}
]
},
"dns": {
"name": "mock"
},
"resolvers": [
"1.1.1.1",
"8.8.8.8"
]
}
}
}
@@ -0,0 +1,112 @@
{
email test@example.com
dns mock
tls_resolvers 1.1.1.1 8.8.8.8
acme_dns
}
site1.example.com {
}
site2.example.com {
tls {
resolvers 9.9.9.9 8.8.4.4
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"site1.example.com"
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"site2.example.com"
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"site2.example.com"
],
"issuers": [
{
"challenges": {
"dns": {
"resolvers": [
"9.9.9.9",
"8.8.4.4"
]
}
},
"email": "test@example.com",
"module": "acme"
}
]
},
{
"issuers": [
{
"challenges": {
"dns": {
"resolvers": [
"1.1.1.1",
"8.8.8.8"
]
}
},
"email": "test@example.com",
"module": "acme"
},
{
"ca": "https://acme.zerossl.com/v2/DV90",
"challenges": {
"dns": {
"resolvers": [
"1.1.1.1",
"8.8.8.8"
]
}
},
"email": "test@example.com",
"module": "acme"
}
]
}
]
},
"dns": {
"name": "mock"
},
"resolvers": [
"1.1.1.1",
"8.8.8.8"
]
}
}
}
@@ -0,0 +1,15 @@
{
admin off
auto_https off
}
import testdata/issue_7557_invalid_subdirective_snippet.conf
:8080 {
import test {
this_is_nonsense
}
}
----------
parsing caddyfile tokens for 'reverse_proxy': unrecognized subdirective this_is_nonsense
@@ -0,0 +1,52 @@
import testdata/issue_7518_unused_block_panic_snippets.conf
example.com {
import snippet
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "headers",
"response": {
"set": {
"Reverse_proxy": [
"localhost:3000"
]
}
}
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -0,0 +1,47 @@
{
log {
format journald {
wrap console
}
}
}
:80 {
respond "Hello, World!"
}
----------
{
"logging": {
"logs": {
"default": {
"encoder": {
"format": "journald",
"wrap": {
"format": "console"
}
}
}
}
},
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"body": "Hello, World!",
"handler": "static_response"
}
]
}
]
}
}
}
}
}
@@ -1,27 +1,47 @@
:80
log {
log one {
output file /var/log/access.log {
mode 0644
dir_mode 0755
roll_size 1gb
roll_uncompressed
roll_compression none
roll_local_time
roll_keep 5
roll_keep_for 90d
}
}
log two {
output file /var/log/access-2.log {
mode 0777
dir_mode from_file
roll_size 1gib
roll_compression zstd
roll_interval 12h
roll_at 00:00 06:00 12:00,18:00
roll_minutes 10 40 45,46
roll_keep 10
roll_keep_for 90d
}
}
----------
{
"logging": {
"logs": {
"default": {
"exclude": [
"http.log.access.log0"
"http.log.access.one",
"http.log.access.two"
]
},
"log0": {
"one": {
"writer": {
"dir_mode": "0755",
"filename": "/var/log/access.log",
"mode": "0644",
"output": "file",
"roll_compression": "none",
"roll_gzip": false,
"roll_keep": 5,
"roll_keep_days": 90,
@@ -29,7 +49,35 @@ log {
"roll_size_mb": 954
},
"include": [
"http.log.access.log0"
"http.log.access.one"
]
},
"two": {
"writer": {
"dir_mode": "from_file",
"filename": "/var/log/access-2.log",
"mode": "0777",
"output": "file",
"roll_at": [
"00:00",
"06:00",
"12:00",
"18:00"
],
"roll_compression": "zstd",
"roll_interval": 43200000000000,
"roll_keep": 10,
"roll_keep_days": 90,
"roll_minutes": [
10,
40,
45,
46
],
"roll_size_mb": 1024
},
"include": [
"http.log.access.two"
]
}
}
@@ -42,7 +90,7 @@ log {
":80"
],
"logs": {
"default_logger_name": "log0"
"default_logger_name": "two"
}
}
}
@@ -0,0 +1,56 @@
https://example.com {
reverse_proxy https://localhost:54321 {
stream_buffer_size 8KB
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"stream_buffer_size": 8000,
"transport": {
"protocol": "http",
"tls": {}
},
"upstreams": [
{
"dial": "localhost:54321"
}
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}
@@ -54,11 +54,6 @@ b.com {
"via": "http"
}
]
},
{
"subjects": [
"b.com"
]
}
]
}
@@ -0,0 +1,96 @@
# example from https://github.com/caddyserver/caddy/issues/7559
*.test.local {
tls {
get_certificate http http://cert-server:9000/certs
}
respond "wildcard"
}
# certificate for this subdomain is covered by wildcard above
subdomain.test.local {
respond "subdomain"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"subdomain.test.local"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "subdomain",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"*.test.local"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "wildcard",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"*.test.local"
],
"get_certificate": [
{
"url": "http://cert-server:9000/certs",
"via": "http"
}
]
}
]
}
}
}
}
@@ -0,0 +1,87 @@
localhost
respond "hello from localhost"
tls {
client_auth {
mode request
trust_pool combined {
source inline {
trust_der MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==
}
source file {
pem_file ../caddy.ca.cer
}
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"match": {
"sni": [
"localhost"
]
},
"client_authentication": {
"ca": {
"provider": "combined",
"sources": [
{
"provider": "inline",
"trusted_ca_certs": [
"MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ=="
]
},
{
"pem_files": [
"../caddy.ca.cer"
],
"provider": "file"
}
]
},
"mode": "request"
}
},
{}
]
}
}
}
}
}
@@ -0,0 +1,87 @@
localhost
respond "hello from localhost"
tls {
client_auth {
mode require_and_verify
trust_pool combined {
source inline {
trust_der MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==
}
source pki_root {
authority local
}
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"match": {
"sni": [
"localhost"
]
},
"client_authentication": {
"ca": {
"provider": "combined",
"sources": [
{
"provider": "inline",
"trusted_ca_certs": [
"MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ=="
]
},
{
"authority": [
"local"
],
"provider": "pki_root"
}
]
},
"mode": "require_and_verify"
}
},
{}
]
}
}
}
}
}
@@ -0,0 +1,66 @@
localhost
respond "hello from localhost"
tls {
client_auth {
mode request
trust_pool system
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"localhost"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "hello from localhost",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
],
"tls_connection_policies": [
{
"match": {
"sni": [
"localhost"
]
},
"client_authentication": {
"ca": {
"provider": "system"
},
"mode": "request"
}
},
{}
]
}
}
}
}
}
+206
View File
@@ -0,0 +1,206 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package integration
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"github.com/caddyserver/caddy/v2/caddytest"
)
// TestForwardAuthCopyHeadersStripsClientHeaders is a regression test for the
// header injection vulnerability in forward_auth copy_headers.
//
// When the auth service returns 200 OK without one of the copy_headers headers,
// the MatchNot guard skips the Set operation. Before this fix, the original
// client-supplied header survived unchanged into the backend request, allowing
// privilege escalation with only a valid (non-privileged) bearer token. After
// the fix, an unconditional delete route runs first, so the backend always
// sees an absent header rather than the attacker-supplied value.
func TestForwardAuthCopyHeadersStripsClientHeaders(t *testing.T) {
// Mock auth service: accepts any Bearer token, returns 200 OK with NO
// identity headers. This is the stateless JWT validator pattern that
// triggers the vulnerability.
authSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
w.WriteHeader(http.StatusOK)
return
}
w.WriteHeader(http.StatusUnauthorized)
}))
defer authSrv.Close()
// Mock backend: records the identity headers it receives. A real application
// would use X-User-Id / X-User-Role to make authorization decisions.
type received struct{ userID, userRole string }
var (
mu sync.Mutex
last received
)
backendSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
last = received{
userID: r.Header.Get("X-User-Id"),
userRole: r.Header.Get("X-User-Role"),
}
mu.Unlock()
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "ok")
}))
defer backendSrv.Close()
authAddr := strings.TrimPrefix(authSrv.URL, "http://")
backendAddr := strings.TrimPrefix(backendSrv.URL, "http://")
tester := caddytest.NewTester(t)
tester.InitServer(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
forward_auth %s {
uri /
copy_headers X-User-Id X-User-Role
}
reverse_proxy %s
}
`, authAddr, backendAddr), "caddyfile")
// Case 1: no token. Auth must still reject the request even when the client
// includes identity headers. This confirms the auth check is not bypassed.
req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
req.Header.Set("X-User-Id", "injected")
req.Header.Set("X-User-Role", "injected")
resp := tester.AssertResponseCode(req, http.StatusUnauthorized)
resp.Body.Close()
// Case 2: valid token, no injected headers. The backend should see absent
// identity headers (the auth service never returns them).
req, _ = http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
req.Header.Set("Authorization", "Bearer token123")
tester.AssertResponse(req, http.StatusOK, "ok")
mu.Lock()
gotID, gotRole := last.userID, last.userRole
mu.Unlock()
if gotID != "" {
t.Errorf("baseline: X-User-Id should be absent, got %q", gotID)
}
if gotRole != "" {
t.Errorf("baseline: X-User-Role should be absent, got %q", gotRole)
}
// Case 3 (the security regression): valid token plus forged identity headers.
// The fix must strip those values so the backend never sees them.
req, _ = http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
req.Header.Set("Authorization", "Bearer token123")
req.Header.Set("X-User-Id", "admin") // forged
req.Header.Set("X-User-Role", "superadmin") // forged
tester.AssertResponse(req, http.StatusOK, "ok")
mu.Lock()
gotID, gotRole = last.userID, last.userRole
mu.Unlock()
if gotID != "" {
t.Errorf("injection: X-User-Id must be stripped, got %q", gotID)
}
if gotRole != "" {
t.Errorf("injection: X-User-Role must be stripped, got %q", gotRole)
}
}
// TestForwardAuthCopyHeadersAuthResponseWins verifies that when the auth
// service does include a copy_headers header in its response, that value
// is forwarded to the backend and takes precedence over any client-supplied
// value for the same header.
func TestForwardAuthCopyHeadersAuthResponseWins(t *testing.T) {
const wantUserID = "service-user-42"
const wantUserRole = "editor"
// Auth service: accepts bearer token and sets identity headers.
authSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
w.Header().Set("X-User-Id", wantUserID)
w.Header().Set("X-User-Role", wantUserRole)
w.WriteHeader(http.StatusOK)
return
}
w.WriteHeader(http.StatusUnauthorized)
}))
defer authSrv.Close()
type received struct{ userID, userRole string }
var (
mu sync.Mutex
last received
)
backendSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
last = received{
userID: r.Header.Get("X-User-Id"),
userRole: r.Header.Get("X-User-Role"),
}
mu.Unlock()
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "ok")
}))
defer backendSrv.Close()
authAddr := strings.TrimPrefix(authSrv.URL, "http://")
backendAddr := strings.TrimPrefix(backendSrv.URL, "http://")
tester := caddytest.NewTester(t)
tester.InitServer(fmt.Sprintf(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
forward_auth %s {
uri /
copy_headers X-User-Id X-User-Role
}
reverse_proxy %s
}
`, authAddr, backendAddr), "caddyfile")
// The client sends forged headers; the auth service overrides them with
// its own values. The backend must receive the auth service values.
req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
req.Header.Set("Authorization", "Bearer token123")
req.Header.Set("X-User-Id", "forged-id") // must be overwritten
req.Header.Set("X-User-Role", "forged-role") // must be overwritten
tester.AssertResponse(req, http.StatusOK, "ok")
mu.Lock()
gotID, gotRole := last.userID, last.userRole
mu.Unlock()
if gotID != wantUserID {
t.Errorf("X-User-Id: want %q, got %q", wantUserID, gotID)
}
if gotRole != wantUserRole {
t.Errorf("X-User-Role: want %q, got %q", wantUserRole, gotRole)
}
}
+595
View File
@@ -0,0 +1,595 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Integration tests for Caddy's PROXY protocol support, covering two distinct
// roles that Caddy can play:
//
// 1. As a PROXY protocol *sender* (reverse proxy outbound transport):
// Caddy receives an inbound request from a test client and the
// reverse_proxy handler forwards it to an upstream with a PROXY protocol
// header (v1 or v2) prepended to the connection. A lightweight backend
// built with go-proxyproto validates that the header was received and
// carries the correct client address.
//
// Transport versions tested:
// - "1.1" -> plain HTTP/1.1 to the upstream
// - "h2c" -> HTTP/2 cleartext (h2c) to the upstream (regression for #7529)
// - "2" -> HTTP/2 over TLS (h2) to the upstream
//
// For each transport version both PROXY protocol v1 and v2 are exercised.
//
// HTTP/3 (h3) is not included because it uses QUIC/UDP and therefore
// bypasses the TCP-level dialContext that injects PROXY protocol headers;
// there is no meaningful h3 + proxy protocol sender combination to test.
//
// 2. As a PROXY protocol *receiver* (server-side listener wrapper):
// A raw TCP client dials Caddy directly, injects a PROXY v2 header
// spoofing a source address, and sends a normal HTTP/1.1 request. The
// Caddy server is configured with the proxy_protocol listener wrapper and
// is expected to surface the spoofed address via the
// {http.request.remote.host} placeholder.
package integration
import (
"crypto/tls"
"encoding/json"
"fmt"
"net"
"net/http"
"net/http/httptest"
"slices"
"strings"
"sync"
"testing"
goproxy "github.com/pires/go-proxyproto"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"github.com/caddyserver/caddy/v2/caddytest"
)
// proxyProtoBackend is a minimal HTTP server that sits behind a
// go-proxyproto listener and records the source address that was
// delivered in the PROXY header for each request.
type proxyProtoBackend struct {
mu sync.Mutex
headerAddrs []string // host:port strings extracted from each PROXY header
ln net.Listener
srv *http.Server
}
// newProxyProtoBackend starts a TCP listener wrapped with go-proxyproto on a
// random local port and serves requests with a simple "OK" body. The PROXY
// header source addresses are accumulated in headerAddrs so tests can
// inspect them.
func newProxyProtoBackend(t *testing.T) *proxyProtoBackend {
t.Helper()
b := &proxyProtoBackend{}
rawLn, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("backend: listen: %v", err)
}
// Wrap with go-proxyproto so the PROXY header is stripped and parsed
// before the HTTP server sees the connection. We use REQUIRE so that a
// missing header returns an error instead of silently passing through.
pLn := &goproxy.Listener{
Listener: rawLn,
Policy: func(_ net.Addr) (goproxy.Policy, error) {
return goproxy.REQUIRE, nil
},
}
b.ln = pLn
// Wrap the handler with h2c support so the backend can speak HTTP/2
// cleartext (h2c) as well as plain HTTP/1.1. Without this, Caddy's
// reverse proxy would receive a 'frame too large' error when the
// upstream transport is configured to use h2c.
h2Server := &http2.Server{}
handlerFn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// go-proxyproto has already updated the net.Conn's remote
// address to the value from the PROXY header; the HTTP server
// surfaces it in r.RemoteAddr.
b.mu.Lock()
b.headerAddrs = append(b.headerAddrs, r.RemoteAddr)
b.mu.Unlock()
w.WriteHeader(http.StatusOK)
_, _ = fmt.Fprint(w, "OK")
})
b.srv = &http.Server{
Handler: h2c.NewHandler(handlerFn, h2Server),
}
go b.srv.Serve(pLn) //nolint:errcheck
t.Cleanup(func() {
_ = b.srv.Close()
_ = rawLn.Close()
})
return b
}
// addr returns the listening address (host:port) of the backend.
func (b *proxyProtoBackend) addr() string {
return b.ln.Addr().String()
}
// recordedAddrs returns a snapshot of all PROXY-header source addresses seen
// so far.
func (b *proxyProtoBackend) recordedAddrs() []string {
b.mu.Lock()
defer b.mu.Unlock()
cp := make([]string, len(b.headerAddrs))
copy(cp, b.headerAddrs)
return cp
}
// tlsProxyProtoBackend is a TLS-enabled backend that sits behind a
// go-proxyproto listener. The PROXY header is stripped before the TLS
// handshake so the layer order on a connection is:
//
// raw TCP → go-proxyproto (strips PROXY header) → TLS handshake → HTTP/2
type tlsProxyProtoBackend struct {
mu sync.Mutex
headerAddrs []string
srv *httptest.Server
}
// newTLSProxyProtoBackend starts a TLS listener that first reads and strips
// PROXY protocol headers (go-proxyproto, REQUIRE policy) and then performs a
// TLS handshake. The backend speaks HTTP/2 over TLS (h2).
//
// The certificate is the standard self-signed certificate generated by
// httptest.Server; the Caddy transport must be configured with
// insecure_skip_verify: true to trust it.
func newTLSProxyProtoBackend(t *testing.T) *tlsProxyProtoBackend {
t.Helper()
b := &tlsProxyProtoBackend{}
handlerFn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b.mu.Lock()
b.headerAddrs = append(b.headerAddrs, r.RemoteAddr)
b.mu.Unlock()
w.WriteHeader(http.StatusOK)
_, _ = fmt.Fprint(w, "OK")
})
rawLn, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("tlsBackend: listen: %v", err)
}
// Wrap with go-proxyproto so the PROXY header is consumed before TLS.
pLn := &goproxy.Listener{
Listener: rawLn,
Policy: func(_ net.Addr) (goproxy.Policy, error) {
return goproxy.REQUIRE, nil
},
}
// httptest.NewUnstartedServer lets us replace the listener before
// calling StartTLS(), which wraps our proxyproto listener with
// tls.NewListener. This gives us the right layer order.
b.srv = httptest.NewUnstartedServer(handlerFn)
b.srv.Listener = pLn
// StartTLS enables HTTP/2 on the server automatically.
b.srv.StartTLS()
t.Cleanup(func() {
b.srv.Close()
})
return b
}
// addr returns the listening address (host:port) of the TLS backend.
func (b *tlsProxyProtoBackend) addr() string {
return b.srv.Listener.Addr().String()
}
// tlsConfig returns the *tls.Config used by the backend server.
// Tests can use it to verify cert details if needed.
func (b *tlsProxyProtoBackend) tlsConfig() *tls.Config {
return b.srv.TLS
}
// recordedAddrs returns a snapshot of all PROXY-header source addresses.
func (b *tlsProxyProtoBackend) recordedAddrs() []string {
b.mu.Lock()
defer b.mu.Unlock()
cp := make([]string, len(b.headerAddrs))
copy(cp, b.headerAddrs)
return cp
}
// proxyProtoTLSConfig builds a Caddy JSON configuration that proxies to a TLS
// upstream with PROXY protocol. The transport uses insecure_skip_verify so
// the self-signed certificate generated by httptest.Server is accepted.
func proxyProtoTLSConfig(listenPort int, backendAddr, ppVersion string, transportVersions []string) string {
versionsJSON, _ := json.Marshal(transportVersions)
return fmt.Sprintf(`{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"pki": {
"certificate_authorities": {
"local": {
"install_trust": false
}
}
},
"http": {
"grace_period": 1,
"servers": {
"proxy": {
"listen": [":%d"],
"automatic_https": {
"disable": true
},
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [{"dial": "%s"}],
"transport": {
"protocol": "http",
"proxy_protocol": "%s",
"versions": %s,
"tls": {
"insecure_skip_verify": true
}
}
}
]
}
]
}
}
}
}
}`, listenPort, backendAddr, ppVersion, string(versionsJSON))
}
// testTLSProxyProtocolMatrix is the shared implementation for TLS-based proxy
// protocol tests. It mirrors testProxyProtocolMatrix but uses a TLS backend.
func testTLSProxyProtocolMatrix(t *testing.T, ppVersion string, transportVersions []string, numRequests int) {
t.Helper()
backend := newTLSProxyProtoBackend(t)
listenPort := freePort(t)
tester := caddytest.NewTester(t)
tester.WithDefaultOverrides(caddytest.Config{
AdminPort: 2999,
})
cfg := proxyProtoTLSConfig(listenPort, backend.addr(), ppVersion, transportVersions)
tester.InitServer(cfg, "json")
proxyURL := fmt.Sprintf("http://127.0.0.1:%d/", listenPort)
for i := 0; i < numRequests; i++ {
resp, err := tester.Client.Get(proxyURL)
if err != nil {
t.Fatalf("request %d/%d: GET %s: %v", i+1, numRequests, proxyURL, err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("request %d/%d: expected status 200, got %d", i+1, numRequests, resp.StatusCode)
}
}
addrs := backend.recordedAddrs()
if len(addrs) == 0 {
t.Fatalf("backend recorded no PROXY protocol addresses (expected at least 1)")
}
for i, addr := range addrs {
host, _, err := net.SplitHostPort(addr)
if err != nil {
t.Errorf("addr[%d] %q: SplitHostPort: %v", i, addr, err)
continue
}
if host != "127.0.0.1" {
t.Errorf("addr[%d]: expected source 127.0.0.1, got %q", i, host)
}
}
}
// proxyProtoConfig builds a Caddy JSON configuration that:
// - listens on listenPort for inbound HTTP requests
// - proxies them to backendAddr with PROXY protocol ppVersion ("v1"/"v2")
// - uses the given transport versions (e.g. ["1.1"] or ["h2c"])
func proxyProtoConfig(listenPort int, backendAddr, ppVersion string, transportVersions []string) string {
versionsJSON, _ := json.Marshal(transportVersions)
return fmt.Sprintf(`{
"admin": {
"listen": "localhost:2999"
},
"apps": {
"pki": {
"certificate_authorities": {
"local": {
"install_trust": false
}
}
},
"http": {
"grace_period": 1,
"servers": {
"proxy": {
"listen": [":%d"],
"automatic_https": {
"disable": true
},
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"upstreams": [{"dial": "%s"}],
"transport": {
"protocol": "http",
"proxy_protocol": "%s",
"versions": %s
}
}
]
}
]
}
}
}
}
}`, listenPort, backendAddr, ppVersion, string(versionsJSON))
}
// freePort returns a free local TCP port by binding briefly and releasing it.
func freePort(t *testing.T) int {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("freePort: %v", err)
}
port := ln.Addr().(*net.TCPAddr).Port
_ = ln.Close()
return port
}
// TestProxyProtocolV1WithH1 verifies that PROXY protocol v1 headers are sent
// correctly when the transport uses HTTP/1.1 to the upstream.
func TestProxyProtocolV1WithH1(t *testing.T) {
testProxyProtocolMatrix(t, "v1", []string{"1.1"}, 1)
}
// TestProxyProtocolV2WithH1 verifies that PROXY protocol v2 headers are sent
// correctly when the transport uses HTTP/1.1 to the upstream.
func TestProxyProtocolV2WithH1(t *testing.T) {
testProxyProtocolMatrix(t, "v2", []string{"1.1"}, 1)
}
// TestProxyProtocolV1WithH2C verifies that PROXY protocol v1 headers are sent
// correctly when the transport uses h2c (HTTP/2 cleartext) to the upstream.
func TestProxyProtocolV1WithH2C(t *testing.T) {
testProxyProtocolMatrix(t, "v1", []string{"h2c"}, 1)
}
// TestProxyProtocolV2WithH2C verifies that PROXY protocol v2 headers are sent
// correctly when the transport uses h2c (HTTP/2 cleartext) to the upstream.
// This is the primary regression test for github.com/caddyserver/caddy/issues/7529:
// before the fix, the h2 transport opened a new TCP connection per request
// (because req.URL.Host was mangled differently for each request due to the
// varying client port), which caused file-descriptor exhaustion under load.
func TestProxyProtocolV2WithH2C(t *testing.T) {
testProxyProtocolMatrix(t, "v2", []string{"h2c"}, 1)
}
// TestProxyProtocolV2WithH2CMultipleRequests sends several sequential requests
// through the h2c + PROXY-protocol path and confirms that:
// 1. Every request receives a 200 response (no connection exhaustion).
// 2. The backend received at least one PROXY header (connection was reused).
//
// This is the core regression guard for issue #7529: without the fix, a new
// TCP connection was opened per request, quickly exhausting file descriptors.
func TestProxyProtocolV2WithH2CMultipleRequests(t *testing.T) {
testProxyProtocolMatrix(t, "v2", []string{"h2c"}, 5)
}
// TestProxyProtocolV1WithH2 verifies that PROXY protocol v1 headers are sent
// correctly when the transport uses HTTP/2 over TLS (h2) to the upstream.
func TestProxyProtocolV1WithH2(t *testing.T) {
testTLSProxyProtocolMatrix(t, "v1", []string{"2"}, 1)
}
// TestProxyProtocolV2WithH2 verifies that PROXY protocol v2 headers are sent
// correctly when the transport uses HTTP/2 over TLS (h2) to the upstream.
func TestProxyProtocolV2WithH2(t *testing.T) {
testTLSProxyProtocolMatrix(t, "v2", []string{"2"}, 1)
}
// TestProxyProtocolServerAndProxy is an end-to-end matrix test that exercises
// all combinations of PROXY protocol version x transport version.
func TestProxyProtocolServerAndProxy(t *testing.T) {
plainTests := []struct {
name string
ppVersion string
transportVersions []string
numRequests int
}{
{"h1-v1", "v1", []string{"1.1"}, 3},
{"h1-v2", "v2", []string{"1.1"}, 3},
{"h2c-v1", "v1", []string{"h2c"}, 3},
{"h2c-v2", "v2", []string{"h2c"}, 3},
}
for _, tc := range plainTests {
t.Run(tc.name, func(t *testing.T) {
testProxyProtocolMatrix(t, tc.ppVersion, tc.transportVersions, tc.numRequests)
})
}
tlsTests := []struct {
name string
ppVersion string
transportVersions []string
numRequests int
}{
{"h2-v1", "v1", []string{"2"}, 3},
{"h2-v2", "v2", []string{"2"}, 3},
}
for _, tc := range tlsTests {
t.Run(tc.name, func(t *testing.T) {
testTLSProxyProtocolMatrix(t, tc.ppVersion, tc.transportVersions, tc.numRequests)
})
}
}
// testProxyProtocolMatrix is the shared implementation for the proxy protocol
// tests. It:
// 1. Starts a go-proxyproto-wrapped backend.
// 2. Configures Caddy as a reverse proxy with the given PROXY protocol
// version and transport versions.
// 3. Sends numRequests GET requests through Caddy and asserts 200 OK each time.
// 4. Asserts the backend recorded at least one PROXY header whose source host
// is 127.0.0.1 (the loopback address used by the test client).
func testProxyProtocolMatrix(t *testing.T, ppVersion string, transportVersions []string, numRequests int) {
t.Helper()
backend := newProxyProtoBackend(t)
listenPort := freePort(t)
tester := caddytest.NewTester(t)
tester.WithDefaultOverrides(caddytest.Config{
AdminPort: 2999,
})
cfg := proxyProtoConfig(listenPort, backend.addr(), ppVersion, transportVersions)
tester.InitServer(cfg, "json")
// If the test is h2c-only (no "1.1" in versions), reconfigure the test
// client transport to use unencrypted HTTP/2 so we actually exercise the
// h2c code path through Caddy.
if slices.Contains(transportVersions, "h2c") && !slices.Contains(transportVersions, "1.1") {
tr, ok := tester.Client.Transport.(*http.Transport)
if ok {
tr.Protocols = new(http.Protocols)
tr.Protocols.SetHTTP1(false)
tr.Protocols.SetUnencryptedHTTP2(true)
}
}
proxyURL := fmt.Sprintf("http://127.0.0.1:%d/", listenPort)
for i := 0; i < numRequests; i++ {
resp, err := tester.Client.Get(proxyURL)
if err != nil {
t.Fatalf("request %d/%d: GET %s: %v", i+1, numRequests, proxyURL, err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("request %d/%d: expected status 200, got %d", i+1, numRequests, resp.StatusCode)
}
}
// The backend must have seen at least one PROXY header. For h1, there is
// one per request; for h2c, requests share the same connection so only one
// header is written at connection establishment.
addrs := backend.recordedAddrs()
if len(addrs) == 0 {
t.Fatalf("backend recorded no PROXY protocol addresses (expected at least 1)")
}
// Every PROXY-decoded source address must be the loopback address since
// the test client always connects from 127.0.0.1.
for i, addr := range addrs {
host, _, err := net.SplitHostPort(addr)
if err != nil {
t.Errorf("addr[%d] %q: SplitHostPort: %v", i, addr, err)
continue
}
if host != "127.0.0.1" {
t.Errorf("addr[%d]: expected source 127.0.0.1, got %q", i, host)
}
}
}
// TestProxyProtocolListenerWrapper verifies that Caddy's
// caddy.listeners.proxy_protocol listener wrapper can successfully parse
// incoming PROXY protocol headers.
//
// The test dials Caddy's listening port directly, injects a raw PROXY v2
// header spoofing source address 10.0.0.1:1234, then sends a normal
// HTTP/1.1 GET request. The Caddy server is configured to echo back the
// remote address ({http.request.remote.host}). The test asserts that the
// echoed address is the spoofed 10.0.0.1.
func TestProxyProtocolListenerWrapper(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
servers :9080 {
listener_wrappers {
proxy_protocol {
timeout 5s
allow 127.0.0.0/8
}
}
}
}
http://localhost:9080 {
respond "{http.request.remote.host}"
}`, "caddyfile")
// Dial the Caddy listener directly and inject a PROXY v2 header that
// claims the connection originates from 10.0.0.1:1234.
conn, err := net.Dial("tcp", "127.0.0.1:9080")
if err != nil {
t.Fatalf("dial: %v", err)
}
defer conn.Close()
spoofedSrc := &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 1234}
spoofedDst := &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 9080}
hdr := goproxy.HeaderProxyFromAddrs(2, spoofedSrc, spoofedDst)
if _, err := hdr.WriteTo(conn); err != nil {
t.Fatalf("write proxy header: %v", err)
}
// Write a minimal HTTP/1.1 GET request.
_, err = fmt.Fprintf(conn,
"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
if err != nil {
t.Fatalf("write HTTP request: %v", err)
}
// Read the raw response and look for the spoofed address in the body.
buf := make([]byte, 4096)
n, _ := conn.Read(buf)
raw := string(buf[:n])
if !strings.Contains(raw, "10.0.0.1") {
t.Errorf("expected spoofed address 10.0.0.1 in response body; full response:\n%s", raw)
}
}
+193
View File
@@ -1,9 +1,14 @@
package integration
import (
"bufio"
"crypto/sha1"
"encoding/base64"
"fmt"
"io"
"net"
"net/http"
"net/textproto"
"os"
"runtime"
"strings"
@@ -386,6 +391,68 @@ func TestReverseProxyHealthCheck(t *testing.T) {
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
}
// TestReverseProxyHealthCheckPortUsed verifies that health_port is actually
// used for active health checks and not the upstream's main port. This is a
// regression test for https://github.com/caddyserver/caddy/issues/7524.
func TestReverseProxyHealthCheckPortUsed(t *testing.T) {
// upstream server: serves proxied requests normally, but returns 503 for
// /health so that if health checks mistakenly hit this port the upstream
// gets marked unhealthy and the proxy returns 503.
upstreamSrv := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/health" {
w.WriteHeader(http.StatusServiceUnavailable)
return
}
_, _ = w.Write([]byte("Hello, World!"))
}),
}
ln0, err := net.Listen("tcp", "127.0.0.1:2022")
if err != nil {
t.Fatalf("failed to listen on 127.0.0.1:2022: %v", err)
}
go upstreamSrv.Serve(ln0)
t.Cleanup(func() { upstreamSrv.Close(); ln0.Close() })
// separate health check server on the configured health_port: returns 200
// so the upstream is marked healthy only if health checks go to this port.
healthSrv := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
_, _ = w.Write([]byte("ok"))
}),
}
ln1, err := net.Listen("tcp", "127.0.0.1:2023")
if err != nil {
t.Fatalf("failed to listen on 127.0.0.1:2023: %v", err)
}
go healthSrv.Serve(ln1)
t.Cleanup(func() { healthSrv.Close(); ln1.Close() })
tester := caddytest.NewTester(t)
tester.InitServer(`
{
skip_install_trust
admin localhost:2999
http_port 9080
https_port 9443
grace_period 1ns
}
http://localhost:9080 {
reverse_proxy {
to localhost:2022
health_uri /health
health_port 2023
health_interval 10ms
health_timeout 100ms
health_passes 1
health_fails 1
}
}
`, "caddyfile")
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
}
func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
if runtime.GOOS == "windows" {
t.SkipNow()
@@ -500,3 +567,129 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) {
tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
}
func TestReverseProxyWebSocketUpgradeUnixSocket(t *testing.T) {
if runtime.GOOS == "windows" {
t.SkipNow()
}
f, err := os.CreateTemp("", "*.sock")
if err != nil {
t.Fatalf("failed to create temporary socket file: %v", err)
}
_ = os.Remove(f.Name())
socketName := f.Name()
backend := http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path != "/ws" {
http.NotFound(w, req)
return
}
if !strings.EqualFold(req.Header.Get("Upgrade"), "websocket") ||
!strings.Contains(strings.ToLower(req.Header.Get("Connection")), "upgrade") {
http.Error(w, "missing websocket upgrade headers", http.StatusBadRequest)
return
}
wsKey := req.Header.Get("Sec-WebSocket-Key")
if wsKey == "" {
http.Error(w, "missing Sec-WebSocket-Key", http.StatusBadRequest)
return
}
hj, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "hijacker not supported", http.StatusInternalServerError)
return
}
conn, brw, err := hj.Hijack()
if err != nil {
return
}
defer conn.Close()
_, _ = brw.WriteString("HTTP/1.1 101 Switching Protocols\r\n")
_, _ = brw.WriteString("Upgrade: websocket\r\n")
_, _ = brw.WriteString("Connection: Upgrade\r\n")
_, _ = brw.WriteString("Sec-WebSocket-Accept: " + computeWebSocketAccept(wsKey) + "\r\n")
_, _ = brw.WriteString("\r\n")
_ = brw.Flush()
}),
}
unixListener, err := net.Listen("unix", socketName)
if err != nil {
t.Fatalf("failed to listen on unix socket: %v", err)
}
go backend.Serve(unixListener)
t.Cleanup(func() {
_ = backend.Close()
_ = unixListener.Close()
_ = os.Remove(socketName)
})
runtime.Gosched()
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 unix/%s
}
`, socketName), "caddyfile")
conn, err := net.Dial("tcp", "127.0.0.1:9080")
if err != nil {
t.Fatalf("failed to dial caddy listener: %v", err)
}
defer conn.Close()
wsKey := "dGhlIHNhbXBsZSBub25jZQ=="
request := strings.Join([]string{
"GET /ws HTTP/1.1",
"Host: localhost:9080",
"Connection: Upgrade",
"Upgrade: websocket",
"Sec-WebSocket-Version: 13",
"Sec-WebSocket-Key: " + wsKey,
"",
"",
}, "\r\n")
if _, err := io.WriteString(conn, request); err != nil {
t.Fatalf("failed to send websocket handshake request: %v", err)
}
tpr := textproto.NewReader(bufio.NewReader(conn))
statusLine, err := tpr.ReadLine()
if err != nil {
t.Fatalf("failed reading handshake status line: %v", err)
}
if !strings.Contains(statusLine, "101") || !strings.Contains(strings.ToLower(statusLine), "switching protocols") {
t.Fatalf("unexpected status line: %q", statusLine)
}
headers, err := tpr.ReadMIMEHeader()
if err != nil {
t.Fatalf("failed reading handshake headers: %v", err)
}
if !strings.EqualFold(headers.Get("Upgrade"), "websocket") {
t.Fatalf("unexpected Upgrade header: %q", headers.Get("Upgrade"))
}
if !strings.Contains(strings.ToLower(headers.Get("Connection")), "upgrade") {
t.Fatalf("unexpected Connection header: %q", headers.Get("Connection"))
}
}
func computeWebSocketAccept(wsKey string) string {
h := sha1.Sum([]byte(wsKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
return base64.StdEncoding.EncodeToString(h[:])
}
@@ -0,0 +1,15 @@
# Used by import_block_snippet_non_replaced_block_from_separate_file.caddyfiletest
(snippet) {
header {
reverse_proxy localhost:3000
{block}
}
}
# This snippet being unused by the test Caddyfile is intentional.
# This is to test that a panic runtime error triggered by an out-of-range slice index access
# will not happen again, please see issue #7518 and pull request #7543 for more information
(unused_snippet) {
header SomeHeader SomeValue
}
@@ -0,0 +1,7 @@
# Used by import_block_snippet_invalid_subdirective.caddyfiletest
(test) {
reverse_proxy {
{block}
}
}
+14 -4
View File
@@ -9,9 +9,14 @@ import (
)
var defaultFactory = newRootCommandFactory(func() *cobra.Command {
return &cobra.Command{
Use: "caddy",
Long: `Caddy is an extensible server platform written in Go.
bin := caddy.CustomBinaryName
if bin == "" {
bin = "caddy"
}
long := caddy.CustomLongDescription
if long == "" {
long = `Caddy is an extensible server platform written in Go.
At its core, Caddy merely manages configuration. Modules are plugged
in statically at compile-time to provide useful functionality. Caddy's
@@ -91,7 +96,12 @@ package installers: https://caddyserver.com/docs/install
Instructions for running Caddy in production are also available:
https://caddyserver.com/docs/running
`,
`
}
return &cobra.Command{
Use: bin,
Long: long,
Example: ` $ caddy run
$ caddy run --config caddy.json
$ caddy reload --config caddy.json
+2 -2
View File
@@ -372,7 +372,7 @@ func cmdReload(fl Flags) (int, error) {
return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
}
adminAddr, err := DetermineAdminAPIAddress(addressFlag, config, configFlag, configAdapterFlag)
adminAddr, err := DetermineAdminAPIAddress(addressFlag, config, configFile, configAdapterFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
}
@@ -697,7 +697,7 @@ func cmdFmt(fl Flags) (int, error) {
output := caddyfile.Format(input)
if fl.Bool("overwrite") {
if err := os.WriteFile(configFile, output, 0o600); err != nil {
if err := os.WriteFile(configFile, output, 0o600); err != nil { //nolint:gosec // path traversal is not really a thing here, this is either "Caddyfile" or admin-controlled
return caddy.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err)
}
return caddy.ExitCodeSuccess, nil
+7 -1
View File
@@ -484,7 +484,13 @@ func setResourceLimits(logger *zap.Logger) func() {
// See https://pkg.go.dev/runtime/debug#SetMemoryLimit
_, _ = memlimit.SetGoMemLimitWithOpts(
memlimit.WithLogger(
slog.New(zapslog.NewHandler(logger.Core())),
slog.New(zapslog.NewHandler(
logger.Core(),
zapslog.WithName("memlimit"),
// the default enables traces at ERROR level, this disables
// them by setting it to a level higher than any other level
zapslog.AddStacktraceAt(slog.Level(127)),
)),
),
memlimit.WithProvider(
memlimit.ApplyFallback(
+18 -6
View File
@@ -63,10 +63,17 @@ type Context struct {
// modules which are loaded will be properly unloaded.
// See standard library context package's documentation.
func NewContext(ctx Context) (Context, context.CancelFunc) {
newCtx, cancelCause := NewContextWithCause(ctx)
return newCtx, func() { cancelCause(nil) }
}
// NewContextWithCause is like NewContext but returns a context.CancelCauseFunc.
// EXPERIMENTAL: This API is subject to change.
func NewContextWithCause(ctx Context) (Context, context.CancelCauseFunc) {
newCtx := Context{moduleInstances: make(map[string][]Module), cfg: ctx.cfg, metricsRegistry: prometheus.NewPedanticRegistry()}
c, cancel := context.WithCancel(ctx.Context)
wrappedCancel := func() {
cancel()
c, cancel := context.WithCancelCause(ctx.Context)
wrappedCancel := func(cause error) {
cancel(cause)
for _, f := range ctx.cleanupFuncs {
f()
@@ -608,6 +615,11 @@ func (ctx Context) Slogger() *slog.Logger {
core zapcore.Core
moduleID string
)
// the default enables traces at ERROR level, this disables
// them by setting it to a level higher than any other level
tracesOpt := zapslog.AddStacktraceAt(slog.Level(127))
if ctx.cfg == nil {
// often the case in tests; just use a dev logger
l, err := zap.NewDevelopment()
@@ -616,16 +628,16 @@ func (ctx Context) Slogger() *slog.Logger {
}
core = l.Core()
handler = zapslog.NewHandler(core)
handler = zapslog.NewHandler(core, tracesOpt)
} else {
mod := ctx.Module()
if mod == nil {
core = Log().Core()
handler = zapslog.NewHandler(core)
handler = zapslog.NewHandler(core, tracesOpt)
} else {
moduleID = string(mod.CaddyModule().ID)
core = ctx.cfg.Logging.Logger(mod).Core()
handler = zapslog.NewHandler(core, zapslog.WithName(moduleID))
handler = zapslog.NewHandler(core, zapslog.WithName(moduleID), tracesOpt)
}
}
+61 -61
View File
@@ -4,7 +4,7 @@ go 1.25.0
require (
github.com/BurntSushi/toml v1.6.0
github.com/DeRuina/timberjack v1.3.9
github.com/DeRuina/timberjack v1.4.1
github.com/KimMachineGun/automemlimit v0.7.5
github.com/Masterminds/sprig/v3 v3.3.0
github.com/alecthomas/chroma/v2 v2.23.1
@@ -14,43 +14,43 @@ require (
github.com/cloudflare/circl v1.6.3
github.com/dustin/go-humanize v1.0.1
github.com/go-chi/chi/v5 v5.2.5
github.com/google/cel-go v0.27.0
github.com/google/cel-go v0.28.0
github.com/google/uuid v1.6.0
github.com/klauspost/compress v1.18.4
github.com/klauspost/compress v1.18.5
github.com/klauspost/cpuid/v2 v2.3.0
github.com/mholt/acmez/v3 v3.1.6
github.com/prometheus/client_golang v1.23.2
github.com/quic-go/quic-go v0.59.0
github.com/smallstep/certificates v0.30.0-rc2.0.20260211214201-20608299c29c
github.com/smallstep/nosql v0.7.0
github.com/smallstep/certificates v0.30.2
github.com/smallstep/nosql v0.8.0
github.com/smallstep/truststore v0.13.0
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/tailscale/tscert v0.0.0-20251216020129-aea342f6d747
github.com/yuin/goldmark v1.7.16
github.com/yuin/goldmark v1.8.2
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0
go.opentelemetry.io/contrib/propagators/autoprop v0.65.0
go.opentelemetry.io/otel v1.40.0
go.opentelemetry.io/otel/sdk v1.40.0
go.step.sm/crypto v0.76.2
go.opentelemetry.io/contrib/exporters/autoexport 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/otel v1.43.0
go.opentelemetry.io/otel/sdk v1.43.0
go.step.sm/crypto v0.77.2
go.uber.org/automaxprocs v1.6.0
go.uber.org/zap v1.27.1
go.uber.org/zap/exp v0.3.0
golang.org/x/crypto v0.48.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541
golang.org/x/net v0.50.0
golang.org/x/sync v0.19.0
golang.org/x/term v0.40.0
golang.org/x/time v0.14.0
golang.org/x/crypto v0.50.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20260323153451-8400f4a93807
golang.org/x/net v0.53.0
golang.org/x/sync v0.20.0
golang.org/x/term v0.42.0
golang.org/x/time v0.15.0
gopkg.in/yaml.v3 v3.0.1
)
require (
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go/auth v0.18.1 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
dario.cat/mergo v1.0.2 // indirect
@@ -62,16 +62,16 @@ require (
github.com/davecgh/go-spew v1.1.1 // 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/v4 v4.1.3 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect
github.com/google/go-tpm v0.9.8 // indirect
github.com/google/go-tspi v0.3.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.19.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/puddle/v2 v2.2.2 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
@@ -87,31 +87,31 @@ require (
github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.40.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.40.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.40.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.62.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 // indirect
go.opentelemetry.io/otel/log v0.16.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.16.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // 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/b3 v1.43.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.43.0 // indirect
go.opentelemetry.io/contrib/propagators/ot v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.65.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric 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/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/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
google.golang.org/api v0.265.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
google.golang.org/api v0.272.0 // 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/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect
)
@@ -133,13 +133,13 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/libdns/libdns v1.1.1
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
@@ -153,7 +153,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.6.2
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
@@ -162,17 +162,17 @@ require (
github.com/slackhq/nebula v1.10.3 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/urfave/cli v1.22.17 // indirect
go.etcd.io/bbolt v1.3.10 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/sys v0.41.0
golang.org/x/text v0.34.0
golang.org/x/tools v0.42.0 // indirect
google.golang.org/grpc v1.78.0 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/sys v0.43.0
golang.org/x/text v0.36.0
golang.org/x/tools v0.43.0 // indirect
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
howett.net/plist v1.0.0 // indirect
)
+162 -162
View File
@@ -2,16 +2,16 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
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/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs=
cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/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/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ=
cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk=
cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU=
cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58=
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
@@ -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.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DeRuina/timberjack v1.3.9 h1:6UXZ1I7ExPGTX/1UNYawR58LlOJUHKBPiYC7WQ91eBo=
github.com/DeRuina/timberjack v1.3.9/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=
github.com/DeRuina/timberjack v1.4.1 h1:JftM5HN/ITKehAXjtdbGqN5XZIS1biHm7VSjU0Qbtqg=
github.com/DeRuina/timberjack v1.4.1/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=
github.com/KimMachineGun/automemlimit v0.7.5 h1:RkbaC0MwhjL1ZuBKunGDjE/ggwAX43DwZrJqVwyveTk=
github.com/KimMachineGun/automemlimit v0.7.5/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
@@ -53,36 +53,36 @@ 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/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/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY=
github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY=
github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
github.com/aws/aws-sdk-go-v2/service/kms v1.49.5 h1:DKibav4XF66XSeaXcrn9GlWGHos6D/vJ4r7jsK7z5CE=
github.com/aws/aws-sdk-go-v2/service/kms v1.49.5/go.mod h1:1SdcmEGUEQE1mrU2sIgeHtcMSxHuybhPvuEPANzIDfI=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k=
github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0=
github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g=
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=
github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk=
github.com/aws/aws-sdk-go-v2/service/kms v1.50.3 h1:s/zDSG/a/Su9aX+v0Ld9cimUCdkr5FWPmBV8owaEbZY=
github.com/aws/aws-sdk-go-v2/service/kms v1.50.3/go.mod h1:/iSgiUor15ZuxFGQSTf3lA2FmKxFsQoc2tADOarQBSw=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc=
@@ -151,15 +151,15 @@ github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
@@ -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/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/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo=
github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw=
github.com/google/cel-go v0.28.0 h1:KjSWstCpz/MN5t4a8gnGJNIYUsJRpdi/r97xWDphIQc=
github.com/google/cel-go v0.28.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/certificate-transparency-go v1.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=
@@ -179,20 +179,20 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/go-tpm-tools v0.4.7 h1:J3ycC8umYxM9A4eF73EofRZu4BxY0jjQnUnkhIBbvws=
github.com/google/go-tpm-tools v0.4.7/go.mod h1:gSyXTZHe3fgbzb6WEGd90QucmsnT1SRdlye82gH8QjQ=
github.com/google/go-tpm-tools v0.4.8 h1:V4oIYyAD3BykOycwYQzO29WefDouQMTsYZqmG3HxOfM=
github.com/google/go-tpm-tools v0.4.8/go.mod h1:4DfiOtiS1KppJjwf1+tqtW4K3PrCJjAAqFKj/TYTJKg=
github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus=
github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI=
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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE=
github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA=
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/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
@@ -203,16 +203,16 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
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/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
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/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.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -276,8 +276,8 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
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/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
@@ -301,16 +301,16 @@ github.com/slackhq/nebula v1.10.3 h1:EstYj8ODEcv6T0R9X5BVq1zgWZnyU5gtPzk99QF1PMU
github.com/slackhq/nebula v1.10.3/go.mod h1:IL5TUQm4x9IFx2kCKPYm1gP47pwd5b8QGnnBH2RHnvs=
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY=
github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc=
github.com/smallstep/certificates v0.30.0-rc2.0.20260211214201-20608299c29c h1:XQpX0IPYUAoJ661YlgfOJmY48ZOhIbglw4E2gw9mcyc=
github.com/smallstep/certificates v0.30.0-rc2.0.20260211214201-20608299c29c/go.mod h1:75NRLmYJq6ZcCb8ApJc+W1eL4oMYwjeufMJDHpv4rx4=
github.com/smallstep/certificates v0.30.2 h1:1G3xBi8sJ740iA1mMPW2Svv7EIZKJ4Zf/iQtA5QlN0Y=
github.com/smallstep/certificates v0.30.2/go.mod h1:oyaE/aEYUGDr+YiCZLAxxP22bOQqcSHTeDgp8Vv2rlY=
github.com/smallstep/cli-utils v0.12.2 h1:lGzM9PJrH/qawbzMC/s2SvgLdJPKDWKwKzx9doCVO+k=
github.com/smallstep/cli-utils v0.12.2/go.mod h1:uCPqefO29goHLGqFnwk0i8W7XJu18X3WHQFRtOm/00Y=
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4=
github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4=
github.com/smallstep/linkedca v0.25.0 h1:txT9QHGbCsJq0MhAghBq7qhurGY727tQuqUi+n4BVBo=
github.com/smallstep/linkedca v0.25.0/go.mod h1:Q3jVAauFKNlF86W5/RFtgQeyDKz98GL/KN3KG4mJOvc=
github.com/smallstep/nosql v0.7.0 h1:YiWC9ZAHcrLCrayfaF+QJUv16I2bZ7KdLC3RpJcnAnE=
github.com/smallstep/nosql v0.7.0/go.mod h1:H5VnKMCbeq9QA6SRY5iqPylfxLfYcLwvUff3onQ8+HU=
github.com/smallstep/nosql v0.8.0 h1:FBTCUfKPmWYbrozW+RBKu+fnvbn+zr5rVli/XB4Jp4A=
github.com/smallstep/nosql v0.8.0/go.mod h1:5dUpNotHLHhOUapP0PLBVVfp3tG1DFC31VRccg+Cqwo=
github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA=
github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0=
github.com/smallstep/scep v0.0.0-20250318231241-a25cabb69492 h1:k23+s51sgYix4Zgbvpmy+1ZgXLjr4ZTkBTqXmpnImwA=
@@ -359,8 +359,8 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
@@ -369,70 +369,70 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0 h1:I/7S/yWobR3QHFLqHsJ8QOndoiFsj1VgHpQiq43KlUI=
go.opentelemetry.io/contrib/bridges/prometheus v0.65.0/go.mod h1:jPF6gn3y1E+nozCAEQj3c6NZ8KY+tvAgSVfvoOJUFac=
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0 h1:2gApdml7SznX9szEKFjKjM4qGcGSvAybYLBY319XG3g=
go.opentelemetry.io/contrib/exporters/autoexport v0.65.0/go.mod h1:0QqAGlbHXhmPYACG3n5hNzO5DnEqqtg4VcK5pr22RI0=
go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 h1:w3zlHYETbDwXyWHZlyyR58ZC39XGi8rAhkBgUgJ9d5w=
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/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.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
go.opentelemetry.io/contrib/propagators/autoprop v0.65.0 h1:kTaCycF9Xkm8VBBvH0rJ4wFeRjtIV55Erk3uuVsIs5s=
go.opentelemetry.io/contrib/propagators/autoprop v0.65.0/go.mod h1:rooPzAbXfxMX9fsPJjmOBg2SN4RhFEV8D7cfGK+N3tE=
go.opentelemetry.io/contrib/propagators/aws v1.40.0 h1:4VIrh75jW4RTimUNx1DSk+6H9/nDr1FvmKoOVDh3K04=
go.opentelemetry.io/contrib/propagators/aws v1.40.0/go.mod h1:B0dCov9KNQGlut3T8wZZjDnLXEXdBroM7bFsHh/gRos=
go.opentelemetry.io/contrib/propagators/b3 v1.40.0 h1:xariChe8OOVF3rNlfzGFgQc61npQmXhzZj/i82mxMfg=
go.opentelemetry.io/contrib/propagators/b3 v1.40.0/go.mod h1:72WvbdxbOfXaELEQfonFfOL6osvcVjI7uJEE8C2nkrs=
go.opentelemetry.io/contrib/propagators/jaeger v1.40.0 h1:aXl9uobjJs5vquMLt9ZkI/3zIuz8XQ3TqOKSWx0/xdU=
go.opentelemetry.io/contrib/propagators/jaeger v1.40.0/go.mod h1:ioMePqe6k6c/ovXSkmkMr1mbN5qRBGJxNTVop7/2XO0=
go.opentelemetry.io/contrib/propagators/ot v1.40.0 h1:Lon8J5SPmWaL1Ko2TIlCNHJ42/J1b5XbJlgJaE/9m7I=
go.opentelemetry.io/contrib/propagators/ot v1.40.0/go.mod h1:dKWtJTlp1Yj+8Cneye5idO46eRPIbi23qVuJYKjNnvY=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0 h1:ZVg+kCXxd9LtAaQNKBxAvJ5NpMf7LpvEr4MIZqb0TMQ=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.16.0/go.mod h1:hh0tMeZ75CCXrHd9OXRYxTlCAdxcXioWHFIpYw2rZu8=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0 h1:djrxvDxAe44mJUrKataUbOhCKhR3F8QCyWucO16hTQs=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.16.0/go.mod h1:dt3nxpQEiSoKvfTVxp3TUg5fHPLhKtbcnN3Z1I1ePD0=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 h1:NOyNnS19BF2SUDApbOKbDtWZ0IK7b8FJ2uAGdIWOGb0=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0/go.mod h1:VL6EgVikRLcJa9ftukrHu/ZkkhFBSo1lzvdBC9CF1ss=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0 h1:9y5sHvAxWzft1WQ4BwqcvA+IFVUJ1Ya75mSAUnFEVwE=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.40.0/go.mod h1:eQqT90eR3X5Dbs1g9YSM30RavwLF725Ris5/XSXWvqE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
go.opentelemetry.io/otel/exporters/prometheus v0.62.0 h1:krvC4JMfIOVdEuNPTtQ0ZjCiXrybhv+uOHMfHRmnvVo=
go.opentelemetry.io/otel/exporters/prometheus v0.62.0/go.mod h1:fgOE6FM/swEnsVQCqCnbOfRV4tOnWPg7bVeo4izBuhQ=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0 h1:ivlbaajBWJqhcCPniDqDJmRwj4lc6sRT+dCAVKNmxlQ=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.16.0/go.mod h1:u/G56dEKDDwXNCVLsbSrllB2o8pbtFLUC4HpR66r2dc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8=
go.opentelemetry.io/otel/log v0.16.0 h1:DeuBPqCi6pQwtCK0pO4fvMB5eBq6sNxEnuTs88pjsN4=
go.opentelemetry.io/otel/log v0.16.0/go.mod h1:rWsmqNVTLIA8UnwYVOItjyEZDbKIkMxdQunsIhpUMes=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/log v0.16.0 h1:e/b4bdlQwC5fnGtG3dlXUrNOnP7c8YLVSpSfEBIkTnI=
go.opentelemetry.io/otel/sdk/log v0.16.0/go.mod h1:JKfP3T6ycy7QEuv3Hj8oKDy7KItrEkus8XJE6EoSzw4=
go.opentelemetry.io/otel/sdk/log/logtest v0.16.0 h1:/XVkpZ41rVRTP4DfMgYv1nEtNmf65XPPyAdqV90TMy4=
go.opentelemetry.io/otel/sdk/log/logtest v0.16.0/go.mod h1:iOOPgQr5MY9oac/F5W86mXdeyWZGleIx3uXO98X2R6Y=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.step.sm/crypto v0.76.2 h1:JJ/yMcs/rmcCAwlo+afrHjq74XBFRTJw5B2y4Q4Z4c4=
go.step.sm/crypto v0.76.2/go.mod h1:m6KlB/HzIuGFep0UWI5e0SYi38UxpoKeCg6qUaHV6/Q=
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/propagators/autoprop v0.68.0 h1:wLGFvNBPqQhzBn0QRBZjrriH8lZ9gqtTz8ufHEjLg7k=
go.opentelemetry.io/contrib/propagators/autoprop v0.68.0/go.mod h1:evWK9nCqCzH8nhclTlpkdUzmxrmJQ2mrWCdKIvyOYec=
go.opentelemetry.io/contrib/propagators/aws v1.43.0 h1:EwnsB3cXRLAh7/Nr/9rMuGw73nfb3z6uAvVDjRrbeUg=
go.opentelemetry.io/contrib/propagators/aws v1.43.0/go.mod h1:CJjTym6F87tEdm61Qvnz5xrV8vKlH4C92djiqcn62k8=
go.opentelemetry.io/contrib/propagators/b3 v1.43.0 h1:CETqV3QLLPTy5yNrqyMr41VnAOOD4lsRved7n4QG00A=
go.opentelemetry.io/contrib/propagators/b3 v1.43.0/go.mod h1:Q4mCiCdziYzpNR0g+6UqVotAlCDZdzz6L8jwY4knOrw=
go.opentelemetry.io/contrib/propagators/jaeger v1.43.0 h1:peiLMz1+aqJE+3L4mOVtR9wlmv+yh/JVYXCBjqmzJJE=
go.opentelemetry.io/contrib/propagators/jaeger v1.43.0/go.mod h1:Agvif+4A8p/3UtZzJ0MCcDEuQwgtrzM71DueU41DCs8=
go.opentelemetry.io/contrib/propagators/ot v1.43.0 h1:Hh1HahlGc81AOE7siqi1tVOlbanY/UxMMWedpb0d5oQ=
go.opentelemetry.io/contrib/propagators/ot v1.43.0/go.mod h1:58MlyS7lghzYvAm5LN9gGmZpCMQEMB5vpZp9SRgOyE4=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
go.opentelemetry.io/otel/exporters/prometheus v0.65.0 h1:jOveH/b4lU9HT7y+Gfamf18BqlOuz2PWEvs8yM7Q6XE=
go.opentelemetry.io/otel/exporters/prometheus v0.65.0/go.mod h1:i1P8pcumauPtUI4YNopea1dhzEMuEqWP1xoUZDylLHo=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 h1:GJkybS+crDMdExT/BUNCEgfrmfboztcS6PhvSo88HKM=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0/go.mod h1:NuAyxRYIG2lKX3YQkB+83StTxM7s52PUUkRRiC0wnYI=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU=
go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4=
go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko=
go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg=
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk=
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
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/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.step.sm/crypto v0.77.2 h1:qFjjei+RHc5kP5R7NW9OUWT7SqWIuAOvOkXqg4fNWj8=
go.step.sm/crypto v0.77.2/go.mod h1:W0YJb9onM5l78qgkXIJ2Up6grnwW8EtpCKIza/NCg0o=
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/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -445,8 +445,8 @@ go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -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.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541 h1:FmKxj9ocLKn45jiR2jQMwCVhDvaK7fKQFzfuT9GvyK8=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260213171211-a408498e5541/go.mod h1:+UoQFNBq2p2wO+Q6ddVtYc25GZ6VNdOMyyrd4nrqrKs=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260323153451-8400f4a93807 h1:sQVhWLXbNsa8CTzHOX3IHc7C4Q2JyxI5AweuMQZ/5H0=
golang.org/x/crypto/x509roots/fallback v0.0.0-20260323153451-8400f4a93807/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/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
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.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.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -477,10 +477,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -488,8 +488,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -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.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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/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=
@@ -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.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -528,31 +528,31 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
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.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.265.0 h1:FZvfUdI8nfmuNrE34aOWFPmLC+qRBEiNm3JdivTvAAU=
google.golang.org/api v0.265.0/go.mod h1:uAvfEl3SLUj/7n6k+lJutcswVojHPp2Sp08jWCu8hLY=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
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.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA=
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE=
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw=
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/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+10 -10
View File
@@ -229,7 +229,7 @@ func (na NetworkAddress) JoinHostPort(offset uint) string {
func (na NetworkAddress) Expand() []NetworkAddress {
size := na.PortRangeSize()
addrs := make([]NetworkAddress, size)
for portOffset := uint(0); portOffset < size; portOffset++ {
for portOffset := range size {
addrs[portOffset] = na.At(portOffset)
}
return addrs
@@ -512,7 +512,7 @@ func ListenerUsage(network, addr string) int {
// contextAndCancelFunc groups context and its cancelFunc
type contextAndCancelFunc struct {
context.Context
context.CancelFunc
context.CancelCauseFunc
}
// sharedQUICState manages GetConfigForClient
@@ -542,17 +542,17 @@ func (sqs *sharedQUICState) getConfigForClient(ch *tls.ClientHelloInfo) (*tls.Co
// addState adds tls.Config and activeRequests to the map if not present and returns the corresponding context and its cancelFunc
// so that when cancelled, the active tls.Config will change
func (sqs *sharedQUICState) addState(tlsConfig *tls.Config) (context.Context, context.CancelFunc) {
func (sqs *sharedQUICState) addState(tlsConfig *tls.Config) (context.Context, context.CancelCauseFunc) {
sqs.rmu.Lock()
defer sqs.rmu.Unlock()
if cacc, ok := sqs.tlsConfs[tlsConfig]; ok {
return cacc.Context, cacc.CancelFunc
return cacc.Context, cacc.CancelCauseFunc
}
ctx, cancel := context.WithCancel(context.Background())
wrappedCancel := func() {
cancel()
ctx, cancel := context.WithCancelCause(context.Background())
wrappedCancel := func(cause error) {
cancel(cause)
sqs.rmu.Lock()
defer sqs.rmu.Unlock()
@@ -608,13 +608,13 @@ func fakeClosedErr(l interface{ Addr() net.Addr }) error {
// indicating that it is pretending to be closed so that the
// server using it can terminate, while the underlying
// socket is actually left open.
var errFakeClosed = fmt.Errorf("listener 'closed' 😉")
var errFakeClosed = fmt.Errorf("QUIC listener 'closed' 😉")
type fakeCloseQuicListener struct {
closed int32 // accessed atomically; belongs to this struct only
*sharedQuicListener // embedded, so we also become a quic.EarlyListener
context context.Context
contextCancel context.CancelFunc
contextCancel context.CancelCauseFunc
}
// Currently Accept ignores the passed context, however a situation where
@@ -637,7 +637,7 @@ func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (*quic.Conn, error)
func (fcql *fakeCloseQuicListener) Close() error {
if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) {
fcql.contextCancel()
fcql.contextCancel(errFakeClosed)
} else if atomic.CompareAndSwapInt32(&fcql.closed, 1, 2) {
_, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
}
+10 -2
View File
@@ -18,6 +18,7 @@ import (
"cmp"
"context"
"crypto/tls"
"errors"
"fmt"
"maps"
"net"
@@ -711,9 +712,10 @@ func (app *App) Stop() error {
// enforce grace period if configured
if app.GracePeriod > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Duration(app.GracePeriod))
timeout := time.Duration(app.GracePeriod)
ctx, cancel = context.WithTimeoutCause(ctx, timeout, fmt.Errorf("server graceful shutdown %ds timeout", int(timeout.Seconds())))
defer cancel()
app.logger.Info("servers shutting down; grace period initiated", zap.Duration("duration", time.Duration(app.GracePeriod)))
app.logger.Info("servers shutting down; grace period initiated", zap.Duration("duration", timeout))
} else {
app.logger.Info("servers shutting down with eternal grace period")
}
@@ -739,6 +741,9 @@ func (app *App) Stop() error {
}
if err := server.server.Shutdown(ctx); err != nil {
if cause := context.Cause(ctx); cause != nil && errors.Is(err, context.DeadlineExceeded) {
err = cause
}
app.logger.Error("server shutdown",
zap.Error(err),
zap.Strings("addresses", server.Listen))
@@ -762,6 +767,9 @@ func (app *App) Stop() error {
}
if err := server.h3server.Shutdown(ctx); err != nil {
if cause := context.Cause(ctx); cause != nil && errors.Is(err, context.DeadlineExceeded) {
err = cause
}
app.logger.Error("HTTP/3 server shutdown",
zap.Error(err),
zap.Strings("addresses", server.Listen))
+78
View File
@@ -424,6 +424,40 @@ redirServersLoop:
// we'll create a new server for all the listener addresses
// that are unused and serve the remaining redirects from it
// Sort redirect routes by host specificity to ensure exact matches
// take precedence over wildcards, preventing ambiguous routing.
slices.SortFunc(routes, func(a, b Route) int {
hostA := getFirstHostFromRoute(a)
hostB := getFirstHostFromRoute(b)
// Catch-all routes (empty host) have the lowest priority
if hostA == "" && hostB != "" {
return 1
}
if hostB == "" && hostA != "" {
return -1
}
hasWildcardA := strings.Contains(hostA, "*")
hasWildcardB := strings.Contains(hostB, "*")
// Exact domains take precedence over wildcards
if !hasWildcardA && hasWildcardB {
return -1
}
if hasWildcardA && !hasWildcardB {
return 1
}
// If both are exact or both are wildcards, the longer one is more specific
if len(hostA) != len(hostB) {
return len(hostB) - len(hostA)
}
// Tie-breaker: alphabetical order to ensure determinism
return strings.Compare(hostA, hostB)
})
// Use the sorted srvNames to consistently find the target server
for _, srvName := range srvNames {
srv := app.Servers[srvName]
@@ -580,6 +614,27 @@ func (app *App) createAutomationPolicies(ctx caddy.Context, internalNames, tails
}
}
// Ensure automation policies' CertMagic configs are rebuilt when
// ACME issuer templates may have been modified above (for example,
// alternate ports filled in by the HTTP app). If a policy is already
// provisioned, perform a lightweight rebuild of the CertMagic config
// so issuers receive SetConfig with the updated templates; otherwise
// run a normal Provision to initialize the policy.
for i, ap := range app.tlsApp.Automation.Policies {
// If the policy is already provisioned, rebuild only the CertMagic
// config so issuers get SetConfig with updated templates. Otherwise
// provision the policy normally (which may load modules).
if ap.IsProvisioned() {
if err := ap.RebuildCertMagic(app.tlsApp); err != nil {
return fmt.Errorf("rebuilding certmagic config for automation policy %d: %v", i, err)
}
} else {
if err := ap.Provision(app.tlsApp); err != nil {
return fmt.Errorf("provisioning automation policy %d after auto-HTTPS defaults: %v", i, err)
}
}
}
if basePolicy == nil {
// no base policy found; we will make one
basePolicy = new(caddytls.AutomationPolicy)
@@ -793,3 +848,26 @@ func isTailscaleDomain(name string) bool {
}
type acmeCapable interface{ GetACMEIssuer() *caddytls.ACMEIssuer }
// getFirstHostFromRoute traverses a route's matchers to find the Host rule.
// Since we are dealing with internally generated redirect routes, the host
// is typically the first string within the MatchHost.
func getFirstHostFromRoute(r Route) string {
for _, matcherSet := range r.MatcherSets {
for _, m := range matcherSet {
// Check if the matcher is of type MatchHost (value or pointer)
switch hm := m.(type) {
case MatchHost:
if len(hm) > 0 {
return hm[0]
}
case *MatchHost:
if len(*hm) > 0 {
return (*hm)[0]
}
}
}
}
// Return an empty string if it's a catch-all route (no specific host)
return ""
}
+4 -1
View File
@@ -41,7 +41,10 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
//
// encode [<matcher>] <formats...> {
// gzip [<level>]
// zstd
// zstd [<level>] {
// level <level>
// disable_checksum
// }
// minimum_length <length>
// # response matcher block
// match {
-8
View File
@@ -307,14 +307,6 @@ func (rw *responseWriter) FlushError() error {
return http.NewResponseController(rw.ResponseWriter).Flush()
}
// Flush calls FlushError() and simply discards any error. It is only implemented for backwards
// compatibility with legacy code that does not use FlushError; we know at least one sponsor
// needs this. It should not be relied upon as a stable part of the exported API, as it may be
// removed in the future.
func (rw *responseWriter) Flush() {
_ = rw.FlushError()
}
// Write writes to the response. If the response qualifies,
// it is encoded using the encoder, which is initialized
// if not done so already.
+84 -24
View File
@@ -33,6 +33,10 @@ type Zstd struct {
// The compression level. Accepted values: fastest, better, best, default.
Level string `json:"level,omitempty"`
// Whether to include the optional 4-byte zstd frame checksum trailer.
// If unset, the upstream zstd library default is preserved.
Checksum *bool `json:"checksum,omitempty"`
// Compression level refer to type constants value from zstd.SpeedFastest to zstd.SpeedBestCompression
level zstd.EncoderLevel
}
@@ -48,19 +52,48 @@ func (Zstd) CaddyModule() caddy.ModuleInfo {
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens.
func (z *Zstd) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume option name
if !d.NextArg() {
return nil
args := d.RemainingArgs()
switch len(args) {
case 0:
case 1:
if _, err := parseEncoderLevel(args[0]); err != nil {
return d.Err(err.Error())
}
z.Level = args[0]
default:
return d.ArgErr()
}
levelStr := d.Val()
if ok, _ := zstd.EncoderLevelFromString(levelStr); !ok {
return d.Errf("unexpected compression level, use one of '%s', '%s', '%s', '%s'",
zstd.SpeedFastest,
zstd.SpeedBetterCompression,
zstd.SpeedBestCompression,
zstd.SpeedDefault,
)
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "level":
args := d.RemainingArgs()
if len(args) != 1 {
return d.ArgErr()
}
if z.Level != "" {
return d.Err("compression level already specified")
}
if _, err := parseEncoderLevel(args[0]); err != nil {
return d.Err(err.Error())
}
z.Level = args[0]
case "disable_checksum":
if d.NextArg() {
return d.ArgErr()
}
if z.Checksum != nil {
return d.Err("checksum already specified")
}
disabled := false
z.Checksum = &disabled
default:
return d.Errf("unknown subdirective '%s'", d.Val())
}
}
z.Level = levelStr
return nil
}
@@ -69,15 +102,11 @@ func (z *Zstd) Provision(ctx caddy.Context) error {
if z.Level == "" {
z.Level = zstd.SpeedDefault.String()
}
var ok bool
if ok, z.level = zstd.EncoderLevelFromString(z.Level); !ok {
return fmt.Errorf("unexpected compression level, use one of '%s', '%s', '%s', '%s'",
zstd.SpeedFastest,
zstd.SpeedDefault,
zstd.SpeedBetterCompression,
zstd.SpeedBestCompression,
)
level, err := parseEncoderLevel(z.Level)
if err != nil {
return err
}
z.level = level
return nil
}
@@ -90,14 +119,45 @@ func (z Zstd) NewEncoder() encode.Encoder {
// The default of 8MB for the window is
// too large for many clients, so we limit
// it to 128K to lighten their load.
writer, _ := zstd.NewWriter(
nil,
zstd.WithWindowSize(128<<10),
writer, _ := zstd.NewWriter(nil, z.writerOptions(128<<10)...)
return writer
}
func (z Zstd) writerOptions(windowSize int) []zstd.EOption {
opts := []zstd.EOption{
zstd.WithWindowSize(windowSize),
zstd.WithEncoderConcurrency(1),
zstd.WithZeroFrames(true),
zstd.WithEncoderLevel(z.level),
zstd.WithEncoderLevel(z.encoderLevel()),
}
if z.Checksum != nil {
opts = append(opts, zstd.WithEncoderCRC(*z.Checksum))
}
return opts
}
func (z Zstd) encoderLevel() zstd.EncoderLevel {
if z.level != 0 {
return z.level
}
if z.Level != "" {
if level, err := parseEncoderLevel(z.Level); err == nil {
return level
}
}
return zstd.SpeedDefault
}
func parseEncoderLevel(level string) (zstd.EncoderLevel, error) {
if ok, encLevel := zstd.EncoderLevelFromString(level); ok {
return encLevel, nil
}
return 0, fmt.Errorf("unexpected compression level, use one of '%s', '%s', '%s', '%s'",
zstd.SpeedFastest,
zstd.SpeedBetterCompression,
zstd.SpeedBestCompression,
zstd.SpeedDefault,
)
return writer
}
// Interface guards
@@ -20,7 +20,6 @@ import (
"net/url"
"os"
"path"
"path/filepath"
"slices"
"sort"
"strconv"
@@ -100,7 +99,7 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS,
}
if fsrv.Browse.RevealSymlinks {
symLinkTarget, err := filepath.EvalSymlinks(path)
symLinkTarget, err := os.Readlink(path)
if err == nil {
symlinkPath = symLinkTarget
}
@@ -125,6 +125,11 @@ type FileServer struct {
// When possible, all paths are resolved to their absolute form before
// comparisons are made. For maximum clarity and explictness, use complete,
// absolute paths; or, for greater portability, use relative paths instead.
//
// Note that hide comparisons are case-sensitive. On case-insensitive
// filesystems, requests with different path casing may still resolve to the
// same file or directory on disk, so hide should not be treated as a
// security boundary for sensitive paths.
Hide []string `json:"hide,omitempty"`
// The names of files to try as index files if a folder is requested.
+3 -3
View File
@@ -161,11 +161,11 @@ func (ops *HeaderOps) Provision(_ caddy.Context) error {
// containsPlaceholders checks if the string contains Caddy placeholder syntax {key}
func containsPlaceholders(s string) bool {
openIdx := strings.Index(s, "{")
if openIdx == -1 {
_, after, ok := strings.Cut(s, "{")
if !ok {
return false
}
closeIdx := strings.Index(s[openIdx+1:], "}")
closeIdx := strings.Index(after, "}")
if closeIdx == -1 {
return false
}
+10 -1
View File
@@ -967,6 +967,7 @@ func TestVarREMatcher(t *testing.T) {
desc string
match MatchVarsRE
input VarsMiddleware
headers http.Header
expect bool
expectRepl map[string]string
}{
@@ -1001,6 +1002,14 @@ func TestVarREMatcher(t *testing.T) {
input: VarsMiddleware{"Var1": "var1Value"},
expect: true,
},
{
desc: "placeholder key value containing braces is not double-expanded",
match: MatchVarsRE{"{http.request.header.X-Input}": &MatchRegexp{Pattern: ".+", Name: "val"}},
input: VarsMiddleware{},
headers: http.Header{"X-Input": []string{"{env.HOME}"}},
expect: true,
expectRepl: map[string]string{"val.0": "{env.HOME}"},
},
} {
t.Run(tc.desc, func(t *testing.T) {
t.Parallel()
@@ -1017,7 +1026,7 @@ func TestVarREMatcher(t *testing.T) {
}
// set up the fake request and its Replacer
req := &http.Request{URL: new(url.URL), Method: http.MethodGet}
req := &http.Request{URL: new(url.URL), Method: http.MethodGet, Header: tc.headers}
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
ctx = context.WithValue(ctx, VarsCtxKey, make(map[string]any))
+11 -8
View File
@@ -214,21 +214,24 @@ func serverNameFromContext(ctx context.Context) string {
return srv.name
}
type metricsInstrumentedHandler struct {
// metricsInstrumentedRoute wraps a compiled route Handler with metrics
// instrumentation. It wraps the entire compiled route chain once,
// collecting metrics only once per route match.
type metricsInstrumentedRoute struct {
handler string
mh MiddlewareHandler
next Handler
metrics *Metrics
}
func newMetricsInstrumentedHandler(ctx caddy.Context, handler string, mh MiddlewareHandler, metrics *Metrics) *metricsInstrumentedHandler {
metrics.init.Do(func() {
initHTTPMetrics(ctx, metrics)
func newMetricsInstrumentedRoute(ctx caddy.Context, handler string, next Handler, m *Metrics) *metricsInstrumentedRoute {
m.init.Do(func() {
initHTTPMetrics(ctx, m)
})
return &metricsInstrumentedHandler{handler, mh, metrics}
return &metricsInstrumentedRoute{handler: handler, next: next, metrics: m}
}
func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error {
func (h *metricsInstrumentedRoute) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
server := serverNameFromContext(r.Context())
labels := prometheus.Labels{"server": server, "handler": h.handler}
method := metrics.SanitizeMethod(r.Method)
@@ -267,7 +270,7 @@ func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
return false
})
wrec := NewResponseRecorder(w, nil, writeHeaderRecorder)
err := h.mh.ServeHTTP(wrec, r, next)
err := h.next.ServeHTTP(wrec, r)
dur := time.Since(start).Seconds()
h.metrics.httpMetrics.requestCount.With(labels).Inc()
+124 -38
View File
@@ -47,16 +47,12 @@ func TestMetricsInstrumentedHandler(t *testing.T) {
return handlerErr
})
mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
return h.ServeHTTP(w, r)
})
ih := newMetricsInstrumentedHandler(ctx, "bar", mh, metrics)
ih := newMetricsInstrumentedRoute(ctx, "bar", h, metrics)
r := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
if actual := ih.ServeHTTP(w, r, h); actual != handlerErr {
if actual := ih.ServeHTTP(w, r); actual != handlerErr {
t.Errorf("Not same: expected %#v, but got %#v", handlerErr, actual)
}
if actual := testutil.ToFloat64(metrics.httpMetrics.requestInFlight); actual != 0.0 {
@@ -64,19 +60,19 @@ func TestMetricsInstrumentedHandler(t *testing.T) {
}
handlerErr = nil
if err := ih.ServeHTTP(w, r, h); err != nil {
if err := ih.ServeHTTP(w, r); err != nil {
t.Errorf("Received unexpected error: %v", err)
}
// an empty handler - no errors, no header written
mh = middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
emptyHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
})
ih = newMetricsInstrumentedHandler(ctx, "empty", mh, metrics)
ih = newMetricsInstrumentedRoute(ctx, "empty", emptyHandler, metrics)
r = httptest.NewRequest("GET", "/", nil)
w = httptest.NewRecorder()
if err := ih.ServeHTTP(w, r, h); err != nil {
if err := ih.ServeHTTP(w, r); err != nil {
t.Errorf("Received unexpected error: %v", err)
}
if actual := w.Result().StatusCode; actual != 200 {
@@ -87,16 +83,16 @@ func TestMetricsInstrumentedHandler(t *testing.T) {
}
// handler returning an error with an HTTP status
mh = middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
errHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return Error(http.StatusTooManyRequests, nil)
})
ih = newMetricsInstrumentedHandler(ctx, "foo", mh, metrics)
ih = newMetricsInstrumentedRoute(ctx, "foo", errHandler, metrics)
r = httptest.NewRequest("GET", "/", nil)
w = httptest.NewRecorder()
if err := ih.ServeHTTP(w, r, nil); err == nil {
if err := ih.ServeHTTP(w, r); err == nil {
t.Errorf("expected error to be propagated")
}
@@ -225,16 +221,12 @@ func TestMetricsInstrumentedHandlerPerHost(t *testing.T) {
return handlerErr
})
mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
return h.ServeHTTP(w, r)
})
ih := newMetricsInstrumentedHandler(ctx, "bar", mh, metrics)
ih := newMetricsInstrumentedRoute(ctx, "bar", h, metrics)
r := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
if actual := ih.ServeHTTP(w, r, h); actual != handlerErr {
if actual := ih.ServeHTTP(w, r); actual != handlerErr {
t.Errorf("Not same: expected %#v, but got %#v", handlerErr, actual)
}
if actual := testutil.ToFloat64(metrics.httpMetrics.requestInFlight); actual != 0.0 {
@@ -242,19 +234,19 @@ func TestMetricsInstrumentedHandlerPerHost(t *testing.T) {
}
handlerErr = nil
if err := ih.ServeHTTP(w, r, h); err != nil {
if err := ih.ServeHTTP(w, r); err != nil {
t.Errorf("Received unexpected error: %v", err)
}
// an empty handler - no errors, no header written
mh = middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
emptyHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
})
ih = newMetricsInstrumentedHandler(ctx, "empty", mh, metrics)
ih = newMetricsInstrumentedRoute(ctx, "empty", emptyHandler, metrics)
r = httptest.NewRequest("GET", "/", nil)
w = httptest.NewRecorder()
if err := ih.ServeHTTP(w, r, h); err != nil {
if err := ih.ServeHTTP(w, r); err != nil {
t.Errorf("Received unexpected error: %v", err)
}
if actual := w.Result().StatusCode; actual != 200 {
@@ -265,16 +257,16 @@ func TestMetricsInstrumentedHandlerPerHost(t *testing.T) {
}
// handler returning an error with an HTTP status
mh = middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
errHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return Error(http.StatusTooManyRequests, nil)
})
ih = newMetricsInstrumentedHandler(ctx, "foo", mh, metrics)
ih = newMetricsInstrumentedRoute(ctx, "foo", errHandler, metrics)
r = httptest.NewRequest("GET", "/", nil)
w = httptest.NewRecorder()
if err := ih.ServeHTTP(w, r, nil); err == nil {
if err := ih.ServeHTTP(w, r); err == nil {
t.Errorf("expected error to be propagated")
}
@@ -397,30 +389,30 @@ func TestMetricsCardinalityProtection(t *testing.T) {
// Add one allowed host
metrics.allowedHosts["allowed.com"] = struct{}{}
mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
h := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
w.Write([]byte("hello"))
return nil
})
ih := newMetricsInstrumentedHandler(ctx, "test", mh, metrics)
ih := newMetricsInstrumentedRoute(ctx, "test", h, metrics)
// Test request to allowed host
r1 := httptest.NewRequest("GET", "http://allowed.com/", nil)
r1.Host = "allowed.com"
w1 := httptest.NewRecorder()
ih.ServeHTTP(w1, r1, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
ih.ServeHTTP(w1, r1)
// Test request to unknown host (should be mapped to "_other")
r2 := httptest.NewRequest("GET", "http://attacker.com/", nil)
r2.Host = "attacker.com"
w2 := httptest.NewRecorder()
ih.ServeHTTP(w2, r2, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
ih.ServeHTTP(w2, r2)
// Test request to another unknown host (should also be mapped to "_other")
r3 := httptest.NewRequest("GET", "http://evil.com/", nil)
r3.Host = "evil.com"
w3 := httptest.NewRecorder()
ih.ServeHTTP(w3, r3, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
ih.ServeHTTP(w3, r3)
// Check that metrics contain:
// - One entry for "allowed.com"
@@ -452,26 +444,26 @@ func TestMetricsHTTPSCatchAll(t *testing.T) {
allowedHosts: make(map[string]struct{}), // Empty - no explicitly allowed hosts
}
mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
h := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
w.Write([]byte("hello"))
return nil
})
ih := newMetricsInstrumentedHandler(ctx, "test", mh, metrics)
ih := newMetricsInstrumentedRoute(ctx, "test", h, metrics)
// Test HTTPS request (should be allowed even though not in allowedHosts)
r1 := httptest.NewRequest("GET", "https://unknown.com/", nil)
r1.Host = "unknown.com"
r1.TLS = &tls.ConnectionState{} // Mark as TLS/HTTPS
w1 := httptest.NewRecorder()
ih.ServeHTTP(w1, r1, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
ih.ServeHTTP(w1, r1)
// Test HTTP request (should be mapped to "_other")
r2 := httptest.NewRequest("GET", "http://unknown.com/", nil)
r2.Host = "unknown.com"
// No TLS field = HTTP request
w2 := httptest.NewRecorder()
ih.ServeHTTP(w2, r2, HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { return nil }))
ih.ServeHTTP(w2, r2)
// Check that HTTPS request gets real host, HTTP gets "_other"
expected := `
@@ -488,8 +480,102 @@ func TestMetricsHTTPSCatchAll(t *testing.T) {
}
}
type middlewareHandlerFunc func(http.ResponseWriter, *http.Request, Handler) error
func TestMetricsInstrumentedRoute(t *testing.T) {
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
m := &Metrics{
init: sync.Once{},
httpMetrics: &httpMetrics{},
}
func (f middlewareHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, h Handler) error {
return f(w, r, h)
handlerErr := errors.New("oh noes")
response := []byte("hello world!")
innerHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
if actual := testutil.ToFloat64(m.httpMetrics.requestInFlight); actual != 1.0 {
t.Errorf("Expected requestInFlight to be 1.0, got %v", actual)
}
if handlerErr == nil {
w.Write(response)
}
return handlerErr
})
ih := newMetricsInstrumentedRoute(ctx, "test_handler", innerHandler, m)
r := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
// Test with error
if actual := ih.ServeHTTP(w, r); actual != handlerErr {
t.Errorf("Expected error %v, got %v", handlerErr, actual)
}
if actual := testutil.ToFloat64(m.httpMetrics.requestInFlight); actual != 0.0 {
t.Errorf("Expected requestInFlight to be 0.0 after request, got %v", actual)
}
if actual := testutil.ToFloat64(m.httpMetrics.requestErrors); actual != 1.0 {
t.Errorf("Expected requestErrors to be 1.0, got %v", actual)
}
// Test without error
handlerErr = nil
w = httptest.NewRecorder()
if err := ih.ServeHTTP(w, r); err != nil {
t.Errorf("Unexpected error: %v", err)
}
}
func BenchmarkMetricsInstrumentedRoute(b *testing.B) {
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
m := &Metrics{
init: sync.Once{},
httpMetrics: &httpMetrics{},
}
noopHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
w.Write([]byte("ok"))
return nil
})
ih := newMetricsInstrumentedRoute(ctx, "bench_handler", noopHandler, m)
r := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
ih.ServeHTTP(w, r)
}
}
// BenchmarkSingleRouteMetrics simulates the new behavior where metrics
// are collected once for the entire route.
func BenchmarkSingleRouteMetrics(b *testing.B) {
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
m := &Metrics{
init: sync.Once{},
httpMetrics: &httpMetrics{},
}
// Build a chain of 5 plain middleware handlers (no per-handler metrics)
var next Handler = HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
})
for i := 0; i < 5; i++ {
capturedNext := next
next = HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return capturedNext.ServeHTTP(w, r)
})
}
// Wrap the entire chain with a single route-level metrics handler
ih := newMetricsInstrumentedRoute(ctx, "handler", next, m)
r := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
ih.ServeHTTP(w, r)
}
}
+10 -1
View File
@@ -420,7 +420,16 @@ func getReqTLSReplacement(req *http.Request, key string) (any, bool) {
if strings.HasPrefix(field, "client.") {
cert := getTLSPeerCert(req.TLS)
if cert == nil {
return nil, false
// Instead of returning (nil, false) here, we set it to a dummy
// value to fix #7530. This way, even if there is no client cert,
// evaluating placeholders with ReplaceKnown() will still remove
// the placeholder, which would be expected. It is not expected
// for the placeholder to sometimes get removed based on whether
// the client presented a cert. We also do not return true here
// because we probably should remain accurate about whether a
// placeholder is, in fact, known or not.
// (This allocation may be slightly inefficient.)
cert = new(x509.Certificate)
}
// subject alternate names (SANs)
+26 -2
View File
@@ -73,8 +73,9 @@ func (adminUpstreams) handleUpstreams(w http.ResponseWriter, r *http.Request) er
// Collect the results to respond with
results := []upstreamStatus{}
knownHosts := make(map[string]struct{})
// Iterate over the upstream pool (needs to be fast)
// Iterate over the static upstream pool (needs to be fast)
var rangeErr error
hosts.Range(func(key, val any) bool {
address, ok := key.(string)
@@ -95,6 +96,8 @@ func (adminUpstreams) handleUpstreams(w http.ResponseWriter, r *http.Request) er
return false
}
knownHosts[address] = struct{}{}
results = append(results, upstreamStatus{
Address: address,
NumRequests: upstream.NumRequests(),
@@ -103,11 +106,32 @@ func (adminUpstreams) handleUpstreams(w http.ResponseWriter, r *http.Request) er
return true
})
// If an error happened during the range, return it
currentInFlight := getInFlightRequests()
for address, count := range currentInFlight {
if _, exists := knownHosts[address]; !exists && count > 0 {
results = append(results, upstreamStatus{
Address: address,
NumRequests: int(count),
Fails: 0,
})
}
}
if rangeErr != nil {
return rangeErr
}
// Also include dynamic upstreams
dynamicHostsMu.RLock()
for address, entry := range dynamicHosts {
results = append(results, upstreamStatus{
Address: address,
NumRequests: entry.host.NumRequests(),
Fails: entry.host.Fails(),
})
}
dynamicHostsMu.RUnlock()
err := enc.Encode(results)
if err != nil {
return caddy.APIError{
@@ -0,0 +1,275 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package reverseproxy
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
)
// adminHandlerFixture sets up the global host state for an admin endpoint test
// and returns a cleanup function that must be deferred by the caller.
//
// staticAddrs are inserted into the UsagePool (as a static upstream would be).
// dynamicAddrs are inserted into the dynamicHosts map (as a dynamic upstream would be).
func adminHandlerFixture(t *testing.T, staticAddrs, dynamicAddrs []string) func() {
t.Helper()
for _, addr := range staticAddrs {
u := &Upstream{Dial: addr}
u.fillHost()
}
dynamicHostsMu.Lock()
for _, addr := range dynamicAddrs {
dynamicHosts[addr] = dynamicHostEntry{host: new(Host), lastSeen: time.Now()}
}
dynamicHostsMu.Unlock()
return func() {
// Remove static entries from the UsagePool.
for _, addr := range staticAddrs {
_, _ = hosts.Delete(addr)
}
// Remove dynamic entries.
dynamicHostsMu.Lock()
for _, addr := range dynamicAddrs {
delete(dynamicHosts, addr)
}
dynamicHostsMu.Unlock()
}
}
// callAdminUpstreams fires a GET against handleUpstreams and returns the
// decoded response body.
func callAdminUpstreams(t *testing.T) []upstreamStatus {
t.Helper()
req := httptest.NewRequest(http.MethodGet, "/reverse_proxy/upstreams", nil)
w := httptest.NewRecorder()
handler := adminUpstreams{}
if err := handler.handleUpstreams(w, req); err != nil {
t.Fatalf("handleUpstreams returned unexpected error: %v", err)
}
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
if ct := w.Header().Get("Content-Type"); ct != "application/json" {
t.Fatalf("expected Content-Type application/json, got %q", ct)
}
var results []upstreamStatus
if err := json.NewDecoder(w.Body).Decode(&results); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
return results
}
// resultsByAddress indexes a slice of upstreamStatus by address for easier
// lookup in assertions.
func resultsByAddress(statuses []upstreamStatus) map[string]upstreamStatus {
m := make(map[string]upstreamStatus, len(statuses))
for _, s := range statuses {
m[s.Address] = s
}
return m
}
// TestAdminUpstreamsMethodNotAllowed verifies that non-GET methods are rejected.
func TestAdminUpstreamsMethodNotAllowed(t *testing.T) {
for _, method := range []string{http.MethodPost, http.MethodPut, http.MethodDelete} {
req := httptest.NewRequest(method, "/reverse_proxy/upstreams", nil)
w := httptest.NewRecorder()
err := (adminUpstreams{}).handleUpstreams(w, req)
if err == nil {
t.Errorf("method %s: expected an error, got nil", method)
continue
}
apiErr, ok := err.(interface{ HTTPStatus() int })
if !ok {
// caddy.APIError stores the code in HTTPStatus field, access via the
// exported interface it satisfies indirectly; just check non-nil.
continue
}
if code := apiErr.HTTPStatus(); code != http.StatusMethodNotAllowed {
t.Errorf("method %s: expected 405, got %d", method, code)
}
}
}
// TestAdminUpstreamsEmpty verifies that an empty response is valid JSON when
// no upstreams are registered.
func TestAdminUpstreamsEmpty(t *testing.T) {
resetDynamicHosts()
results := callAdminUpstreams(t)
if results == nil {
t.Error("expected non-nil (empty) slice, got nil")
}
if len(results) != 0 {
t.Errorf("expected 0 results with empty pools, got %d", len(results))
}
}
// TestAdminUpstreamsStaticOnly verifies that static upstreams (from the
// UsagePool) appear in the response with correct addresses.
func TestAdminUpstreamsStaticOnly(t *testing.T) {
resetDynamicHosts()
cleanup := adminHandlerFixture(t,
[]string{"10.0.0.1:80", "10.0.0.2:80"},
nil,
)
defer cleanup()
results := callAdminUpstreams(t)
byAddr := resultsByAddress(results)
for _, addr := range []string{"10.0.0.1:80", "10.0.0.2:80"} {
if _, ok := byAddr[addr]; !ok {
t.Errorf("expected static upstream %q in response", addr)
}
}
if len(results) != 2 {
t.Errorf("expected exactly 2 results, got %d", len(results))
}
}
// TestAdminUpstreamsDynamicOnly verifies that dynamic upstreams (from
// dynamicHosts) appear in the response with correct addresses.
func TestAdminUpstreamsDynamicOnly(t *testing.T) {
resetDynamicHosts()
cleanup := adminHandlerFixture(t,
nil,
[]string{"10.0.1.1:80", "10.0.1.2:80"},
)
defer cleanup()
results := callAdminUpstreams(t)
byAddr := resultsByAddress(results)
for _, addr := range []string{"10.0.1.1:80", "10.0.1.2:80"} {
if _, ok := byAddr[addr]; !ok {
t.Errorf("expected dynamic upstream %q in response", addr)
}
}
if len(results) != 2 {
t.Errorf("expected exactly 2 results, got %d", len(results))
}
}
// TestAdminUpstreamsBothPools verifies that static and dynamic upstreams are
// both present in the same response and that there is no overlap or omission.
func TestAdminUpstreamsBothPools(t *testing.T) {
resetDynamicHosts()
cleanup := adminHandlerFixture(t,
[]string{"10.0.2.1:80"},
[]string{"10.0.2.2:80"},
)
defer cleanup()
results := callAdminUpstreams(t)
if len(results) != 2 {
t.Fatalf("expected 2 results (1 static + 1 dynamic), got %d", len(results))
}
byAddr := resultsByAddress(results)
if _, ok := byAddr["10.0.2.1:80"]; !ok {
t.Error("static upstream missing from response")
}
if _, ok := byAddr["10.0.2.2:80"]; !ok {
t.Error("dynamic upstream missing from response")
}
}
// TestAdminUpstreamsNoOverlapBetweenPools verifies that an address registered
// only as a static upstream does not also appear as a dynamic entry, and
// vice-versa.
func TestAdminUpstreamsNoOverlapBetweenPools(t *testing.T) {
resetDynamicHosts()
cleanup := adminHandlerFixture(t,
[]string{"10.0.3.1:80"},
[]string{"10.0.3.2:80"},
)
defer cleanup()
results := callAdminUpstreams(t)
seen := make(map[string]int)
for _, r := range results {
seen[r.Address]++
}
for addr, count := range seen {
if count > 1 {
t.Errorf("address %q appeared %d times; expected exactly once", addr, count)
}
}
}
// TestAdminUpstreamsReportsFailCounts verifies that fail counts accumulated on
// a dynamic upstream's Host are reflected in the response.
func TestAdminUpstreamsReportsFailCounts(t *testing.T) {
resetDynamicHosts()
const addr = "10.0.4.1:80"
h := new(Host)
_ = h.countFail(3)
dynamicHostsMu.Lock()
dynamicHosts[addr] = dynamicHostEntry{host: h, lastSeen: time.Now()}
dynamicHostsMu.Unlock()
defer func() {
dynamicHostsMu.Lock()
delete(dynamicHosts, addr)
dynamicHostsMu.Unlock()
}()
results := callAdminUpstreams(t)
byAddr := resultsByAddress(results)
status, ok := byAddr[addr]
if !ok {
t.Fatalf("expected %q in response", addr)
}
if status.Fails != 3 {
t.Errorf("expected Fails=3, got %d", status.Fails)
}
}
// TestAdminUpstreamsReportsNumRequests verifies that the active request count
// for a static upstream is reflected in the response.
func TestAdminUpstreamsReportsNumRequests(t *testing.T) {
resetDynamicHosts()
const addr = "10.0.4.2:80"
u := &Upstream{Dial: addr}
u.fillHost()
defer func() { _, _ = hosts.Delete(addr) }()
_ = u.Host.countRequest(2)
defer func() { _ = u.Host.countRequest(-2) }()
results := callAdminUpstreams(t)
byAddr := resultsByAddress(results)
status, ok := byAddr[addr]
if !ok {
t.Fatalf("expected %q in response", addr)
}
if status.NumRequests != 2 {
t.Errorf("expected NumRequests=2, got %d", status.NumRequests)
}
}
+12 -4
View File
@@ -96,6 +96,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
// flush_interval <duration>
// request_buffers <size>
// response_buffers <size>
// stream_buffer_size <size>
// stream_timeout <duration>
// stream_close_delay <duration>
// verbose_logs
@@ -646,7 +647,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
h.FlushInterval = caddy.Duration(dur)
}
case "request_buffers", "response_buffers":
case "request_buffers", "response_buffers", "stream_buffer_size":
subdir := d.Val()
if !d.NextArg() {
return d.ArgErr()
@@ -670,6 +671,8 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
h.RequestBuffers = size
case "response_buffers":
h.ResponseBuffers = size
case "stream_buffer_size":
h.StreamBufferSize = int(size)
}
case "stream_timeout":
@@ -725,9 +728,6 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
err = headers.CaddyfileHeaderOp(h.Headers.Request, args[0], "", nil)
case 2:
// some lint checks, I guess
if strings.EqualFold(args[0], "host") && (args[1] == "{hostport}" || args[1] == "{http.request.hostport}") {
caddy.Log().Named("caddyfile").Warn("Unnecessary header_up Host: the reverse proxy's default behavior is to pass headers to the upstream")
}
if strings.EqualFold(args[0], "x-forwarded-for") && (args[1] == "{remote}" || args[1] == "{http.request.remote}" || args[1] == "{remote_host}" || args[1] == "{http.request.remote.host}") {
caddy.Log().Named("caddyfile").Warn("Unnecessary header_up X-Forwarded-For: the reverse proxy's default behavior is to pass headers to the upstream")
}
@@ -885,6 +885,14 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return err
}
}
// check if the user set 'header_up host upstream_hostport' when proxying to HTTPS
// this is unnecessary because it's the default behavior already
if te.TLSEnabled() && h.Headers != nil && h.Headers.Request != nil {
hostVal := h.Headers.Request.Set.Get("Host")
if hostVal == "{upstream_hostport}" || hostVal == "{http.reverse_proxy.upstream.hostport}" {
caddy.Log().Named("caddyfile").Warn("Unnecessary header_up Host: the reverse proxy's default behavior is to pass the configured upstream address to the upstream when proxying to HTTPS")
}
}
if commonScheme == "http" && te.TLSEnabled() {
return d.Errf("upstream address scheme is HTTP but transport is configured for HTTP+TLS (HTTPS)")
}
@@ -0,0 +1,345 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package reverseproxy
import (
"sync"
"testing"
"time"
"github.com/caddyserver/caddy/v2"
)
// resetDynamicHosts clears global dynamic host state between tests.
func resetDynamicHosts() {
dynamicHostsMu.Lock()
dynamicHosts = make(map[string]dynamicHostEntry)
dynamicHostsMu.Unlock()
// Reset the Once so cleanup goroutine tests can re-trigger if needed.
dynamicHostsCleanerOnce = sync.Once{}
}
// TestFillDynamicHostCreatesEntry verifies that calling fillDynamicHost on a
// new address inserts an entry into dynamicHosts and assigns a non-nil Host.
func TestFillDynamicHostCreatesEntry(t *testing.T) {
resetDynamicHosts()
u := &Upstream{Dial: "192.0.2.1:80"}
u.fillDynamicHost()
if u.Host == nil {
t.Fatal("expected Host to be set after fillDynamicHost")
}
dynamicHostsMu.RLock()
entry, ok := dynamicHosts["192.0.2.1:80"]
dynamicHostsMu.RUnlock()
if !ok {
t.Fatal("expected entry in dynamicHosts map")
}
if entry.host != u.Host {
t.Error("dynamicHosts entry host should be the same pointer assigned to Upstream.Host")
}
if entry.lastSeen.IsZero() {
t.Error("expected lastSeen to be set")
}
}
// TestFillDynamicHostReusesSameHost verifies that two calls for the same address
// return the exact same *Host pointer so that state (e.g. fail counts) is shared.
func TestFillDynamicHostReusesSameHost(t *testing.T) {
resetDynamicHosts()
u1 := &Upstream{Dial: "192.0.2.2:80"}
u1.fillDynamicHost()
u2 := &Upstream{Dial: "192.0.2.2:80"}
u2.fillDynamicHost()
if u1.Host != u2.Host {
t.Error("expected both upstreams to share the same *Host pointer")
}
}
// TestFillDynamicHostUpdatesLastSeen verifies that a second call for the same
// address advances the lastSeen timestamp.
func TestFillDynamicHostUpdatesLastSeen(t *testing.T) {
resetDynamicHosts()
u := &Upstream{Dial: "192.0.2.3:80"}
u.fillDynamicHost()
dynamicHostsMu.RLock()
first := dynamicHosts["192.0.2.3:80"].lastSeen
dynamicHostsMu.RUnlock()
// Ensure measurable time passes.
time.Sleep(2 * time.Millisecond)
u2 := &Upstream{Dial: "192.0.2.3:80"}
u2.fillDynamicHost()
dynamicHostsMu.RLock()
second := dynamicHosts["192.0.2.3:80"].lastSeen
dynamicHostsMu.RUnlock()
if !second.After(first) {
t.Error("expected lastSeen to be updated on second fillDynamicHost call")
}
}
// TestFillDynamicHostIndependentAddresses verifies that different addresses get
// independent Host entries.
func TestFillDynamicHostIndependentAddresses(t *testing.T) {
resetDynamicHosts()
u1 := &Upstream{Dial: "192.0.2.4:80"}
u1.fillDynamicHost()
u2 := &Upstream{Dial: "192.0.2.5:80"}
u2.fillDynamicHost()
if u1.Host == u2.Host {
t.Error("different addresses should have different *Host entries")
}
}
// TestFillDynamicHostPreservesFailCount verifies that fail counts on a dynamic
// host survive across multiple fillDynamicHost calls (simulating sequential
// requests), which is the core behaviour fixed by this change.
func TestFillDynamicHostPreservesFailCount(t *testing.T) {
resetDynamicHosts()
// First "request": provision and record a failure.
u1 := &Upstream{Dial: "192.0.2.6:80"}
u1.fillDynamicHost()
_ = u1.Host.countFail(1)
if u1.Host.Fails() != 1 {
t.Fatalf("expected 1 fail, got %d", u1.Host.Fails())
}
// Second "request": provision the same address again (new *Upstream, same address).
u2 := &Upstream{Dial: "192.0.2.6:80"}
u2.fillDynamicHost()
if u2.Host.Fails() != 1 {
t.Errorf("expected fail count to persist across fillDynamicHost calls, got %d", u2.Host.Fails())
}
}
// TestProvisionUpstreamDynamic verifies that provisionUpstream with dynamic=true
// uses fillDynamicHost (not the UsagePool) and sets healthCheckPolicy /
// MaxRequests correctly from handler config.
func TestProvisionUpstreamDynamic(t *testing.T) {
resetDynamicHosts()
passive := &PassiveHealthChecks{
FailDuration: caddy.Duration(10 * time.Second),
MaxFails: 3,
UnhealthyRequestCount: 5,
}
h := Handler{
HealthChecks: &HealthChecks{
Passive: passive,
},
}
u := &Upstream{Dial: "192.0.2.7:80"}
h.provisionUpstream(u, true)
if u.Host == nil {
t.Fatal("Host should be set after provisionUpstream")
}
if u.healthCheckPolicy != passive {
t.Error("healthCheckPolicy should point to the handler's PassiveHealthChecks")
}
if u.MaxRequests != 5 {
t.Errorf("expected MaxRequests=5 from UnhealthyRequestCount, got %d", u.MaxRequests)
}
// Must be in dynamicHosts, not in the static UsagePool.
dynamicHostsMu.RLock()
_, inDynamic := dynamicHosts["192.0.2.7:80"]
dynamicHostsMu.RUnlock()
if !inDynamic {
t.Error("dynamic upstream should be stored in dynamicHosts")
}
_, inPool := hosts.References("192.0.2.7:80")
if inPool {
t.Error("dynamic upstream should NOT be stored in the static UsagePool")
}
}
// TestProvisionUpstreamStatic verifies that provisionUpstream with dynamic=false
// uses the UsagePool and does NOT insert into dynamicHosts.
func TestProvisionUpstreamStatic(t *testing.T) {
resetDynamicHosts()
h := Handler{}
u := &Upstream{Dial: "192.0.2.8:80"}
h.provisionUpstream(u, false)
if u.Host == nil {
t.Fatal("Host should be set after provisionUpstream")
}
refs, inPool := hosts.References("192.0.2.8:80")
if !inPool {
t.Error("static upstream should be in the UsagePool")
}
if refs != 1 {
t.Errorf("expected ref count 1, got %d", refs)
}
dynamicHostsMu.RLock()
_, inDynamic := dynamicHosts["192.0.2.8:80"]
dynamicHostsMu.RUnlock()
if inDynamic {
t.Error("static upstream should NOT be in dynamicHosts")
}
// Clean up the pool entry we just added.
_, _ = hosts.Delete("192.0.2.8:80")
}
// TestDynamicHostHealthyConsultsFails verifies the end-to-end passive health
// check path: after enough failures are recorded against a dynamic upstream's
// shared *Host, Healthy() returns false for a newly provisioned *Upstream with
// the same address.
func TestDynamicHostHealthyConsultsFails(t *testing.T) {
resetDynamicHosts()
passive := &PassiveHealthChecks{
FailDuration: caddy.Duration(time.Minute),
MaxFails: 2,
}
h := Handler{
HealthChecks: &HealthChecks{Passive: passive},
}
// First request: provision and record two failures.
u1 := &Upstream{Dial: "192.0.2.9:80"}
h.provisionUpstream(u1, true)
_ = u1.Host.countFail(1)
_ = u1.Host.countFail(1)
// Second request: fresh *Upstream, same address.
u2 := &Upstream{Dial: "192.0.2.9:80"}
h.provisionUpstream(u2, true)
if u2.Healthy() {
t.Error("upstream should be unhealthy after MaxFails failures have been recorded against its shared Host")
}
}
// TestDynamicHostCleanupEvictsStaleEntries verifies that the cleanup sweep
// removes entries whose lastSeen is older than dynamicHostIdleExpiry.
func TestDynamicHostCleanupEvictsStaleEntries(t *testing.T) {
resetDynamicHosts()
const addr = "192.0.2.10:80"
// Insert an entry directly with a lastSeen far in the past.
dynamicHostsMu.Lock()
dynamicHosts[addr] = dynamicHostEntry{
host: new(Host),
lastSeen: time.Now().Add(-2 * dynamicHostIdleExpiry),
}
dynamicHostsMu.Unlock()
// Run the cleanup logic inline (same logic as the goroutine).
dynamicHostsMu.Lock()
for a, entry := range dynamicHosts {
if time.Since(entry.lastSeen) > dynamicHostIdleExpiry {
delete(dynamicHosts, a)
}
}
dynamicHostsMu.Unlock()
dynamicHostsMu.RLock()
_, stillPresent := dynamicHosts[addr]
dynamicHostsMu.RUnlock()
if stillPresent {
t.Error("stale dynamic host entry should have been evicted by cleanup sweep")
}
}
// TestDynamicHostCleanupRetainsFreshEntries verifies that the cleanup sweep
// keeps entries whose lastSeen is within dynamicHostIdleExpiry.
func TestDynamicHostCleanupRetainsFreshEntries(t *testing.T) {
resetDynamicHosts()
const addr = "192.0.2.11:80"
dynamicHostsMu.Lock()
dynamicHosts[addr] = dynamicHostEntry{
host: new(Host),
lastSeen: time.Now(),
}
dynamicHostsMu.Unlock()
// Run the cleanup logic inline.
dynamicHostsMu.Lock()
for a, entry := range dynamicHosts {
if time.Since(entry.lastSeen) > dynamicHostIdleExpiry {
delete(dynamicHosts, a)
}
}
dynamicHostsMu.Unlock()
dynamicHostsMu.RLock()
_, stillPresent := dynamicHosts[addr]
dynamicHostsMu.RUnlock()
if !stillPresent {
t.Error("fresh dynamic host entry should be retained by cleanup sweep")
}
}
// TestDynamicHostConcurrentFillHost verifies that concurrent calls to
// fillDynamicHost for the same address all get the same *Host pointer and
// don't race (run with -race).
func TestDynamicHostConcurrentFillHost(t *testing.T) {
resetDynamicHosts()
const addr = "192.0.2.12:80"
const goroutines = 50
var wg sync.WaitGroup
hosts := make([]*Host, goroutines)
for i := range goroutines {
wg.Add(1)
go func(idx int) {
defer wg.Done()
u := &Upstream{Dial: addr}
u.fillDynamicHost()
hosts[idx] = u.Host
}(i)
}
wg.Wait()
first := hosts[0]
for i, h := range hosts {
if h != first {
t.Errorf("goroutine %d got a different *Host pointer; expected all to share the same entry", i)
}
}
}
@@ -442,7 +442,7 @@ func (t Transport) splitPos(path string) int {
for _, split := range t.SplitPath {
splitLen := len(split)
for i := 0; i < pathLen; i++ {
for i := range pathLen {
if path[i] >= utf8.RuneSelf {
if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 {
return end
@@ -456,7 +456,7 @@ func (t Transport) splitPos(path string) int {
}
match := true
for j := 0; j < splitLen; j++ {
for j := range splitLen {
c := path[i+j]
if c >= utf8.RuneSelf {
@@ -208,6 +208,24 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
for _, from := range sortedHeadersToCopy {
to := http.CanonicalHeaderKey(headersToCopy[from])
placeholderName := "http.reverse_proxy.header." + http.CanonicalHeaderKey(from)
// Always delete the client-supplied header before conditionally setting
// it from the auth response. Without this, a client that pre-supplies a
// header listed in copy_headers can inject arbitrary values when the auth
// service does not return that header: the MatchNot guard below would
// skip the Set entirely, leaving the original client-controlled value
// intact and forwarding it to the backend.
copyHeaderRoutes = append(copyHeaderRoutes, caddyhttp.Route{
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
&headers.Handler{
Request: &headers.HeaderOps{
Delete: []string{to},
},
},
"handler", "headers", nil,
)},
})
handler := &headers.Handler{
Request: &headers.HeaderOps{
Set: http.Header{
@@ -17,7 +17,7 @@ func TestAddForwardedHeadersNonIP(t *testing.T) {
// Mock the context variables required by Caddy.
// We need to inject the variable map manually since we aren't running the full server.
vars := map[string]interface{}{
vars := map[string]any{
caddyhttp.TrustedProxyVarKey: false,
}
ctx := context.WithValue(req.Context(), caddyhttp.VarsCtxKey, vars)
@@ -42,7 +42,7 @@ func TestAddForwardedHeaders_UnixSocketTrusted(t *testing.T) {
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Forwarded-Host", "original.example.com")
vars := map[string]interface{}{
vars := map[string]any{
caddyhttp.TrustedProxyVarKey: true,
caddyhttp.ClientIPVarKey: "1.2.3.4",
}
@@ -74,7 +74,7 @@ func TestAddForwardedHeaders_UnixSocketUntrusted(t *testing.T) {
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Forwarded-Host", "spoofed.example.com")
vars := map[string]interface{}{
vars := map[string]any{
caddyhttp.TrustedProxyVarKey: false,
caddyhttp.ClientIPVarKey: "",
}
@@ -103,7 +103,7 @@ func TestAddForwardedHeaders_UnixSocketTrustedNoExistingHeaders(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com/", nil)
req.RemoteAddr = "@"
vars := map[string]interface{}{
vars := map[string]any{
caddyhttp.TrustedProxyVarKey: true,
caddyhttp.ClientIPVarKey: "5.6.7.8",
}
@@ -359,6 +359,12 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
dialInfoUpstream = &Upstream{
Dial: h.HealthChecks.Active.Upstream,
}
} else if upstream.activeHealthCheckPort != 0 {
// health_port overrides the port; addr has already been updated
// with the health port, so use its address for dialing
dialInfoUpstream = &Upstream{
Dial: addr.JoinHostPort(0),
}
}
dialInfo, _ := dialInfoUpstream.fillDialInfo(repl)
+79 -17
View File
@@ -19,7 +19,9 @@ import (
"fmt"
"net/netip"
"strconv"
"sync"
"sync/atomic"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
@@ -60,7 +62,7 @@ type Upstream struct {
activeHealthCheckUpstream string
healthCheckPolicy *PassiveHealthChecks
cb CircuitBreaker
unhealthy int32 // accessed atomically; status from active health checker
unhealthy atomic.Int32 // status from active health checker
}
// (pointer receiver necessary to avoid a race condition, since
@@ -132,39 +134,76 @@ func (u *Upstream) fillHost() {
u.Host = host
}
// fillDynamicHost is like fillHost, but stores the host in the separate
// dynamicHosts map rather than the reference-counted UsagePool. Dynamic
// hosts are not reference-counted; instead, they are retained as long as
// they are actively seen and are evicted by a background cleanup goroutine
// after dynamicHostIdleExpiry of inactivity. This preserves health state
// (e.g. passive fail counts) across sequential requests.
func (u *Upstream) fillDynamicHost() {
dynamicHostsMu.Lock()
entry, ok := dynamicHosts[u.String()]
if ok {
entry.lastSeen = time.Now()
dynamicHosts[u.String()] = entry
u.Host = entry.host
} else {
h := new(Host)
dynamicHosts[u.String()] = dynamicHostEntry{host: h, lastSeen: time.Now()}
u.Host = h
}
dynamicHostsMu.Unlock()
// ensure the cleanup goroutine is running
dynamicHostsCleanerOnce.Do(func() {
go func() {
for {
time.Sleep(dynamicHostCleanupInterval)
dynamicHostsMu.Lock()
for addr, entry := range dynamicHosts {
if time.Since(entry.lastSeen) > dynamicHostIdleExpiry {
delete(dynamicHosts, addr)
}
}
dynamicHostsMu.Unlock()
}
}()
})
}
// Host is the basic, in-memory representation of the state of a remote host.
// Its fields are accessed atomically and Host values must not be copied.
type Host struct {
numRequests int64 // must be 64-bit aligned on 32-bit systems (see https://golang.org/pkg/sync/atomic/#pkg-note-BUG)
fails int64
activePasses int64
activeFails int64
numRequests atomic.Int64 // atomic.Int64 is automatically aligned for us (see https://golang.org/pkg/sync/atomic/#pkg-note-BUG)
fails atomic.Int64
activePasses atomic.Int64
activeFails atomic.Int64
}
// NumRequests returns the number of active requests to the upstream.
func (h *Host) NumRequests() int {
return int(atomic.LoadInt64(&h.numRequests))
return int(h.numRequests.Load())
}
// Fails returns the number of recent failures with the upstream.
func (h *Host) Fails() int {
return int(atomic.LoadInt64(&h.fails))
return int(h.fails.Load())
}
// activeHealthPasses returns the number of consecutive active health check passes with the upstream.
func (h *Host) activeHealthPasses() int {
return int(atomic.LoadInt64(&h.activePasses))
return int(h.activePasses.Load())
}
// activeHealthFails returns the number of consecutive active health check failures with the upstream.
func (h *Host) activeHealthFails() int {
return int(atomic.LoadInt64(&h.activeFails))
return int(h.activeFails.Load())
}
// countRequest mutates the active request count by
// delta. It returns an error if the adjustment fails.
func (h *Host) countRequest(delta int) error {
result := atomic.AddInt64(&h.numRequests, int64(delta))
result := h.numRequests.Add(int64(delta))
if result < 0 {
return fmt.Errorf("count below 0: %d", result)
}
@@ -174,7 +213,7 @@ func (h *Host) countRequest(delta int) error {
// countFail mutates the recent failures count by
// delta. It returns an error if the adjustment fails.
func (h *Host) countFail(delta int) error {
result := atomic.AddInt64(&h.fails, int64(delta))
result := h.fails.Add(int64(delta))
if result < 0 {
return fmt.Errorf("count below 0: %d", result)
}
@@ -184,7 +223,7 @@ func (h *Host) countFail(delta int) error {
// countHealthPass mutates the recent passes count by
// delta. It returns an error if the adjustment fails.
func (h *Host) countHealthPass(delta int) error {
result := atomic.AddInt64(&h.activePasses, int64(delta))
result := h.activePasses.Add(int64(delta))
if result < 0 {
return fmt.Errorf("count below 0: %d", result)
}
@@ -194,7 +233,7 @@ func (h *Host) countHealthPass(delta int) error {
// countHealthFail mutates the recent failures count by
// delta. It returns an error if the adjustment fails.
func (h *Host) countHealthFail(delta int) error {
result := atomic.AddInt64(&h.activeFails, int64(delta))
result := h.activeFails.Add(int64(delta))
if result < 0 {
return fmt.Errorf("count below 0: %d", result)
}
@@ -203,14 +242,15 @@ func (h *Host) countHealthFail(delta int) error {
// resetHealth resets the health check counters.
func (h *Host) resetHealth() {
atomic.StoreInt64(&h.activePasses, 0)
atomic.StoreInt64(&h.activeFails, 0)
h.activePasses.Store(0)
h.activeFails.Store(0)
}
// healthy returns true if the upstream is not actively marked as unhealthy.
// (This returns the status only from the "active" health checks.)
func (u *Upstream) healthy() bool {
return atomic.LoadInt32(&u.unhealthy) == 0
return u.unhealthy.Load() == 0
// return atomic.LoadInt32(&u.unhealthy) == 0
}
// SetHealthy sets the upstream has healthy or unhealthy
@@ -221,7 +261,7 @@ func (u *Upstream) setHealthy(healthy bool) bool {
if healthy {
unhealthy, compare = 0, 1
}
return atomic.CompareAndSwapInt32(&u.unhealthy, compare, unhealthy)
return u.unhealthy.CompareAndSwap(compare, unhealthy)
}
// DialInfo contains information needed to dial a
@@ -268,6 +308,28 @@ func GetDialInfo(ctx context.Context) (DialInfo, bool) {
// through config reloads.
var hosts = caddy.NewUsagePool()
// dynamicHosts tracks hosts that were provisioned from dynamic upstream
// sources. Unlike static upstreams which are reference-counted via the
// UsagePool, dynamic upstream hosts are not reference-counted. Instead,
// their last-seen time is updated on each request, and a background
// goroutine evicts entries that have been idle for dynamicHostIdleExpiry.
// This preserves health state (e.g. passive fail counts) across requests
// to the same dynamic backend.
var (
dynamicHosts = make(map[string]dynamicHostEntry)
dynamicHostsMu sync.RWMutex
dynamicHostsCleanerOnce sync.Once
dynamicHostCleanupInterval = 5 * time.Minute
dynamicHostIdleExpiry = time.Hour
)
// dynamicHostEntry holds a Host and the last time it was seen
// in a set of dynamic upstreams returned for a request.
type dynamicHostEntry struct {
host *Host
lastSeen time.Time
}
// dialInfoVarKey is the key used for the variable that holds
// the dial info for the upstream connection.
const dialInfoVarKey = "reverse_proxy.dial_info"
@@ -384,6 +384,9 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
}
// we need to keep track if a proxy is used for a request
proxyWrapper := func(req *http.Request) (*url.URL, error) {
if proxy == nil {
return nil, nil
}
u, err := proxy(req)
if u == nil || err != nil {
return u, err
@@ -412,8 +415,13 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
return nil, fmt.Errorf("making TLS client config: %v", err)
}
// servername has a placeholder, so we need to replace it
if strings.Contains(h.TLS.ServerName, "{") {
serverNameHasPlaceholder := strings.Contains(h.TLS.ServerName, "{")
// We need to use custom DialTLSContext if:
// 1. ServerName has a placeholder that needs to be replaced at request-time, OR
// 2. ProxyProtocol is enabled, because req.URL.Host is modified to include
// client address info with "->" separator which breaks Go's address parsing
if serverNameHasPlaceholder || h.ProxyProtocol != "" {
rt.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
// reuses the dialer from above to establish a plaintext connection
conn, err := dialContext(ctx, network, addr)
@@ -422,9 +430,11 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
}
// but add our own handshake logic
repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
tlsConfig := rt.TLSClientConfig.Clone()
tlsConfig.ServerName = repl.ReplaceAll(tlsConfig.ServerName, "")
if serverNameHasPlaceholder {
repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
tlsConfig.ServerName = repl.ReplaceAll(tlsConfig.ServerName, "")
}
// h1 only
if caddyhttp.GetVar(ctx, tlsH1OnlyVarKey) == true {
@@ -438,7 +448,7 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
// complete the handshake before returning the connection
if rt.TLSHandshakeTimeout != 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, rt.TLSHandshakeTimeout)
ctx, cancel = context.WithTimeoutCause(ctx, rt.TLSHandshakeTimeout, fmt.Errorf("HTTP transport TLS handshake %ds timeout", int(rt.TLSHandshakeTimeout.Seconds())))
defer cancel()
}
err = tlsConn.HandshakeContext(ctx)
@@ -1,11 +1,13 @@
package reverseproxy
import (
"context"
"encoding/json"
"fmt"
"reflect"
"testing"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
@@ -115,3 +117,80 @@ func TestHTTPTransport_RequestHeaderOps_TLS(t *testing.T) {
t.Fatalf("unexpected Host value; want placeholder, got: %s", got)
}
}
// TestHTTPTransport_DialTLSContext_ProxyProtocol verifies that when TLS and
// ProxyProtocol are both enabled, DialTLSContext is set. This is critical because
// ProxyProtocol modifies req.URL.Host to include client info with "->" separator
// (e.g., "[2001:db8::1]:12345->127.0.0.1:443"), which breaks Go's address parsing.
// Without a custom DialTLSContext, Go's HTTP library would fail with
// "too many colons in address" when trying to parse the mangled host.
func TestHTTPTransport_DialTLSContext_ProxyProtocol(t *testing.T) {
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
defer cancel()
tests := []struct {
name string
tls *TLSConfig
proxyProtocol string
serverNameHasPlaceholder bool
expectDialTLSContext bool
}{
{
name: "no TLS, no proxy protocol",
tls: nil,
proxyProtocol: "",
expectDialTLSContext: false,
},
{
name: "TLS without proxy protocol",
tls: &TLSConfig{},
proxyProtocol: "",
expectDialTLSContext: false,
},
{
name: "TLS with proxy protocol v1",
tls: &TLSConfig{},
proxyProtocol: "v1",
expectDialTLSContext: true,
},
{
name: "TLS with proxy protocol v2",
tls: &TLSConfig{},
proxyProtocol: "v2",
expectDialTLSContext: true,
},
{
name: "TLS with placeholder ServerName",
tls: &TLSConfig{ServerName: "{http.request.host}"},
proxyProtocol: "",
serverNameHasPlaceholder: true,
expectDialTLSContext: true,
},
{
name: "TLS with placeholder ServerName and proxy protocol",
tls: &TLSConfig{ServerName: "{http.request.host}"},
proxyProtocol: "v2",
serverNameHasPlaceholder: true,
expectDialTLSContext: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ht := &HTTPTransport{
TLS: tt.tls,
ProxyProtocol: tt.proxyProtocol,
}
rt, err := ht.NewTransport(ctx)
if err != nil {
t.Fatalf("NewTransport() error = %v", err)
}
hasDialTLSContext := rt.DialTLSContext != nil
if hasDialTLSContext != tt.expectDialTLSContext {
t.Errorf("DialTLSContext set = %v, want %v", hasDialTLSContext, tt.expectDialTLSContext)
}
})
}
}
@@ -0,0 +1,391 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package reverseproxy
import (
"context"
"testing"
"time"
"github.com/caddyserver/caddy/v2"
)
// newPassiveHandler builds a minimal Handler with passive health checks
// configured and a live caddy.Context so the fail-forgetter goroutine can
// be cancelled cleanly. The caller must call cancel() when done.
func newPassiveHandler(t *testing.T, maxFails int, failDuration time.Duration) (*Handler, context.CancelFunc) {
t.Helper()
caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
h := &Handler{
ctx: caddyCtx,
HealthChecks: &HealthChecks{
Passive: &PassiveHealthChecks{
MaxFails: maxFails,
FailDuration: caddy.Duration(failDuration),
},
},
}
return h, cancel
}
// provisionedStaticUpstream creates a static upstream, registers it in the
// UsagePool, and returns a cleanup func that removes it from the pool.
func provisionedStaticUpstream(t *testing.T, h *Handler, addr string) (*Upstream, func()) {
t.Helper()
u := &Upstream{Dial: addr}
h.provisionUpstream(u, false)
return u, func() { _, _ = hosts.Delete(addr) }
}
// provisionedDynamicUpstream creates a dynamic upstream, registers it in
// dynamicHosts, and returns a cleanup func that removes it.
func provisionedDynamicUpstream(t *testing.T, h *Handler, addr string) (*Upstream, func()) {
t.Helper()
u := &Upstream{Dial: addr}
h.provisionUpstream(u, true)
return u, func() {
dynamicHostsMu.Lock()
delete(dynamicHosts, addr)
dynamicHostsMu.Unlock()
}
}
// --- countFailure behaviour ---
// TestCountFailureNoopWhenNoHealthChecks verifies that countFailure is a no-op
// when HealthChecks is nil.
func TestCountFailureNoopWhenNoHealthChecks(t *testing.T) {
resetDynamicHosts()
h := &Handler{}
u := &Upstream{Dial: "10.1.0.1:80", Host: new(Host)}
h.countFailure(u)
if u.Host.Fails() != 0 {
t.Errorf("expected 0 fails with no HealthChecks config, got %d", u.Host.Fails())
}
}
// TestCountFailureNoopWhenZeroDuration verifies that countFailure is a no-op
// when FailDuration is 0 (the zero value disables passive checks).
func TestCountFailureNoopWhenZeroDuration(t *testing.T) {
resetDynamicHosts()
caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
defer cancel()
h := &Handler{
ctx: caddyCtx,
HealthChecks: &HealthChecks{
Passive: &PassiveHealthChecks{MaxFails: 1, FailDuration: 0},
},
}
u := &Upstream{Dial: "10.1.0.2:80", Host: new(Host)}
h.countFailure(u)
if u.Host.Fails() != 0 {
t.Errorf("expected 0 fails with zero FailDuration, got %d", u.Host.Fails())
}
}
// TestCountFailureIncrementsCount verifies that countFailure increments the
// fail count on the upstream's Host.
func TestCountFailureIncrementsCount(t *testing.T) {
resetDynamicHosts()
h, cancel := newPassiveHandler(t, 2, time.Minute)
defer cancel()
u := &Upstream{Dial: "10.1.0.3:80", Host: new(Host)}
h.countFailure(u)
if u.Host.Fails() != 1 {
t.Errorf("expected 1 fail after countFailure, got %d", u.Host.Fails())
}
}
// TestCountFailureDecrementsAfterDuration verifies that the fail count is
// decremented back after FailDuration elapses.
func TestCountFailureDecrementsAfterDuration(t *testing.T) {
resetDynamicHosts()
const failDuration = 50 * time.Millisecond
h, cancel := newPassiveHandler(t, 2, failDuration)
defer cancel()
u := &Upstream{Dial: "10.1.0.4:80", Host: new(Host)}
h.countFailure(u)
if u.Host.Fails() != 1 {
t.Fatalf("expected 1 fail immediately after countFailure, got %d", u.Host.Fails())
}
// Wait long enough for the forgetter goroutine to fire.
time.Sleep(3 * failDuration)
if u.Host.Fails() != 0 {
t.Errorf("expected fail count to return to 0 after FailDuration, got %d", u.Host.Fails())
}
}
// TestCountFailureCancelledContextForgets verifies that cancelling the handler
// context (simulating a config unload) also triggers the forgetter to run,
// decrementing the fail count.
func TestCountFailureCancelledContextForgets(t *testing.T) {
resetDynamicHosts()
h, cancel := newPassiveHandler(t, 2, time.Hour) // very long duration
u := &Upstream{Dial: "10.1.0.5:80", Host: new(Host)}
h.countFailure(u)
if u.Host.Fails() != 1 {
t.Fatalf("expected 1 fail immediately after countFailure, got %d", u.Host.Fails())
}
// Cancelling the context should cause the forgetter goroutine to exit and
// decrement the count.
cancel()
time.Sleep(50 * time.Millisecond)
if u.Host.Fails() != 0 {
t.Errorf("expected fail count to be decremented after context cancel, got %d", u.Host.Fails())
}
}
// --- static upstream passive health check ---
// TestStaticUpstreamHealthyWithNoFailures verifies that a static upstream with
// no recorded failures is considered healthy.
func TestStaticUpstreamHealthyWithNoFailures(t *testing.T) {
resetDynamicHosts()
h, cancel := newPassiveHandler(t, 2, time.Minute)
defer cancel()
u, cleanup := provisionedStaticUpstream(t, h, "10.2.0.1:80")
defer cleanup()
if !u.Healthy() {
t.Error("upstream with no failures should be healthy")
}
}
// TestStaticUpstreamUnhealthyAtMaxFails verifies that a static upstream is
// marked unhealthy once its fail count reaches MaxFails.
func TestStaticUpstreamUnhealthyAtMaxFails(t *testing.T) {
resetDynamicHosts()
h, cancel := newPassiveHandler(t, 2, time.Minute)
defer cancel()
u, cleanup := provisionedStaticUpstream(t, h, "10.2.0.2:80")
defer cleanup()
h.countFailure(u)
if !u.Healthy() {
t.Error("upstream should still be healthy after 1 of 2 allowed failures")
}
h.countFailure(u)
if u.Healthy() {
t.Error("upstream should be unhealthy after reaching MaxFails=2")
}
}
// TestStaticUpstreamRecoversAfterFailDuration verifies that a static upstream
// returns to healthy once its failures expire.
func TestStaticUpstreamRecoversAfterFailDuration(t *testing.T) {
resetDynamicHosts()
const failDuration = 50 * time.Millisecond
h, cancel := newPassiveHandler(t, 1, failDuration)
defer cancel()
u, cleanup := provisionedStaticUpstream(t, h, "10.2.0.3:80")
defer cleanup()
h.countFailure(u)
if u.Healthy() {
t.Fatal("upstream should be unhealthy immediately after MaxFails failure")
}
time.Sleep(3 * failDuration)
if !u.Healthy() {
t.Errorf("upstream should recover to healthy after FailDuration, Fails=%d", u.Host.Fails())
}
}
// TestStaticUpstreamHealthPersistedAcrossReprovisioning verifies that static
// upstreams share a Host via the UsagePool, so a second call to provisionUpstream
// for the same address (as happens on config reload) sees the accumulated state.
func TestStaticUpstreamHealthPersistedAcrossReprovisioning(t *testing.T) {
resetDynamicHosts()
h, cancel := newPassiveHandler(t, 2, time.Minute)
defer cancel()
u1, cleanup1 := provisionedStaticUpstream(t, h, "10.2.0.4:80")
defer cleanup1()
h.countFailure(u1)
h.countFailure(u1)
// Simulate a second handler instance referencing the same upstream
// (e.g. after a config reload that keeps the same backend address).
u2, cleanup2 := provisionedStaticUpstream(t, h, "10.2.0.4:80")
defer cleanup2()
if u1.Host != u2.Host {
t.Fatal("expected both Upstream structs to share the same *Host via UsagePool")
}
if u2.Healthy() {
t.Error("re-provisioned upstream should still see the prior fail count and be unhealthy")
}
}
// --- dynamic upstream passive health check ---
// TestDynamicUpstreamHealthyWithNoFailures verifies that a freshly provisioned
// dynamic upstream is healthy.
func TestDynamicUpstreamHealthyWithNoFailures(t *testing.T) {
resetDynamicHosts()
h, cancel := newPassiveHandler(t, 2, time.Minute)
defer cancel()
u, cleanup := provisionedDynamicUpstream(t, h, "10.3.0.1:80")
defer cleanup()
if !u.Healthy() {
t.Error("dynamic upstream with no failures should be healthy")
}
}
// TestDynamicUpstreamUnhealthyAtMaxFails verifies that a dynamic upstream is
// marked unhealthy once its fail count reaches MaxFails.
func TestDynamicUpstreamUnhealthyAtMaxFails(t *testing.T) {
resetDynamicHosts()
h, cancel := newPassiveHandler(t, 2, time.Minute)
defer cancel()
u, cleanup := provisionedDynamicUpstream(t, h, "10.3.0.2:80")
defer cleanup()
h.countFailure(u)
if !u.Healthy() {
t.Error("dynamic upstream should still be healthy after 1 of 2 allowed failures")
}
h.countFailure(u)
if u.Healthy() {
t.Error("dynamic upstream should be unhealthy after reaching MaxFails=2")
}
}
// TestDynamicUpstreamFailCountPersistedBetweenRequests is the core regression
// test: it simulates two sequential (non-concurrent) requests to the same
// dynamic upstream. Before the fix, the UsagePool entry would be deleted
// between requests, wiping the fail count. Now it should survive.
func TestDynamicUpstreamFailCountPersistedBetweenRequests(t *testing.T) {
resetDynamicHosts()
h, cancel := newPassiveHandler(t, 2, time.Minute)
defer cancel()
// --- first request ---
u1 := &Upstream{Dial: "10.3.0.3:80"}
h.provisionUpstream(u1, true)
h.countFailure(u1)
if u1.Host.Fails() != 1 {
t.Fatalf("expected 1 fail after first request, got %d", u1.Host.Fails())
}
// Simulate end of first request: no delete from any pool (key difference
// vs. the old behaviour where hosts.Delete was deferred).
// --- second request: brand-new *Upstream struct, same dial address ---
u2 := &Upstream{Dial: "10.3.0.3:80"}
h.provisionUpstream(u2, true)
if u1.Host != u2.Host {
t.Fatal("expected both requests to share the same *Host pointer from dynamicHosts")
}
if u2.Host.Fails() != 1 {
t.Errorf("expected fail count to persist across requests, got %d", u2.Host.Fails())
}
// A second failure now tips it over MaxFails=2.
h.countFailure(u2)
if u2.Healthy() {
t.Error("upstream should be unhealthy after accumulated failures across requests")
}
// Cleanup.
dynamicHostsMu.Lock()
delete(dynamicHosts, "10.3.0.3:80")
dynamicHostsMu.Unlock()
}
// TestDynamicUpstreamRecoveryAfterFailDuration verifies that a dynamic
// upstream's fail count expires and it returns to healthy.
func TestDynamicUpstreamRecoveryAfterFailDuration(t *testing.T) {
resetDynamicHosts()
const failDuration = 50 * time.Millisecond
h, cancel := newPassiveHandler(t, 1, failDuration)
defer cancel()
u, cleanup := provisionedDynamicUpstream(t, h, "10.3.0.4:80")
defer cleanup()
h.countFailure(u)
if u.Healthy() {
t.Fatal("upstream should be unhealthy immediately after MaxFails failure")
}
time.Sleep(3 * failDuration)
// Re-provision (as a new request would) to get fresh *Upstream with policy set.
u2 := &Upstream{Dial: "10.3.0.4:80"}
h.provisionUpstream(u2, true)
if !u2.Healthy() {
t.Errorf("dynamic upstream should recover to healthy after FailDuration, Fails=%d", u2.Host.Fails())
}
}
// TestDynamicUpstreamMaxRequestsFromUnhealthyRequestCount verifies that
// UnhealthyRequestCount is copied into MaxRequests so Full() works correctly.
func TestDynamicUpstreamMaxRequestsFromUnhealthyRequestCount(t *testing.T) {
resetDynamicHosts()
caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
defer cancel()
h := &Handler{
ctx: caddyCtx,
HealthChecks: &HealthChecks{
Passive: &PassiveHealthChecks{
UnhealthyRequestCount: 3,
},
},
}
u, cleanup := provisionedDynamicUpstream(t, h, "10.3.0.5:80")
defer cleanup()
if u.MaxRequests != 3 {
t.Errorf("expected MaxRequests=3 from UnhealthyRequestCount, got %d", u.MaxRequests)
}
// Should not be full with fewer requests than the limit.
_ = u.Host.countRequest(2)
if u.Full() {
t.Error("upstream should not be full with 2 of 3 allowed requests")
}
_ = u.Host.countRequest(1)
if !u.Full() {
t.Error("upstream should be full at UnhealthyRequestCount concurrent requests")
}
}
@@ -0,0 +1,257 @@
package reverseproxy
import (
"errors"
"io"
"net"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"go.uber.org/zap"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
// prepareTestRequest injects the context values that ServeHTTP and
// proxyLoopIteration require (caddy.ReplacerCtxKey, VarsCtxKey, etc.) using
// the same helper that the real HTTP server uses.
//
// A zero-value Server is passed so that caddyhttp.ServerCtxKey is set to a
// non-nil pointer; reverseProxy dereferences it to check ShouldLogCredentials.
func prepareTestRequest(req *http.Request) *http.Request {
repl := caddy.NewReplacer()
return caddyhttp.PrepareRequest(req, repl, nil, &caddyhttp.Server{})
}
// closeOnCloseReader is an io.ReadCloser whose Close method actually makes
// subsequent reads fail, mimicking the behaviour of a real HTTP request body
// (as opposed to io.NopCloser, whose Close is a no-op and would mask the bug
// we are testing).
type closeOnCloseReader struct {
mu sync.Mutex
r *strings.Reader
closed bool
}
func newCloseOnCloseReader(s string) *closeOnCloseReader {
return &closeOnCloseReader{r: strings.NewReader(s)}
}
func (c *closeOnCloseReader) Read(p []byte) (int, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.closed {
return 0, errors.New("http: invalid Read on closed Body")
}
return c.r.Read(p)
}
func (c *closeOnCloseReader) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
c.closed = true
return nil
}
// deadUpstreamAddr returns a TCP address that is guaranteed to refuse
// connections: we bind a listener, note its address, close it immediately,
// and return the address. Any dial to that address will get ECONNREFUSED.
func deadUpstreamAddr(t *testing.T) string {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("failed to create dead upstream listener: %v", err)
}
addr := ln.Addr().String()
ln.Close()
return addr
}
// testTransport wraps http.Transport to:
// 1. Set the URL scheme to "http" when it is empty (matching what
// HTTPTransport.SetScheme does in production; cloneRequest strips the
// scheme intentionally so a plain *http.Transport would fail with
// "unsupported protocol scheme").
// 2. Wrap dial errors as DialError so that tryAgain correctly identifies them
// as safe-to-retry regardless of request method (as HTTPTransport does in
// production via its custom dialer).
type testTransport struct{ *http.Transport }
func (t testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if req.URL.Scheme == "" {
req.URL.Scheme = "http"
}
resp, err := t.Transport.RoundTrip(req)
if err != nil {
// Wrap dial errors as DialError to match production behaviour.
// Without this wrapping, tryAgain treats ECONNREFUSED on a POST
// request as non-retryable (only GET is retried by default when
// the error is not a DialError).
var opErr *net.OpError
if errors.As(err, &opErr) && opErr.Op == "dial" {
return nil, DialError{err}
}
}
return resp, err
}
// minimalHandler returns a Handler with only the fields required by ServeHTTP
// set directly, bypassing Provision (which requires a full Caddy runtime).
// RoundRobinSelection is used so that successive iterations of the proxy loop
// advance through the upstream pool in a predictable order.
func minimalHandler(retries int, upstreams ...*Upstream) *Handler {
return &Handler{
logger: zap.NewNop(),
Transport: testTransport{&http.Transport{}},
Upstreams: upstreams,
LoadBalancing: &LoadBalancing{
Retries: retries,
SelectionPolicy: &RoundRobinSelection{},
// RetryMatch intentionally nil: dial errors are always retried
// regardless of RetryMatch or request method.
},
// ctx, connections, connectionsMu, events: zero/nil values are safe
// for the code paths exercised by these tests (TryInterval=0 so
// ctx.Done() is never consulted; no WebSocket hijacking; no passive
// health-check event emission).
}
}
// TestDialErrorBodyRetry verifies that a POST request whose body has NOT been
// pre-buffered via request_buffers can still be retried after a dial error.
//
// Before the fix, a dial error caused Go's transport to close the shared body
// (via cloneRequest's shallow copy), so the retry attempt would read from an
// already-closed io.ReadCloser and produce:
//
// http: invalid Read on closed Body → HTTP 502
//
// After the fix the handler wraps the body in noCloseBody when retries are
// configured, preventing the transport's Close() from propagating to the
// shared body. Since dial errors never read any bytes, the body remains at
// position 0 for the retry.
func TestDialErrorBodyRetry(t *testing.T) {
// Good upstream: echoes the request body with 200 OK.
goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "read body: "+err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write(body)
}))
t.Cleanup(goodServer.Close)
const requestBody = "hello, retry"
tests := []struct {
name string
method string
body string
retries int
wantStatus int
wantBody string
}{
{
// Core regression case: POST with a body, no request_buffers,
// dial error on first upstream → retry to second upstream succeeds.
name: "POST body retried after dial error",
method: http.MethodPost,
body: requestBody,
retries: 1,
wantStatus: http.StatusOK,
wantBody: requestBody,
},
{
// Dial errors are always retried regardless of method, but there
// is no body to re-read, so GET has always worked. Keep it as a
// sanity check that we did not break the no-body path.
name: "GET without body retried after dial error",
method: http.MethodGet,
body: "",
retries: 1,
wantStatus: http.StatusOK,
wantBody: "",
},
{
// Without any retry configuration the handler must give up on the
// first dial error and return a 502. Confirms no wrapping occurs
// in the no-retry path.
name: "no retries configured returns 502 on dial error",
method: http.MethodPost,
body: requestBody,
retries: 0,
wantStatus: http.StatusBadGateway,
wantBody: "",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
dead := deadUpstreamAddr(t)
// Build the upstream pool. RoundRobinSelection starts its
// counter at 0 and increments before returning, so with a
// two-element pool it picks index 1 first, then index 0.
// Put the good upstream at index 0 and the dead one at
// index 1 so that:
// attempt 1 → pool[1] = dead → DialError (ECONNREFUSED)
// attempt 2 → pool[0] = good → 200
upstreams := []*Upstream{
{Host: new(Host), Dial: goodServer.Listener.Addr().String()},
{Host: new(Host), Dial: dead},
}
if tc.retries == 0 {
// For the "no retries" case use only the dead upstream so
// there is nowhere to retry to.
upstreams = []*Upstream{
{Host: new(Host), Dial: dead},
}
}
h := minimalHandler(tc.retries, upstreams...)
// Use closeOnCloseReader so that Close() truly prevents further
// reads, matching real http.body semantics. io.NopCloser would
// mask the bug because its Close is a no-op.
var bodyReader io.ReadCloser
if tc.body != "" {
bodyReader = newCloseOnCloseReader(tc.body)
}
req := httptest.NewRequest(tc.method, "http://example.com/", bodyReader)
if bodyReader != nil {
// httptest.NewRequest wraps the reader in NopCloser; replace
// it with our close-aware reader so Close() is propagated.
req.Body = bodyReader
req.ContentLength = int64(len(tc.body))
}
req = prepareTestRequest(req)
rec := httptest.NewRecorder()
err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
return nil
}))
// For error cases (e.g. 502) ServeHTTP returns a HandlerError
// rather than writing the status itself.
gotStatus := rec.Code
if err != nil {
if herr, ok := err.(caddyhttp.HandlerError); ok {
gotStatus = herr.StatusCode
}
}
if gotStatus != tc.wantStatus {
t.Errorf("status: got %d, want %d (err=%v)", gotStatus, tc.wantStatus, err)
}
if tc.wantBody != "" && rec.Body.String() != tc.wantBody {
t.Errorf("body: got %q, want %q", rec.Body.String(), tc.wantBody)
}
})
}
}
+83 -25
View File
@@ -32,6 +32,7 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"go.uber.org/zap"
@@ -46,6 +47,31 @@ import (
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
)
// inFlightRequests uses sync.Map with atomic.Int64 for lock-free updates on the hot path
var inFlightRequests sync.Map
func incInFlightRequest(address string) {
v, _ := inFlightRequests.LoadOrStore(address, new(atomic.Int64))
v.(*atomic.Int64).Add(1)
}
func decInFlightRequest(address string) {
if v, ok := inFlightRequests.Load(address); ok {
if v.(*atomic.Int64).Add(-1) <= 0 {
inFlightRequests.Delete(address)
}
}
}
func getInFlightRequests() map[string]int64 {
copyMap := make(map[string]int64)
inFlightRequests.Range(func(key, value any) bool {
copyMap[key.(string)] = value.(*atomic.Int64).Load()
return true
})
return copyMap
}
func init() {
caddy.RegisterModule(Handler{})
}
@@ -145,6 +171,12 @@ type Handler struct {
// forcibly closed at the end of the timeout. Default: no timeout.
StreamTimeout caddy.Duration `json:"stream_timeout,omitempty"`
// The size of the buffer used for each direction of streaming
// requests such as WebSockets. If zero, the default size is 32 KiB.
// This only affects upgraded bidirectional streams, not normal
// request or response buffering.
StreamBufferSize int `json:"stream_buffer_size,omitempty"`
// If nonzero, streaming requests such as WebSockets will not be
// closed when the proxy config is unloaded, and instead the stream
// will remain open until the delay is complete. In other words,
@@ -366,7 +398,7 @@ func (h *Handler) Provision(ctx caddy.Context) error {
// set up upstreams
for _, u := range h.Upstreams {
h.provisionUpstream(u)
h.provisionUpstream(u, false)
}
if h.HealthChecks != nil {
@@ -456,18 +488,31 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
reqHost := clonedReq.Host
reqHeader := clonedReq.Header
// If the cloned request body was fully buffered, keep a reference to its
// buffer so we can reuse it across retries and return it to the pool
// once were done.
// When retries are configured and there is a body, wrap it in
// io.NopCloser to prevent Go's transport from closing it on dial
// errors. cloneRequest does a shallow copy, so clonedReq.Body and
// r.Body share the same io.ReadCloser — a dial-failure Close()
// would kill the original body for all subsequent retry attempts.
// The real body is closed by the HTTP server when the handler
// returns.
//
// If the body was already fully buffered (via request_buffers),
// we also extract the buffer so the retry loop can replay it
// from the beginning on each attempt. (see #6259, #7546)
var bufferedReqBody *bytes.Buffer
if reqBodyBuf, ok := clonedReq.Body.(bodyReadCloser); ok && reqBodyBuf.body == nil && reqBodyBuf.buf != nil {
bufferedReqBody = reqBodyBuf.buf
reqBodyBuf.buf = nil
defer func() {
bufferedReqBody.Reset()
bufPool.Put(bufferedReqBody)
}()
if clonedReq.Body != nil && h.LoadBalancing != nil &&
(h.LoadBalancing.Retries > 0 || h.LoadBalancing.TryDuration > 0) {
if reqBodyBuf, ok := clonedReq.Body.(bodyReadCloser); ok && reqBodyBuf.body == nil && reqBodyBuf.buf != nil {
bufferedReqBody = reqBodyBuf.buf
reqBodyBuf.buf = nil
clonedReq.Body = io.NopCloser(bytes.NewReader(bufferedReqBody.Bytes()))
defer func() {
bufferedReqBody.Reset()
bufPool.Put(bufferedReqBody)
}()
} else {
clonedReq.Body = io.NopCloser(clonedReq.Body)
}
}
start := time.Now()
@@ -537,18 +582,11 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
} else {
upstreams = dUpstreams
for _, dUp := range dUpstreams {
h.provisionUpstream(dUp)
h.provisionUpstream(dUp, true)
}
if c := h.logger.Check(zapcore.DebugLevel, "provisioned dynamic upstreams"); c != nil {
c.Write(zap.Int("count", len(dUpstreams)))
}
defer func() {
// these upstreams are dynamic, so they are only used for this iteration
// of the proxy loop; be sure to let them go away when we're done with them
for _, upstream := range dUpstreams {
_, _ = hosts.Delete(upstream.String())
}
}()
}
}
@@ -904,8 +942,16 @@ func (h Handler) addForwardedHeaders(req *http.Request) error {
// Go standard library which was used as the foundation.)
func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origReq *http.Request, repl *caddy.Replacer, di DialInfo, next caddyhttp.Handler) error {
_ = di.Upstream.Host.countRequest(1)
// Increment the in-flight request count
incInFlightRequest(di.Address)
//nolint:errcheck
defer di.Upstream.Host.countRequest(-1)
defer func() {
di.Upstream.Host.countRequest(-1)
// Decrement the in-flight request count
decInFlightRequest(di.Address)
}()
// point the request to this upstream
h.directRequest(req, di)
@@ -1278,16 +1324,28 @@ func (h *Handler) directRequest(req *http.Request, di DialInfo) {
// add client address to the host to let transport differentiate requests from different clients
if ppt, ok := h.Transport.(ProxyProtocolTransport); ok && ppt.ProxyProtocolEnabled() {
if proxyProtocolInfo, ok := caddyhttp.GetVar(req.Context(), proxyProtocolInfoVarKey).(ProxyProtocolInfo); ok {
reqHost = proxyProtocolInfo.AddrPort.String() + "->" + reqHost
// encode the request so it plays well with h2 transport, it's unnecessary for h1 but anyway
// The issue is that h2 transport will use the address to determine if new connections are needed
// to roundtrip requests but the without escaping, new connections are constantly created and closed until
// file descriptors are exhausted.
// see: https://github.com/caddyserver/caddy/issues/7529
reqHost = url.QueryEscape(proxyProtocolInfo.AddrPort.String() + "->" + reqHost)
}
}
req.URL.Host = reqHost
}
func (h Handler) provisionUpstream(upstream *Upstream) {
// create or get the host representation for this upstream
upstream.fillHost()
func (h Handler) provisionUpstream(upstream *Upstream, dynamic bool) {
// create or get the host representation for this upstream;
// dynamic upstreams are tracked in a separate map with last-seen
// timestamps so their health state persists across requests without
// being reference-counted (and thus discarded between requests).
if dynamic {
upstream.fillDynamicHost()
} else {
upstream.fillHost()
}
// give it the circuit breaker, if any
upstream.cb = h.CB
@@ -40,8 +40,8 @@ func init() {
caddy.RegisterModule(RandomSelection{})
caddy.RegisterModule(RandomChoiceSelection{})
caddy.RegisterModule(LeastConnSelection{})
caddy.RegisterModule(RoundRobinSelection{})
caddy.RegisterModule(WeightedRoundRobinSelection{})
caddy.RegisterModule(new(RoundRobinSelection))
caddy.RegisterModule(new(WeightedRoundRobinSelection))
caddy.RegisterModule(FirstSelection{})
caddy.RegisterModule(IPHashSelection{})
caddy.RegisterModule(ClientIPHashSelection{})
@@ -83,12 +83,12 @@ type WeightedRoundRobinSelection struct {
// The weight of each upstream in order,
// corresponding with the list of upstreams configured.
Weights []int `json:"weights,omitempty"`
index uint32
index atomic.Uint32
totalWeight int
}
// CaddyModule returns the Caddy module information.
func (WeightedRoundRobinSelection) CaddyModule() caddy.ModuleInfo {
func (*WeightedRoundRobinSelection) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.reverse_proxy.selection_policies.weighted_round_robin",
New: func() caddy.Module {
@@ -143,7 +143,7 @@ func (r *WeightedRoundRobinSelection) Select(pool UpstreamPool, _ *http.Request,
weights = append(weights, w)
}
}
currentWeight := int(atomic.AddUint32(&r.index, 1)) % r.totalWeight
currentWeight := int(r.index.Add(1)) % r.totalWeight
for i, weight := range weights {
totalWeight += weight
if currentWeight < totalWeight {
@@ -295,11 +295,11 @@ func (r *LeastConnSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// RoundRobinSelection is a policy that selects
// a host based on round-robin ordering.
type RoundRobinSelection struct {
robin uint32
robin atomic.Uint32
}
// CaddyModule returns the Caddy module information.
func (RoundRobinSelection) CaddyModule() caddy.ModuleInfo {
func (*RoundRobinSelection) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.reverse_proxy.selection_policies.round_robin",
New: func() caddy.Module { return new(RoundRobinSelection) },
@@ -312,8 +312,8 @@ func (r *RoundRobinSelection) Select(pool UpstreamPool, _ *http.Request, _ http.
if n == 0 {
return nil
}
for i := uint32(0); i < n; i++ {
robin := atomic.AddUint32(&r.robin, 1)
for range n {
robin := r.robin.Add(1)
host := pool[robin%n]
if host.Available() {
return host
+18 -4
View File
@@ -204,7 +204,12 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
defer deleteFrontConn()
defer deleteBackConn()
spc := switchProtocolCopier{user: conn, backend: backConn, wg: wg}
spc := switchProtocolCopier{
user: conn,
backend: backConn,
wg: wg,
bufferSize: h.StreamBufferSize,
}
// setup the timeout if requested
var timeoutc <-chan time.Time
@@ -536,7 +541,7 @@ func maskBytes(key [4]byte, pos int, b []byte) int {
// Mask one word at a time.
n := (len(b) / wordSize) * wordSize
for i := 0; i < n; i += wordSize {
*(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(i))) ^= kw
*(*uintptr)(unsafe.Add(unsafe.Pointer(&b[0]), i)) ^= kw
}
// Mask one byte at a time for remaining bytes.
@@ -636,20 +641,29 @@ func (m *maxLatencyWriter) stop() {
type switchProtocolCopier struct {
user, backend io.ReadWriteCloser
wg *sync.WaitGroup
bufferSize int
}
func (c switchProtocolCopier) copyFromBackend(errc chan<- error) {
_, err := io.Copy(c.user, c.backend)
_, err := io.CopyBuffer(c.user, c.backend, c.buffer())
errc <- err
c.wg.Done()
}
func (c switchProtocolCopier) copyToBackend(errc chan<- error) {
_, err := io.Copy(c.backend, c.user)
_, err := io.CopyBuffer(c.backend, c.user, c.buffer())
errc <- err
c.wg.Done()
}
func (c switchProtocolCopier) buffer() []byte {
size := c.bufferSize
if size <= 0 {
size = defaultBufferSize
}
return make([]byte, size)
}
var streamingBufPool = sync.Pool{
New: func() any {
// The Pool's New function should generally only return pointer
@@ -2,8 +2,10 @@ package reverseproxy
import (
"bytes"
"io"
"net/http/httptest"
"strings"
"sync"
"testing"
"github.com/caddyserver/caddy/v2"
@@ -34,3 +36,47 @@ func TestHandlerCopyResponse(t *testing.T) {
}
}
}
func TestSwitchProtocolCopierBufferSize(t *testing.T) {
var wg sync.WaitGroup
var errc = make(chan error, 1)
var dst bytes.Buffer
copier := switchProtocolCopier{
user: nopReadWriteCloser{Reader: strings.NewReader("hello")},
backend: nopReadWriteCloser{Writer: &dst},
wg: &wg,
bufferSize: 7,
}
buf := copier.buffer()
if got := len(buf); got != 7 {
t.Fatalf("buffer len = %d, want 7", got)
}
wg.Add(1)
go copier.copyToBackend(errc)
wg.Wait()
if err := <-errc; err != nil {
t.Fatalf("copyToBackend() error = %v", err)
}
if got := dst.String(); got != "hello" {
t.Fatalf("copied data = %q, want %q", got, "hello")
}
}
func TestSwitchProtocolCopierDefaultBufferSize(t *testing.T) {
copier := switchProtocolCopier{}
buf := copier.buffer()
if got := len(buf); got != defaultBufferSize {
t.Fatalf("buffer len = %d, want %d", got, defaultBufferSize)
}
}
type nopReadWriteCloser struct {
io.Reader
io.Writer
}
func (nopReadWriteCloser) Close() error { return nil }
+9 -1
View File
@@ -247,6 +247,7 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
} else {
r.URL.Path = path
}
r.URL.RawPath = "" // force recomputing when EscapedPath() is called
}
if qsStart >= 0 {
r.URL.RawQuery = newQuery
@@ -528,7 +529,14 @@ func (q *queryOps) do(r *http.Request, repl *caddy.Replacer) {
if key == "" || val == "" {
continue
}
query[val] = query[key]
if key == val {
continue
}
originalValues, ok := query[key]
if !ok {
continue
}
query[val] = originalValues
delete(query, key)
}
+55
View File
@@ -16,6 +16,7 @@ package rewrite
import (
"net/http"
"reflect"
"regexp"
"testing"
@@ -224,6 +225,11 @@ func TestRewrite(t *testing.T) {
input: newRequest(t, "GET", "/foo#fragFirst?c=d"),
expect: newRequest(t, "GET", "/bar#fragFirst?c=d"),
},
{
rule: Rewrite{URI: "/api/admin/panel"},
input: newRequest(t, "GET", "/api/admin%2Fpanel"),
expect: newRequest(t, "GET", "/api/admin/panel"),
},
{
rule: Rewrite{StripPathPrefix: "/prefix"},
@@ -392,6 +398,55 @@ func TestRewrite(t *testing.T) {
}
}
func TestQueryOpsRenameNoOpCases(t *testing.T) {
repl := caddy.NewReplacer()
for i, tc := range []struct {
input *http.Request
expect map[string][]string
ops *queryOps
}{
{
ops: &queryOps{
Rename: []queryOpsArguments{{Key: "ID", Val: "id"}},
},
input: newRequest(t, "GET", "/?page=test&id=5&test=100"),
expect: map[string][]string{"id": {"5"}, "page": {"test"}, "test": {"100"}},
},
{
ops: &queryOps{
Rename: []queryOpsArguments{{Key: "id", Val: "id"}},
},
input: newRequest(t, "GET", "/?page=test&id=5&test=100"),
expect: map[string][]string{"id": {"5"}, "page": {"test"}, "test": {"100"}},
},
{
ops: &queryOps{
Rename: []queryOpsArguments{{Key: "ID", Val: "id"}},
},
input: newRequest(t, "GET", "/?page=test&ID=5&test=100"),
expect: map[string][]string{"id": {"5"}, "page": {"test"}, "test": {"100"}},
},
{
ops: &queryOps{
Rename: []queryOpsArguments{{Key: "ID", Val: "id"}},
},
input: newRequest(t, "GET", "/?page=test&ID=5&id=7&test=100"),
expect: map[string][]string{"id": {"5"}, "page": {"test"}, "test": {"100"}},
},
} {
repl.Set("http.request.uri", tc.input.RequestURI)
repl.Set("http.request.uri.path", tc.input.URL.Path)
repl.Set("http.request.uri.query", tc.input.URL.RawQuery)
tc.ops.do(tc.input, repl)
if actual := tc.input.URL.Query(); !reflect.DeepEqual(tc.expect, map[string][]string(actual)) {
t.Errorf("Test %d: Expected query=%v but got %v", i, tc.expect, actual)
}
}
}
func newRequest(t *testing.T, method, uri string) *http.Request {
req, err := http.NewRequest(method, uri, nil)
if err != nil {
+26 -11
View File
@@ -97,7 +97,10 @@ type Route struct {
MatcherSets MatcherSets `json:"-"`
Handlers []MiddlewareHandler `json:"-"`
middleware []Middleware
middleware []Middleware
metrics *Metrics
metricsCtx caddy.Context
handlerName string
}
// Empty returns true if the route has all zero/default values.
@@ -162,12 +165,20 @@ func (r *Route) ProvisionHandlers(ctx caddy.Context, metrics *Metrics) error {
r.Handlers = append(r.Handlers, handler.(MiddlewareHandler))
}
// Store metrics info for route-level instrumentation (applied once
// per route in wrapRoute, instead of per-handler which was redundant).
r.metrics = metrics
r.metricsCtx = ctx
if len(r.Handlers) > 0 {
r.handlerName = caddy.GetModuleName(r.Handlers[0])
}
// Make ProvisionHandlers idempotent by clearing the middleware field
r.middleware = []Middleware{}
// pre-compile the middleware handler chain
for _, midhandler := range r.Handlers {
r.middleware = append(r.middleware, wrapMiddleware(ctx, midhandler, metrics))
r.middleware = append(r.middleware, wrapMiddleware(ctx, midhandler))
}
return nil
}
@@ -298,6 +309,16 @@ func wrapRoute(route Route) Middleware {
nextCopy = route.middleware[i](nextCopy)
}
// Apply metrics instrumentation once for the entire route,
// rather than wrapping each individual handler. This avoids
// redundant metrics collection that caused significant CPU
// overhead (see issue #4644).
if route.metrics != nil {
nextCopy = newMetricsInstrumentedRoute(
route.metricsCtx, route.handlerName, nextCopy, route.metrics,
)
}
return nextCopy.ServeHTTP(rw, req)
})
}
@@ -306,20 +327,14 @@ func wrapRoute(route Route) Middleware {
// wrapMiddleware wraps mh such that it can be correctly
// appended to a list of middleware in preparation for
// compiling into a handler chain.
func wrapMiddleware(ctx caddy.Context, mh MiddlewareHandler, metrics *Metrics) Middleware {
handlerToUse := mh
if metrics != nil {
// wrap the middleware with metrics instrumentation
handlerToUse = newMetricsInstrumentedHandler(ctx, caddy.GetModuleName(mh), mh, metrics)
}
func wrapMiddleware(ctx caddy.Context, mh MiddlewareHandler) Middleware {
return func(next Handler) Handler {
return HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
// EXPERIMENTAL: Trace each module that gets invoked
if server, ok := r.Context().Value(ServerCtxKey).(*Server); ok && server != nil {
server.logTrace(handlerToUse)
server.logTrace(mh)
}
return handlerToUse.ServeHTTP(w, r, next)
return mh.ServeHTTP(w, r, next)
})
}
}
+1 -1
View File
@@ -486,7 +486,7 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) error {
// code to any HTTP/1.1 request message that lacks a Host header field and to any
// request message that contains more than one Host header field line or a Host
// header field with an invalid field value."
if r.Host == "" {
if r.ProtoMajor == 1 && r.ProtoMinor == 1 && r.Host == "" {
return HandlerError{
Err: errors.New("rfc9112 forbids empty Host"),
StatusCode: http.StatusBadRequest,
+32 -20
View File
@@ -181,8 +181,9 @@ func (m VarsMatcher) MatchWithError(r *http.Request) (bool, error) {
vars := r.Context().Value(VarsCtxKey).(map[string]any)
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
var matcherValExpanded, varStr, v string
var varValue any
for key, vals := range m {
var varValue any
if strings.HasPrefix(key, "{") &&
strings.HasSuffix(key, "}") &&
strings.Count(key, "{") == 1 {
@@ -191,22 +192,27 @@ func (m VarsMatcher) MatchWithError(r *http.Request) (bool, error) {
varValue = vars[key]
}
switch vv := varValue.(type) {
case string:
varStr = vv
case fmt.Stringer:
varStr = vv.String()
case error:
varStr = vv.Error()
case nil:
varStr = ""
default:
varStr = fmt.Sprintf("%v", vv)
}
// Don't expand placeholders in values from literal variable names
// (e.g. map outputs) or other placeholders. These values are
// already final and must not be re-expanded, as that would allow
// user input like {env.SECRET} to be evaluated.
// see if any of the values given in the matcher match the actual value
for _, v := range vals {
matcherValExpanded := repl.ReplaceAll(v, "")
var varStr string
switch vv := varValue.(type) {
case string:
varStr = vv
case fmt.Stringer:
varStr = vv.String()
case error:
varStr = vv.Error()
case nil:
varStr = ""
default:
varStr = fmt.Sprintf("%v", vv)
}
for _, v = range vals {
matcherValExpanded = repl.ReplaceAll(v, "")
if varStr == matcherValExpanded {
return true, nil
}
@@ -310,8 +316,11 @@ func (m MatchVarsRE) Match(r *http.Request) bool {
func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) {
vars := r.Context().Value(VarsCtxKey).(map[string]any)
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
var match bool
var varStr string
var varValue any
for key, val := range m {
var varValue any
if strings.HasPrefix(key, "{") &&
strings.HasSuffix(key, "}") &&
strings.Count(key, "{") == 1 {
@@ -320,7 +329,6 @@ func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) {
varValue = vars[key]
}
var varStr string
switch vv := varValue.(type) {
case string:
varStr = vv
@@ -334,8 +342,12 @@ func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) {
varStr = fmt.Sprintf("%v", vv)
}
valExpanded := repl.ReplaceAll(varStr, "")
if match := val.Match(valExpanded, repl); match {
// Don't expand placeholders in values from literal variable names
// (e.g. map outputs) or other placeholders. These values are
// already final and must not be re-expanded, as that would allow
// user input like {env.SECRET} to be evaluated.
if match = val.Match(varStr, repl); match {
return match, nil
}
}
+159
View File
@@ -0,0 +1,159 @@
// 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 caddyhttp
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/caddyserver/caddy/v2"
)
func newVarsTestRequest(t *testing.T, target string, headers http.Header, vars map[string]any) (*http.Request, *caddy.Replacer) {
t.Helper()
if target == "" {
target = "https://example.com/test"
}
req := httptest.NewRequest(http.MethodGet, target, nil)
req.Header = headers
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
if vars == nil {
vars = make(map[string]any)
}
// Inject vars directly so these tests exercise matcher-side handling of
// already-resolved values, not VarsMiddleware placeholder expansion.
ctx = context.WithValue(ctx, VarsCtxKey, vars)
req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
return req, repl
}
func TestVarsMatcherDoesNotExpandResolvedValues(t *testing.T) {
t.Setenv("CADDY_VARS_TEST_SECRET", "topsecret")
for _, tc := range []struct {
name string
target string
match VarsMatcher
headers http.Header
vars map[string]any
expect bool
}{
{
name: "literal variable value containing placeholder syntax is not re-expanded",
match: VarsMatcher{"secret": []string{"topsecret"}},
vars: map[string]any{"secret": "{env.CADDY_VARS_TEST_SECRET}"},
expect: false,
},
{
name: "placeholder key value containing placeholder syntax is not re-expanded",
match: VarsMatcher{"{http.request.header.X-Input}": []string{"topsecret"}},
headers: http.Header{"X-Input": []string{"{env.CADDY_VARS_TEST_SECRET}"}},
expect: false,
},
{
name: "query placeholder value containing placeholder syntax is not re-expanded",
target: "https://example.com/test?foo=%7Benv.CADDY_VARS_TEST_SECRET%7D",
match: VarsMatcher{"{http.request.uri.query.foo}": []string{"topsecret"}},
expect: false,
},
{
name: "matcher values still expand placeholders",
match: VarsMatcher{"secret": []string{"{env.CADDY_VARS_TEST_SECRET}"}},
vars: map[string]any{"secret": "topsecret"},
expect: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
req, _ := newVarsTestRequest(t, tc.target, tc.headers, tc.vars)
actual, err := tc.match.MatchWithError(req)
if err != nil {
t.Fatalf("MatchWithError() error = %v", err)
}
if actual != tc.expect {
t.Fatalf("MatchWithError() = %t, want %t", actual, tc.expect)
}
})
}
}
func TestMatchVarsREDoesNotExpandResolvedValues(t *testing.T) {
t.Setenv("CADDY_VARS_TEST_SECRET", "topsecret")
for _, tc := range []struct {
name string
target string
match MatchVarsRE
headers http.Header
vars map[string]any
expect bool
}{
{
name: "literal variable value containing placeholder syntax is not re-expanded",
match: MatchVarsRE{"secret": &MatchRegexp{Pattern: "^topsecret$"}},
vars: map[string]any{"secret": "{env.CADDY_VARS_TEST_SECRET}"},
expect: false,
},
{
name: "placeholder key value containing placeholder syntax is not re-expanded",
match: MatchVarsRE{"{http.request.header.X-Input}": &MatchRegexp{Pattern: "^topsecret$"}},
headers: http.Header{"X-Input": []string{"{env.CADDY_VARS_TEST_SECRET}"}},
expect: false,
},
{
name: "query placeholder value containing placeholder syntax is not re-expanded",
target: "https://example.com/test?foo=%7Benv.CADDY_VARS_TEST_SECRET%7D",
match: MatchVarsRE{"{http.request.uri.query.foo}": &MatchRegexp{Pattern: "^topsecret$"}},
expect: false,
},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
err := tc.match.Provision(caddy.Context{})
if err != nil {
t.Fatalf("Provision() error = %v", err)
}
err = tc.match.Validate()
if err != nil {
t.Fatalf("Validate() error = %v", err)
}
req, _ := newVarsTestRequest(t, tc.target, tc.headers, tc.vars)
actual, err := tc.match.MatchWithError(req)
if err != nil {
t.Fatalf("MatchWithError() error = %v", err)
}
if actual != tc.expect {
t.Fatalf("MatchWithError() = %t, want %t", actual, tc.expect)
}
})
}
}
+14 -1
View File
@@ -40,6 +40,7 @@ import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddypki"
"github.com/caddyserver/caddy/v2/modules/caddytls"
)
func init() {
@@ -304,7 +305,19 @@ func (ash Handler) openDatabase() (*db.AuthDB, error) {
// makeClient creates an ACME client which will use a custom
// resolver instead of net.DefaultResolver.
func (ash Handler) makeClient() (acme.Client, error) {
for _, v := range ash.Resolvers {
// If no local resolvers are configured, check for global resolvers from TLS app
resolversToUse := ash.Resolvers
if len(resolversToUse) == 0 {
tlsAppIface, err := ash.ctx.App("tls")
if err == nil {
tlsApp := tlsAppIface.(*caddytls.TLS)
if len(tlsApp.Resolvers) > 0 {
resolversToUse = tlsApp.Resolvers
}
}
}
for _, v := range resolversToUse {
addr, err := caddy.ParseNetworkAddressWithDefaults(v, "udp", 53)
if err != nil {
return nil, err
+9
View File
@@ -149,6 +149,15 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error {
iss.AccountKey = accountKey
}
// expand DNS override domain, if non-empty
if iss.Challenges != nil && iss.Challenges.DNS != nil && iss.Challenges.DNS.OverrideDomain != "" {
overrideDomain, err := repl.ReplaceOrErr(iss.Challenges.DNS.OverrideDomain, true, true)
if err != nil {
return fmt.Errorf("expanding DNS override domain '%s': %v", iss.Challenges.DNS.OverrideDomain, err)
}
iss.Challenges.DNS.OverrideDomain = overrideDomain
}
// DNS challenge provider, if not already established
if iss.Challenges != nil && iss.Challenges.DNS != nil && iss.Challenges.DNS.solver == nil {
var prov certmagic.DNSProvider
+80 -7
View File
@@ -235,7 +235,7 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
}
issuers := ap.Issuers
if len(issuers) == 0 {
if len(issuers) == 0 && !ap.implicitTailscaleManagersOnly() {
var err error
issuers, err = DefaultIssuersProvisioned(tlsApp.ctx)
if err != nil {
@@ -243,22 +243,49 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
}
}
// build certmagic.Config and attach it to the policy
storage := ap.storage
if storage == nil {
storage = tlsApp.ctx.Storage()
}
cfg, err := ap.makeCertMagicConfig(tlsApp, issuers, storage)
if err != nil {
return err
}
certCacheMu.RLock()
ap.magic = certmagic.New(certCache, cfg)
certCacheMu.RUnlock()
// give issuers a chance to see the config pointer
for _, issuer := range ap.magic.Issuers {
if annoying, ok := issuer.(ConfigSetter); ok {
annoying.SetConfig(ap.magic)
}
}
return nil
}
// makeCertMagicConfig constructs a certmagic.Config for this policy using the
// provided issuers and storage. It encapsulates common logic shared between
// Provision and RebuildCertMagic so we don't duplicate code.
func (ap *AutomationPolicy) makeCertMagicConfig(tlsApp *TLS, issuers []certmagic.Issuer, storage certmagic.Storage) (certmagic.Config, error) {
// key source
keyType := ap.KeyType
if keyType != "" {
var err error
keyType, err = caddy.NewReplacer().ReplaceOrErr(ap.KeyType, true, true)
if err != nil {
return fmt.Errorf("invalid key type %s: %s", ap.KeyType, err)
return certmagic.Config{}, fmt.Errorf("invalid key type %s: %s", ap.KeyType, err)
}
if _, ok := supportedCertKeyTypes[keyType]; !ok {
return fmt.Errorf("unrecognized key type: %s", keyType)
return certmagic.Config{}, fmt.Errorf("unrecognized key type: %s", keyType)
}
}
keySource := certmagic.StandardKeyGenerator{
KeyType: supportedCertKeyTypes[keyType],
}
storage := ap.storage
if storage == nil {
storage = tlsApp.ctx.Storage()
}
@@ -277,7 +304,7 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
if noProtections {
if !ap.hadExplicitManagers {
// no managers, no explicitly-configured permission module, this is a config error
return fmt.Errorf("on-demand TLS cannot be enabled without a permission module to prevent abuse; please refer to documentation for details")
return certmagic.Config{}, fmt.Errorf("on-demand TLS cannot be enabled without a permission module to prevent abuse; please refer to documentation for details")
}
// allow on-demand to be enabled but only for the purpose of the Managers; issuance won't be allowed from Issuers
tlsApp.logger.Warn("on-demand TLS can only get certificates from the configured external manager(s) because no ask endpoint / permission module is specified")
@@ -334,7 +361,7 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
}
}
template := certmagic.Config{
cfg := certmagic.Config{
MustStaple: ap.MustStaple,
RenewalWindowRatio: ap.RenewalWindowRatio,
KeySource: keySource,
@@ -349,8 +376,31 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
Issuers: issuers,
Logger: tlsApp.logger,
}
return cfg, nil
}
// IsProvisioned reports whether the automation policy has been
// provisioned. A provisioned policy has an initialized CertMagic
// instance (i.e. ap.magic != nil).
func (ap *AutomationPolicy) IsProvisioned() bool { return ap.magic != nil }
// RebuildCertMagic rebuilds the policy's CertMagic configuration from the
// policy's already-populated fields (Issuers, Managers, storage, etc.) and
// replaces the internal CertMagic instance. This is a lightweight
// alternative to calling Provision because it does not re-provision
// modules or re-run module Provision; instead, it constructs a new
// certmagic.Config and calls SetConfig on issuers so they receive updated
// templates (for example, alternate HTTP/TLS ports supplied by the HTTP
// app). RebuildCertMagic should only be called when the policy's required
// fields are already populated.
func (ap *AutomationPolicy) RebuildCertMagic(tlsApp *TLS) error {
cfg, err := ap.makeCertMagicConfig(tlsApp, ap.Issuers, ap.storage)
if err != nil {
return err
}
certCacheMu.RLock()
ap.magic = certmagic.New(certCache, template)
ap.magic = certmagic.New(certCache, cfg)
certCacheMu.RUnlock()
// sometimes issuers may need the parent certmagic.Config in
@@ -379,6 +429,29 @@ func (ap *AutomationPolicy) AllInternalSubjects() bool {
})
}
// implicitTailscaleManagersOnly returns true if this policy is configured to
// serve only Tailscale names from the Tailscale manager at handshake-time.
func (ap *AutomationPolicy) implicitTailscaleManagersOnly() bool {
if len(ap.subjects) == 0 {
return false
}
for _, subject := range ap.subjects {
if !strings.HasSuffix(strings.ToLower(subject), tailscaleDomainAliasEnding) {
return false
}
}
for _, manager := range ap.Managers {
switch manager.(type) {
case Tailscale, *Tailscale:
return true
}
}
return false
}
func (ap *AutomationPolicy) onlyInternalIssuer() bool {
if len(ap.Issuers) != 1 {
return false
+37
View File
@@ -0,0 +1,37 @@
package caddytls
import (
"testing"
"github.com/caddyserver/certmagic"
"go.uber.org/zap"
)
func TestAutomationPolicyMakeCertMagicConfigImplicitTailscaleManagersOnly(t *testing.T) {
ap := AutomationPolicy{
Managers: []certmagic.Manager{Tailscale{}},
subjects: []string{"test-node.example.ts.net"},
}
cfg, err := ap.makeCertMagicConfig(&TLS{
logger: zap.NewNop(),
}, nil, &certmagic.FileStorage{Path: t.TempDir()})
if err != nil {
t.Fatalf("making certmagic config: %v", err)
}
if cfg.OnDemand == nil {
t.Fatal("expected on-demand config to be set")
}
if len(cfg.Issuers) != 0 {
t.Fatalf("expected no issuers for tailscale-managed ts.net policy, got %d", len(cfg.Issuers))
}
}
func TestAutomationPolicyImplicitTailscaleManagersOnlyCatchAll(t *testing.T) {
ap := AutomationPolicy{
Managers: []certmagic.Manager{Tailscale{}},
}
if ap.implicitTailscaleManagersOnly() {
t.Fatal("expected catch-all manager policy to remain outside tailscale-only special case")
}
}
+307 -15
View File
@@ -4,6 +4,7 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
@@ -27,6 +28,8 @@ func init() {
caddy.RegisterModule(PKIIntermediateCAPool{})
caddy.RegisterModule(StoragePool{})
caddy.RegisterModule(HTTPCertPool{})
caddy.RegisterModule(SystemCAPool{})
caddy.RegisterModule(CombinedCAPool{})
}
// The interface to be implemented by all guest modules part of
@@ -35,6 +38,12 @@ type CA interface {
CertPool() *x509.CertPool
}
// CertificateProvider is an optional interface that CA pool sources
// can implement to expose their underlying certificates for combining.
type CertificateProvider interface {
Certificates() []*x509.Certificate
}
// InlineCAPool is a certificate authority pool provider coming from
// a DER-encoded certificates in the config
type InlineCAPool struct {
@@ -44,7 +53,8 @@ type InlineCAPool struct {
// these CAs will be rejected.
TrustedCACerts []string `json:"trusted_ca_certs,omitempty"`
pool *x509.CertPool
pool *x509.CertPool
certs []*x509.Certificate
}
// CaddyModule implements caddy.Module.
@@ -60,14 +70,17 @@ func (icp InlineCAPool) CaddyModule() caddy.ModuleInfo {
// Provision implements caddy.Provisioner.
func (icp *InlineCAPool) Provision(ctx caddy.Context) error {
caPool := x509.NewCertPool()
var certs []*x509.Certificate
for i, clientCAString := range icp.TrustedCACerts {
clientCA, err := decodeBase64DERCert(clientCAString)
if err != nil {
return fmt.Errorf("parsing certificate at index %d: %v", i, err)
}
caPool.AddCert(clientCA)
certs = append(certs, clientCA)
}
icp.pool = caPool
icp.certs = certs
return nil
}
@@ -103,6 +116,11 @@ func (icp InlineCAPool) CertPool() *x509.CertPool {
return icp.pool
}
// Certificates implements CertificateProvider.
func (icp InlineCAPool) Certificates() []*x509.Certificate {
return icp.certs
}
// FileCAPool generates trusted root certificates pool from the designated DER and PEM file
type FileCAPool struct {
// TrustedCACertPEMFiles is a list of PEM file names
@@ -111,7 +129,8 @@ type FileCAPool struct {
// these CA certificates will be rejected.
TrustedCACertPEMFiles []string `json:"pem_files,omitempty"`
pool *x509.CertPool
pool *x509.CertPool
certs []*x509.Certificate
}
// CaddyModule implements caddy.Module.
@@ -127,14 +146,32 @@ func (FileCAPool) CaddyModule() caddy.ModuleInfo {
// Loads and decodes the DER and pem files to generate the certificate pool
func (f *FileCAPool) Provision(ctx caddy.Context) error {
caPool := x509.NewCertPool()
var certs []*x509.Certificate
for _, pemFile := range f.TrustedCACertPEMFiles {
pemContents, err := os.ReadFile(pemFile)
if err != nil {
return fmt.Errorf("reading %s: %v", pemFile, err)
}
caPool.AppendCertsFromPEM(pemContents)
// Parse PEM to extract certificates
for len(pemContents) > 0 {
var block *pem.Block
block, pemContents = pem.Decode(pemContents)
if block == nil {
break
}
if block.Type != "CERTIFICATE" {
continue
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return fmt.Errorf("parsing certificate in %s: %v", pemFile, err)
}
caPool.AddCert(cert)
certs = append(certs, cert)
}
}
f.pool = caPool
f.certs = certs
return nil
}
@@ -166,13 +203,19 @@ func (f FileCAPool) CertPool() *x509.CertPool {
return f.pool
}
// Certificates implements CertificateProvider.
func (f FileCAPool) Certificates() []*x509.Certificate {
return f.certs
}
// PKIRootCAPool extracts the trusted root certificates from Caddy's native 'pki' app
type PKIRootCAPool struct {
// List of the Authority names that are configured in the `pki` app whose root certificates are trusted
Authority []string `json:"authority,omitempty"`
ca []*caddypki.CA
pool *x509.CertPool
ca []*caddypki.CA
pool *x509.CertPool
certs []*x509.Certificate
}
// CaddyModule implements caddy.Module.
@@ -201,10 +244,17 @@ func (p *PKIRootCAPool) Provision(ctx caddy.Context) error {
}
caPool := x509.NewCertPool()
var certs []*x509.Certificate
for _, ca := range p.ca {
caPool.AddCert(ca.RootCertificate())
rootCert := ca.RootCertificate()
if rootCert == nil {
return fmt.Errorf("CA %s has no root certificate", ca.ID)
}
caPool.AddCert(rootCert)
certs = append(certs, rootCert)
}
p.pool = caPool
p.certs = certs
return nil
}
@@ -238,13 +288,19 @@ func (p PKIRootCAPool) CertPool() *x509.CertPool {
return p.pool
}
// Certificates implements CertificateProvider.
func (p PKIRootCAPool) Certificates() []*x509.Certificate {
return p.certs
}
// PKIIntermediateCAPool extracts the trusted intermediate certificates from Caddy's native 'pki' app
type PKIIntermediateCAPool struct {
// List of the Authority names that are configured in the `pki` app whose intermediate certificates are trusted
Authority []string `json:"authority,omitempty"`
ca []*caddypki.CA
pool *x509.CertPool
ca []*caddypki.CA
pool *x509.CertPool
certs []*x509.Certificate
}
// CaddyModule implements caddy.Module.
@@ -273,12 +329,18 @@ func (p *PKIIntermediateCAPool) Provision(ctx caddy.Context) error {
}
caPool := x509.NewCertPool()
var certs []*x509.Certificate
for _, ca := range p.ca {
for _, c := range ca.IntermediateCertificateChain() {
if c == nil {
return fmt.Errorf("CA %s has a nil certificate in its intermediate chain", ca.ID)
}
caPool.AddCert(c)
certs = append(certs, c)
}
}
p.pool = caPool
p.certs = certs
return nil
}
@@ -311,6 +373,11 @@ func (p PKIIntermediateCAPool) CertPool() *x509.CertPool {
return p.pool
}
// Certificates implements CertificateProvider.
func (p PKIIntermediateCAPool) Certificates() []*x509.Certificate {
return p.certs
}
// StoragePool extracts the trusted certificates root from Caddy storage
type StoragePool struct {
// The storage module where the trusted root certificates are stored. Absent
@@ -322,6 +389,7 @@ type StoragePool struct {
storage certmagic.Storage
pool *x509.CertPool
certs []*x509.Certificate
}
// CaddyModule implements caddy.Module.
@@ -354,16 +422,33 @@ func (ca *StoragePool) Provision(ctx caddy.Context) error {
return fmt.Errorf("no PEM keys specified")
}
caPool := x509.NewCertPool()
var certs []*x509.Certificate
for _, caID := range ca.PEMKeys {
bs, err := ca.storage.Load(ctx, caID)
if err != nil {
return fmt.Errorf("error loading cert '%s' from storage: %s", caID, err)
}
if !caPool.AppendCertsFromPEM(bs) {
return fmt.Errorf("failed to add certificate '%s' to pool", caID)
// Parse PEM to extract certificates
pemData := bs
for len(pemData) > 0 {
var block *pem.Block
block, pemData = pem.Decode(pemData)
if block == nil {
break
}
if block.Type != "CERTIFICATE" {
continue
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return fmt.Errorf("parsing certificate '%s': %v", caID, err)
}
caPool.AddCert(cert)
certs = append(certs, cert)
}
}
ca.pool = caPool
ca.certs = certs
return nil
}
@@ -413,9 +498,13 @@ func (p StoragePool) CertPool() *x509.CertPool {
return p.pool
}
// Certificates implements CertificateProvider.
func (p StoragePool) Certificates() []*x509.Certificate {
return p.certs
}
// TLSConfig holds configuration related to the TLS configuration for the
// transport/client.
// copied from with minor modifications: modules/caddyhttp/reverseproxy/httptransport.go
type TLSConfig struct {
// Provides the guest module that provides the trusted certificate authority (CA) certificates
CARaw json.RawMessage `json:"ca,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"`
@@ -500,7 +589,6 @@ func (t *TLSConfig) unmarshalCaddyfile(d *caddyfile.Dispenser) error {
// MakeTLSClientConfig returns a tls.Config usable by a client to a backend.
// If there is no custom TLS configuration, a nil config may be returned.
// copied from with minor modifications: modules/caddyhttp/reverseproxy/httptransport.go
func (t *TLSConfig) makeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) {
repl, ok := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
if !ok || repl == nil {
@@ -554,7 +642,8 @@ type HTTPCertPool struct {
// Customize the TLS connection knobs to used during the HTTP call
TLS *TLSConfig `json:"tls,omitempty"`
pool *x509.CertPool
pool *x509.CertPool
certs []*x509.Certificate
}
// CaddyModule implements caddy.Module.
@@ -570,6 +659,7 @@ func (HTTPCertPool) CaddyModule() caddy.ModuleInfo {
// Provision implements caddy.Provisioner.
func (hcp *HTTPCertPool) Provision(ctx caddy.Context) error {
caPool := x509.NewCertPool()
var certs []*x509.Certificate
customTransport := http.DefaultTransport.(*http.Transport).Clone()
if hcp.TLS != nil {
@@ -597,11 +687,30 @@ func (hcp *HTTPCertPool) Provision(ctx caddy.Context) error {
if err != nil {
return err
}
if !caPool.AppendCertsFromPEM(pembs) {
return fmt.Errorf("failed to add certs from URL: %s", uri)
if res.StatusCode < 200 || res.StatusCode >= 300 {
return fmt.Errorf("HTTP %d fetching CA certificate bundle from %s", res.StatusCode, uri)
}
// Parse PEM to extract certificates
pemData := pembs
for len(pemData) > 0 {
var block *pem.Block
block, pemData = pem.Decode(pemData)
if block == nil {
break
}
if block.Type != "CERTIFICATE" {
continue
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return fmt.Errorf("parsing certificate from URL %s: %v", uri, err)
}
caPool.AddCert(cert)
certs = append(certs, cert)
}
}
hcp.pool = caPool
hcp.certs = certs
return nil
}
@@ -665,6 +774,179 @@ func (hcp HTTPCertPool) CertPool() *x509.CertPool {
return hcp.pool
}
// Certificates implements CertificateProvider.
func (hcp HTTPCertPool) Certificates() []*x509.Certificate {
return hcp.certs
}
// SystemCAPool obtains the trusted root certificates from the system's
// certificate pool using x509.SystemCertPool()
type SystemCAPool struct {
pool *x509.CertPool
}
// CaddyModule implements caddy.Module.
func (SystemCAPool) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "tls.ca_pool.source.system",
New: func() caddy.Module {
return new(SystemCAPool)
},
}
}
// Provision implements caddy.Provisioner.
func (scp *SystemCAPool) Provision(ctx caddy.Context) error {
pool, err := x509.SystemCertPool()
if err != nil {
return fmt.Errorf("failed to load system cert pool: %v", err)
}
scp.pool = pool
return nil
}
func (scp *SystemCAPool) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume module name
if d.CountRemainingArgs() > 0 {
return d.ArgErr()
}
if d.NextBlock(0) {
return d.Err("system trust pool does not support any configuration")
}
return nil
}
// CertPool implements CA.
func (scp SystemCAPool) CertPool() *x509.CertPool {
return scp.pool
}
// The `combined` pool type merges multiple pools. The `sources` pools must implement the
// `CertificateProvider` interface, which allows them to export their certificate set.
//
// Note: SystemCAPool does not implement CertificateProvider because
// x509.SystemCertPool() doesn't expose its certificates, so it cannot
// be used as a source in CombinedCAPool.
type CombinedCAPool struct {
// The CA pool sources to combine. Each source is a CA pool provider module.
SourcesRaw []json.RawMessage `json:"sources,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"`
sources []CA
pool *x509.CertPool
certs []*x509.Certificate
}
// CaddyModule implements caddy.Module.
func (CombinedCAPool) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "tls.ca_pool.source.combined",
New: func() caddy.Module {
return new(CombinedCAPool)
},
}
}
// Provision implements caddy.Provisioner.
func (ccp *CombinedCAPool) Provision(ctx caddy.Context) error {
if len(ccp.SourcesRaw) == 0 {
return fmt.Errorf("no sources specified for combined CA pool")
}
// Load all source modules
sources, err := ctx.LoadModule(ccp, "SourcesRaw")
if err != nil {
return fmt.Errorf("loading CA pool sources: %v", err)
}
caPool := x509.NewCertPool()
var allCerts []*x509.Certificate
for _, src := range sources.([]any) {
ca, ok := src.(CA)
if !ok {
return fmt.Errorf("source module is not a CA pool provider")
}
ccp.sources = append(ccp.sources, ca)
certProvider, ok := ca.(CertificateProvider)
if !ok {
return fmt.Errorf("source %T does not implement CertificateProvider (required for combining)", ca)
}
certs := certProvider.Certificates()
if certs == nil {
return fmt.Errorf("source %T returned nil certificates", ca)
}
for _, cert := range certs {
if cert == nil {
return fmt.Errorf("source %T returned a nil certificate", ca)
}
caPool.AddCert(cert)
allCerts = append(allCerts, cert)
}
}
ccp.pool = caPool
ccp.certs = allCerts
return nil
}
// Syntax:
//
// trust_pool combined {
// source <module_name> {
// <module_config>
// }
// }
//
// The 'source' directive can be specified multiple times. Sources that
// don't implement CertificateProvider (like 'system') cannot be combined.
func (ccp *CombinedCAPool) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume module name
if d.CountRemainingArgs() > 0 {
return d.ArgErr()
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "source":
if !d.NextArg() {
return d.ArgErr()
}
modStem := d.Val()
modID := "tls.ca_pool.source." + modStem
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return err
}
ca, ok := unm.(CA)
if !ok {
return d.Errf("module %s is not a CA pool provider", modID)
}
ccp.SourcesRaw = append(ccp.SourcesRaw, caddyconfig.JSONModuleObject(ca, "provider", modStem, nil))
default:
return d.Errf("unrecognized directive: %s", d.Val())
}
}
if len(ccp.SourcesRaw) == 0 {
return d.Err("no sources specified")
}
return nil
}
// CertPool implements CA.
func (ccp CombinedCAPool) CertPool() *x509.CertPool {
return ccp.pool
}
// Certificates implements CertificateProvider.
func (ccp CombinedCAPool) Certificates() []*x509.Certificate {
return ccp.certs
}
var (
_ caddy.Module = (*InlineCAPool)(nil)
_ caddy.Provisioner = (*InlineCAPool)(nil)
@@ -696,4 +978,14 @@ var (
_ caddy.Validator = (*HTTPCertPool)(nil)
_ CA = (*HTTPCertPool)(nil)
_ caddyfile.Unmarshaler = (*HTTPCertPool)(nil)
_ caddy.Module = (*SystemCAPool)(nil)
_ caddy.Provisioner = (*SystemCAPool)(nil)
_ CA = (*SystemCAPool)(nil)
_ caddyfile.Unmarshaler = (*SystemCAPool)(nil)
_ caddy.Module = (*CombinedCAPool)(nil)
_ caddy.Provisioner = (*CombinedCAPool)(nil)
_ CA = (*CombinedCAPool)(nil)
_ caddyfile.Unmarshaler = (*CombinedCAPool)(nil)
)

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