Compare commits

...

7 Commits

Author SHA1 Message Date
Matthew Holt cc58caa109 go.mod: Upgrade quic-go to v0.59.1
Tests / goreleaser-check (push) Has been skipped
Tests / test (s390x on IBM Z) (push) Has been skipped
Tests / test (./cmd/caddy/caddy, ~1.26.0, ubuntu-latest, 0, 1.26, linux) (push) Failing after 1m32s
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Successful in 2m47s
Cross-Build / build (~1.26.0, 1.26, aix) (push) Successful in 2m48s
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Successful in 1m26s
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Successful in 3m24s
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Successful in 3m25s
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Successful in 1m23s
Cross-Build / build (~1.26.0, 1.26, linux) (push) Successful in 2m50s
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Successful in 2m53s
Lint / lint (ubuntu-latest, linux) (push) Successful in 2m21s
Lint / dependency-review (push) Failing after 1m14s
Lint / govulncheck (push) Successful in 1m44s
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Successful in 4m26s
Cross-Build / build (~1.26.0, 1.26, windows) (push) Successful in 4m27s
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Failing after 7m5s
Tests / test (./cmd/caddy/caddy, ~1.26.0, macos-14, 0, 1.26, mac) (push) Has been cancelled
Tests / test (./cmd/caddy/caddy.exe, ~1.26.0, windows-latest, True, 1.26, windows) (push) Has been cancelled
Lint / lint (macos-14, mac) (push) Has been cancelled
Lint / lint (windows-latest, windows) (push) Has been cancelled
2026-05-11 17:33:42 -06:00
Br1an d80774cb3f metrics: Add nil check for metricsHandler in AdminMetrics.serveHTTP (#7553)
* metrics: Add nil check for metricsHandler in AdminMetrics.serveHTTP

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

* admin: provision router modules before registering routes

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

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

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

* fix: go fmt admin.go

---------

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

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

* test: cover nested escaped try_files rewrite paths

* test: cover encoded slash try_files rewrite paths

* fix: assert file matcher placeholder as string

---------

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

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

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

* respond to review comments

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

* remove unnecessary mutex lock

* refactor: -integration test, +explicit cases

* refactor: remove redundant test, add comment

* rename file and add header

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

* tls: centralise HTTPS RR ALPN defaults and registration

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

* http: centralise effective protocol resolution for HTTPS RR ALPN
2026-05-10 13:10:29 +10:00
17 changed files with 571 additions and 168 deletions
+13 -38
View File
@@ -120,10 +120,6 @@ type AdminConfig struct {
// //
// EXPERIMENTAL: This feature is subject to change. // EXPERIMENTAL: This feature is subject to change.
Remote *RemoteAdmin `json:"remote,omitempty"` Remote *RemoteAdmin `json:"remote,omitempty"`
// Holds onto the routers so that we can later provision them
// if they require provisioning.
routers []AdminRouter
} }
// ConfigSettings configures the management of configuration. // ConfigSettings configures the management of configuration.
@@ -222,7 +218,7 @@ type AdminPermissions struct {
// newAdminHandler reads admin's config and returns an http.Handler suitable // newAdminHandler reads admin's config and returns an http.Handler suitable
// for use in an admin endpoint server, which will be listening on listenAddr. // for use in an admin endpoint server, which will be listening on listenAddr.
func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Context) adminHandler { func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, ctx Context) (adminHandler, error) {
muxWrap := adminHandler{mux: http.NewServeMux()} muxWrap := adminHandler{mux: http.NewServeMux()}
// secure the local or remote endpoint respectively // secure the local or remote endpoint respectively
@@ -279,34 +275,21 @@ func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, _ Co
// register third-party module endpoints // register third-party module endpoints
for _, m := range GetModules("admin.api") { for _, m := range GetModules("admin.api") {
router := m.New().(AdminRouter) router := m.New().(AdminRouter)
// provision the router before registering its routes, so
// handlers have access to all provisioned state
if provisioner, ok := router.(Provisioner); ok {
if err := provisioner.Provision(ctx); err != nil {
return adminHandler{}, fmt.Errorf("provisioning admin router module %s: %v", m.ID, err)
}
}
for _, route := range router.Routes() { for _, route := range router.Routes() {
addRoute(route.Pattern, handlerLabel, route.Handler) addRoute(route.Pattern, handlerLabel, route.Handler)
} }
admin.routers = append(admin.routers, router)
} }
return muxWrap return muxWrap, nil
}
// provisionAdminRouters provisions all the router modules
// in the admin.api namespace that need provisioning.
func (admin *AdminConfig) provisionAdminRouters(ctx Context) error {
for _, router := range admin.routers {
provisioner, ok := router.(Provisioner)
if !ok {
continue
}
err := provisioner.Provision(ctx)
if err != nil {
return err
}
}
// We no longer need the routers once provisioned, allow for GC
admin.routers = nil
return nil
} }
// allowedOrigins returns a list of origins that are allowed. // allowedOrigins returns a list of origins that are allowed.
@@ -430,11 +413,7 @@ func replaceLocalAdminServer(cfg *Config, ctx Context) error {
return err return err
} }
handler := cfg.Admin.newAdminHandler(addr, false, ctx) handler, err := cfg.Admin.newAdminHandler(addr, false, ctx)
// run the provisioners for loaded modules to make sure local
// state is properly re-initialized in the new admin server
err = cfg.Admin.provisionAdminRouters(ctx)
if err != nil { if err != nil {
return err return err
} }
@@ -558,11 +537,7 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
// make the HTTP handler but disable Host/Origin enforcement // make the HTTP handler but disable Host/Origin enforcement
// because we are using TLS authentication instead // because we are using TLS authentication instead
handler := cfg.Admin.newAdminHandler(addr, true, ctx) handler, err := cfg.Admin.newAdminHandler(addr, true, ctx)
// run the provisioners for loaded modules to make sure local
// state is properly re-initialized in the new admin server
err = cfg.Admin.provisionAdminRouters(ctx)
if err != nil { if err != nil {
return err return err
} }
+9 -15
View File
@@ -340,7 +340,10 @@ func TestAdminHandlerBuiltinRouteErrors(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Failed to parse address: %v", err) t.Fatalf("Failed to parse address: %v", err)
} }
handler := cfg.Admin.newAdminHandler(addr, false, Context{}) handler, err := cfg.Admin.newAdminHandler(addr, false, Context{})
if err != nil {
t.Fatalf("Failed to create admin handler: %v", err)
}
tests := []struct { tests := []struct {
name string name string
@@ -461,7 +464,10 @@ func TestNewAdminHandlerRouterRegistration(t *testing.T) {
admin := &AdminConfig{ admin := &AdminConfig{
EnforceOrigin: false, EnforceOrigin: false,
} }
handler := admin.newAdminHandler(addr, false, Context{}) handler, err := admin.newAdminHandler(addr, false, Context{})
if err != nil {
t.Fatalf("Failed to create admin handler: %v", err)
}
req := httptest.NewRequest("GET", "/mock", nil) req := httptest.NewRequest("GET", "/mock", nil)
req.Host = "localhost:2019" req.Host = "localhost:2019"
@@ -473,10 +479,6 @@ func TestNewAdminHandlerRouterRegistration(t *testing.T) {
t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code) t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code)
t.Logf("Response body: %s", rr.Body.String()) t.Logf("Response body: %s", rr.Body.String())
} }
if len(admin.routers) != 1 {
t.Errorf("Expected 1 router to be stored, got %d", len(admin.routers))
}
} }
type mockProvisionableRouter struct { type mockProvisionableRouter struct {
@@ -514,19 +516,16 @@ func TestAdminRouterProvisioning(t *testing.T) {
name string name string
provisionErr error provisionErr error
wantErr bool wantErr bool
routersAfter int // expected number of routers after provisioning
}{ }{
{ {
name: "successful provisioning", name: "successful provisioning",
provisionErr: nil, provisionErr: nil,
wantErr: false, wantErr: false,
routersAfter: 0,
}, },
{ {
name: "provisioning error", name: "provisioning error",
provisionErr: fmt.Errorf("provision failed"), provisionErr: fmt.Errorf("provision failed"),
wantErr: true, wantErr: true,
routersAfter: 1,
}, },
} }
@@ -562,8 +561,7 @@ func TestAdminRouterProvisioning(t *testing.T) {
t.Fatalf("Failed to parse address: %v", err) t.Fatalf("Failed to parse address: %v", err)
} }
_ = admin.newAdminHandler(addr, false, Context{}) _, err = admin.newAdminHandler(addr, false, Context{})
err = admin.provisionAdminRouters(Context{})
if test.wantErr { if test.wantErr {
if err == nil { if err == nil {
@@ -574,10 +572,6 @@ func TestAdminRouterProvisioning(t *testing.T) {
t.Errorf("Expected no error but got: %v", err) t.Errorf("Expected no error but got: %v", err)
} }
} }
if len(admin.routers) != test.routersAfter {
t.Errorf("Expected %d routers after provisioning, got %d", test.routersAfter, len(admin.routers))
}
}) })
} }
} }
-7
View File
@@ -440,13 +440,6 @@ func run(newCfg *Config, start bool) (Context, error) {
} }
}() }()
// Provision any admin routers which may need to access
// some of the other apps at runtime
err = ctx.cfg.Admin.provisionAdminRouters(ctx)
if err != nil {
return ctx, err
}
// Start // Start
err = func() error { err = func() error {
started := make([]string, 0, len(ctx.cfg.apps)) started := make([]string, 0, len(ctx.cfg.apps))
+1 -1
View File
@@ -20,7 +20,7 @@ require (
github.com/klauspost/cpuid/v2 v2.3.0 github.com/klauspost/cpuid/v2 v2.3.0
github.com/mholt/acmez/v3 v3.1.6 github.com/mholt/acmez/v3 v3.1.6
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/quic-go/quic-go v0.59.0 github.com/quic-go/quic-go v0.59.1
github.com/smallstep/certificates v0.30.2 github.com/smallstep/certificates v0.30.2
github.com/smallstep/nosql v0.8.0 github.com/smallstep/nosql v0.8.0
github.com/smallstep/truststore v0.13.0 github.com/smallstep/truststore v0.13.0
+2 -2
View File
@@ -280,8 +280,8 @@ github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEy
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
+2 -32
View File
@@ -20,7 +20,6 @@ import (
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
"maps"
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
@@ -241,12 +240,7 @@ func (app *App) Provision(ctx caddy.Context) error {
// if no protocols configured explicitly, enable all except h2c // if no protocols configured explicitly, enable all except h2c
if len(srv.Protocols) == 0 { if len(srv.Protocols) == 0 {
srv.Protocols = []string{"h1", "h2", "h3"} srv.Protocols = srv.protocolsWithDefaults()
}
srvProtocolsUnique := map[string]struct{}{}
for _, srvProtocol := range srv.Protocols {
srvProtocolsUnique[srvProtocol] = struct{}{}
} }
if srv.ListenProtocols != nil { if srv.ListenProtocols != nil {
@@ -257,31 +251,7 @@ func (app *App) Provision(ctx caddy.Context) error {
for i, lnProtocols := range srv.ListenProtocols { for i, lnProtocols := range srv.ListenProtocols {
if lnProtocols != nil { if lnProtocols != nil {
// populate empty listen protocols with server protocols srv.ListenProtocols[i] = srv.listenerProtocolsWithDefaults(lnProtocols)
lnProtocolsDefault := false
var lnProtocolsInclude []string
srvProtocolsInclude := maps.Clone(srvProtocolsUnique)
// keep existing listener protocols unless they are empty
for _, lnProtocol := range lnProtocols {
if lnProtocol == "" {
lnProtocolsDefault = true
} else {
lnProtocolsInclude = append(lnProtocolsInclude, lnProtocol)
delete(srvProtocolsInclude, lnProtocol)
}
}
// append server protocols to listener protocols if any listener protocols were empty
if lnProtocolsDefault {
for _, srvProtocol := range srv.Protocols {
if _, ok := srvProtocolsInclude[srvProtocol]; ok {
lnProtocolsInclude = append(lnProtocolsInclude, srvProtocol)
}
}
}
srv.ListenProtocols[i] = lnProtocolsInclude
} }
} }
} }
+15 -1
View File
@@ -173,7 +173,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
for d := range serverDomainSet { for d := range serverDomainSet {
echDomains = append(echDomains, d) echDomains = append(echDomains, d)
} }
app.tlsApp.RegisterServerNames(echDomains) app.tlsApp.RegisterServerNames(echDomains, httpsRRALPNs(srv))
// nothing more to do here if there are no domains that qualify for // nothing more to do here if there are no domains that qualify for
// automatic HTTPS and there are no explicit TLS connection policies: // automatic HTTPS and there are no explicit TLS connection policies:
@@ -574,6 +574,20 @@ func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route {
} }
} }
func httpsRRALPNs(srv *Server) []string {
alpn := make(map[string]struct{}, 3)
if srv.protocol("h3") {
alpn["h3"] = struct{}{}
}
if srv.protocol("h2") {
alpn["h2"] = struct{}{}
}
if srv.protocol("h1") {
alpn["http/1.1"] = struct{}{}
}
return caddytls.OrderedHTTPSRRALPN(alpn)
}
// createAutomationPolicies ensures that automated certificates for this // createAutomationPolicies ensures that automated certificates for this
// app are managed properly. This adds up to two automation policies: // app are managed properly. This adds up to two automation policies:
// one for the public names, and one for the internal names. If a catch-all // one for the public names, and one for the internal names. If a catch-all
+33 -30
View File
@@ -1,44 +1,47 @@
package caddyhttp package caddyhttp
import ( import (
"reflect"
"testing" "testing"
"github.com/caddyserver/caddy/v2"
) )
func TestRecordAutoHTTPSRedirectAddressPrefersHTTPSPort(t *testing.T) { func TestHTTPSRRALPNsDefaultProtocols(t *testing.T) {
app := &App{HTTPSPort: 443} srv := &Server{}
redirDomains := make(map[string][]caddy.NetworkAddress)
app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", StartPort: 2345, EndPort: 2345}) got := httpsRRALPNs(srv)
app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", StartPort: 443, EndPort: 443}) want := []string{"h3", "h2", "http/1.1"}
app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", StartPort: 8443, EndPort: 8443})
got := redirDomains["example.com"] if !reflect.DeepEqual(got, want) {
if len(got) != 1 { t.Fatalf("unexpected ALPN values: got %v want %v", got, want)
t.Fatalf("expected 1 redirect address, got %d: %#v", len(got), got)
}
if got[0].StartPort != 443 {
t.Fatalf("expected redirect to prefer HTTPS port 443, got %#v", got[0])
} }
} }
func TestRecordAutoHTTPSRedirectAddressKeepsAllBindAddressesOnWinningPort(t *testing.T) { func TestHTTPSRRALPNsListenProtocolOverrides(t *testing.T) {
app := &App{HTTPSPort: 443} srv := &Server{
redirDomains := make(map[string][]caddy.NetworkAddress) Protocols: []string{"h1", "h2"},
ListenProtocols: [][]string{
app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", Host: "10.0.0.189", StartPort: 8443, EndPort: 8443}) {"h1"},
app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", Host: "10.0.0.189", StartPort: 443, EndPort: 443}) nil,
app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", Host: "2603:c024:8002:9500:9eb:e5d3:3975:d056", StartPort: 443, EndPort: 443}) {},
{"h3", ""},
got := redirDomains["example.com"] },
if len(got) != 2 {
t.Fatalf("expected 2 redirect addresses for both bind addresses on the winning port, got %d: %#v", len(got), got)
} }
if got[0].StartPort != 443 || got[1].StartPort != 443 {
t.Fatalf("expected both redirect addresses to stay on HTTPS port 443, got %#v", got) got := httpsRRALPNs(srv)
} want := []string{"h3", "h2", "http/1.1"}
if got[0].Host != "10.0.0.189" || got[1].Host != "2603:c024:8002:9500:9eb:e5d3:3975:d056" {
t.Fatalf("expected both bind addresses to be preserved, got %#v", got) if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected ALPN values: got %v want %v", got, want)
}
}
func TestHTTPSRRALPNsIgnoresH2COnly(t *testing.T) {
srv := &Server{
Protocols: []string{"h2c"},
}
got := httpsRRALPNs(srv)
if len(got) != 0 {
t.Fatalf("unexpected ALPN values: got %v want none", got)
} }
} }
@@ -28,6 +28,7 @@ import (
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/internal/filesystems" "github.com/caddyserver/caddy/v2/internal/filesystems"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
) )
type testCase struct { type testCase struct {
@@ -188,6 +189,105 @@ func fileMatcherTest(t *testing.T, i int, tc testCase) {
} }
} }
func TestTryFilesRewriteEscapesMatchedPath(t *testing.T) {
root := t.TempDir()
tests := []struct {
name string
requestTarget string
filename string
extraFiles []string
wantPath string
wantRequestURI string
skipWindows bool
}{
{
name: "question mark in path",
requestTarget: "/%3F.html",
filename: "?.html",
wantPath: "/?.html",
wantRequestURI: "/%3F.html",
skipWindows: true,
},
{
name: "percent in path",
requestTarget: "/%25.html",
filename: "%.html",
wantPath: "/%.html",
wantRequestURI: "/%25.html",
},
{
name: "encoded question mark remains percent-encoded",
requestTarget: "/%253F.html",
filename: "%3F.html",
wantPath: "/%3F.html",
wantRequestURI: "/%253F.html",
},
{
name: "question mark in nested path",
requestTarget: "/nested/%3F.html",
filename: filepath.Join("nested", "?.html"),
wantPath: "/nested/?.html",
wantRequestURI: "/nested/%3F.html",
skipWindows: true,
},
{
name: "encoded slash in filename does not conflict with nesting",
requestTarget: "/nested%252Ffile.html",
filename: "nested%2Ffile.html",
extraFiles: []string{filepath.Join("nested", "file.html")},
wantPath: "/nested%2Ffile.html",
wantRequestURI: "/nested%252Ffile.html",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if tc.skipWindows && runtime.GOOS == "windows" {
t.Skip("Windows file names cannot contain question marks")
}
for _, name := range append([]string{tc.filename}, tc.extraFiles...) {
filename := filepath.Join(root, name)
if err := os.MkdirAll(filepath.Dir(filename), 0o700); err != nil {
t.Fatalf("creating test file parent directory: %v", err)
}
if err := os.WriteFile(filename, []byte(name), 0o600); err != nil {
t.Fatalf("writing test file: %v", err)
}
}
m := &MatchFile{
fsmap: &filesystems.FileSystemMap{},
Root: root,
TryFiles: []string{"{http.request.uri.path}"},
}
req := httptest.NewRequest(http.MethodGet, "http://example.com"+tc.requestTarget, nil)
repl := caddyhttp.NewTestReplacer(req)
matched, err := m.MatchWithError(req)
if err != nil {
t.Fatalf("matching file: %v", err)
}
if !matched {
t.Fatalf("expected request %s to match %s", tc.requestTarget, tc.filename)
}
rewrite.Rewrite{URI: "{http.matchers.file.relative}"}.Rewrite(req, repl)
if req.URL.Path != tc.wantPath {
t.Errorf("rewritten path = %q, want %q", req.URL.Path, tc.wantPath)
}
if req.RequestURI != tc.wantRequestURI {
t.Errorf("rewritten request URI = %q, want %q", req.RequestURI, tc.wantRequestURI)
}
if req.URL.RawQuery != "" {
t.Errorf("rewritten raw query = %q, want empty", req.URL.RawQuery)
}
})
}
}
func TestPHPFileMatcher(t *testing.T) { func TestPHPFileMatcher(t *testing.T) {
for i, tc := range []struct { for i, tc := range []struct {
path string path string
+26 -6
View File
@@ -211,12 +211,7 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
var newPath, newQuery, newFrag string var newPath, newQuery, newFrag string
if path != "" { if path != "" {
// replace the `path` placeholder to escaped path path = escapePathPlaceholders(path, r, repl)
pathPlaceholder := "{http.request.uri.path}"
if strings.Contains(path, pathPlaceholder) {
path = strings.ReplaceAll(path, pathPlaceholder, r.URL.EscapedPath())
}
newPath = repl.ReplaceAll(path, "") newPath = repl.ReplaceAll(path, "")
} }
@@ -300,6 +295,31 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
return r.Method != oldMethod || r.RequestURI != oldURI return r.Method != oldMethod || r.RequestURI != oldURI
} }
func escapePathPlaceholders(path string, r *http.Request, repl *caddy.Replacer) string {
// Replace path-valued placeholders in escaped form before the URI is parsed,
// otherwise literal '?' and '%' bytes from the path can be interpreted as URI
// delimiters or percent-escape sequences during the rewrite.
pathPlaceholder := "{http.request.uri.path}"
if strings.Contains(path, pathPlaceholder) {
path = strings.ReplaceAll(path, pathPlaceholder, r.URL.EscapedPath())
}
fileMatchRelativePlaceholder := "{http.matchers.file.relative}"
if strings.Contains(path, fileMatchRelativePlaceholder) {
if val, ok := repl.Get("http.matchers.file.relative"); ok {
if relativePath, ok := val.(string); ok {
path = strings.ReplaceAll(path, fileMatchRelativePlaceholder, escapePathPreservingSlashes(relativePath))
}
}
}
return path
}
func escapePathPreservingSlashes(path string) string {
return strings.ReplaceAll(url.PathEscape(path), "%2F", "/")
}
// buildQueryString takes an input query string and // buildQueryString takes an input query string and
// performs replacements on each component, returning // performs replacements on each component, returning
// the resulting query string. This function appends // the resulting query string. This function appends
+47 -9
View File
@@ -300,6 +300,8 @@ type Server struct {
onStopFuncs []func(context.Context) error // TODO: Experimental (Nov. 2023) onStopFuncs []func(context.Context) error // TODO: Experimental (Nov. 2023)
} }
var defaultProtocols = []string{"h1", "h2", "h3"}
var ( var (
ServerHeader = "Caddy" ServerHeader = "Caddy"
serverHeader = []string{ServerHeader} serverHeader = []string{ServerHeader}
@@ -899,22 +901,58 @@ func (s *Server) logRequest(
// protocol returns true if the protocol proto is configured/enabled. // protocol returns true if the protocol proto is configured/enabled.
func (s *Server) protocol(proto string) bool { func (s *Server) protocol(proto string) bool {
if s.ListenProtocols == nil { if s.ListenProtocols == nil {
if slices.Contains(s.Protocols, proto) { return slices.Contains(s.protocolsWithDefaults(), proto)
}
for _, lnProtocols := range s.ListenProtocols {
if slices.Contains(s.listenerProtocolsWithDefaults(lnProtocols), proto) {
return true return true
} }
} else {
for _, lnProtocols := range s.ListenProtocols {
for _, lnProtocol := range lnProtocols {
if lnProtocol == "" && slices.Contains(s.Protocols, proto) || lnProtocol == proto {
return true
}
}
}
} }
return false return false
} }
func (s *Server) protocolsWithDefaults() []string {
if len(s.Protocols) == 0 {
return defaultProtocols
}
return s.Protocols
}
func (s *Server) listenerProtocolsWithDefaults(lnProtocols []string) []string {
serverProtocols := s.protocolsWithDefaults()
if len(lnProtocols) == 0 {
return serverProtocols
}
lnProtocolsDefault := false
lnProtocolsInclude := make([]string, 0, len(lnProtocols)+len(serverProtocols))
srvProtocolsInclude := make(map[string]struct{}, len(serverProtocols))
for _, srvProtocol := range serverProtocols {
srvProtocolsInclude[srvProtocol] = struct{}{}
}
for _, lnProtocol := range lnProtocols {
if lnProtocol == "" {
lnProtocolsDefault = true
continue
}
lnProtocolsInclude = append(lnProtocolsInclude, lnProtocol)
delete(srvProtocolsInclude, lnProtocol)
}
if lnProtocolsDefault {
for _, srvProtocol := range serverProtocols {
if _, ok := srvProtocolsInclude[srvProtocol]; ok {
lnProtocolsInclude = append(lnProtocolsInclude, srvProtocol)
}
}
}
return lnProtocolsInclude
}
// Listeners returns the server's listeners. These are active listeners, // Listeners returns the server's listeners. These are active listeners,
// so calling Accept() or Close() on them will probably break things. // so calling Accept() or Close() on them will probably break things.
// They are made available here for read-only purposes (e.g. Addr()) // They are made available here for read-only purposes (e.g. Addr())
+30 -2
View File
@@ -36,13 +36,22 @@ func init() {
// Templates is a middleware which executes response bodies as Go templates. // Templates is a middleware which executes response bodies as Go templates.
// The syntax is documented in the Go standard library's // The syntax is documented in the Go standard library's
// [text/template package](https://golang.org/pkg/text/template/). // [text/template package](https://golang.org/pkg/text/template/).
// Note that ANY response body that matches and qualifies may be evaluated,
// even if it comes from a proxied backend.
// //
// ⚠️ Template functions/actions are still experimental, so they are subject to change. // ⚠️ Template functions/actions can access the environment, files on disk,
// and make HTTP requests. This is extremely useful, but you need to make
// sure templates are only evaluated on content that you trust, control, or
// at least sanitize properly.
// //
// Custom template functions can be registered by creating a plugin module under the `http.handlers.templates.functions.*` namespace that implements the `CustomFunctions` interface. // ⚠️ Templates are still experimental, so they are subject to change.
// //
// [All Sprig functions](https://masterminds.github.io/sprig/) are supported. // [All Sprig functions](https://masterminds.github.io/sprig/) are supported.
// //
// Custom template functions can be registered by creating a plugin module
// under the `http.handlers.templates.functions.*` namespace that implements
// the `CustomFunctions` interface.
//
// In addition to the standard functions and the Sprig library, Caddy adds // In addition to the standard functions and the Sprig library, Caddy adds
// extra functions and data that are available to a template: // extra functions and data that are available to a template:
// //
@@ -162,6 +171,25 @@ func init() {
// {{listFiles "/mydir"}} // {{listFiles "/mydir"}}
// ``` // ```
// //
// ##### `fileExists`
//
// Returns true if the given file name, relative to the template context's file root,
// can be opened successfully.
//
// ```
// {{fileExists "path/to/file.html"}}
// ```
//
// ##### `fileStat`
//
// Returns [FileInfo](https://pkg.go.dev/io/fs#FileInfo) using [Stat](https://pkg.go.dev/io/fs#Stat)
// on the given file name, relative to the template context's file root.
//
// ```
// {{$css := fileStat "css/style.css" -}}
// <link rel="stylesheet" href="/css/style.css?v={{ $css.ModTime.Unix }}">
// ```
//
// ##### `markdown` // ##### `markdown`
// //
// Renders the given Markdown text as HTML and returns it. This uses the // Renders the given Markdown text as HTML and returns it. This uses the
+2 -2
View File
@@ -153,9 +153,9 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
// in its config (remember, TLS connection policies are used by *other* apps to // in its config (remember, TLS connection policies are used by *other* apps to
// run TLS servers) -- we skip names with placeholders // run TLS servers) -- we skip names with placeholders
if tlsApp.EncryptedClientHello.Publication == nil { if tlsApp.EncryptedClientHello.Publication == nil {
var echNames []string
repl := caddy.NewReplacer() repl := caddy.NewReplacer()
for _, p := range cp { for _, p := range cp {
var echNames []string
for _, m := range p.matchers { for _, m := range p.matchers {
if sni, ok := m.(MatchServerName); ok { if sni, ok := m.(MatchServerName); ok {
for _, name := range sni { for _, name := range sni {
@@ -164,8 +164,8 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
} }
} }
} }
tlsApp.RegisterServerNames(echNames, p.ALPN)
} }
tlsApp.RegisterServerNames(echNames)
} }
tlsCfg.GetEncryptedClientHelloKeys = func(chi *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) { tlsCfg.GetEncryptedClientHelloKeys = func(chi *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
+26 -7
View File
@@ -440,6 +440,10 @@ func (t *TLS) publishECHConfigs(logger *zap.Logger) error {
zap.Strings("domains", dnsNamesToPublish), zap.Strings("domains", dnsNamesToPublish),
zap.Uint8s("config_ids", configIDs)) zap.Uint8s("config_ids", configIDs))
if dnsPublisher, ok := publisher.(*ECHDNSPublisher); ok {
dnsPublisher.alpnByDomain = t.alpnValuesForServerNames(dnsNamesToPublish)
}
// publish this ECH config list with this publisher // publish this ECH config list with this publisher
pubTime := time.Now() pubTime := time.Now()
err := publisher.PublishECHConfigList(t.ctx, dnsNamesToPublish, echCfgListBin) err := publisher.PublishECHConfigList(t.ctx, dnsNamesToPublish, echCfgListBin)
@@ -776,7 +780,8 @@ type ECHDNSPublisher struct {
ProviderRaw json.RawMessage `json:"provider,omitempty" caddy:"namespace=dns.providers inline_key=name"` ProviderRaw json.RawMessage `json:"provider,omitempty" caddy:"namespace=dns.providers inline_key=name"`
provider ECHDNSProvider provider ECHDNSProvider
logger *zap.Logger alpnByDomain map[string][]string
logger *zap.Logger
} }
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
@@ -872,12 +877,7 @@ nextName:
continue continue
} }
params := httpsRec.Params params := httpsRec.Params
if params == nil { params = dnsPub.publishedSvcParams(domain, params, configListBin)
params = make(libdns.SvcParams)
}
// overwrite only the "ech" SvcParamKey
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{
@@ -903,6 +903,25 @@ nextName:
return nil return nil
} }
func (dnsPub *ECHDNSPublisher) publishedSvcParams(domain string, existing libdns.SvcParams, configListBin []byte) libdns.SvcParams {
params := make(libdns.SvcParams, len(existing)+2)
for key, values := range existing {
params[key] = append([]string(nil), values...)
}
params["ech"] = []string{base64.StdEncoding.EncodeToString(configListBin)}
if len(dnsPub.alpnByDomain) == 0 {
return params
}
if alpn := dnsPub.alpnByDomain[strings.ToLower(domain)]; len(alpn) > 0 {
params["alpn"] = append([]string(nil), alpn...)
}
return params
}
// echConfig represents an ECHConfig from the specification, // echConfig represents an ECHConfig from the specification,
// [draft-ietf-tls-esni-22](https://www.ietf.org/archive/id/draft-ietf-tls-esni-22.html). // [draft-ietf-tls-esni-22](https://www.ietf.org/archive/id/draft-ietf-tls-esni-22.html).
type echConfig struct { type echConfig struct {
+65
View File
@@ -0,0 +1,65 @@
package caddytls
import (
"encoding/base64"
"reflect"
"sync"
"testing"
"github.com/libdns/libdns"
)
func TestRegisterServerNamesWithALPN(t *testing.T) {
tlsApp := &TLS{
serverNames: make(map[string]serverNameRegistration),
serverNamesMu: new(sync.Mutex),
}
tlsApp.RegisterServerNames([]string{
"Example.com:443",
"example.com",
"127.0.0.1:443",
}, []string{"h2", "http/1.1"})
tlsApp.RegisterServerNames([]string{"EXAMPLE.COM"}, []string{"h3"})
got := tlsApp.alpnValuesForServerNames([]string{"example.com:443", "127.0.0.1:443"})
want := map[string][]string{
"example.com": {"h3", "h2", "http/1.1"},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected ALPN values: got %#v want %#v", got, want)
}
}
func TestECHDNSPublisherPublishedSvcParams(t *testing.T) {
dnsPub := &ECHDNSPublisher{
alpnByDomain: map[string][]string{
"example.com": {"h3", "h2", "http/1.1"},
},
}
existing := libdns.SvcParams{
"alpn": {"h2"},
"ipv4hint": {"203.0.113.10"},
}
got := dnsPub.publishedSvcParams("Example.com", existing, []byte{0x01, 0x02, 0x03})
if !reflect.DeepEqual(existing["alpn"], []string{"h2"}) {
t.Fatalf("existing params mutated: got %v", existing["alpn"])
}
if !reflect.DeepEqual(got["alpn"], []string{"h3", "h2", "http/1.1"}) {
t.Fatalf("unexpected ALPN params: got %v", got["alpn"])
}
if !reflect.DeepEqual(got["ipv4hint"], []string{"203.0.113.10"}) {
t.Fatalf("unexpected preserved params: got %v", got["ipv4hint"])
}
wantECH := base64.StdEncoding.EncodeToString([]byte{0x01, 0x02, 0x03})
if !reflect.DeepEqual(got["ech"], []string{wantECH}) {
t.Fatalf("unexpected ECH params: got %v want %v", got["ech"], wantECH)
}
}
+104 -16
View File
@@ -23,6 +23,7 @@ import (
"net" "net"
"net/http" "net/http"
"runtime/debug" "runtime/debug"
"slices"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -140,7 +141,7 @@ type TLS struct {
logger *zap.Logger logger *zap.Logger
events *caddyevents.App events *caddyevents.App
serverNames map[string]struct{} serverNames map[string]serverNameRegistration
serverNamesMu *sync.Mutex serverNamesMu *sync.Mutex
// set of subjects with managed certificates, // set of subjects with managed certificates,
@@ -168,7 +169,7 @@ func (t *TLS) Provision(ctx caddy.Context) error {
t.logger = ctx.Logger() t.logger = ctx.Logger()
repl := caddy.NewReplacer() repl := caddy.NewReplacer()
t.managing, t.loaded = make(map[string]string), make(map[string]string) t.managing, t.loaded = make(map[string]string), make(map[string]string)
t.serverNames = make(map[string]struct{}) t.serverNames = make(map[string]serverNameRegistration)
t.serverNamesMu = new(sync.Mutex) t.serverNamesMu = new(sync.Mutex)
// set up default DNS module, if any, and make sure it implements all the // set up default DNS module, if any, and make sure it implements all the
@@ -613,8 +614,8 @@ func (t *TLS) Manage(subjects map[string]struct{}) error {
// managingWildcardFor returns true if the app is managing a certificate that covers that // 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 // 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 // names that it IS managing certs for, from the otherSubjsToManage which includes names
// that WILL be managed. // that WILL be managed, or from names configured in the 'automate' loader.
func (t *TLS) managingWildcardFor(subj string, otherSubjsToManage map[string]struct{}) bool { func (t *TLS) managingWildcardFor(subj string, otherSubjsToManage map[string]struct{}) bool {
// TODO: we could also consider manually-loaded certs using t.HasCertificateForSubject(), // 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 // but that does not account for how manually-loaded certs may be restricted as to which
@@ -629,7 +630,9 @@ func (t *TLS) managingWildcardFor(subj string, otherSubjsToManage map[string]str
return managing return managing
} }
// replace labels of the domain with wildcards until we get a match // replace labels of the domain with wildcards until we get a match from names
// already being managed, those about to be managed in this batch, or those
// configured for automation
labels := strings.Split(subj, ".") labels := strings.Split(subj, ".")
for i := range labels { for i := range labels {
if labels[i] == "*" { if labels[i] == "*" {
@@ -643,32 +646,117 @@ func (t *TLS) managingWildcardFor(subj string, otherSubjsToManage map[string]str
if _, ok := otherSubjsToManage[candidate]; ok { if _, ok := otherSubjsToManage[candidate]; ok {
return true return true
} }
if _, ok := t.automateNames[candidate]; ok {
return true
}
} }
return false return false
} }
// RegisterServerNames registers the provided DNS names with the TLS app. // RegisterServerNames registers the provided DNS names with the TLS app and
// This is currently used to auto-publish Encrypted ClientHello (ECH) // associates them with the given HTTPS RR ALPN values, if any. This is
// configurations, if enabled. Use of this function by apps using the TLS // currently used to auto-publish Encrypted ClientHello (ECH) configurations,
// app removes the need for the user to redundantly specify domain names // if enabled. Use of this function by apps using the TLS app removes the need
// in their configuration. This function separates hostname and port // for the user to redundantly specify domain names in their configuration.
// (keeping only the hotsname) and filters IP addresses, which can't be // This function separates hostname and port, keeping only the hostname, and
// used with ECH. // filters IP addresses which can't be used with ECH.
// //
// EXPERIMENTAL: This function and its semantics/behavior are subject to change. // EXPERIMENTAL: This function and its semantics/behavior are subject to change.
func (t *TLS) RegisterServerNames(dnsNames []string) { func (t *TLS) RegisterServerNames(dnsNames, alpnValues []string) {
t.serverNamesMu.Lock() t.serverNamesMu.Lock()
defer t.serverNamesMu.Unlock()
for _, name := range dnsNames { for _, name := range dnsNames {
host, _, err := net.SplitHostPort(name) host, _, err := net.SplitHostPort(name)
if err != nil { if err != nil {
host = name host = name
} }
if strings.TrimSpace(host) != "" && !certmagic.SubjectIsIP(host) { host = strings.ToLower(strings.TrimSpace(host))
t.serverNames[strings.ToLower(host)] = struct{}{} if host == "" || certmagic.SubjectIsIP(host) {
continue
}
registration := t.serverNames[host]
if len(alpnValues) == 0 {
t.serverNames[host] = registration
continue
}
if registration.alpnValues == nil {
registration.alpnValues = make(map[string]struct{}, len(alpnValues))
}
for _, alpn := range alpnValues {
if alpn == "" {
continue
}
registration.alpnValues[alpn] = struct{}{}
}
t.serverNames[host] = registration
}
}
func (t *TLS) alpnValuesForServerNames(dnsNames []string) map[string][]string {
t.serverNamesMu.Lock()
defer t.serverNamesMu.Unlock()
result := make(map[string][]string, len(dnsNames))
for _, name := range dnsNames {
host, _, err := net.SplitHostPort(name)
if err != nil {
host = name
}
host = strings.ToLower(strings.TrimSpace(host))
if host == "" {
continue
}
registration, ok := t.serverNames[host]
if !ok || len(registration.alpnValues) == 0 {
continue
}
result[host] = OrderedHTTPSRRALPN(registration.alpnValues)
}
return result
}
// OrderedHTTPSRRALPN returns the HTTPS RR ALPN values in preferred order.
func OrderedHTTPSRRALPN(alpnSet map[string]struct{}) []string {
if len(alpnSet) == 0 {
return nil
}
knownOrder := append([]string{"h3"}, defaultALPN...)
ordered := make([]string, 0, len(alpnSet))
seen := make(map[string]struct{}, len(alpnSet))
for _, alpn := range knownOrder {
if _, ok := alpnSet[alpn]; ok {
ordered = append(ordered, alpn)
seen[alpn] = struct{}{}
} }
} }
t.serverNamesMu.Unlock()
if len(ordered) == len(alpnSet) {
return ordered
}
var remaining []string
for alpn := range alpnSet {
if _, ok := seen[alpn]; ok {
continue
}
remaining = append(remaining, alpn)
}
slices.Sort(remaining)
return append(ordered, remaining...)
}
type serverNameRegistration struct {
alpnValues map[string]struct{}
} }
// HandleHTTPChallenge ensures that the ACME HTTP challenge or ZeroSSL HTTP // HandleHTTPChallenge ensures that the ACME HTTP challenge or ZeroSSL HTTP
+96
View File
@@ -0,0 +1,96 @@
// 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 caddytls
import (
"encoding/json"
"testing"
"github.com/caddyserver/caddy/v2"
)
func TestAvoidDuplicateAutomation(t *testing.T) {
tests := []struct {
name string
automateNames []string
expectedToManage bool
}{
{
name: "do not manage if wildcard is automated",
automateNames: []string{"*.example.com"},
expectedToManage: false,
},
{
name: "manage if no automation configured",
automateNames: []string{},
expectedToManage: true,
},
{
name: "manage if explicitly requested even when wildcard automated",
automateNames: []string{"*.example.com", "sub.example.com"},
expectedToManage: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
automateJSON, err := json.Marshal(tc.automateNames)
if err != nil {
t.Fatal(err)
}
tlsApp := &TLS{
Automation: &AutomationConfig{
Policies: []*AutomationPolicy{
{
IssuersRaw: []json.RawMessage{
[]byte(`{"module": "internal"}`),
},
},
},
},
CertificatesRaw: map[string]json.RawMessage{
"automate": automateJSON,
},
}
var cfg caddy.Config
ctx, err := caddy.ProvisionContext(&cfg)
if err != nil {
t.Fatal(err)
}
if err := tlsApp.Provision(ctx); err != nil {
t.Fatal(err)
}
// simulate a case wherein the HTTP app starts first and
// tells the TLS app about the following auto-HTTPS domains
httpDomains := map[string]struct{}{"sub.example.com": {}}
if err := tlsApp.Manage(httpDomains); err != nil {
t.Fatal(err)
}
_, actuallyManaged := tlsApp.managing["sub.example.com"]
if actuallyManaged != tc.expectedToManage {
t.Errorf(
"expected sub.example.com individually managed: %v, got: %v",
tc.expectedToManage,
actuallyManaged,
)
}
})
}
}