Compare commits

...

28 Commits

Author SHA1 Message Date
eveneast
a76d005a94
Use maps.Copy for simpler map handling (#7009)
Some checks failed
Tests / test (./cmd/caddy/caddy, ~1.24.1, ubuntu-latest, 0, 1.24, linux) (push) Failing after 1m19s
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.24.1, 1.24, aix) (push) Successful in 1m15s
Cross-Build / build (~1.24.1, 1.24, darwin) (push) Successful in 1m15s
Cross-Build / build (~1.24.1, 1.24, dragonfly) (push) Successful in 1m14s
Cross-Build / build (~1.24.1, 1.24, freebsd) (push) Successful in 1m14s
Cross-Build / build (~1.24.1, 1.24, illumos) (push) Successful in 1m14s
Cross-Build / build (~1.24.1, 1.24, linux) (push) Successful in 1m15s
Cross-Build / build (~1.24.1, 1.24, netbsd) (push) Successful in 1m14s
Cross-Build / build (~1.24.1, 1.24, openbsd) (push) Successful in 1m45s
Cross-Build / build (~1.24.1, 1.24, solaris) (push) Successful in 1m17s
Cross-Build / build (~1.24.1, 1.24, windows) (push) Successful in 1m14s
Lint / lint (ubuntu-latest, linux) (push) Successful in 1m48s
Lint / govulncheck (push) Successful in 1m16s
Tests / test (./cmd/caddy/caddy, ~1.24.1, macos-14, 0, 1.24, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.24.1, windows-latest, True, 1.24, 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: eveneast <qcqs@foxmail.com>
2025-05-13 15:16:47 -06:00
WeidiDeng
8524386737
caddyhttp: Compare paths w/o wildcard if prefixes differ (#7015)
* fix route sort by comparing paths without wildcard if they don't share the same prefix

* sort lexically if paths have the same length
2025-05-13 13:17:52 -06:00
Jimmy Lipham
94147caf31
fileserver: map invalid path errors to fs.ErrInvalid, and return 400 for any invalid path errors. (close #7008) (#7017)
Some checks failed
Tests / test (./cmd/caddy/caddy, ~1.24.1, ubuntu-latest, 0, 1.24, linux) (push) Failing after 1m18s
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.24.1, 1.24, aix) (push) Successful in 1m12s
Cross-Build / build (~1.24.1, 1.24, darwin) (push) Successful in 1m13s
Cross-Build / build (~1.24.1, 1.24, dragonfly) (push) Successful in 1m12s
Cross-Build / build (~1.24.1, 1.24, freebsd) (push) Successful in 1m12s
Cross-Build / build (~1.24.1, 1.24, illumos) (push) Successful in 1m15s
Cross-Build / build (~1.24.1, 1.24, linux) (push) Successful in 1m12s
Cross-Build / build (~1.24.1, 1.24, netbsd) (push) Successful in 1m12s
Cross-Build / build (~1.24.1, 1.24, openbsd) (push) Successful in 1m36s
Cross-Build / build (~1.24.1, 1.24, solaris) (push) Successful in 1m22s
Cross-Build / build (~1.24.1, 1.24, windows) (push) Successful in 1m15s
Lint / lint (ubuntu-latest, linux) (push) Successful in 1m46s
Lint / govulncheck (push) Successful in 1m15s
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy, ~1.24.1, macos-14, 0, 1.24, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.24.1, windows-latest, True, 1.24, windows) (push) Has been cancelled
2025-05-13 07:43:27 -06:00
WeidiDeng
716d72e475
intercept: implement Unwrap for interceptedResponseHandler (#7016)
Some checks failed
Tests / test (./cmd/caddy/caddy, ~1.24.1, ubuntu-latest, 0, 1.24, linux) (push) Failing after 1m20s
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Lint / lint (ubuntu-latest, linux) (push) Successful in 1m50s
Lint / govulncheck (push) Successful in 1m16s
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy, ~1.24.1, macos-14, 0, 1.24, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.24.1, windows-latest, True, 1.24, windows) (push) Has been cancelled
2025-05-12 12:15:34 -06:00
Mohammed Al Sahaf
44d078b670
acme_server: fix policy parsing in caddyfile (#7006)
Some checks failed
Tests / test (./cmd/caddy/caddy, ~1.24.1, ubuntu-latest, 0, 1.24, linux) (push) Failing after 1m55s
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.24.1, 1.24, aix) (push) Successful in 1m12s
Cross-Build / build (~1.24.1, 1.24, darwin) (push) Successful in 1m14s
Cross-Build / build (~1.24.1, 1.24, dragonfly) (push) Successful in 1m12s
Cross-Build / build (~1.24.1, 1.24, freebsd) (push) Successful in 1m14s
Cross-Build / build (~1.24.1, 1.24, illumos) (push) Successful in 1m12s
Cross-Build / build (~1.24.1, 1.24, linux) (push) Successful in 1m13s
Cross-Build / build (~1.24.1, 1.24, netbsd) (push) Successful in 1m15s
Cross-Build / build (~1.24.1, 1.24, openbsd) (push) Successful in 1m19s
Cross-Build / build (~1.24.1, 1.24, solaris) (push) Successful in 1m13s
Cross-Build / build (~1.24.1, 1.24, windows) (push) Successful in 1m15s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m2s
Lint / govulncheck (push) Successful in 1m32s
Tests / test (./cmd/caddy/caddy, ~1.24.1, macos-14, 0, 1.24, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.24.1, windows-latest, True, 1.24, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2025-05-08 11:54:07 -06:00
Jimmy Lipham
051e73aefc
core: Replace admin server later in provisionContext (#7004) 2025-05-08 11:52:55 -06:00
Mohammed Al Sahaf
9f7148392a
log: default logger should respect {in,ex}clude (#6995)
Some checks failed
Tests / test (./cmd/caddy/caddy, ~1.24.1, ubuntu-latest, 0, 1.24, 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.24.1, 1.24, aix) (push) Successful in 1m36s
Cross-Build / build (~1.24.1, 1.24, darwin) (push) Successful in 1m16s
Cross-Build / build (~1.24.1, 1.24, dragonfly) (push) Successful in 1m14s
Cross-Build / build (~1.24.1, 1.24, freebsd) (push) Successful in 1m14s
Cross-Build / build (~1.24.1, 1.24, illumos) (push) Successful in 1m13s
Cross-Build / build (~1.24.1, 1.24, linux) (push) Successful in 1m14s
Cross-Build / build (~1.24.1, 1.24, netbsd) (push) Successful in 1m15s
Cross-Build / build (~1.24.1, 1.24, openbsd) (push) Successful in 1m14s
Cross-Build / build (~1.24.1, 1.24, solaris) (push) Successful in 1m15s
Cross-Build / build (~1.24.1, 1.24, windows) (push) Successful in 1m16s
Lint / lint (ubuntu-latest, linux) (push) Successful in 1m49s
Lint / govulncheck (push) Successful in 1m16s
Tests / test (./cmd/caddy/caddy, ~1.24.1, macos-14, 0, 1.24, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.24.1, windows-latest, True, 1.24, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
* log: default logger should respect `{in,ex}clude`

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

* add tests

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

---------

Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2025-05-06 22:06:09 +00:00
Jimmy Lipham
320c57291d
admin: Make sure that any admin routers are provisioned when local/re… (#6997)
* admin: Make sure that any admin routers are provisioned when local/remote admin servers are replaced at runtime.

* admin: check for provisioning errors during admin server replacements
2025-05-06 15:28:38 -06:00
WeidiDeng
aa3d20be3e
reverseproxy: Use DialTLSContext if ServerName has placeholder (#6955)
Some checks failed
Tests / test (./cmd/caddy/caddy, ~1.24.1, ubuntu-latest, 0, 1.24, linux) (push) Failing after 1m56s
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.24.1, 1.24, aix) (push) Successful in 1m12s
Cross-Build / build (~1.24.1, 1.24, darwin) (push) Successful in 1m11s
Cross-Build / build (~1.24.1, 1.24, dragonfly) (push) Successful in 1m11s
Cross-Build / build (~1.24.1, 1.24, freebsd) (push) Successful in 1m11s
Cross-Build / build (~1.24.1, 1.24, illumos) (push) Successful in 1m11s
Cross-Build / build (~1.24.1, 1.24, linux) (push) Successful in 1m17s
Cross-Build / build (~1.24.1, 1.24, netbsd) (push) Successful in 1m12s
Cross-Build / build (~1.24.1, 1.24, openbsd) (push) Successful in 1m12s
Cross-Build / build (~1.24.1, 1.24, solaris) (push) Successful in 1m14s
Cross-Build / build (~1.24.1, 1.24, windows) (push) Successful in 1m13s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m9s
Lint / govulncheck (push) Successful in 1m30s
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy, ~1.24.1, macos-14, 0, 1.24, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.24.1, windows-latest, True, 1.24, windows) (push) Has been cancelled
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2025-04-28 09:14:09 -06:00
Steffen Busch
54d03ced48
fileserver: Add support for .avif image format (#6988) 2025-04-28 08:32:59 -06:00
Indra Gunawan
89ed5f44de
fix: Remove nil arg from zapslog.NewHandler call (#6984) 2025-04-28 08:31:10 -06:00
Matthew Holt
105eee671c caddytls: Set local_ip, not remote_ip (#6952)
Follow-up on 35c8c2d92d26208642cea0d1549c77a00124e154 where I was a dum-dum
2025-04-21 18:32:51 -06:00
Mohammed Al Sahaf
737936c06b
reverseproxy: reference correct field name in LoadModule (#6978)
Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com>
2025-04-21 08:43:27 -06:00
Marten Seemann
a6d488a15b
go.mod: update quic-go to v0.51.0 (#6972) 2025-04-20 07:39:00 -06:00
Matthew Holt
fb22a26b1a
caddytls: Allow missing ECH meta file 2025-04-18 12:20:21 -06:00
Matt Holt
1bfa111552
caddytls: Prefer managed wildcard certs over individual subdomain certs (#6959)
* caddytls: Prefer managed wildcard certs over individual subdomain certs

* Repurpose force_automate as no_wildcard

* Fix a couple bugs

* Restore force_automate and use automate loader as wildcard override
2025-04-18 11:44:23 -06:00
Matthew Holt
35c8c2d92d
caddytls: Add remote_ip to HTTP cert manager (close #6952) 2025-04-17 16:43:06 -06:00
dependabot[bot]
0b2802faa4
build(deps): bump golang.org/x/net from 0.37.0 to 0.38.0 (#6960)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.37.0 to 0.38.0.
- [Commits](https://github.com/golang/net/compare/v0.37.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.38.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-16 23:34:35 +00:00
Steffen Busch
5be77d07ab
caddyauth: Set authentication provider error in placeholder (#6932)
Some checks failed
Tests / test (./cmd/caddy/caddy, ~1.24.1, ubuntu-latest, 0, 1.24, linux) (push) Failing after 14m36s
Cross-Build / build (~1.24.1, 1.24, aix) (push) Failing after 11m6s
Tests / goreleaser-check (push) Failing after 11m28s
Tests / test (s390x on IBM Z) (push) Failing after 14m29s
Cross-Build / build (~1.24.1, 1.24, linux) (push) Failing after 12m46s
Cross-Build / build (~1.24.1, 1.24, illumos) (push) Failing after 13m31s
Cross-Build / build (~1.24.1, 1.24, freebsd) (push) Failing after 14m3s
Cross-Build / build (~1.24.1, 1.24, dragonfly) (push) Failing after 15m15s
Cross-Build / build (~1.24.1, 1.24, darwin) (push) Failing after 16m19s
Cross-Build / build (~1.24.1, 1.24, windows) (push) Failing after 10m2s
Cross-Build / build (~1.24.1, 1.24, solaris) (push) Failing after 12m22s
Cross-Build / build (~1.24.1, 1.24, openbsd) (push) Failing after 13m50s
Cross-Build / build (~1.24.1, 1.24, netbsd) (push) Failing after 14m12s
Lint / lint (ubuntu-latest, linux) (push) Failing after 14m26s
Lint / govulncheck (push) Failing after 14m14s
Tests / test (./cmd/caddy/caddy, ~1.24.1, macos-14, 0, 1.24, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.24.1, windows-latest, True, 1.24, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
* caddyauth: Set authentication provider error in placeholder for handle_errors directive

* caddyauth: Simplify error placeholder setting for authentication provider
2025-04-15 22:32:08 +00:00
Matthew Holt
137711ae3e
go.mod: Upgrade acmez and certmagic 2025-04-15 15:08:12 -06:00
Matthew Holt
f297bc0a04
admin: Remove host checking for UDS (close #6832)
The consensus is that host enforcement on unix sockets is ineffective, frustrating, and confusing. (Unix sockets have their own OS-level permissions system.)
2025-04-15 14:20:22 -06:00
Jesper Brix Rosenkilde
6c38ae7381
reverseproxy: Add valid Upstream to DialInfo in active health checks (#6949)
Currently if we extract the DialInfo from a Request Context during an active health check, then the Upstream in the DialInfo is nil.

This PR attempts to set the Upstream to a sensible value, based on wether or not the Upstream has been overriden in the active health check's config.
2025-04-15 08:44:53 -06:00
cui fliter
def9db1f16
Fix the incorrect parameter order (#6951)
Some checks failed
Tests / test (./cmd/caddy/caddy, ~1.24.1, macos-14, 0, 1.24, mac) (push) Waiting to run
Lint / lint (macos-14, mac) (push) Waiting to run
Lint / lint (windows-latest, windows) (push) Waiting to run
Tests / test (./cmd/caddy/caddy, ~1.24.1, ubuntu-latest, 0, 1.24, linux) (push) Failing after 1m53s
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.24.1, 1.24, aix) (push) Successful in 1m21s
Cross-Build / build (~1.24.1, 1.24, darwin) (push) Successful in 1m16s
Cross-Build / build (~1.24.1, 1.24, dragonfly) (push) Successful in 1m14s
Cross-Build / build (~1.24.1, 1.24, freebsd) (push) Successful in 1m14s
Cross-Build / build (~1.24.1, 1.24, illumos) (push) Successful in 1m14s
Cross-Build / build (~1.24.1, 1.24, linux) (push) Successful in 1m12s
Cross-Build / build (~1.24.1, 1.24, netbsd) (push) Successful in 1m13s
Cross-Build / build (~1.24.1, 1.24, openbsd) (push) Successful in 1m12s
Cross-Build / build (~1.24.1, 1.24, solaris) (push) Successful in 1m12s
Cross-Build / build (~1.24.1, 1.24, windows) (push) Successful in 1m14s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m7s
Lint / govulncheck (push) Successful in 1m27s
Tests / test (./cmd/caddy/caddy.exe, ~1.24.1, windows-latest, True, 1.24, windows) (push) Has been cancelled
Signed-off-by: cuishuang <imcusg@gmail.com>
2025-04-12 21:19:32 -06:00
riyueguang
ce926b87ed
chore: fix comment (#6950)
Signed-off-by: riyueguang <rustruby@outlook.com>
2025-04-12 04:24:17 +00:00
Matthew Holt
b06a9496d1
caddyhttp: Document side effect of HTTP/3 early data (close #6936) 2025-04-08 13:59:02 -06:00
Matthew Holt
9becf61a9f
go.mod: Upgrade to libdns 1.0 beta APIs (requires upgraded DNS providers)
Some checks failed
Tests / test (./cmd/caddy/caddy, ~1.24.1, ubuntu-latest, 0, 1.24, linux) (push) Failing after 2m27s
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.24.1, 1.24, aix) (push) Successful in 1m23s
Cross-Build / build (~1.24.1, 1.24, darwin) (push) Successful in 1m20s
Cross-Build / build (~1.24.1, 1.24, dragonfly) (push) Successful in 1m21s
Cross-Build / build (~1.24.1, 1.24, freebsd) (push) Successful in 1m21s
Cross-Build / build (~1.24.1, 1.24, illumos) (push) Successful in 1m17s
Cross-Build / build (~1.24.1, 1.24, linux) (push) Successful in 1m23s
Cross-Build / build (~1.24.1, 1.24, netbsd) (push) Successful in 1m18s
Cross-Build / build (~1.24.1, 1.24, openbsd) (push) Successful in 1m18s
Cross-Build / build (~1.24.1, 1.24, solaris) (push) Successful in 1m18s
Cross-Build / build (~1.24.1, 1.24, windows) (push) Successful in 1m19s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m26s
Lint / govulncheck (push) Successful in 1m38s
Tests / test (./cmd/caddy/caddy, ~1.24.1, macos-14, 0, 1.24, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.24.1, windows-latest, True, 1.24, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
This is the only way we can properly, reliably support ECH.
2025-04-07 12:43:11 -06:00
Matt Holt
5a6b2f8d1d
events: Refactor; move Event into core, so core can emit events (#6930)
Some checks failed
Tests / test (./cmd/caddy/caddy, ~1.24.1, ubuntu-latest, 0, 1.24, linux) (push) Failing after 2m46s
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / goreleaser-check (push) Has been skipped
Cross-Build / build (~1.24.1, 1.24, aix) (push) Successful in 2m49s
Cross-Build / build (~1.24.1, 1.24, darwin) (push) Successful in 2m10s
Cross-Build / build (~1.24.1, 1.24, dragonfly) (push) Successful in 1m40s
Cross-Build / build (~1.24.1, 1.24, freebsd) (push) Successful in 1m50s
Cross-Build / build (~1.24.1, 1.24, illumos) (push) Successful in 1m49s
Cross-Build / build (~1.24.1, 1.24, linux) (push) Successful in 2m4s
Cross-Build / build (~1.24.1, 1.24, netbsd) (push) Successful in 2m7s
Cross-Build / build (~1.24.1, 1.24, openbsd) (push) Successful in 2m2s
Cross-Build / build (~1.24.1, 1.24, solaris) (push) Successful in 3m30s
Cross-Build / build (~1.24.1, 1.24, windows) (push) Successful in 2m56s
Lint / lint (ubuntu-latest, linux) (push) Failing after 1m17s
Lint / govulncheck (push) Successful in 2m18s
Tests / test (./cmd/caddy/caddy, ~1.24.1, macos-14, 0, 1.24, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.24.1, windows-latest, True, 1.24, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
* events: Refactor; move Event into core, so core can emit events

Requires some slight trickery to invert dependencies. We can't have the caddy package import the caddyevents package, because caddyevents imports caddy. Interface to the rescue!

Also add two new events, experimentally: started, and stopping. At the request of a sponsor.

Also rename "Filesystems" to "FileSystems" to match Go convention (unrelated to events, was just bugging me when I noticed it).

* Coupla bug fixes

* lol whoops
2025-03-29 08:15:43 -06:00
Matthew Holt
ea77a9ab67
caddytls: Temporarily treat "" and "@" as equivalent for DNS publication
Some checks failed
Tests / test (./cmd/caddy/caddy, ~1.24.1, ubuntu-latest, 0, 1.24, 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.24.1, 1.24, aix) (push) Successful in 1m33s
Cross-Build / build (~1.24.1, 1.24, darwin) (push) Successful in 1m29s
Cross-Build / build (~1.24.1, 1.24, dragonfly) (push) Successful in 1m29s
Cross-Build / build (~1.24.1, 1.24, freebsd) (push) Failing after 13m24s
Cross-Build / build (~1.24.1, 1.24, illumos) (push) Successful in 1m45s
Cross-Build / build (~1.24.1, 1.24, linux) (push) Successful in 1m20s
Cross-Build / build (~1.24.1, 1.24, netbsd) (push) Successful in 1m22s
Cross-Build / build (~1.24.1, 1.24, openbsd) (push) Successful in 1m19s
Cross-Build / build (~1.24.1, 1.24, solaris) (push) Successful in 1m21s
Cross-Build / build (~1.24.1, 1.24, windows) (push) Successful in 1m32s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m0s
Lint / govulncheck (push) Successful in 1m12s
Tests / test (./cmd/caddy/caddy, ~1.24.1, macos-14, 0, 1.24, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.24.1, windows-latest, True, 1.24, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
Fixes https://github.com/caddyserver/caddy/issues/6895#issuecomment-2750111096
2025-03-25 16:24:16 -06:00
42 changed files with 929 additions and 1126 deletions

View File

@ -221,7 +221,8 @@ func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Co
if remote { if remote {
muxWrap.remoteControl = admin.Remote muxWrap.remoteControl = admin.Remote
} else { } else {
muxWrap.enforceHost = !addr.isWildcardInterface() // see comment in allowedOrigins() as to why we disable the host check for unix/fd networks
muxWrap.enforceHost = !addr.isWildcardInterface() && !addr.IsUnixNetwork() && !addr.IsFdNetwork()
muxWrap.allowedOrigins = admin.allowedOrigins(addr) muxWrap.allowedOrigins = admin.allowedOrigins(addr)
muxWrap.enforceOrigin = admin.EnforceOrigin muxWrap.enforceOrigin = admin.EnforceOrigin
} }
@ -310,9 +311,6 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
for _, o := range admin.Origins { for _, o := range admin.Origins {
uniqueOrigins[o] = struct{}{} uniqueOrigins[o] = struct{}{}
} }
if admin.Origins == nil {
if addr.isLoopback() {
if addr.IsUnixNetwork() || addr.IsFdNetwork() {
// RFC 2616, Section 14.26: // RFC 2616, Section 14.26:
// "A client MUST include a Host header field in all HTTP/1.1 request // "A client MUST include a Host header field in all HTTP/1.1 request
// messages. If the requested URI does not include an Internet host // messages. If the requested URI does not include an Internet host
@ -330,27 +328,26 @@ func (admin AdminConfig) allowedOrigins(addr NetworkAddress) []*url.URL {
// bogus host; see https://superuser.com/a/925610.) If we disable Host/Origin // bogus host; see https://superuser.com/a/925610.) If we disable Host/Origin
// security checks, the infosec community assures me that it is secure to do // security checks, the infosec community assures me that it is secure to do
// so, because: // so, because:
//
// 1) Browsers do not allow access to unix sockets // 1) Browsers do not allow access to unix sockets
// 2) DNS is irrelevant to unix sockets // 2) DNS is irrelevant to unix sockets
// //
// I am not quite ready to trust either of those external factors, so instead // If either of those two statements ever fail to hold true, it is not the
// of disabling Host/Origin checks, we now allow specific Host values when // fault of Caddy.
// accessing the admin endpoint over unix sockets. I definitely don't trust //
// DNS (e.g. I don't trust 'localhost' to always resolve to the local host), // Thus, we do not fill out allowed origins and do not enforce Host
// and IP shouldn't even be used, but if it is for some reason, I think we can // requirements for unix sockets. Enforcing it leads to confusion and
// at least be reasonably assured that 127.0.0.1 and ::1 route to the local // frustration, when UDS have their own permissions from the OS.
// machine, meaning that a hypothetical browser origin would have to be on the // Enforcing host requirements here is effectively security theater,
// local machine as well. // and a false sense of security.
uniqueOrigins[""] = struct{}{} //
uniqueOrigins["127.0.0.1"] = struct{}{} // See also the discussion in #6832.
uniqueOrigins["::1"] = struct{}{} if admin.Origins == nil && !addr.IsUnixNetwork() && !addr.IsFdNetwork() {
} else { if addr.isLoopback() {
uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{} uniqueOrigins[net.JoinHostPort("localhost", addr.port())] = struct{}{}
uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{} uniqueOrigins[net.JoinHostPort("::1", addr.port())] = struct{}{}
uniqueOrigins[net.JoinHostPort("127.0.0.1", addr.port())] = struct{}{} uniqueOrigins[net.JoinHostPort("127.0.0.1", addr.port())] = struct{}{}
} } else {
}
if !addr.IsUnixNetwork() && !addr.IsFdNetwork() {
uniqueOrigins[addr.JoinHostPort(0)] = struct{}{} uniqueOrigins[addr.JoinHostPort(0)] = struct{}{}
} }
} }
@ -427,6 +424,13 @@ func replaceLocalAdminServer(cfg *Config, ctx Context) error {
handler := cfg.Admin.newAdminHandler(addr, false, ctx) handler := cfg.Admin.newAdminHandler(addr, false, ctx)
// run the provisioners for loaded modules to make sure local
// state is properly re-initialized in the new admin server
err = cfg.Admin.provisionAdminRouters(ctx)
if err != nil {
return err
}
ln, err := addr.Listen(context.TODO(), 0, net.ListenConfig{}) ln, err := addr.Listen(context.TODO(), 0, net.ListenConfig{})
if err != nil { if err != nil {
return err return err
@ -548,6 +552,13 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
// because we are using TLS authentication instead // because we are using TLS authentication instead
handler := cfg.Admin.newAdminHandler(addr, true, ctx) handler := cfg.Admin.newAdminHandler(addr, true, ctx)
// run the provisioners for loaded modules to make sure local
// state is properly re-initialized in the new admin server
err = cfg.Admin.provisionAdminRouters(ctx)
if err != nil {
return err
}
// create client certificate pool for TLS mutual auth, and extract public keys // create client certificate pool for TLS mutual auth, and extract public keys
// so that we can enforce access controls at the application layer // so that we can enforce access controls at the application layer
clientCertPool := x509.NewCertPool() clientCertPool := x509.NewCertPool()

View File

@ -19,6 +19,7 @@ import (
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"fmt" "fmt"
"maps"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect" "reflect"
@ -335,9 +336,7 @@ func TestAdminHandlerBuiltinRouteErrors(t *testing.T) {
func testGetMetricValue(labels map[string]string) float64 { func testGetMetricValue(labels map[string]string) float64 {
promLabels := prometheus.Labels{} promLabels := prometheus.Labels{}
for k, v := range labels { maps.Copy(promLabels, labels)
promLabels[k] = v
}
metric, err := adminMetrics.requestErrors.GetMetricWith(promLabels) metric, err := adminMetrics.requestErrors.GetMetricWith(promLabels)
if err != nil { if err != nil {
@ -377,9 +376,7 @@ func (m *mockModule) CaddyModule() ModuleInfo {
func TestNewAdminHandlerRouterRegistration(t *testing.T) { func TestNewAdminHandlerRouterRegistration(t *testing.T) {
originalModules := make(map[string]ModuleInfo) originalModules := make(map[string]ModuleInfo)
for k, v := range modules { maps.Copy(originalModules, modules)
originalModules[k] = v
}
defer func() { defer func() {
modules = originalModules modules = originalModules
}() }()
@ -479,9 +476,7 @@ func TestAdminRouterProvisioning(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
originalModules := make(map[string]ModuleInfo) originalModules := make(map[string]ModuleInfo)
for k, v := range modules { maps.Copy(originalModules, modules)
originalModules[k] = v
}
defer func() { defer func() {
modules = originalModules modules = originalModules
}() }()
@ -531,6 +526,7 @@ func TestAdminRouterProvisioning(t *testing.T) {
} }
func TestAllowedOriginsUnixSocket(t *testing.T) { func TestAllowedOriginsUnixSocket(t *testing.T) {
// see comment in allowedOrigins() as to why we do not fill out allowed origins for UDS
tests := []struct { tests := []struct {
name string name string
addr NetworkAddress addr NetworkAddress
@ -544,11 +540,7 @@ func TestAllowedOriginsUnixSocket(t *testing.T) {
Host: "/tmp/caddy.sock", Host: "/tmp/caddy.sock",
}, },
origins: nil, // default origins origins: nil, // default origins
expectOrigins: []string{ expectOrigins: []string{},
"", // empty host as per RFC 2616
"127.0.0.1",
"::1",
},
}, },
{ {
name: "unix socket with custom origins", name: "unix socket with custom origins",
@ -578,7 +570,7 @@ func TestAllowedOriginsUnixSocket(t *testing.T) {
}, },
} }
for _, test := range tests { for i, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
admin := AdminConfig{ admin := AdminConfig{
Origins: test.origins, Origins: test.origins,
@ -592,7 +584,7 @@ func TestAllowedOriginsUnixSocket(t *testing.T) {
} }
if len(gotOrigins) != len(test.expectOrigins) { if len(gotOrigins) != len(test.expectOrigins) {
t.Errorf("Expected %d origins but got %d", len(test.expectOrigins), len(gotOrigins)) t.Errorf("%d: Expected %d origins but got %d", i, len(test.expectOrigins), len(gotOrigins))
return return
} }
@ -607,7 +599,7 @@ func TestAllowedOriginsUnixSocket(t *testing.T) {
} }
if !reflect.DeepEqual(expectMap, gotMap) { if !reflect.DeepEqual(expectMap, gotMap) {
t.Errorf("Origins mismatch.\nExpected: %v\nGot: %v", test.expectOrigins, gotOrigins) t.Errorf("%d: Origins mismatch.\nExpected: %v\nGot: %v", i, test.expectOrigins, gotOrigins)
} }
}) })
} }
@ -777,9 +769,7 @@ func (m *mockIssuerModule) CaddyModule() ModuleInfo {
func TestManageIdentity(t *testing.T) { func TestManageIdentity(t *testing.T) {
originalModules := make(map[string]ModuleInfo) originalModules := make(map[string]ModuleInfo)
for k, v := range modules { maps.Copy(originalModules, modules)
originalModules[k] = v
}
defer func() { defer func() {
modules = originalModules modules = originalModules
}() }()

116
caddy.go
View File

@ -83,11 +83,12 @@ type Config struct {
apps map[string]App apps map[string]App
storage certmagic.Storage storage certmagic.Storage
eventEmitter eventEmitter
cancelFunc context.CancelFunc cancelFunc context.CancelFunc
// filesystems is a dict of filesystems that will later be loaded from and added to. // fileSystems is a dict of fileSystems that will later be loaded from and added to.
filesystems FileSystems fileSystems FileSystems
} }
// App is a thing that Caddy runs. // App is a thing that Caddy runs.
@ -442,6 +443,10 @@ func run(newCfg *Config, start bool) (Context, error) {
} }
globalMetrics.configSuccess.Set(1) globalMetrics.configSuccess.Set(1)
globalMetrics.configSuccessTime.SetToCurrentTime() globalMetrics.configSuccessTime.SetToCurrentTime()
// TODO: This event is experimental and subject to change.
ctx.emitEvent("started", nil)
// now that the user's config is running, finish setting up anything else, // now that the user's config is running, finish setting up anything else,
// such as remote admin endpoint, config loader, etc. // such as remote admin endpoint, config loader, etc.
return ctx, finishSettingUp(ctx, ctx.cfg) return ctx, finishSettingUp(ctx, ctx.cfg)
@ -500,16 +505,8 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
return ctx, err return ctx, err
} }
// start the admin endpoint (and stop any prior one)
if replaceAdminServer {
err = replaceLocalAdminServer(newCfg, ctx)
if err != nil {
return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err)
}
}
// create the new filesystem map // create the new filesystem map
newCfg.filesystems = &filesystems.FilesystemMap{} newCfg.fileSystems = &filesystems.FileSystemMap{}
// prepare the new config for use // prepare the new config for use
newCfg.apps = make(map[string]App) newCfg.apps = make(map[string]App)
@ -539,6 +536,14 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
return ctx, err return ctx, err
} }
// start the admin endpoint (and stop any prior one)
if replaceAdminServer {
err = replaceLocalAdminServer(newCfg, ctx)
if err != nil {
return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err)
}
}
// Load and Provision each app and their submodules // Load and Provision each app and their submodules
err = func() error { err = func() error {
for appName := range newCfg.AppsRaw { for appName := range newCfg.AppsRaw {
@ -696,6 +701,9 @@ func unsyncedStop(ctx Context) {
return return
} }
// TODO: This event is experimental and subject to change.
ctx.emitEvent("stopping", nil)
// stop each app // stop each app
for name, a := range ctx.cfg.apps { for name, a := range ctx.cfg.apps {
err := a.Stop() err := a.Stop()
@ -1038,6 +1046,92 @@ func Version() (simple, full string) {
return return
} }
// Event represents something that has happened or is happening.
// An Event value is not synchronized, so it should be copied if
// being used in goroutines.
//
// EXPERIMENTAL: Events are subject to change.
type Event struct {
// If non-nil, the event has been aborted, meaning
// propagation has stopped to other handlers and
// the code should stop what it was doing. Emitters
// may choose to use this as a signal to adjust their
// code path appropriately.
Aborted error
// The data associated with the event. Usually the
// original emitter will be the only one to set or
// change these values, but the field is exported
// so handlers can have full access if needed.
// However, this map is not synchronized, so
// handlers must not use this map directly in new
// goroutines; instead, copy the map to use it in a
// goroutine. Data may be nil.
Data map[string]any
id uuid.UUID
ts time.Time
name string
origin Module
}
// NewEvent creates a new event, but does not emit the event. To emit an
// event, call Emit() on the current instance of the caddyevents app insteaad.
//
// EXPERIMENTAL: Subject to change.
func NewEvent(ctx Context, name string, data map[string]any) (Event, error) {
id, err := uuid.NewRandom()
if err != nil {
return Event{}, fmt.Errorf("generating new event ID: %v", err)
}
name = strings.ToLower(name)
return Event{
Data: data,
id: id,
ts: time.Now(),
name: name,
origin: ctx.Module(),
}, nil
}
func (e Event) ID() uuid.UUID { return e.id }
func (e Event) Timestamp() time.Time { return e.ts }
func (e Event) Name() string { return e.name }
func (e Event) Origin() Module { return e.origin } // Returns the module that originated the event. May be nil, usually if caddy core emits the event.
// CloudEvent exports event e as a structure that, when
// serialized as JSON, is compatible with the
// CloudEvents spec.
func (e Event) CloudEvent() CloudEvent {
dataJSON, _ := json.Marshal(e.Data)
return CloudEvent{
ID: e.id.String(),
Source: e.origin.CaddyModule().String(),
SpecVersion: "1.0",
Type: e.name,
Time: e.ts,
DataContentType: "application/json",
Data: dataJSON,
}
}
// CloudEvent is a JSON-serializable structure that
// is compatible with the CloudEvents specification.
// See https://cloudevents.io.
// EXPERIMENTAL: Subject to change.
type CloudEvent struct {
ID string `json:"id"`
Source string `json:"source"`
SpecVersion string `json:"specversion"`
Type string `json:"type"`
Time time.Time `json:"time"`
DataContentType string `json:"datacontenttype,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
}
// ErrEventAborted cancels an event.
var ErrEventAborted = errors.New("event aborted")
// ActiveContext returns the currently-active context. // ActiveContext returns the currently-active context.
// This function is experimental and might be changed // This function is experimental and might be changed
// or removed in the future. // or removed in the future.

View File

@ -16,6 +16,7 @@ package httpcaddyfile
import ( import (
"encoding/json" "encoding/json"
"maps"
"net" "net"
"slices" "slices"
"sort" "sort"
@ -365,9 +366,7 @@ func parseSegmentAsConfig(h Helper) ([]ConfigValue, error) {
// copy existing matcher definitions so we can augment // copy existing matcher definitions so we can augment
// new ones that are defined only in this scope // new ones that are defined only in this scope
matcherDefs := make(map[string]caddy.ModuleMap, len(h.matcherDefs)) matcherDefs := make(map[string]caddy.ModuleMap, len(h.matcherDefs))
for key, val := range h.matcherDefs { maps.Copy(matcherDefs, h.matcherDefs)
matcherDefs[key] = val
}
// find and extract any embedded matcher definitions in this scope // find and extract any embedded matcher definitions in this scope
for i := 0; i < len(segments); i++ { for i := 0; i < len(segments); i++ {
@ -483,12 +482,29 @@ func sortRoutes(routes []ConfigValue) {
// we can only confidently compare path lengths if both // we can only confidently compare path lengths if both
// directives have a single path to match (issue #5037) // directives have a single path to match (issue #5037)
if iPathLen > 0 && jPathLen > 0 { if iPathLen > 0 && jPathLen > 0 {
// trim the trailing wildcard if there is one
iPathTrimmed := strings.TrimSuffix(iPM[0], "*")
jPathTrimmed := strings.TrimSuffix(jPM[0], "*")
// if both paths are the same except for a trailing wildcard, // if both paths are the same except for a trailing wildcard,
// sort by the shorter path first (which is more specific) // sort by the shorter path first (which is more specific)
if strings.TrimSuffix(iPM[0], "*") == strings.TrimSuffix(jPM[0], "*") { if iPathTrimmed == jPathTrimmed {
return iPathLen < jPathLen return iPathLen < jPathLen
} }
// we use the trimmed length to compare the paths
// https://github.com/caddyserver/caddy/issues/7012#issuecomment-2870142195
// credit to https://github.com/Hellio404
// for sorts with many items, mixing matchers w/ and w/o wildcards will confuse the sort and result in incorrect orders
iPathLen = len(iPathTrimmed)
jPathLen = len(jPathTrimmed)
// if both paths have the same length, sort lexically
// https://github.com/caddyserver/caddy/pull/7015#issuecomment-2871993588
if iPathLen == jPathLen {
return iPathTrimmed < jPathTrimmed
}
// sort most-specific (longest) path first // sort most-specific (longest) path first
return iPathLen > jPathLen return iPathLen > jPathLen
} }

View File

@ -633,12 +633,6 @@ func (st *ServerType) serversFromPairings(
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig) srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
} }
srv.AutoHTTPS.IgnoreLoadedCerts = true srv.AutoHTTPS.IgnoreLoadedCerts = true
case "prefer_wildcard":
if srv.AutoHTTPS == nil {
srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
}
srv.AutoHTTPS.PreferWildcard = true
} }
} }
@ -706,16 +700,6 @@ func (st *ServerType) serversFromPairings(
return specificity(iLongestHost) > specificity(jLongestHost) return specificity(iLongestHost) > specificity(jLongestHost)
}) })
// collect all hosts that have a wildcard in them
wildcardHosts := []string{}
for _, sblock := range p.serverBlocks {
for _, addr := range sblock.parsedKeys {
if strings.HasPrefix(addr.Host, "*.") {
wildcardHosts = append(wildcardHosts, addr.Host[2:])
}
}
}
var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool var hasCatchAllTLSConnPolicy, addressQualifiesForTLS bool
autoHTTPSWillAddConnPolicy := srv.AutoHTTPS == nil || !srv.AutoHTTPS.Disabled autoHTTPSWillAddConnPolicy := srv.AutoHTTPS == nil || !srv.AutoHTTPS.Disabled
@ -801,7 +785,13 @@ func (st *ServerType) serversFromPairings(
cp.FallbackSNI = fallbackSNI cp.FallbackSNI = fallbackSNI
} }
// only append this policy if it actually changes something // only append this policy if it actually changes something,
// or if the configuration explicitly automates certs for
// these names (this is necessary to hoist a connection policy
// above one that may manually load a wildcard cert that would
// otherwise clobber the automated one; the code that appends
// policies that manually load certs comes later, so they're
// lower in the list)
if !cp.SettingsEmpty() || mapContains(forceAutomatedNames, hosts) { if !cp.SettingsEmpty() || mapContains(forceAutomatedNames, hosts) {
srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp) srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp)
hasCatchAllTLSConnPolicy = len(hosts) == 0 hasCatchAllTLSConnPolicy = len(hosts) == 0
@ -841,18 +831,6 @@ func (st *ServerType) serversFromPairings(
addressQualifiesForTLS = true addressQualifiesForTLS = true
} }
// If prefer wildcard is enabled, then we add hosts that are
// already covered by the wildcard to the skip list
if addressQualifiesForTLS && srv.AutoHTTPS != nil && srv.AutoHTTPS.PreferWildcard {
baseDomain := addr.Host
if idx := strings.Index(baseDomain, "."); idx != -1 {
baseDomain = baseDomain[idx+1:]
}
if !strings.HasPrefix(addr.Host, "*.") && slices.Contains(wildcardHosts, baseDomain) {
srv.AutoHTTPS.SkipCerts = append(srv.AutoHTTPS.SkipCerts, addr.Host)
}
}
// predict whether auto-HTTPS will add the conn policy for us; if so, we // predict whether auto-HTTPS will add the conn policy for us; if so, we
// may not need to add one for this server // may not need to add one for this server
autoHTTPSWillAddConnPolicy = autoHTTPSWillAddConnPolicy && autoHTTPSWillAddConnPolicy = autoHTTPSWillAddConnPolicy &&
@ -1083,11 +1061,40 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti
// if they're exactly equal in every way, just keep one of them // if they're exactly equal in every way, just keep one of them
if reflect.DeepEqual(cps[i], cps[j]) { if reflect.DeepEqual(cps[i], cps[j]) {
cps = append(cps[:j], cps[j+1:]...) cps = slices.Delete(cps, j, j+1)
i-- i--
break break
} }
// as a special case, if there are adjacent TLS conn policies that are identical except
// by their matchers, and the matchers are specifically just ServerName ("sni") matchers
// (by far the most common), we can combine them into a single policy
if i == j-1 && len(cps[i].MatchersRaw) == 1 && len(cps[j].MatchersRaw) == 1 {
if iSNIMatcherJSON, ok := cps[i].MatchersRaw["sni"]; ok {
if jSNIMatcherJSON, ok := cps[j].MatchersRaw["sni"]; ok {
// position of policies and the matcher criteria check out; if settings are
// the same, then we can combine the policies; we have to unmarshal and
// remarshal the matchers though
if cps[i].SettingsEqual(*cps[j]) {
var iSNIMatcher caddytls.MatchServerName
if err := json.Unmarshal(iSNIMatcherJSON, &iSNIMatcher); err == nil {
var jSNIMatcher caddytls.MatchServerName
if err := json.Unmarshal(jSNIMatcherJSON, &jSNIMatcher); err == nil {
iSNIMatcher = append(iSNIMatcher, jSNIMatcher...)
cps[i].MatchersRaw["sni"], err = json.Marshal(iSNIMatcher)
if err != nil {
return nil, fmt.Errorf("recombining SNI matchers: %v", err)
}
cps = slices.Delete(cps, j, j+1)
i--
break
}
}
}
}
}
}
// if they have the same matcher, try to reconcile each field: either they must // if they have the same matcher, try to reconcile each field: either they must
// be identical, or we have to be able to combine them safely // be identical, or we have to be able to combine them safely
if reflect.DeepEqual(cps[i].MatchersRaw, cps[j].MatchersRaw) { if reflect.DeepEqual(cps[i].MatchersRaw, cps[j].MatchersRaw) {
@ -1189,12 +1196,13 @@ func consolidateConnPolicies(cps caddytls.ConnectionPolicies) (caddytls.Connecti
} }
} }
cps = append(cps[:j], cps[j+1:]...) cps = slices.Delete(cps, j, j+1)
i-- i--
break break
} }
} }
} }
return cps, nil return cps, nil
} }

View File

@ -92,11 +92,9 @@ func (st ServerType) buildTLSApp(
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP) tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP)
} }
// collect all hosts that have a wildcard in them, and arent HTTP var wildcardHosts []string // collect all hosts that have a wildcard in them, and aren't HTTP
wildcardHosts := []string{} forcedAutomatedNames := make(map[string]struct{}) // explicitly configured to be automated, even if covered by a wildcard
// hosts that have been explicitly marked to be automated,
// even if covered by another wildcard
forcedAutomatedNames := make(map[string]struct{})
for _, p := range pairings { for _, p := range pairings {
var addresses []string var addresses []string
for _, addressWithProtocols := range p.addressesWithProtocols { for _, addressWithProtocols := range p.addressesWithProtocols {
@ -153,7 +151,7 @@ func (st ServerType) buildTLSApp(
ap.OnDemand = true ap.OnDemand = true
} }
// collect hosts that are forced to be automated // collect hosts that are forced to have certs automated for their specific name
if _, ok := sblock.pile["tls.force_automate"]; ok { if _, ok := sblock.pile["tls.force_automate"]; ok {
for _, host := range sblockHosts { for _, host := range sblockHosts {
forcedAutomatedNames[host] = struct{}{} forcedAutomatedNames[host] = struct{}{}
@ -375,8 +373,10 @@ func (st ServerType) buildTLSApp(
return nil, warnings, err return nil, warnings, err
} }
for _, cfg := range ech.Configs { for _, cfg := range ech.Configs {
if cfg.PublicName != "" {
ap.SubjectsRaw = append(ap.SubjectsRaw, cfg.PublicName) ap.SubjectsRaw = append(ap.SubjectsRaw, cfg.PublicName)
} }
}
if tlsApp.Automation == nil { if tlsApp.Automation == nil {
tlsApp.Automation = new(caddytls.AutomationConfig) tlsApp.Automation = new(caddytls.AutomationConfig)
} }

View File

@ -0,0 +1,72 @@
{
pki {
ca custom-ca {
name "Custom CA"
}
}
}
acme.example.com {
acme_server {
ca custom-ca
allow {
domains host-1.internal.example.com host-2.internal.example.com
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"acme.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "custom-ca",
"handler": "acme_server",
"policy": {
"allow": {
"domains": [
"host-1.internal.example.com",
"host-2.internal.example.com"
]
}
}
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"pki": {
"certificate_authorities": {
"custom-ca": {
"name": "Custom CA"
}
}
}
}
}

View File

@ -0,0 +1,80 @@
{
pki {
ca custom-ca {
name "Custom CA"
}
}
}
acme.example.com {
acme_server {
ca custom-ca
allow {
domains host-1.internal.example.com host-2.internal.example.com
}
deny {
domains dc.internal.example.com
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"acme.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "custom-ca",
"handler": "acme_server",
"policy": {
"allow": {
"domains": [
"host-1.internal.example.com",
"host-2.internal.example.com"
]
},
"deny": {
"domains": [
"dc.internal.example.com"
]
}
}
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"pki": {
"certificate_authorities": {
"custom-ca": {
"name": "Custom CA"
}
}
}
}
}

View File

@ -0,0 +1,71 @@
{
pki {
ca custom-ca {
name "Custom CA"
}
}
}
acme.example.com {
acme_server {
ca custom-ca
deny {
domains dc.internal.example.com
}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"acme.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"ca": "custom-ca",
"handler": "acme_server",
"policy": {
"deny": {
"domains": [
"dc.internal.example.com"
]
}
}
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"pki": {
"certificate_authorities": {
"custom-ca": {
"name": "Custom CA"
}
}
}
}
}

View File

@ -1,109 +0,0 @@
{
auto_https prefer_wildcard
}
*.example.com {
tls {
dns mock
}
respond "fallback"
}
foo.example.com {
respond "foo"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"foo.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "foo",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"*.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "fallback",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip_certificates": [
"foo.example.com"
],
"prefer_wildcard": true
}
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"*.example.com"
],
"issuers": [
{
"challenges": {
"dns": {
"provider": {
"name": "mock"
}
}
},
"module": "acme"
}
]
}
]
}
}
}
}

View File

@ -1,268 +0,0 @@
{
auto_https prefer_wildcard
}
# Covers two domains
*.one.example.com {
tls {
dns mock
}
respond "one fallback"
}
# Is covered, should not get its own AP
foo.one.example.com {
respond "foo one"
}
# This one has its own tls config so it doesn't get covered (escape hatch)
bar.one.example.com {
respond "bar one"
tls bar@bar.com
}
# Covers nothing but AP gets consolidated with the first
*.two.example.com {
tls {
dns mock
}
respond "two fallback"
}
# Is HTTP so it should not cover
http://*.three.example.com {
respond "three fallback"
}
# Has no wildcard coverage so it gets an AP
foo.three.example.com {
respond "foo three"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"foo.three.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "foo three",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"foo.one.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "foo one",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"bar.one.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "bar one",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"*.one.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "one fallback",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
},
{
"match": [
{
"host": [
"*.two.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "two fallback",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
],
"automatic_https": {
"skip_certificates": [
"foo.one.example.com",
"bar.one.example.com"
],
"prefer_wildcard": true
}
},
"srv1": {
"listen": [
":80"
],
"routes": [
{
"match": [
{
"host": [
"*.three.example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "three fallback",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
],
"automatic_https": {
"prefer_wildcard": true
}
}
}
},
"tls": {
"automation": {
"policies": [
{
"subjects": [
"foo.three.example.com"
]
},
{
"subjects": [
"bar.one.example.com"
],
"issuers": [
{
"email": "bar@bar.com",
"module": "acme"
},
{
"ca": "https://acme.zerossl.com/v2/DV90",
"email": "bar@bar.com",
"module": "acme"
}
]
},
{
"subjects": [
"*.one.example.com",
"*.two.example.com"
],
"issuers": [
{
"challenges": {
"dns": {
"provider": {
"name": "mock"
}
}
},
"module": "acme"
}
]
}
]
}
}
}
}

View File

@ -131,13 +131,7 @@ shadowed.example.com {
{ {
"match": { "match": {
"sni": [ "sni": [
"automated1.example.com" "automated1.example.com",
]
}
},
{
"match": {
"sni": [
"automated2.example.com" "automated2.example.com"
] ]
} }

View File

@ -24,6 +24,7 @@ import (
"io" "io"
"io/fs" "io/fs"
"log" "log"
"maps"
"net" "net"
"net/http" "net/http"
"os" "os"
@ -703,9 +704,7 @@ func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io
if body != nil { if body != nil {
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
} }
for k, v := range headers { maps.Copy(req.Header, headers)
req.Header[k] = v
}
// make an HTTP client that dials our network type, since admin // make an HTTP client that dials our network type, since admin
// endpoints aren't always TCP, which is what the default transport // endpoints aren't always TCP, which is what the default transport

View File

@ -91,14 +91,14 @@ func (ctx *Context) OnCancel(f func()) {
ctx.cleanupFuncs = append(ctx.cleanupFuncs, f) ctx.cleanupFuncs = append(ctx.cleanupFuncs, f)
} }
// Filesystems returns a ref to the FilesystemMap. // FileSystems returns a ref to the FilesystemMap.
// EXPERIMENTAL: This API is subject to change. // EXPERIMENTAL: This API is subject to change.
func (ctx *Context) Filesystems() FileSystems { func (ctx *Context) FileSystems() FileSystems {
// if no config is loaded, we use a default filesystemmap, which includes the osfs // if no config is loaded, we use a default filesystemmap, which includes the osfs
if ctx.cfg == nil { if ctx.cfg == nil {
return &filesystems.FilesystemMap{} return &filesystems.FileSystemMap{}
} }
return ctx.cfg.filesystems return ctx.cfg.fileSystems
} }
// Returns the active metrics registry for the context // Returns the active metrics registry for the context
@ -277,6 +277,14 @@ func (ctx Context) LoadModule(structPointer any, fieldName string) (any, error)
return result, nil return result, nil
} }
// emitEvent is a small convenience method so the caddy core can emit events, if the event app is configured.
func (ctx Context) emitEvent(name string, data map[string]any) Event {
if ctx.cfg == nil || ctx.cfg.eventEmitter == nil {
return Event{}
}
return ctx.cfg.eventEmitter.Emit(ctx, name, data)
}
// loadModulesFromSomeMap loads modules from val, which must be a type of map[string]any. // loadModulesFromSomeMap loads modules from val, which must be a type of map[string]any.
// Depending on inlineModuleKey, it will be interpreted as either a ModuleMap (key is the module // Depending on inlineModuleKey, it will be interpreted as either a ModuleMap (key is the module
// name) or as a regular map (key is not the module name, and module name is defined inline). // name) or as a regular map (key is not the module name, and module name is defined inline).
@ -429,6 +437,14 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
ctx.moduleInstances[id] = append(ctx.moduleInstances[id], val) ctx.moduleInstances[id] = append(ctx.moduleInstances[id], val)
// if the loaded module happens to be an app that can emit events, store it so the
// core can have access to emit events without an import cycle
if ee, ok := val.(eventEmitter); ok {
if _, ok := ee.(App); ok {
ctx.cfg.eventEmitter = ee
}
}
return val, nil return val, nil
} }
@ -561,11 +577,11 @@ func (ctx Context) Slogger() *slog.Logger {
if err != nil { if err != nil {
panic("config missing, unable to create dev logger: " + err.Error()) panic("config missing, unable to create dev logger: " + err.Error())
} }
return slog.New(zapslog.NewHandler(l.Core(), nil)) return slog.New(zapslog.NewHandler(l.Core()))
} }
mod := ctx.Module() mod := ctx.Module()
if mod == nil { if mod == nil {
return slog.New(zapslog.NewHandler(Log().Core(), nil)) return slog.New(zapslog.NewHandler(Log().Core()))
} }
return slog.New(zapslog.NewHandler(ctx.cfg.Logging.Logger(mod).Core(), return slog.New(zapslog.NewHandler(ctx.cfg.Logging.Logger(mod).Core(),
zapslog.WithName(string(mod.CaddyModule().ID)), zapslog.WithName(string(mod.CaddyModule().ID)),
@ -600,3 +616,11 @@ func (ctx *Context) WithValue(key, value any) Context {
exitFuncs: ctx.exitFuncs, exitFuncs: ctx.exitFuncs,
} }
} }
// eventEmitter is a small interface that inverts dependencies for
// the caddyevents package, so the core can emit events without an
// import cycle (i.e. the caddy package doesn't have to import
// the caddyevents package, which imports the caddy package).
type eventEmitter interface {
Emit(ctx Context, eventName string, data map[string]any) Event
}

10
go.mod
View File

@ -8,7 +8,7 @@ require (
github.com/Masterminds/sprig/v3 v3.3.0 github.com/Masterminds/sprig/v3 v3.3.0
github.com/alecthomas/chroma/v2 v2.15.0 github.com/alecthomas/chroma/v2 v2.15.0
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
github.com/caddyserver/certmagic v0.22.2 github.com/caddyserver/certmagic v0.23.0
github.com/caddyserver/zerossl v0.1.3 github.com/caddyserver/zerossl v0.1.3
github.com/cloudflare/circl v1.6.0 github.com/cloudflare/circl v1.6.0
github.com/dustin/go-humanize v1.0.1 github.com/dustin/go-humanize v1.0.1
@ -17,9 +17,9 @@ require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/klauspost/compress v1.18.0 github.com/klauspost/compress v1.18.0
github.com/klauspost/cpuid/v2 v2.2.10 github.com/klauspost/cpuid/v2 v2.2.10
github.com/mholt/acmez/v3 v3.1.1 github.com/mholt/acmez/v3 v3.1.2
github.com/prometheus/client_golang v1.19.1 github.com/prometheus/client_golang v1.19.1
github.com/quic-go/quic-go v0.50.1 github.com/quic-go/quic-go v0.51.0
github.com/smallstep/certificates v0.26.1 github.com/smallstep/certificates v0.26.1
github.com/smallstep/nosql v0.6.1 github.com/smallstep/nosql v0.6.1
github.com/smallstep/truststore v0.13.0 github.com/smallstep/truststore v0.13.0
@ -39,7 +39,7 @@ require (
go.uber.org/zap/exp v0.3.0 go.uber.org/zap/exp v0.3.0
golang.org/x/crypto v0.36.0 golang.org/x/crypto v0.36.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810 golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810
golang.org/x/net v0.37.0 golang.org/x/net v0.38.0
golang.org/x/sync v0.12.0 golang.org/x/sync v0.12.0
golang.org/x/term v0.30.0 golang.org/x/term v0.30.0
golang.org/x/time v0.11.0 golang.org/x/time v0.11.0
@ -116,7 +116,7 @@ require (
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/pgx/v4 v4.18.3 // indirect github.com/jackc/pgx/v4 v4.18.3 // indirect
github.com/libdns/libdns v0.2.3 github.com/libdns/libdns v1.0.0-beta.1
github.com/manifoldco/promptui v0.9.0 // indirect github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect

20
go.sum
View File

@ -93,8 +93,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/caddyserver/certmagic v0.22.2 h1:qzZURXlrxwR5m25/jpvVeEyJHeJJMvAwe5zlMufOTQk= github.com/caddyserver/certmagic v0.23.0 h1:CfpZ/50jMfG4+1J/u2LV6piJq4HOfO6ppOnOf7DkFEU=
github.com/caddyserver/certmagic v0.22.2/go.mod h1:hbqE7BnkjhX5IJiFslPmrSeobSeZvI6ux8tyxhsd6qs= github.com/caddyserver/certmagic v0.23.0/go.mod h1:9mEZIWqqWoI+Gf+4Trh04MOVPD0tGSxtqsxg87hAIH4=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
@ -327,8 +327,8 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/libdns/libdns v0.2.3 h1:ba30K4ObwMGB/QTmqUxf3H4/GmUrCAIkMWejeGl12v8= github.com/libdns/libdns v1.0.0-beta.1 h1:KIf4wLfsrEpXpZ3vmc/poM8zCATXT2klbdPe6hyOBjQ=
github.com/libdns/libdns v0.2.3/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/libdns/libdns v1.0.0-beta.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@ -347,8 +347,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mholt/acmez/v3 v3.1.1 h1:Jh+9uKHkPxUJdxM16q5mOr+G2V0aqkuFtNA28ihCxhQ= github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc=
github.com/mholt/acmez/v3 v3.1.1/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
@ -397,8 +397,8 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.50.1 h1:unsgjFIUqW8a2oopkY7YNONpV1gYND6Nt9hnt1PN94Q= github.com/quic-go/quic-go v0.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc=
github.com/quic-go/quic-go v0.50.1/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
@ -633,8 +633,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=

View File

@ -7,10 +7,10 @@ import (
) )
const ( const (
DefaultFilesystemKey = "default" DefaultFileSystemKey = "default"
) )
var DefaultFilesystem = &wrapperFs{key: DefaultFilesystemKey, FS: OsFS{}} var DefaultFileSystem = &wrapperFs{key: DefaultFileSystemKey, FS: OsFS{}}
// wrapperFs exists so can easily add to wrapperFs down the line // wrapperFs exists so can easily add to wrapperFs down the line
type wrapperFs struct { type wrapperFs struct {
@ -18,24 +18,24 @@ type wrapperFs struct {
fs.FS fs.FS
} }
// FilesystemMap stores a map of filesystems // FileSystemMap stores a map of filesystems
// the empty key will be overwritten to be the default key // the empty key will be overwritten to be the default key
// it includes a default filesystem, based off the os fs // it includes a default filesystem, based off the os fs
type FilesystemMap struct { type FileSystemMap struct {
m sync.Map m sync.Map
} }
// note that the first invocation of key cannot be called in a racy context. // note that the first invocation of key cannot be called in a racy context.
func (f *FilesystemMap) key(k string) string { func (f *FileSystemMap) key(k string) string {
if k == "" { if k == "" {
k = DefaultFilesystemKey k = DefaultFileSystemKey
} }
return k return k
} }
// Register will add the filesystem with key to later be retrieved // Register will add the filesystem with key to later be retrieved
// A call with a nil fs will call unregister, ensuring that a call to Default() will never be nil // A call with a nil fs will call unregister, ensuring that a call to Default() will never be nil
func (f *FilesystemMap) Register(k string, v fs.FS) { func (f *FileSystemMap) Register(k string, v fs.FS) {
k = f.key(k) k = f.key(k)
if v == nil { if v == nil {
f.Unregister(k) f.Unregister(k)
@ -47,23 +47,23 @@ func (f *FilesystemMap) Register(k string, v fs.FS) {
// Unregister will remove the filesystem with key from the filesystem map // Unregister will remove the filesystem with key from the filesystem map
// if the key is the default key, it will set the default to the osFS instead of deleting it // if the key is the default key, it will set the default to the osFS instead of deleting it
// modules should call this on cleanup to be safe // modules should call this on cleanup to be safe
func (f *FilesystemMap) Unregister(k string) { func (f *FileSystemMap) Unregister(k string) {
k = f.key(k) k = f.key(k)
if k == DefaultFilesystemKey { if k == DefaultFileSystemKey {
f.m.Store(k, DefaultFilesystem) f.m.Store(k, DefaultFileSystem)
} else { } else {
f.m.Delete(k) f.m.Delete(k)
} }
} }
// Get will get a filesystem with a given key // Get will get a filesystem with a given key
func (f *FilesystemMap) Get(k string) (v fs.FS, ok bool) { func (f *FileSystemMap) Get(k string) (v fs.FS, ok bool) {
k = f.key(k) k = f.key(k)
c, ok := f.m.Load(strings.TrimSpace(k)) c, ok := f.m.Load(strings.TrimSpace(k))
if !ok { if !ok {
if k == DefaultFilesystemKey { if k == DefaultFileSystemKey {
f.m.Store(k, DefaultFilesystem) f.m.Store(k, DefaultFileSystem)
return DefaultFilesystem, true return DefaultFileSystem, true
} }
return nil, ok return nil, ok
} }
@ -71,7 +71,7 @@ func (f *FilesystemMap) Get(k string) (v fs.FS, ok bool) {
} }
// Default will get the default filesystem in the filesystem map // Default will get the default filesystem in the filesystem map
func (f *FilesystemMap) Default() fs.FS { func (f *FileSystemMap) Default() fs.FS {
val, _ := f.Get(DefaultFilesystemKey) val, _ := f.Get(DefaultFileSystemKey)
return val return val
} }

22
internal/logs.go Normal file
View File

@ -0,0 +1,22 @@
package internal
import "fmt"
// MaxSizeSubjectsListForLog returns the keys in the map as a slice of maximum length
// maxToDisplay. It is useful for logging domains being managed, for example, since a
// map is typically needed for quick lookup, but a slice is needed for logging, and this
// can be quite a doozy since there may be a huge amount (hundreds of thousands).
func MaxSizeSubjectsListForLog(subjects map[string]struct{}, maxToDisplay int) []string {
numberOfNamesToDisplay := min(len(subjects), maxToDisplay)
domainsToDisplay := make([]string, 0, numberOfNamesToDisplay)
for domain := range subjects {
domainsToDisplay = append(domainsToDisplay, domain)
if len(domainsToDisplay) >= numberOfNamesToDisplay {
break
}
}
if len(subjects) > maxToDisplay {
domainsToDisplay = append(domainsToDisplay, fmt.Sprintf("(and %d more...)", len(subjects)-maxToDisplay))
}
return domainsToDisplay
}

View File

@ -210,7 +210,7 @@ func (na NetworkAddress) IsUnixNetwork() bool {
return IsUnixNetwork(na.Network) return IsUnixNetwork(na.Network)
} }
// IsUnixNetwork returns true if na.Network is // IsFdNetwork returns true if na.Network is
// fd or fdgram. // fd or fdgram.
func (na NetworkAddress) IsFdNetwork() bool { func (na NetworkAddress) IsFdNetwork() bool {
return IsFdNetwork(na.Network) return IsFdNetwork(na.Network)
@ -641,7 +641,7 @@ func RegisterNetwork(network string, getListener ListenerFunc) {
if network == "tcp" || network == "tcp4" || network == "tcp6" || if network == "tcp" || network == "tcp4" || network == "tcp6" ||
network == "udp" || network == "udp4" || network == "udp6" || network == "udp" || network == "udp4" || network == "udp6" ||
network == "unix" || network == "unixpacket" || network == "unixgram" || network == "unix" || network == "unixpacket" || network == "unixgram" ||
strings.HasPrefix("ip:", network) || strings.HasPrefix("ip4:", network) || strings.HasPrefix("ip6:", network) || strings.HasPrefix(network, "ip:") || strings.HasPrefix(network, "ip4:") || strings.HasPrefix(network, "ip6:") ||
network == "fd" || network == "fdgram" { network == "fd" || network == "fdgram" {
panic("network type " + network + " is reserved") panic("network type " + network + " is reserved")
} }

View File

@ -20,6 +20,7 @@ import (
"io" "io"
"log" "log"
"os" "os"
"slices"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -161,7 +162,9 @@ func (logging *Logging) setupNewDefault(ctx Context) error {
if err != nil { if err != nil {
return fmt.Errorf("setting up default log: %v", err) return fmt.Errorf("setting up default log: %v", err)
} }
newDefault.logger = zap.New(newDefault.CustomLog.core, options...)
filteringCore := &filteringCore{newDefault.CustomLog.core, newDefault.CustomLog}
newDefault.logger = zap.New(filteringCore, options...)
// redirect the default caddy logs // redirect the default caddy logs
defaultLoggerMu.Lock() defaultLoggerMu.Lock()
@ -490,12 +493,10 @@ func (cl *CustomLog) provision(ctx Context, logging *Logging) error {
if len(cl.Include) > 0 && len(cl.Exclude) > 0 { if len(cl.Include) > 0 && len(cl.Exclude) > 0 {
// prevent intersections // prevent intersections
for _, allow := range cl.Include { for _, allow := range cl.Include {
for _, deny := range cl.Exclude { if slices.Contains(cl.Exclude, allow) {
if allow == deny {
return fmt.Errorf("include and exclude must not intersect, but found %s in both lists", allow) return fmt.Errorf("include and exclude must not intersect, but found %s in both lists", allow)
} }
} }
}
// ensure namespaces are nested // ensure namespaces are nested
outer: outer:

106
logging_test.go Normal file
View File

@ -0,0 +1,106 @@
// 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 caddy
import "testing"
func TestCustomLog_loggerAllowed(t *testing.T) {
type fields struct {
BaseLog BaseLog
Include []string
Exclude []string
}
type args struct {
name string
isModule bool
}
tests := []struct {
name string
fields fields
args args
want bool
}{
{
name: "include",
fields: fields{
Include: []string{"foo"},
},
args: args{
name: "foo",
isModule: true,
},
want: true,
},
{
name: "exclude",
fields: fields{
Exclude: []string{"foo"},
},
args: args{
name: "foo",
isModule: true,
},
want: false,
},
{
name: "include and exclude",
fields: fields{
Include: []string{"foo"},
Exclude: []string{"foo"},
},
args: args{
name: "foo",
isModule: true,
},
want: false,
},
{
name: "include and exclude (longer namespace)",
fields: fields{
Include: []string{"foo.bar"},
Exclude: []string{"foo"},
},
args: args{
name: "foo.bar",
isModule: true,
},
want: true,
},
{
name: "excluded module is not printed",
fields: fields{
Include: []string{"admin.api.load"},
Exclude: []string{"admin.api"},
},
args: args{
name: "admin.api",
isModule: false,
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cl := &CustomLog{
BaseLog: tt.fields.BaseLog,
Include: tt.fields.Include,
Exclude: tt.fields.Exclude,
}
if got := cl.loggerAllowed(tt.args.name, tt.args.isModule); got != tt.want {
t.Errorf("CustomLog.loggerAllowed() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -20,9 +20,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"time"
"github.com/google/uuid"
"go.uber.org/zap" "go.uber.org/zap"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
@ -206,27 +204,26 @@ func (app *App) On(eventName string, handler Handler) error {
// //
// Note that the data map is not copied, for efficiency. After Emit() is called, the // Note that the data map is not copied, for efficiency. After Emit() is called, the
// data passed in should not be changed in other goroutines. // data passed in should not be changed in other goroutines.
func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) Event { func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) caddy.Event {
logger := app.logger.With(zap.String("name", eventName)) logger := app.logger.With(zap.String("name", eventName))
id, err := uuid.NewRandom() e, err := caddy.NewEvent(ctx, eventName, data)
if err != nil { if err != nil {
logger.Error("failed generating new event ID", zap.Error(err)) logger.Error("failed to create event", zap.Error(err))
} }
eventName = strings.ToLower(eventName) var originModule caddy.ModuleInfo
var originModuleID caddy.ModuleID
e := Event{ var originModuleName string
Data: data, if origin := e.Origin(); origin != nil {
id: id, originModule = origin.CaddyModule()
ts: time.Now(), originModuleID = originModule.ID
name: eventName, originModuleName = originModule.String()
origin: ctx.Module(),
} }
logger = logger.With( logger = logger.With(
zap.String("id", e.id.String()), zap.String("id", e.ID().String()),
zap.String("origin", e.origin.CaddyModule().String())) zap.String("origin", originModuleName))
// add event info to replacer, make sure it's in the context // add event info to replacer, make sure it's in the context
repl, ok := ctx.Context.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) repl, ok := ctx.Context.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
@ -239,15 +236,15 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E
case "event": case "event":
return e, true return e, true
case "event.id": case "event.id":
return e.id, true return e.ID(), true
case "event.name": case "event.name":
return e.name, true return e.Name(), true
case "event.time": case "event.time":
return e.ts, true return e.Timestamp(), true
case "event.time_unix": case "event.time_unix":
return e.ts.UnixMilli(), true return e.Timestamp().UnixMilli(), true
case "event.module": case "event.module":
return e.origin.CaddyModule().ID, true return originModuleID, true
case "event.data": case "event.data":
return e.Data, true return e.Data, true
} }
@ -269,7 +266,7 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E
// invoke handlers bound to the event by name and also all events; this for loop // invoke handlers bound to the event by name and also all events; this for loop
// iterates twice at most: once for the event name, once for "" (all events) // iterates twice at most: once for the event name, once for "" (all events)
for { for {
moduleID := e.origin.CaddyModule().ID moduleID := originModuleID
// implement propagation up the module tree (i.e. start with "a.b.c" then "a.b" then "a" then "") // implement propagation up the module tree (i.e. start with "a.b.c" then "a.b" then "a" then "")
for { for {
@ -292,7 +289,7 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E
zap.Any("handler", handler)) zap.Any("handler", handler))
if err := handler.Handle(ctx, e); err != nil { if err := handler.Handle(ctx, e); err != nil {
aborted := errors.Is(err, ErrAborted) aborted := errors.Is(err, caddy.ErrEventAborted)
logger.Error("handler error", logger.Error("handler error",
zap.Error(err), zap.Error(err),
@ -326,76 +323,9 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E
return e return e
} }
// Event represents something that has happened or is happening.
// An Event value is not synchronized, so it should be copied if
// being used in goroutines.
//
// EXPERIMENTAL: As with the rest of this package, events are
// subject to change.
type Event struct {
// If non-nil, the event has been aborted, meaning
// propagation has stopped to other handlers and
// the code should stop what it was doing. Emitters
// may choose to use this as a signal to adjust their
// code path appropriately.
Aborted error
// The data associated with the event. Usually the
// original emitter will be the only one to set or
// change these values, but the field is exported
// so handlers can have full access if needed.
// However, this map is not synchronized, so
// handlers must not use this map directly in new
// goroutines; instead, copy the map to use it in a
// goroutine.
Data map[string]any
id uuid.UUID
ts time.Time
name string
origin caddy.Module
}
func (e Event) ID() uuid.UUID { return e.id }
func (e Event) Timestamp() time.Time { return e.ts }
func (e Event) Name() string { return e.name }
func (e Event) Origin() caddy.Module { return e.origin }
// CloudEvent exports event e as a structure that, when
// serialized as JSON, is compatible with the
// CloudEvents spec.
func (e Event) CloudEvent() CloudEvent {
dataJSON, _ := json.Marshal(e.Data)
return CloudEvent{
ID: e.id.String(),
Source: e.origin.CaddyModule().String(),
SpecVersion: "1.0",
Type: e.name,
Time: e.ts,
DataContentType: "application/json",
Data: dataJSON,
}
}
// CloudEvent is a JSON-serializable structure that
// is compatible with the CloudEvents specification.
// See https://cloudevents.io.
type CloudEvent struct {
ID string `json:"id"`
Source string `json:"source"`
SpecVersion string `json:"specversion"`
Type string `json:"type"`
Time time.Time `json:"time"`
DataContentType string `json:"datacontenttype,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
}
// ErrAborted cancels an event.
var ErrAborted = errors.New("event aborted")
// Handler is a type that can handle events. // Handler is a type that can handle events.
type Handler interface { type Handler interface {
Handle(context.Context, Event) error Handle(context.Context, caddy.Event) error
} }
// Interface guards // Interface guards

View File

@ -69,11 +69,11 @@ func (xs *Filesystems) Provision(ctx caddy.Context) error {
} }
// register that module // register that module
ctx.Logger().Debug("registering fs", zap.String("fs", f.Key)) ctx.Logger().Debug("registering fs", zap.String("fs", f.Key))
ctx.Filesystems().Register(f.Key, f.fileSystem) ctx.FileSystems().Register(f.Key, f.fileSystem)
// remember to unregister the module when we are done // remember to unregister the module when we are done
xs.defers = append(xs.defers, func() { xs.defers = append(xs.defers, func() {
ctx.Logger().Debug("unregistering fs", zap.String("fs", f.Key)) ctx.Logger().Debug("unregistering fs", zap.String("fs", f.Key))
ctx.Filesystems().Unregister(f.Key) ctx.FileSystems().Unregister(f.Key)
}) })
} }
return nil return nil

View File

@ -73,7 +73,7 @@ func init() {
// `{http.request.local.host}` | The host (IP) part of the local address the connection arrived on // `{http.request.local.host}` | The host (IP) part of the local address the connection arrived on
// `{http.request.local.port}` | The port part of the local address the connection arrived on // `{http.request.local.port}` | The port part of the local address the connection arrived on
// `{http.request.local}` | The local address the connection arrived on // `{http.request.local}` | The local address the connection arrived on
// `{http.request.remote.host}` | The host (IP) part of the remote client's address // `{http.request.remote.host}` | The host (IP) part of the remote client's address, if available (not known with HTTP/3 early data)
// `{http.request.remote.port}` | The port part of the remote client's address // `{http.request.remote.port}` | The port part of the remote client's address
// `{http.request.remote}` | The address of the remote client // `{http.request.remote}` | The address of the remote client
// `{http.request.scheme}` | The request scheme, typically `http` or `https` // `{http.request.scheme}` | The request scheme, typically `http` or `https`
@ -152,7 +152,7 @@ type App struct {
tlsApp *caddytls.TLS tlsApp *caddytls.TLS
// used temporarily between phases 1 and 2 of auto HTTPS // used temporarily between phases 1 and 2 of auto HTTPS
allCertDomains []string allCertDomains map[string]struct{}
} }
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.

View File

@ -25,6 +25,7 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/internal"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
) )
@ -65,12 +66,6 @@ type AutoHTTPSConfig struct {
// enabled. To force automated certificate management // enabled. To force automated certificate management
// regardless of loaded certificates, set this to true. // regardless of loaded certificates, set this to true.
IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"` IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"`
// If true, automatic HTTPS will prefer wildcard names
// and ignore non-wildcard names if both are available.
// This allows for writing a config with top-level host
// matchers without having those names produce certificates.
PreferWildcard bool `json:"prefer_wildcard,omitempty"`
} }
// automaticHTTPSPhase1 provisions all route matchers, determines // automaticHTTPSPhase1 provisions all route matchers, determines
@ -163,33 +158,8 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
} }
} }
// trim the list of domains covered by wildcards, if configured
if srv.AutoHTTPS.PreferWildcard {
wildcards := make(map[string]struct{})
for d := range serverDomainSet {
if strings.HasPrefix(d, "*.") {
wildcards[d[2:]] = struct{}{}
}
}
for d := range serverDomainSet {
if strings.HasPrefix(d, "*.") {
continue
}
base := d
if idx := strings.Index(d, "."); idx != -1 {
base = d[idx+1:]
}
if _, ok := wildcards[base]; ok {
delete(serverDomainSet, d)
}
}
}
// build the list of domains that could be used with ECH (if enabled) // build the list of domains that could be used with ECH (if enabled)
// so the TLS app can know to publish ECH configs for them; we do this // so the TLS app can know to publish ECH configs for them
// after trimming domains covered by wildcards because, presumably,
// if the user wants to use wildcard certs, they also want to use the
// wildcard for ECH, rather than individual subdomains
echDomains := make([]string, 0, len(serverDomainSet)) echDomains := make([]string, 0, len(serverDomainSet))
for d := range serverDomainSet { for d := range serverDomainSet {
echDomains = append(echDomains, d) echDomains = append(echDomains, d)
@ -295,19 +265,10 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
} }
} }
// we now have a list of all the unique names for which we need certs; // we now have a list of all the unique names for which we need certs
// turn the set into a slice so that phase 2 can use it
app.allCertDomains = make([]string, 0, len(uniqueDomainsForCerts))
var internal, tailscale []string var internal, tailscale []string
uniqueDomainsLoop: uniqueDomainsLoop:
for d := range uniqueDomainsForCerts { for d := range uniqueDomainsForCerts {
if !isTailscaleDomain(d) {
// whether or not there is already an automation policy for this
// name, we should add it to the list to manage a cert for it,
// unless it's a Tailscale domain, because we don't manage those
app.allCertDomains = append(app.allCertDomains, d)
}
// some names we've found might already have automation policies // some names we've found might already have automation policies
// explicitly specified for them; we should exclude those from // explicitly specified for them; we should exclude those from
// our hidden/implicit policy, since applying a name to more than // our hidden/implicit policy, since applying a name to more than
@ -346,6 +307,7 @@ uniqueDomainsLoop:
} }
if isTailscaleDomain(d) { if isTailscaleDomain(d) {
tailscale = append(tailscale, d) tailscale = append(tailscale, d)
delete(uniqueDomainsForCerts, d) // not managed by us; handled separately
} else if shouldUseInternal(d) { } else if shouldUseInternal(d) {
internal = append(internal, d) internal = append(internal, d)
} }
@ -475,6 +437,9 @@ redirServersLoop:
} }
} }
// persist the domains/IPs we're managing certs for through provisioning/startup
app.allCertDomains = uniqueDomainsForCerts
logger.Debug("adjusted config", logger.Debug("adjusted config",
zap.Reflect("tls", app.tlsApp), zap.Reflect("tls", app.tlsApp),
zap.Reflect("http", app)) zap.Reflect("http", app))
@ -777,7 +742,7 @@ func (app *App) automaticHTTPSPhase2() error {
return nil return nil
} }
app.logger.Info("enabling automatic TLS certificate management", app.logger.Info("enabling automatic TLS certificate management",
zap.Strings("domains", app.allCertDomains), zap.Strings("domains", internal.MaxSizeSubjectsListForLog(app.allCertDomains, 1000)),
) )
err := app.tlsApp.Manage(app.allCertDomains) err := app.tlsApp.Manage(app.allCertDomains)
if err != nil { if err != nil {

View File

@ -37,6 +37,10 @@ func init() {
// `{http.auth.user.*}` placeholders may be set for any authentication // `{http.auth.user.*}` placeholders may be set for any authentication
// modules that provide user metadata. // modules that provide user metadata.
// //
// In case of an error, the placeholder `{http.auth.<provider>.error}`
// will be set to the error message returned by the authentication
// provider.
//
// Its API is still experimental and may be subject to change. // Its API is still experimental and may be subject to change.
type Authentication struct { type Authentication struct {
// A set of authentication providers. If none are specified, // A set of authentication providers. If none are specified,
@ -71,6 +75,7 @@ func (a *Authentication) Provision(ctx caddy.Context) error {
} }
func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
var user User var user User
var authed bool var authed bool
var err error var err error
@ -80,6 +85,9 @@ func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
if c := a.logger.Check(zapcore.ErrorLevel, "auth provider returned error"); c != nil { if c := a.logger.Check(zapcore.ErrorLevel, "auth provider returned error"); c != nil {
c.Write(zap.String("provider", provName), zap.Error(err)) c.Write(zap.String("provider", provName), zap.Error(err))
} }
// Set the error from the authentication provider in a placeholder,
// so it can be used in the handle_errors directive.
repl.Set("http.auth."+provName+".error", err.Error())
continue continue
} }
if authed { if authed {
@ -90,7 +98,6 @@ func (a Authentication) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
return caddyhttp.Error(http.StatusUnauthorized, fmt.Errorf("not authenticated")) return caddyhttp.Error(http.StatusUnauthorized, fmt.Errorf("not authenticated"))
} }
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
repl.Set("http.auth.user.id", user.ID) repl.Set("http.auth.user.id", user.ID)
for k, v := range user.Metadata { for k, v := range user.Metadata {
repl.Set("http.auth.user."+k, v) repl.Set("http.auth.user."+k, v)

View File

@ -26,7 +26,7 @@
<path d="M9 7l4 0"/> <path d="M9 7l4 0"/>
<path d="M9 11l4 0"/> <path d="M9 11l4 0"/>
</svg> </svg>
{{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp" ".tiff" ".bmp" ".heif" ".heic" ".svg"}} {{- else if .HasExt ".jpg" ".jpeg" ".png" ".gif" ".webp" ".tiff" ".bmp" ".heif" ".heic" ".svg" ".avif"}}
{{- if eq .Tpl.Layout "grid"}} {{- if eq .Tpl.Layout "grid"}}
<img loading="lazy" src="{{.Name | pathEscape}}"> <img loading="lazy" src="{{.Name | pathEscape}}">
{{- else}} {{- else}}

View File

@ -274,7 +274,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
func (m *MatchFile) Provision(ctx caddy.Context) error { func (m *MatchFile) Provision(ctx caddy.Context) error {
m.logger = ctx.Logger() m.logger = ctx.Logger()
m.fsmap = ctx.Filesystems() m.fsmap = ctx.FileSystems()
if m.Root == "" { if m.Root == "" {
m.Root = "{http.vars.root}" m.Root = "{http.vars.root}"

View File

@ -117,7 +117,7 @@ func TestFileMatcher(t *testing.T) {
}, },
} { } {
m := &MatchFile{ m := &MatchFile{
fsmap: &filesystems.FilesystemMap{}, fsmap: &filesystems.FileSystemMap{},
Root: "./testdata", Root: "./testdata",
TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"}, TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/"},
} }
@ -229,7 +229,7 @@ func TestPHPFileMatcher(t *testing.T) {
}, },
} { } {
m := &MatchFile{ m := &MatchFile{
fsmap: &filesystems.FilesystemMap{}, fsmap: &filesystems.FileSystemMap{},
Root: "./testdata", Root: "./testdata",
TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php"}, TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php"},
SplitPath: []string{".php"}, SplitPath: []string{".php"},
@ -273,7 +273,7 @@ func TestPHPFileMatcher(t *testing.T) {
func TestFirstSplit(t *testing.T) { func TestFirstSplit(t *testing.T) {
m := MatchFile{ m := MatchFile{
SplitPath: []string{".php"}, SplitPath: []string{".php"},
fsmap: &filesystems.FilesystemMap{}, fsmap: &filesystems.FileSystemMap{},
} }
actual, remainder := m.firstSplit("index.PHP/somewhere") actual, remainder := m.firstSplit("index.PHP/somewhere")
expected := "index.PHP" expected := "index.PHP"

View File

@ -186,7 +186,7 @@ func (FileServer) CaddyModule() caddy.ModuleInfo {
func (fsrv *FileServer) Provision(ctx caddy.Context) error { func (fsrv *FileServer) Provision(ctx caddy.Context) error {
fsrv.logger = ctx.Logger() fsrv.logger = ctx.Logger()
fsrv.fsmap = ctx.Filesystems() fsrv.fsmap = ctx.FileSystems()
if fsrv.FileSystem == "" { if fsrv.FileSystem == "" {
fsrv.FileSystem = "{http.vars.fs}" fsrv.FileSystem = "{http.vars.fs}"
@ -300,8 +300,10 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
info, err := fs.Stat(fileSystem, filename) info, err := fs.Stat(fileSystem, filename)
if err != nil { if err != nil {
err = fsrv.mapDirOpenError(fileSystem, err, filename) err = fsrv.mapDirOpenError(fileSystem, err, filename)
if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrInvalid) { if errors.Is(err, fs.ErrNotExist) {
return fsrv.notFound(w, r, next) return fsrv.notFound(w, r, next)
} else if errors.Is(err, fs.ErrInvalid) {
return caddyhttp.Error(http.StatusBadRequest, err)
} else if errors.Is(err, fs.ErrPermission) { } else if errors.Is(err, fs.ErrPermission) {
return caddyhttp.Error(http.StatusForbidden, err) return caddyhttp.Error(http.StatusForbidden, err)
} }
@ -611,6 +613,11 @@ func (fsrv *FileServer) mapDirOpenError(fileSystem fs.FS, originalErr error, nam
return originalErr return originalErr
} }
var pathErr *fs.PathError
if errors.As(originalErr, &pathErr) {
return fs.ErrInvalid
}
parts := strings.Split(name, separator) parts := strings.Split(name, separator)
for i := range parts { for i := range parts {
if parts[i] == "" { if parts[i] == "" {

View File

@ -118,6 +118,11 @@ func (irh interceptedResponseHandler) WriteHeader(statusCode int) {
irh.ResponseRecorder.WriteHeader(statusCode) irh.ResponseRecorder.WriteHeader(statusCode)
} }
// EXPERIMENTAL: Subject to change or removal.
func (irh interceptedResponseHandler) Unwrap() http.ResponseWriter {
return irh.ResponseRecorder
}
// EXPERIMENTAL: Subject to change or removal. // EXPERIMENTAL: Subject to change or removal.
func (ir Intercept) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { func (ir Intercept) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
buf := bufPool.Get().(*bytes.Buffer) buf := bufPool.Get().(*bytes.Buffer)

View File

@ -309,7 +309,9 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
} }
}() }()
networkAddr, err := caddy.NewReplacer().ReplaceOrErr(upstream.Dial, true, true) repl := caddy.NewReplacer()
networkAddr, err := repl.ReplaceOrErr(upstream.Dial, true, true)
if err != nil { if err != nil {
if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "invalid use of placeholders in dial address for active health checks"); c != nil { if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "invalid use of placeholders in dial address for active health checks"); c != nil {
c.Write( c.Write(
@ -344,14 +346,24 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
return return
} }
hostAddr := addr.JoinHostPort(0) hostAddr := addr.JoinHostPort(0)
dialAddr := hostAddr
if addr.IsUnixNetwork() || addr.IsFdNetwork() { if addr.IsUnixNetwork() || addr.IsFdNetwork() {
// this will be used as the Host portion of a http.Request URL, and // this will be used as the Host portion of a http.Request URL, and
// paths to socket files would produce an error when creating URL, // paths to socket files would produce an error when creating URL,
// so use a fake Host value instead; unix sockets are usually local // so use a fake Host value instead; unix sockets are usually local
hostAddr = "localhost" hostAddr = "localhost"
} }
err = h.doActiveHealthCheck(DialInfo{Network: addr.Network, Address: dialAddr}, hostAddr, networkAddr, upstream)
// Fill in the dial info for the upstream
// If the upstream is set, use that instead
dialInfoUpstream := upstream
if h.HealthChecks.Active.Upstream != "" {
dialInfoUpstream = &Upstream{
Dial: h.HealthChecks.Active.Upstream,
}
}
dialInfo, _ := dialInfoUpstream.fillDialInfo(repl)
err = h.doActiveHealthCheck(dialInfo, hostAddr, networkAddr, upstream)
if err != nil { if err != nil {
if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "active health check failed"); c != nil { if c := h.HealthChecks.Active.logger.Check(zapcore.ErrorLevel, "active health check failed"); c != nil {
c.Write( c.Write(

View File

@ -17,7 +17,6 @@ package reverseproxy
import ( import (
"context" "context"
"fmt" "fmt"
"net/http"
"net/netip" "net/netip"
"strconv" "strconv"
"sync/atomic" "sync/atomic"
@ -100,8 +99,7 @@ func (u *Upstream) Full() bool {
// fillDialInfo returns a filled DialInfo for upstream u, using the request // fillDialInfo returns a filled DialInfo for upstream u, using the request
// context. Note that the returned value is not a pointer. // context. Note that the returned value is not a pointer.
func (u *Upstream) fillDialInfo(r *http.Request) (DialInfo, error) { func (u *Upstream) fillDialInfo(repl *caddy.Replacer) (DialInfo, error) {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
var addr caddy.NetworkAddress var addr caddy.NetworkAddress
// use provided dial address // use provided dial address

View File

@ -353,7 +353,7 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
h.NetworkProxyRaw = caddyconfig.JSONModuleObject(u, "from", "url", nil) h.NetworkProxyRaw = caddyconfig.JSONModuleObject(u, "from", "url", nil)
} }
if len(h.NetworkProxyRaw) != 0 { if len(h.NetworkProxyRaw) != 0 {
proxyMod, err := caddyCtx.LoadModule(h, "ForwardProxyRaw") proxyMod, err := caddyCtx.LoadModule(h, "NetworkProxyRaw")
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load network_proxy module: %v", err) return nil, fmt.Errorf("failed to load network_proxy module: %v", err)
} }
@ -382,6 +382,36 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
if err != nil { if err != nil {
return nil, fmt.Errorf("making TLS client config: %v", err) 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, "{") {
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)
if err != nil {
return nil, err
}
// but add our own handshake logic
repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
tlsConfig := rt.TLSClientConfig.Clone()
tlsConfig.ServerName = repl.ReplaceAll(tlsConfig.ServerName, "")
tlsConn := tls.Client(conn, tlsConfig)
// complete the handshake before returning the connection
if rt.TLSHandshakeTimeout != 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, rt.TLSHandshakeTimeout)
defer cancel()
}
err = tlsConn.HandshakeContext(ctx)
if err != nil {
_ = tlsConn.Close()
return nil, err
}
return tlsConn, nil
}
}
} }
if h.KeepAlive != nil { if h.KeepAlive != nil {
@ -453,45 +483,9 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
return rt, nil return rt, nil
} }
// replaceTLSServername checks TLS servername to see if it needs replacing
// if it does need replacing, it creates a new cloned HTTPTransport object to avoid any races
// and does the replacing of the TLS servername on that and returns the new object
// if no replacement is necessary it returns the original
func (h *HTTPTransport) replaceTLSServername(repl *caddy.Replacer) *HTTPTransport {
// check whether we have TLS and need to replace the servername in the TLSClientConfig
if h.TLSEnabled() && strings.Contains(h.TLS.ServerName, "{") {
// make a new h, "copy" the parts we don't need to touch, add a new *tls.Config and replace servername
newtransport := &HTTPTransport{
Resolver: h.Resolver,
TLS: h.TLS,
KeepAlive: h.KeepAlive,
Compression: h.Compression,
MaxConnsPerHost: h.MaxConnsPerHost,
DialTimeout: h.DialTimeout,
FallbackDelay: h.FallbackDelay,
ResponseHeaderTimeout: h.ResponseHeaderTimeout,
ExpectContinueTimeout: h.ExpectContinueTimeout,
MaxResponseHeaderSize: h.MaxResponseHeaderSize,
WriteBufferSize: h.WriteBufferSize,
ReadBufferSize: h.ReadBufferSize,
Versions: h.Versions,
Transport: h.Transport.Clone(),
h2cTransport: h.h2cTransport,
}
newtransport.Transport.TLSClientConfig.ServerName = repl.ReplaceAll(newtransport.Transport.TLSClientConfig.ServerName, "")
return newtransport
}
return h
}
// RoundTrip implements http.RoundTripper. // RoundTrip implements http.RoundTripper.
func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) { func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Try to replace TLS servername if needed h.SetScheme(req)
repl := req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
transport := h.replaceTLSServername(repl)
transport.SetScheme(req)
// use HTTP/3 if enabled (TODO: This is EXPERIMENTAL) // use HTTP/3 if enabled (TODO: This is EXPERIMENTAL)
if h.h3Transport != nil { if h.h3Transport != nil {
@ -507,7 +501,7 @@ func (h *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return h.h2cTransport.RoundTrip(req) return h.h2cTransport.RoundTrip(req)
} }
return transport.Transport.RoundTrip(req) return h.Transport.RoundTrip(req)
} }
// SetScheme ensures that the outbound request req // SetScheme ensures that the outbound request req
@ -652,7 +646,7 @@ func (t *TLSConfig) MakeTLSClientConfig(ctx caddy.Context) (*tls.Config, error)
return nil, fmt.Errorf("getting tls app: %v", err) return nil, fmt.Errorf("getting tls app: %v", err)
} }
tlsApp := tlsAppIface.(*caddytls.TLS) tlsApp := tlsAppIface.(*caddytls.TLS)
err = tlsApp.Manage([]string{t.ClientCertificateAutomate}) err = tlsApp.Manage(map[string]struct{}{t.ClientCertificateAutomate: {}})
if err != nil { if err != nil {
return nil, fmt.Errorf("managing client certificate: %v", err) return nil, fmt.Errorf("managing client certificate: %v", err)
} }

View File

@ -532,7 +532,7 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
// the dial address may vary per-request if placeholders are // the dial address may vary per-request if placeholders are
// used, so perform those replacements here; the resulting // used, so perform those replacements here; the resulting
// DialInfo struct should have valid network address syntax // DialInfo struct should have valid network address syntax
dialInfo, err := upstream.fillDialInfo(r) dialInfo, err := upstream.fillDialInfo(repl)
if err != nil { if err != nil {
return true, fmt.Errorf("making dial info: %v", err) return true, fmt.Errorf("making dial info: %v", err)
} }

View File

@ -91,8 +91,7 @@ func parseACMEServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
acmeServer.Policy.AllowWildcardNames = true acmeServer.Policy.AllowWildcardNames = true
case "allow": case "allow":
r := &RuleSet{} r := &RuleSet{}
for h.Next() { for nesting := h.Nesting(); h.NextBlock(nesting); {
for h.NextBlock(h.Nesting() - 1) {
if h.CountRemainingArgs() == 0 { if h.CountRemainingArgs() == 0 {
return nil, h.ArgErr() // TODO: return nil, h.ArgErr() // TODO:
} }
@ -105,15 +104,13 @@ func parseACMEServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
return nil, h.Errf("unrecognized 'allow' subdirective: %s", h.Val()) return nil, h.Errf("unrecognized 'allow' subdirective: %s", h.Val())
} }
} }
}
if acmeServer.Policy == nil { if acmeServer.Policy == nil {
acmeServer.Policy = &Policy{} acmeServer.Policy = &Policy{}
} }
acmeServer.Policy.Allow = r acmeServer.Policy.Allow = r
case "deny": case "deny":
r := &RuleSet{} r := &RuleSet{}
for h.Next() { for nesting := h.Nesting(); h.NextBlock(nesting); {
for h.NextBlock(h.Nesting() - 1) {
if h.CountRemainingArgs() == 0 { if h.CountRemainingArgs() == 0 {
return nil, h.ArgErr() // TODO: return nil, h.ArgErr() // TODO:
} }
@ -126,7 +123,6 @@ func parseACMEServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
return nil, h.Errf("unrecognized 'deny' subdirective: %s", h.Val()) return nil, h.Errf("unrecognized 'deny' subdirective: %s", h.Val())
} }
} }
}
if acmeServer.Policy == nil { if acmeServer.Policy == nil {
acmeServer.Policy = &Policy{} acmeServer.Policy = &Policy{}
} }

View File

@ -220,7 +220,7 @@ func (iss *ACMEIssuer) makeIssuerTemplate(ctx caddy.Context) (certmagic.ACMEIssu
} }
if len(iss.NetworkProxyRaw) != 0 { if len(iss.NetworkProxyRaw) != 0 {
proxyMod, err := ctx.LoadModule(iss, "ForwardProxyRaw") proxyMod, err := ctx.LoadModule(iss, "NetworkProxyRaw")
if err != nil { if err != nil {
return template, fmt.Errorf("failed to load network_proxy module: %v", err) return template, fmt.Errorf("failed to load network_proxy module: %v", err)
} }

View File

@ -5,6 +5,7 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"io" "io"
"net"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@ -143,6 +144,10 @@ func (hcg HTTPCertGetter) GetCertificate(ctx context.Context, hello *tls.ClientH
qs.Set("server_name", hello.ServerName) qs.Set("server_name", hello.ServerName)
qs.Set("signature_schemes", strings.Join(sigs, ",")) qs.Set("signature_schemes", strings.Join(sigs, ","))
qs.Set("cipher_suites", strings.Join(suites, ",")) qs.Set("cipher_suites", strings.Join(suites, ","))
localIP, _, err := net.SplitHostPort(hello.Conn.LocalAddr().String())
if err == nil && localIP != "" {
qs.Set("local_ip", localIP)
}
parsed.RawQuery = qs.Encode() parsed.RawQuery = qs.Encode()
req, err := http.NewRequestWithContext(hcg.ctx, http.MethodGet, parsed.String(), nil) req, err := http.NewRequestWithContext(hcg.ctx, http.MethodGet, parsed.String(), nil)

View File

@ -24,6 +24,7 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"reflect"
"strings" "strings"
"github.com/mholt/acmez/v3" "github.com/mholt/acmez/v3"
@ -461,6 +462,14 @@ func (p ConnectionPolicy) SettingsEmpty() bool {
p.InsecureSecretsLog == "" p.InsecureSecretsLog == ""
} }
// SettingsEmpty returns true if p's settings (fields
// except the matchers) are the same as q.
func (p ConnectionPolicy) SettingsEqual(q ConnectionPolicy) bool {
p.MatchersRaw = nil
q.MatchersRaw = nil
return reflect.DeepEqual(p, q)
}
// UnmarshalCaddyfile sets up the ConnectionPolicy from Caddyfile tokens. Syntax: // UnmarshalCaddyfile sets up the ConnectionPolicy from Caddyfile tokens. Syntax:
// //
// connection_policy { // connection_policy {

View File

@ -138,7 +138,6 @@ func (ech *ECH) Provision(ctx caddy.Context) ([]string, error) {
// all existing configs are now loaded; see if we need to make any new ones // all existing configs are now loaded; see if we need to make any new ones
// based on the input configuration, and also mark the most recent one(s) as // based on the input configuration, and also mark the most recent one(s) as
// current/active, so they can be used for ECH retries // current/active, so they can be used for ECH retries
for _, cfg := range ech.Configs { for _, cfg := range ech.Configs {
publicName := strings.ToLower(strings.TrimSpace(cfg.PublicName)) publicName := strings.ToLower(strings.TrimSpace(cfg.PublicName))
@ -279,7 +278,7 @@ func (t *TLS) publishECHConfigs() error {
// if all the (inner) domains have had this ECH config list published // if all the (inner) domains have had this ECH config list published
// by this publisher, then try the next publication config // by this publisher, then try the next publication config
if len(serverNamesSet) == 0 { if len(serverNamesSet) == 0 {
logger.Debug("ECH config list already published by publisher for associated domains", logger.Debug("ECH config list already published by publisher for associated domains (or no domains to publish for)",
zap.Uint8s("config_ids", configIDs), zap.Uint8s("config_ids", configIDs),
zap.String("publisher", publisherKey)) zap.String("publisher", publisherKey))
continue continue
@ -300,7 +299,7 @@ func (t *TLS) publishECHConfigs() error {
err := publisher.PublishECHConfigList(t.ctx, dnsNamesToPublish, echCfgListBin) err := publisher.PublishECHConfigList(t.ctx, dnsNamesToPublish, echCfgListBin)
if err == nil { if err == nil {
t.logger.Info("published ECH configuration list", t.logger.Info("published ECH configuration list",
zap.Strings("domains", publication.Domains), zap.Strings("domains", dnsNamesToPublish),
zap.Uint8s("config_ids", configIDs), zap.Uint8s("config_ids", configIDs),
zap.Error(err)) zap.Error(err))
// update publication history, so that we don't unnecessarily republish every time // update publication history, so that we don't unnecessarily republish every time
@ -390,17 +389,22 @@ func loadECHConfig(ctx caddy.Context, configID string) (echConfig, error) {
return echConfig{}, nil return echConfig{}, nil
} }
metaBytes, err := storage.Load(ctx, metaKey) metaBytes, err := storage.Load(ctx, metaKey)
if err != nil { if errors.Is(err, fs.ErrNotExist) {
logger.Warn("ECH config metadata file missing; will recreate at next publication",
zap.String("config_id", configID),
zap.Error(err))
} else if err != nil {
delErr := storage.Delete(ctx, cfgIDKey) delErr := storage.Delete(ctx, cfgIDKey)
if delErr != nil { if delErr != nil {
return echConfig{}, fmt.Errorf("error loading ECH metadata (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr) return echConfig{}, fmt.Errorf("error loading ECH config metadata (%v) and cleaning up parent storage key %s: %v", err, cfgIDKey, delErr)
} }
logger.Warn("could not load ECH metadata; deleted its config folder", logger.Warn("could not load ECH config metadata; deleted its folder",
zap.String("config_id", configID), zap.String("config_id", configID),
zap.Error(err)) zap.Error(err))
return echConfig{}, nil return echConfig{}, nil
} }
var meta echConfigMeta var meta echConfigMeta
if len(metaBytes) > 0 {
if err := json.Unmarshal(metaBytes, &meta); err != nil { if err := json.Unmarshal(metaBytes, &meta); err != nil {
// even though it's just metadata, reset the whole config since we can't reliably maintain it // even though it's just metadata, reset the whole config since we can't reliably maintain it
delErr := storage.Delete(ctx, cfgIDKey) delErr := storage.Delete(ctx, cfgIDKey)
@ -412,6 +416,7 @@ func loadECHConfig(ctx caddy.Context, configID string) (echConfig, error) {
zap.Error(err)) zap.Error(err))
return echConfig{}, nil return echConfig{}, nil
} }
}
cfg.privKeyBin = privKeyBytes cfg.privKeyBin = privKeyBytes
cfg.configBin = echConfigBytes cfg.configBin = echConfigBytes
@ -641,10 +646,6 @@ nextName:
} }
relName := libdns.RelativeName(domain+".", zone) relName := libdns.RelativeName(domain+".", zone)
// TODO: libdns.RelativeName should probably return "@" instead of "". (The latest commits of libdns do this, so remove this logic once upgraded.)
if relName == "" {
relName = "@"
}
// get existing records for this domain; we need to make sure another // get existing records for this domain; we need to make sure another
// record exists for it so we don't accidentally trample a wildcard; we // record exists for it so we don't accidentally trample a wildcard; we
@ -657,25 +658,28 @@ nextName:
zap.Error(err)) zap.Error(err))
continue continue
} }
var httpsRec libdns.Record var httpsRec libdns.ServiceBinding
var nameHasExistingRecord bool var nameHasExistingRecord bool
for _, rec := range recs { for _, rec := range recs {
if rec.Name == relName { rr := rec.RR()
if rr.Name == relName {
// CNAME records are exclusive of all other records, so we cannot publish an HTTPS // CNAME records are exclusive of all other records, so we cannot publish an HTTPS
// record for a domain that is CNAME'd. See #6922. // record for a domain that is CNAME'd. See #6922.
if rec.Type == "CNAME" { if rr.Type == "CNAME" {
dnsPub.logger.Warn("domain has CNAME record, so unable to publish ECH data to HTTPS record", dnsPub.logger.Warn("domain has CNAME record, so unable to publish ECH data to HTTPS record",
zap.String("domain", domain), zap.String("domain", domain),
zap.String("cname_value", rec.Value)) zap.String("cname_value", rr.Data))
continue nextName continue nextName
} }
nameHasExistingRecord = true nameHasExistingRecord = true
if rec.Type == "HTTPS" && (rec.Target == "" || rec.Target == ".") { if svcb, ok := rec.(libdns.ServiceBinding); ok && svcb.Scheme == "https" {
httpsRec = rec if svcb.Target == "" || svcb.Target == "." {
httpsRec = svcb
break break
} }
} }
} }
}
if !nameHasExistingRecord { if !nameHasExistingRecord {
// Turns out if you publish a DNS record for a name that doesn't have any DNS record yet, // Turns out if you publish a DNS record for a name that doesn't have any DNS record yet,
// any wildcard records won't apply for the name anymore, meaning if a wildcard A/AAAA record // any wildcard records won't apply for the name anymore, meaning if a wildcard A/AAAA record
@ -688,31 +692,24 @@ nextName:
zap.String("zone", zone)) zap.String("zone", zone))
continue continue
} }
params := make(svcParams) params := httpsRec.Params
if httpsRec.Value != "" { if params == nil {
params, err = parseSvcParams(httpsRec.Value) params = make(libdns.SvcParams)
if err != nil {
dnsPub.logger.Error("unable to parse existing DNS record to publish ECH data to HTTPS DNS record",
zap.String("domain", domain),
zap.String("https_rec_value", httpsRec.Value),
zap.Error(err))
continue
}
} }
// overwrite only the ech SvcParamKey // overwrite only the "ech" SvcParamKey
params["ech"] = []string{base64.StdEncoding.EncodeToString(configListBin)} params["ech"] = []string{base64.StdEncoding.EncodeToString(configListBin)}
// publish record // publish record
_, err = dnsPub.provider.SetRecords(ctx, zone, []libdns.Record{ _, err = dnsPub.provider.SetRecords(ctx, zone, []libdns.Record{
{ libdns.ServiceBinding{
// HTTPS and SVCB RRs: RFC 9460 (https://www.rfc-editor.org/rfc/rfc9460) // HTTPS and SVCB RRs: RFC 9460 (https://www.rfc-editor.org/rfc/rfc9460)
Type: "HTTPS", Scheme: "https",
Name: relName, Name: relName,
TTL: 5 * time.Minute, // TODO: low hard-coded value only temporary; change to a higher value once more field-tested and key rotation is implemented
Priority: 2, // allows a manual override with priority 1 Priority: 2, // allows a manual override with priority 1
Target: ".", Target: ".",
Value: params.String(), Params: params,
TTL: 1 * time.Minute, // TODO: for testing only
}, },
}) })
if err != nil { if err != nil {
@ -951,172 +948,6 @@ func newECHConfigID(ctx caddy.Context) (uint8, error) {
return 0, fmt.Errorf("depleted attempts to find an available config_id") return 0, fmt.Errorf("depleted attempts to find an available config_id")
} }
// svcParams represents SvcParamKey and SvcParamValue pairs as
// described in https://www.rfc-editor.org/rfc/rfc9460 (section 2.1).
type svcParams map[string][]string
// parseSvcParams parses service parameters into a structured type
// for safer manipulation.
func parseSvcParams(input string) (svcParams, error) {
if len(input) > 4096 {
return nil, fmt.Errorf("input too long: %d", len(input))
}
params := make(svcParams)
input = strings.TrimSpace(input) + " "
for cursor := 0; cursor < len(input); cursor++ {
var key, rawVal string
keyValPair:
for i := cursor; i < len(input); i++ {
switch input[i] {
case '=':
key = strings.ToLower(strings.TrimSpace(input[cursor:i]))
i++
cursor = i
var quoted bool
if input[cursor] == '"' {
quoted = true
i++
cursor = i
}
var escaped bool
for j := cursor; j < len(input); j++ {
switch input[j] {
case '"':
if !quoted {
return nil, fmt.Errorf("illegal DQUOTE at position %d", j)
}
if !escaped {
// end of quoted value
rawVal = input[cursor:j]
j++
cursor = j
break keyValPair
}
case '\\':
escaped = true
case ' ', '\t', '\n', '\r':
if !quoted {
// end of unquoted value
rawVal = input[cursor:j]
cursor = j
break keyValPair
}
default:
escaped = false
}
}
case ' ', '\t', '\n', '\r':
// key with no value (flag)
key = input[cursor:i]
params[key] = []string{}
cursor = i
break keyValPair
}
}
if rawVal == "" {
continue
}
var sb strings.Builder
var escape int // start of escape sequence (after \, so 0 is never a valid start)
for i := 0; i < len(rawVal); i++ {
ch := rawVal[i]
if escape > 0 {
// validate escape sequence
// (RFC 9460 Appendix A)
// escaped: "\" ( non-digit / dec-octet )
// non-digit: "%x21-2F / %x3A-7E"
// dec-octet: "0-255 as a 3-digit decimal number"
if ch >= '0' && ch <= '9' {
// advance to end of decimal octet, which must be 3 digits
i += 2
if i > len(rawVal) {
return nil, fmt.Errorf("value ends with incomplete escape sequence: %s", rawVal[escape:])
}
decOctet, err := strconv.Atoi(rawVal[escape : i+1])
if err != nil {
return nil, err
}
if decOctet < 0 || decOctet > 255 {
return nil, fmt.Errorf("invalid decimal octet in escape sequence: %s (%d)", rawVal[escape:i], decOctet)
}
sb.WriteRune(rune(decOctet))
escape = 0
continue
} else if (ch < 0x21 || ch > 0x2F) && (ch < 0x3A && ch > 0x7E) {
return nil, fmt.Errorf("illegal escape sequence %s", rawVal[escape:i])
}
}
switch ch {
case ';', '(', ')':
// RFC 9460 Appendix A:
// > contiguous = 1*( non-special / escaped )
// > non-special is VCHAR minus DQUOTE, ";", "(", ")", and "\".
return nil, fmt.Errorf("illegal character in value %q at position %d: %s", rawVal, i, string(ch))
case '\\':
escape = i + 1
default:
sb.WriteByte(ch)
escape = 0
}
}
params[key] = strings.Split(sb.String(), ",")
}
return params, nil
}
// String serializes svcParams into zone presentation format.
func (params svcParams) String() string {
var sb strings.Builder
for key, vals := range params {
if sb.Len() > 0 {
sb.WriteRune(' ')
}
sb.WriteString(key)
var hasVal, needsQuotes bool
for _, val := range vals {
if len(val) > 0 {
hasVal = true
}
if strings.ContainsAny(val, `" `) {
needsQuotes = true
}
if hasVal && needsQuotes {
break
}
}
if hasVal {
sb.WriteRune('=')
}
if needsQuotes {
sb.WriteRune('"')
}
for i, val := range vals {
if i > 0 {
sb.WriteRune(',')
}
val = strings.ReplaceAll(val, `"`, `\"`)
val = strings.ReplaceAll(val, `,`, `\,`)
sb.WriteString(val)
}
if needsQuotes {
sb.WriteRune('"')
}
}
return sb.String()
}
// ECHPublisher is an interface for publishing ECHConfigList values // ECHPublisher is an interface for publishing ECHConfigList values
// so that they can be used by clients. // so that they can be used by clients.
type ECHPublisher interface { type ECHPublisher interface {

View File

@ -1,129 +0,0 @@
package caddytls
import (
"reflect"
"testing"
)
func TestParseSvcParams(t *testing.T) {
for i, test := range []struct {
input string
expect svcParams
shouldErr bool
}{
{
input: `alpn="h2,h3" no-default-alpn ipv6hint=2001:db8::1 port=443`,
expect: svcParams{
"alpn": {"h2", "h3"},
"no-default-alpn": {},
"ipv6hint": {"2001:db8::1"},
"port": {"443"},
},
},
{
input: `key=value quoted="some string" flag`,
expect: svcParams{
"key": {"value"},
"quoted": {"some string"},
"flag": {},
},
},
{
input: `key="nested \"quoted\" value,foobar"`,
expect: svcParams{
"key": {`nested "quoted" value`, "foobar"},
},
},
{
input: `alpn=h3,h2 tls-supported-groups=29,23 no-default-alpn ech="foobar"`,
expect: svcParams{
"alpn": {"h3", "h2"},
"tls-supported-groups": {"29", "23"},
"no-default-alpn": {},
"ech": {"foobar"},
},
},
{
input: `escape=\097`,
expect: svcParams{
"escape": {"a"},
},
},
{
input: `escapes=\097\098c`,
expect: svcParams{
"escapes": {"abc"},
},
},
} {
actual, err := parseSvcParams(test.input)
if err != nil && !test.shouldErr {
t.Errorf("Test %d: Expected no error, but got: %v (input=%q)", i, err, test.input)
continue
} else if err == nil && test.shouldErr {
t.Errorf("Test %d: Expected an error, but got no error (input=%q)", i, test.input)
continue
}
if !reflect.DeepEqual(test.expect, actual) {
t.Errorf("Test %d: Expected %v, got %v (input=%q)", i, test.expect, actual, test.input)
continue
}
}
}
func TestSvcParamsString(t *testing.T) {
// this test relies on the parser also working
// because we can't just compare string outputs
// since map iteration is unordered
for i, test := range []svcParams{
{
"alpn": {"h2", "h3"},
"no-default-alpn": {},
"ipv6hint": {"2001:db8::1"},
"port": {"443"},
},
{
"key": {"value"},
"quoted": {"some string"},
"flag": {},
},
{
"key": {`nested "quoted" value`, "foobar"},
},
{
"alpn": {"h3", "h2"},
"tls-supported-groups": {"29", "23"},
"no-default-alpn": {},
"ech": {"foobar"},
},
} {
combined := test.String()
parsed, err := parseSvcParams(combined)
if err != nil {
t.Errorf("Test %d: Expected no error, but got: %v (input=%q)", i, err, test)
continue
}
if len(parsed) != len(test) {
t.Errorf("Test %d: Expected %d keys, but got %d", i, len(test), len(parsed))
continue
}
for key, expectedVals := range test {
if expected, actual := len(expectedVals), len(parsed[key]); expected != actual {
t.Errorf("Test %d: Expected key %s to have %d values, but had %d", i, key, expected, actual)
continue
}
for j, expected := range expectedVals {
if actual := parsed[key][j]; actual != expected {
t.Errorf("Test %d key %q value %d: Expected '%s' but got '%s'", i, key, j, expected, actual)
continue
}
}
}
if !reflect.DeepEqual(parsed, test) {
t.Errorf("Test %d: Expected %#v, got %#v", i, test, combined)
continue
}
}
}

View File

@ -33,6 +33,7 @@ import (
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/internal"
"github.com/caddyserver/caddy/v2/modules/caddyevents" "github.com/caddyserver/caddy/v2/modules/caddyevents"
) )
@ -55,8 +56,10 @@ type TLS struct {
// //
// The "automate" certificate loader module can be used to // The "automate" certificate loader module can be used to
// specify a list of subjects that need certificates to be // specify a list of subjects that need certificates to be
// managed automatically. The first matching automation // managed automatically, including subdomains that may
// policy will be applied to manage the certificate(s). // already be covered by a managed wildcard certificate.
// The first matching automation policy will be used
// to manage automated certificate(s).
// //
// All loaded certificates get pooled // All loaded certificates get pooled
// into the same cache and may be used to complete TLS // into the same cache and may be used to complete TLS
@ -123,7 +126,7 @@ type TLS struct {
dns any // technically, it should be any/all of the libdns interfaces (RecordSetter, RecordAppender, etc.) dns any // technically, it should be any/all of the libdns interfaces (RecordSetter, RecordAppender, etc.)
certificateLoaders []CertificateLoader certificateLoaders []CertificateLoader
automateNames []string automateNames map[string]struct{}
ctx caddy.Context ctx caddy.Context
storageCleanTicker *time.Ticker storageCleanTicker *time.Ticker
storageCleanStop chan struct{} storageCleanStop chan struct{}
@ -218,12 +221,13 @@ func (t *TLS) Provision(ctx caddy.Context) error {
// special case; these will be loaded in later using our automation facilities, // special case; these will be loaded in later using our automation facilities,
// which we want to avoid doing during provisioning // which we want to avoid doing during provisioning
if automateNames, ok := modIface.(*AutomateLoader); ok && automateNames != nil { if automateNames, ok := modIface.(*AutomateLoader); ok && automateNames != nil {
repl := caddy.NewReplacer() if t.automateNames == nil {
subjects := make([]string, len(*automateNames)) t.automateNames = make(map[string]struct{})
for i, sub := range *automateNames { }
subjects[i] = repl.ReplaceAll(sub, "") repl := caddy.NewReplacer()
for _, sub := range *automateNames {
t.automateNames[repl.ReplaceAll(sub, "")] = struct{}{}
} }
t.automateNames = append(t.automateNames, subjects...)
} else { } else {
return fmt.Errorf("loading certificates with 'automate' requires array of strings, got: %T", modIface) return fmt.Errorf("loading certificates with 'automate' requires array of strings, got: %T", modIface)
} }
@ -283,7 +287,7 @@ func (t *TLS) Provision(ctx caddy.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("provisioning default public automation policy: %v", err) return fmt.Errorf("provisioning default public automation policy: %v", err)
} }
for _, n := range t.automateNames { for n := range t.automateNames {
// if any names specified by the "automate" loader do not qualify for a public // if any names specified by the "automate" loader do not qualify for a public
// certificate, we should initialize a default internal automation policy // certificate, we should initialize a default internal automation policy
// (but we don't want to do this unnecessarily, since it may prompt for password!) // (but we don't want to do this unnecessarily, since it may prompt for password!)
@ -339,8 +343,14 @@ func (t *TLS) Provision(ctx caddy.Context) error {
// outer names should have certificates to reduce client brittleness // outer names should have certificates to reduce client brittleness
for _, outerName := range outerNames { for _, outerName := range outerNames {
if outerName == "" {
continue
}
if !t.HasCertificateForSubject(outerName) { if !t.HasCertificateForSubject(outerName) {
t.automateNames = append(t.automateNames, outerNames...) if t.automateNames == nil {
t.automateNames = make(map[string]struct{})
}
t.automateNames[outerName] = struct{}{}
} }
} }
} }
@ -449,7 +459,8 @@ func (t *TLS) Cleanup() error {
// app instance (which is being stopped) that are not managed or loaded by the // app instance (which is being stopped) that are not managed or loaded by the
// new app instance (which just started), and remove them from the cache // new app instance (which just started), and remove them from the cache
var noLongerManaged []certmagic.SubjectIssuer var noLongerManaged []certmagic.SubjectIssuer
var reManage, noLongerLoaded []string var noLongerLoaded []string
reManage := make(map[string]struct{})
for subj, currentIssuerKey := range t.managing { for subj, currentIssuerKey := range t.managing {
// It's a bit nuanced: managed certs can sometimes be different enough that we have to // It's a bit nuanced: managed certs can sometimes be different enough that we have to
// swap them out for a different one, even if they are for the same subject/domain. // swap them out for a different one, even if they are for the same subject/domain.
@ -467,7 +478,7 @@ func (t *TLS) Cleanup() error {
// then, if the next app is managing a cert for this name, but with a different issuer, re-manage it // then, if the next app is managing a cert for this name, but with a different issuer, re-manage it
if ok && nextIssuerKey != currentIssuerKey { if ok && nextIssuerKey != currentIssuerKey {
reManage = append(reManage, subj) reManage[subj] = struct{}{}
} }
} }
} }
@ -488,7 +499,7 @@ func (t *TLS) Cleanup() error {
if err := nextTLSApp.Manage(reManage); err != nil { if err := nextTLSApp.Manage(reManage); err != nil {
if c := t.logger.Check(zapcore.ErrorLevel, "re-managing unloaded certificates with new config"); c != nil { if c := t.logger.Check(zapcore.ErrorLevel, "re-managing unloaded certificates with new config"); c != nil {
c.Write( c.Write(
zap.Strings("subjects", reManage), zap.Strings("subjects", internal.MaxSizeSubjectsListForLog(reManage, 1000)),
zap.Error(err), zap.Error(err),
) )
} }
@ -509,17 +520,31 @@ func (t *TLS) Cleanup() error {
return nil return nil
} }
// Manage immediately begins managing names according to the // Manage immediately begins managing subjects according to the
// matching automation policy. // matching automation policy. The subjects are given in a map
func (t *TLS) Manage(names []string) error { // to prevent duplication and also because quick lookups are
// needed to assess wildcard coverage, if any, depending on
// certain config parameters (with lots of subjects, computing
// wildcard coverage over a slice can be highly inefficient).
func (t *TLS) Manage(subjects map[string]struct{}) error {
// for a large number of names, we can be more memory-efficient // for a large number of names, we can be more memory-efficient
// by making only one certmagic.Config for all the names that // by making only one certmagic.Config for all the names that
// use that config, rather than calling ManageAsync once for // use that config, rather than calling ManageAsync once for
// every name; so first, bin names by AutomationPolicy // every name; so first, bin names by AutomationPolicy
policyToNames := make(map[*AutomationPolicy][]string) policyToNames := make(map[*AutomationPolicy][]string)
for _, name := range names { for subj := range subjects {
ap := t.getAutomationPolicyForName(name) ap := t.getAutomationPolicyForName(subj)
policyToNames[ap] = append(policyToNames[ap], name) // by default, if a wildcard that covers the subj is also being
// managed, either by a previous call to Manage or by this one,
// prefer using that over individual certs for its subdomains;
// but users can disable this and force getting a certificate for
// subdomains by adding the name to the 'automate' cert loader
if t.managingWildcardFor(subj, subjects) {
if _, ok := t.automateNames[subj]; !ok {
continue
}
}
policyToNames[ap] = append(policyToNames[ap], subj)
} }
// now that names are grouped by policy, we can simply make one // now that names are grouped by policy, we can simply make one
@ -530,7 +555,7 @@ func (t *TLS) Manage(names []string) error {
if err != nil { if err != nil {
const maxNamesToDisplay = 100 const maxNamesToDisplay = 100
if len(names) > maxNamesToDisplay { if len(names) > maxNamesToDisplay {
names = append(names[:maxNamesToDisplay], fmt.Sprintf("(%d more...)", len(names)-maxNamesToDisplay)) names = append(names[:maxNamesToDisplay], fmt.Sprintf("(and %d more...)", len(names)-maxNamesToDisplay))
} }
return fmt.Errorf("automate: manage %v: %v", names, err) return fmt.Errorf("automate: manage %v: %v", names, err)
} }
@ -555,6 +580,43 @@ func (t *TLS) Manage(names []string) error {
return nil return nil
} }
// managingWildcardFor returns true if the app is managing a certificate that covers that
// subject name (including consideration of wildcards), either from its internal list of
// names that it IS managing certs for, or from the otherSubjsToManage which includes names
// that WILL be managed.
func (t *TLS) managingWildcardFor(subj string, otherSubjsToManage map[string]struct{}) bool {
// TODO: we could also consider manually-loaded certs using t.HasCertificateForSubject(),
// but that does not account for how manually-loaded certs may be restricted as to which
// hostnames or ClientHellos they can be used with by tags, etc; I don't *think* anyone
// necessarily wants this anyway, but I thought I'd note this here for now (if we did
// consider manually-loaded certs, we'd probably want to rename the method since it
// wouldn't be just about managed certs anymore)
// IP addresses must match exactly
if ip := net.ParseIP(subj); ip != nil {
_, managing := t.managing[subj]
return managing
}
// replace labels of the domain with wildcards until we get a match
labels := strings.Split(subj, ".")
for i := range labels {
if labels[i] == "*" {
continue
}
labels[i] = "*"
candidate := strings.Join(labels, ".")
if _, ok := t.managing[candidate]; ok {
return true
}
if _, ok := otherSubjsToManage[candidate]; ok {
return true
}
}
return false
}
// RegisterServerNames registers the provided DNS names with the TLS app. // RegisterServerNames registers the provided DNS names with the TLS app.
// This is currently used to auto-publish Encrypted ClientHello (ECH) // This is currently used to auto-publish Encrypted ClientHello (ECH)
// configurations, if enabled. Use of this function by apps using the TLS // configurations, if enabled. Use of this function by apps using the TLS