mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-26 08:42:31 -04:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ab3020f62 | |||
| 77e9ce7404 | |||
| cc58caa109 | |||
| d80774cb3f | |||
| a4a38c3e88 | |||
| 761347aa63 | |||
| 4ba16fe82c | |||
| 0fab9f0f7d | |||
| 5e76b5ee43 | |||
| 9c78b97f9e | |||
| fb324331f4 | |||
| 0780d4489c | |||
| d2172bea61 | |||
| c7c9f3108a | |||
| 7e77eec0ae | |||
| ef496e58ef |
@@ -120,10 +120,6 @@ type AdminConfig struct {
|
||||
//
|
||||
// EXPERIMENTAL: This feature is subject to change.
|
||||
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.
|
||||
@@ -222,7 +218,7 @@ type AdminPermissions struct {
|
||||
|
||||
// newAdminHandler reads admin's config and returns an http.Handler suitable
|
||||
// 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()}
|
||||
|
||||
// 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
|
||||
for _, m := range GetModules("admin.api") {
|
||||
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() {
|
||||
addRoute(route.Pattern, handlerLabel, route.Handler)
|
||||
}
|
||||
admin.routers = append(admin.routers, router)
|
||||
}
|
||||
|
||||
return muxWrap
|
||||
}
|
||||
|
||||
// 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
|
||||
return muxWrap, nil
|
||||
}
|
||||
|
||||
// allowedOrigins returns a list of origins that are allowed.
|
||||
@@ -430,11 +413,7 @@ func replaceLocalAdminServer(cfg *Config, ctx Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
handler, err := cfg.Admin.newAdminHandler(addr, false, ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -558,11 +537,7 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
|
||||
|
||||
// make the HTTP handler but disable Host/Origin enforcement
|
||||
// because we are using TLS authentication instead
|
||||
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)
|
||||
handler, err := cfg.Admin.newAdminHandler(addr, true, ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+9
-15
@@ -340,7 +340,10 @@ func TestAdminHandlerBuiltinRouteErrors(t *testing.T) {
|
||||
if err != nil {
|
||||
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 {
|
||||
name string
|
||||
@@ -461,7 +464,10 @@ func TestNewAdminHandlerRouterRegistration(t *testing.T) {
|
||||
admin := &AdminConfig{
|
||||
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.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.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 {
|
||||
@@ -514,19 +516,16 @@ func TestAdminRouterProvisioning(t *testing.T) {
|
||||
name string
|
||||
provisionErr error
|
||||
wantErr bool
|
||||
routersAfter int // expected number of routers after provisioning
|
||||
}{
|
||||
{
|
||||
name: "successful provisioning",
|
||||
provisionErr: nil,
|
||||
wantErr: false,
|
||||
routersAfter: 0,
|
||||
},
|
||||
{
|
||||
name: "provisioning error",
|
||||
provisionErr: fmt.Errorf("provision failed"),
|
||||
wantErr: true,
|
||||
routersAfter: 1,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -562,8 +561,7 @@ func TestAdminRouterProvisioning(t *testing.T) {
|
||||
t.Fatalf("Failed to parse address: %v", err)
|
||||
}
|
||||
|
||||
_ = admin.newAdminHandler(addr, false, Context{})
|
||||
err = admin.provisionAdminRouters(Context{})
|
||||
_, err = admin.newAdminHandler(addr, false, Context{})
|
||||
|
||||
if test.wantErr {
|
||||
if err == nil {
|
||||
@@ -574,10 +572,6 @@ func TestAdminRouterProvisioning(t *testing.T) {
|
||||
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
err = func() error {
|
||||
started := make([]string, 0, len(ctx.cfg.apps))
|
||||
|
||||
@@ -1053,7 +1053,7 @@ func parseLogHelper(h Helper, globalLogNames map[string]struct{}) ([]ConfigValue
|
||||
if !d.NextArg() {
|
||||
return nil, d.ArgErr()
|
||||
}
|
||||
interval, err := time.ParseDuration(d.Val() + "ns")
|
||||
interval, err := caddy.ParseDuration(d.Val())
|
||||
if err != nil {
|
||||
return nil, d.Errf("failed to parse interval: %v", err)
|
||||
}
|
||||
|
||||
@@ -66,14 +66,14 @@ func TestLogDirectiveSyntax(t *testing.T) {
|
||||
input: `:8080 {
|
||||
log {
|
||||
sampling {
|
||||
interval 2
|
||||
interval 2s
|
||||
first 3
|
||||
thereafter 4
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"sampling":{"interval":2,"first":3,"thereafter":4},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
|
||||
output: `{"logging":{"logs":{"default":{"exclude":["http.log.access.log0"]},"log0":{"sampling":{"interval":2000000000,"first":3,"thereafter":4},"include":["http.log.access.log0"]}}},"apps":{"http":{"servers":{"srv0":{"listen":[":8080"],"logs":{"default_logger_name":"log0"}}}}}}`,
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
log {
|
||||
sampling {
|
||||
interval 300
|
||||
interval 5m
|
||||
first 50
|
||||
thereafter 40
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
"logs": {
|
||||
"default": {
|
||||
"sampling": {
|
||||
"interval": 300,
|
||||
"interval": 300000000000,
|
||||
"first": 50,
|
||||
"thereafter": 40
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
:80 {
|
||||
log {
|
||||
sampling {
|
||||
interval 300
|
||||
interval 5m
|
||||
first 50
|
||||
thereafter 40
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"log0": {
|
||||
"sampling": {
|
||||
"interval": 300,
|
||||
"interval": 300000000000,
|
||||
"first": 50,
|
||||
"thereafter": 40
|
||||
},
|
||||
|
||||
@@ -234,7 +234,7 @@ func getModules() (standard, nonstandard, unknown []moduleInfo, err error) {
|
||||
// not sure why), and since New() should return a pointer
|
||||
// value, we need to dereference it first
|
||||
iface := any(modInfo.New())
|
||||
if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr {
|
||||
if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Pointer {
|
||||
iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
|
||||
}
|
||||
modPkgPath := reflect.TypeOf(iface).PkgPath()
|
||||
|
||||
+1
-1
@@ -378,7 +378,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
|
||||
// value must be a pointer for unmarshaling into concrete type, even if
|
||||
// the module's concrete type is a slice or map; New() *should* return
|
||||
// a pointer, otherwise unmarshaling errors or panics will occur
|
||||
if rv := reflect.ValueOf(val); rv.Kind() != reflect.Ptr {
|
||||
if rv := reflect.ValueOf(val); rv.Kind() != reflect.Pointer {
|
||||
log.Printf("[WARNING] ModuleInfo.New() for module '%s' did not return a pointer,"+
|
||||
" so we are using reflection to make a pointer instead; please fix this by"+
|
||||
" using new(Type) or &Type notation in your module's New() function.", id)
|
||||
|
||||
@@ -20,7 +20,7 @@ require (
|
||||
github.com/klauspost/cpuid/v2 v2.3.0
|
||||
github.com/mholt/acmez/v3 v3.1.6
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/quic-go/quic-go v0.59.0
|
||||
github.com/quic-go/quic-go v0.59.1
|
||||
github.com/smallstep/certificates v0.30.2
|
||||
github.com/smallstep/nosql v0.8.0
|
||||
github.com/smallstep/truststore v0.13.0
|
||||
@@ -63,7 +63,7 @@ require (
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.5 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
@@ -170,7 +170,7 @@ require (
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
golang.org/x/sys v0.43.0
|
||||
golang.org/x/text v0.36.0
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
google.golang.org/grpc v1.80.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
|
||||
@@ -149,8 +149,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
|
||||
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
|
||||
github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -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/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
|
||||
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/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -241,12 +240,7 @@ func (app *App) Provision(ctx caddy.Context) error {
|
||||
|
||||
// if no protocols configured explicitly, enable all except h2c
|
||||
if len(srv.Protocols) == 0 {
|
||||
srv.Protocols = []string{"h1", "h2", "h3"}
|
||||
}
|
||||
|
||||
srvProtocolsUnique := map[string]struct{}{}
|
||||
for _, srvProtocol := range srv.Protocols {
|
||||
srvProtocolsUnique[srvProtocol] = struct{}{}
|
||||
srv.Protocols = srv.protocolsWithDefaults()
|
||||
}
|
||||
|
||||
if srv.ListenProtocols != nil {
|
||||
@@ -257,31 +251,7 @@ func (app *App) Provision(ctx caddy.Context) error {
|
||||
|
||||
for i, lnProtocols := range srv.ListenProtocols {
|
||||
if lnProtocols != nil {
|
||||
// populate empty listen protocols with server protocols
|
||||
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
|
||||
srv.ListenProtocols[i] = srv.listenerProtocolsWithDefaults(lnProtocols)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
|
||||
for d := range serverDomainSet {
|
||||
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
|
||||
// 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
|
||||
// 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
|
||||
|
||||
@@ -1,44 +1,47 @@
|
||||
package caddyhttp
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func TestRecordAutoHTTPSRedirectAddressPrefersHTTPSPort(t *testing.T) {
|
||||
app := &App{HTTPSPort: 443}
|
||||
redirDomains := make(map[string][]caddy.NetworkAddress)
|
||||
func TestHTTPSRRALPNsDefaultProtocols(t *testing.T) {
|
||||
srv := &Server{}
|
||||
|
||||
app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", StartPort: 2345, EndPort: 2345})
|
||||
app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", StartPort: 443, EndPort: 443})
|
||||
app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", StartPort: 8443, EndPort: 8443})
|
||||
got := httpsRRALPNs(srv)
|
||||
want := []string{"h3", "h2", "http/1.1"}
|
||||
|
||||
got := redirDomains["example.com"]
|
||||
if len(got) != 1 {
|
||||
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])
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("unexpected ALPN values: got %v want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordAutoHTTPSRedirectAddressKeepsAllBindAddressesOnWinningPort(t *testing.T) {
|
||||
app := &App{HTTPSPort: 443}
|
||||
redirDomains := make(map[string][]caddy.NetworkAddress)
|
||||
|
||||
app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", Host: "10.0.0.189", StartPort: 8443, EndPort: 8443})
|
||||
app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", Host: "10.0.0.189", StartPort: 443, EndPort: 443})
|
||||
app.recordAutoHTTPSRedirectAddress(redirDomains, "example.com", caddy.NetworkAddress{Network: "tcp", Host: "2603:c024:8002:9500:9eb:e5d3:3975:d056", StartPort: 443, EndPort: 443})
|
||||
|
||||
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)
|
||||
func TestHTTPSRRALPNsListenProtocolOverrides(t *testing.T) {
|
||||
srv := &Server{
|
||||
Protocols: []string{"h1", "h2"},
|
||||
ListenProtocols: [][]string{
|
||||
{"h1"},
|
||||
nil,
|
||||
{},
|
||||
{"h3", ""},
|
||||
},
|
||||
}
|
||||
if got[0].StartPort != 443 || got[1].StartPort != 443 {
|
||||
t.Fatalf("expected both redirect addresses to stay on HTTPS port 443, got %#v", got)
|
||||
}
|
||||
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)
|
||||
|
||||
got := httpsRRALPNs(srv)
|
||||
want := []string{"h3", "h2", "http/1.1"}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("unexpected ALPN values: got %v want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPSRRALPNsIgnoresH2COnly(t *testing.T) {
|
||||
srv := &Server{
|
||||
Protocols: []string{"h2c"},
|
||||
}
|
||||
|
||||
got := httpsRRALPNs(srv)
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("unexpected ALPN values: got %v want none", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,7 +281,13 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re
|
||||
sortParam = sortCookie.Value
|
||||
}
|
||||
case sortByName, sortByNameDirFirst, sortBySize, sortByTime:
|
||||
http.SetCookie(w, &http.Cookie{Name: "sort", Value: sortParam, Secure: r.TLS != nil})
|
||||
http.SetCookie(w, &http.Cookie{ //nolint:gosec // Secure depends on whether the request itself used TLS
|
||||
Name: "sort",
|
||||
Value: sortParam,
|
||||
Secure: r.TLS != nil,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
}
|
||||
|
||||
// then figure out the order
|
||||
@@ -292,7 +298,13 @@ func (fsrv *FileServer) browseApplyQueryParams(w http.ResponseWriter, r *http.Re
|
||||
orderParam = orderCookie.Value
|
||||
}
|
||||
case sortOrderAsc, sortOrderDesc:
|
||||
http.SetCookie(w, &http.Cookie{Name: "order", Value: orderParam, Secure: r.TLS != nil})
|
||||
http.SetCookie(w, &http.Cookie{ //nolint:gosec // Secure depends on whether the request itself used TLS
|
||||
Name: "order",
|
||||
Value: orderParam,
|
||||
Secure: r.TLS != nil,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
}
|
||||
|
||||
// finally, apply the sorting and limiting
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/internal/filesystems"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
|
||||
)
|
||||
|
||||
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) {
|
||||
for i, tc := range []struct {
|
||||
path string
|
||||
|
||||
@@ -785,7 +785,7 @@ func redirect(w http.ResponseWriter, r *http.Request, toPath string) error {
|
||||
if r.URL.RawQuery != "" {
|
||||
toPath += "?" + r.URL.RawQuery
|
||||
}
|
||||
http.Redirect(w, r, toPath, http.StatusPermanentRedirect)
|
||||
http.Redirect(w, r, toPath, http.StatusPermanentRedirect) //nolint:gosec // toPath is a same-origin path and leading // is stripped above
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -28,8 +28,6 @@ import (
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/search"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
@@ -418,14 +416,19 @@ func (t Transport) buildEnv(r *http.Request) (envVars, error) {
|
||||
return env, nil
|
||||
}
|
||||
|
||||
var splitSearchNonASCII = search.New(language.Und, search.IgnoreCase)
|
||||
|
||||
// splitPos returns the index where path should
|
||||
// be split based on t.SplitPath.
|
||||
//
|
||||
// example: if splitPath is [".php"]
|
||||
// "/path/to/script.php/some/path": ("/path/to/script.php", "/some/path")
|
||||
//
|
||||
// Matching is strictly ASCII case-insensitive. Bytes >= utf8.RuneSelf in path
|
||||
// never match any split entry: split strings are validated ASCII-only and
|
||||
// lower-cased in Provision(), so any Unicode equivalence (e.g. fullwidth or
|
||||
// mathematical letters folding to ASCII) would let an attacker upload a file
|
||||
// whose name contains such code points and have it served as PHP. See
|
||||
// FrankenPHP advisories GHSA-3g8v-8r37-cgjm and GHSA-v4h7-cj44-8fc8.
|
||||
//
|
||||
// Adapted from FrankenPHP's code (copyright 2026 Kévin Dunglas, MIT license)
|
||||
func (t Transport) splitPos(path string) int {
|
||||
// TODO: from v1...
|
||||
@@ -438,31 +441,18 @@ func (t Transport) splitPos(path string) int {
|
||||
|
||||
pathLen := len(path)
|
||||
|
||||
// We are sure that split strings are all ASCII-only and lower-case because of validation and normalization in Provision().
|
||||
for _, split := range t.SplitPath {
|
||||
splitLen := len(split)
|
||||
if splitLen == 0 || splitLen > pathLen {
|
||||
continue
|
||||
}
|
||||
|
||||
for i := range pathLen {
|
||||
if path[i] >= utf8.RuneSelf {
|
||||
if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 {
|
||||
return end
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if i+splitLen > pathLen {
|
||||
continue
|
||||
}
|
||||
|
||||
for i := 0; i <= pathLen-splitLen; i++ {
|
||||
match := true
|
||||
for j := range splitLen {
|
||||
c := path[i+j]
|
||||
|
||||
if c >= utf8.RuneSelf {
|
||||
if _, end := splitSearchNonASCII.IndexString(path, split); end > -1 {
|
||||
return end
|
||||
}
|
||||
match = false
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
@@ -191,6 +191,65 @@ func TestSplitPos(t *testing.T) {
|
||||
splitPath: []string{".php"},
|
||||
wantPos: 9,
|
||||
},
|
||||
// Regression tests adapted from FrankenPHP advisories
|
||||
// GHSA-3g8v-8r37-cgjm and GHSA-v4h7-cj44-8fc8: search.IgnoreCase
|
||||
// matched Unicode equivalents of ASCII letters as ".php", and an
|
||||
// inner non-ASCII byte path could leave the match flag stale.
|
||||
{
|
||||
name: "non-ascii byte after dot must not match",
|
||||
path: "/PoC-match-unset.¡.txt",
|
||||
splitPath: []string{".php"},
|
||||
wantPos: -1,
|
||||
},
|
||||
{
|
||||
name: "non-ascii byte mid-extension must not match",
|
||||
path: "/script.p\xc2\xa1p",
|
||||
splitPath: []string{".php"},
|
||||
wantPos: -1,
|
||||
},
|
||||
{
|
||||
name: "small full stop ﹒ in extension must not match",
|
||||
path: "/shell﹒php",
|
||||
splitPath: []string{".php"},
|
||||
wantPos: -1,
|
||||
},
|
||||
{
|
||||
name: "fullwidth full stop . in extension must not match",
|
||||
path: "/shell.php",
|
||||
splitPath: []string{".php"},
|
||||
wantPos: -1,
|
||||
},
|
||||
{
|
||||
name: "fullwidth p in extension must not match",
|
||||
path: "/shell.php",
|
||||
splitPath: []string{".php"},
|
||||
wantPos: -1,
|
||||
},
|
||||
{
|
||||
name: "circled php must not match",
|
||||
path: "/shell.ⓟⓗⓟ",
|
||||
splitPath: []string{".php"},
|
||||
wantPos: -1,
|
||||
},
|
||||
{
|
||||
name: "mathematical sans-serif bold php must not match",
|
||||
path: "/shell.\U0001D5FD\U0001D5F5\U0001D5FD",
|
||||
splitPath: []string{".php"},
|
||||
wantPos: -1,
|
||||
},
|
||||
{
|
||||
name: "mathematical script php must not match",
|
||||
path: "/shell.\U0001D4C5\U0001D4BD\U0001D4C5",
|
||||
splitPath: []string{".php"},
|
||||
wantPos: -1,
|
||||
},
|
||||
{
|
||||
name: "circled php with later real php still picks the real one",
|
||||
path: "/shell.ⓟⓗⓟ.anything-after-payload.php",
|
||||
splitPath: []string{".php"},
|
||||
// "/shell." (7) + "ⓟⓗⓟ" (3*3 bytes) + ".anything-after-payload.php" (27) = 43
|
||||
wantPos: 43,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -244,3 +303,31 @@ func TestSplitPosUnicodeSecurityRegression(t *testing.T) {
|
||||
assert.Equal(t, ".txt.php", pathInfo, "path info should be the remainder after first .php")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSplitPosSecurityRegressionUnicodeBypass guards against the FrankenPHP
|
||||
// advisories GHSA-3g8v-8r37-cgjm (uninitialized match flag on inner non-ASCII
|
||||
// byte) and GHSA-v4h7-cj44-8fc8 (Unicode equivalence via search.IgnoreCase
|
||||
// folding fullwidth/mathematical/circled letters onto ASCII). Every payload
|
||||
// below produced a false positive in the vulnerable implementation; none
|
||||
// must match here.
|
||||
func TestSplitPosSecurityRegressionUnicodeBypass(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tr := Transport{SplitPath: []string{".php"}}
|
||||
payloads := []string{
|
||||
"/PoC-match-unset.¡.txt", // GHSA-3g8v: stale match=true on IndexString fallback
|
||||
"/shell﹒php", // U+FE52 small full stop
|
||||
"/shell.php", // U+FF0E fullwidth full stop
|
||||
"/shell.php", // U+FF50 fullwidth p
|
||||
"/shell.php", // U+FF48 fullwidth h
|
||||
"/shell.php", // U+FF50 fullwidth p (trailing)
|
||||
"/shell.\U0001D5C1\U0001D5B5\U0001D5C1", // mathematical sans-serif p/h
|
||||
"/shell.\U0001D5FD\U0001D5F5\U0001D5FD", // mathematical sans-serif bold p/h
|
||||
"/shell.\U0001D4C5\U0001D4BD\U0001D4C5", // mathematical script p/h
|
||||
"/shell.ⓟⓗⓟ", // circled latin small
|
||||
}
|
||||
|
||||
for _, p := range payloads {
|
||||
assert.Equalf(t, -1, tr.splitPos(p), "payload %q must not be detected as .php", p)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -730,3 +730,58 @@ func TestRetryMatchAllowsExpressionMixedWithOtherMatchers(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSubrouteErrorFallbackWithBody is similar to TestDialErrorBodyRetry but
|
||||
// mimics Subroute's Error handler rather than testing retries specifically
|
||||
func TestSubrouteErrorFallbackWithBody(t *testing.T) {
|
||||
// Good upstream: echoes the request body with 200 OK.
|
||||
goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "read body: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, err = w.Write(body)
|
||||
if err != nil {
|
||||
t.Errorf("error writing in good server: %v", err)
|
||||
}
|
||||
}))
|
||||
t.Cleanup(goodServer.Close)
|
||||
|
||||
// Handler which will dial error
|
||||
badProxy := minimalHandler(0, &Upstream{Host: new(Host), Dial: deadUpstreamAddr(t)})
|
||||
|
||||
bodyReader := newCloseOnCloseReader("hello world")
|
||||
req := httptest.NewRequest("POST", "http://localhost/", bodyReader)
|
||||
// httptest.NewRequest wraps the reader in NopCloser; replace
|
||||
// it with our close-aware reader so Close() is propagated.
|
||||
req.Body = bodyReader
|
||||
|
||||
req = prepareTestRequest(req)
|
||||
rec := httptest.NewRecorder()
|
||||
err := badProxy.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||
return nil
|
||||
}))
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error from badProxy.ServeHTTP")
|
||||
}
|
||||
|
||||
// Simulate the Subroute's Error handler by calling another handler with the
|
||||
// same request and recorder
|
||||
goodProxy := minimalHandler(0, &Upstream{Host: new(Host), Dial: goodServer.Listener.Addr().String()})
|
||||
err = goodProxy.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||
return nil
|
||||
}))
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error from goodProxy.ServeHTTP, got: %v", err)
|
||||
}
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("status: got %d, want %d", rec.Code, http.StatusOK)
|
||||
}
|
||||
expectedBody := "hello world"
|
||||
if rec.Body.String() != expectedBody {
|
||||
t.Errorf("body: got %q, want %q", rec.Body.String(), expectedBody)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,20 +488,19 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
|
||||
reqHost := clonedReq.Host
|
||||
reqHeader := clonedReq.Header
|
||||
|
||||
// When retries are configured and there is a body, wrap it in
|
||||
// io.NopCloser to prevent Go's transport from closing it on dial
|
||||
// errors. cloneRequest does a shallow copy, so clonedReq.Body and
|
||||
// If the request contained a body, wrap it in io.NopCloser
|
||||
// to prevent Go's transport from closing it on dial errors.
|
||||
// cloneRequest does a shallow copy, so clonedReq.Body and
|
||||
// r.Body share the same io.ReadCloser — a dial-failure Close()
|
||||
// would kill the original body for all subsequent retry attempts.
|
||||
// The real body is closed by the HTTP server when the handler
|
||||
// returns.
|
||||
// would kill the original body for all subsequent retry
|
||||
// attempts or subsequent handlers. The real body is closed by
|
||||
// the HTTP server when the handler returns.
|
||||
//
|
||||
// If the body was already fully buffered (via request_buffers),
|
||||
// we also extract the buffer so the retry loop can replay it
|
||||
// from the beginning on each attempt. (see #6259, #7546)
|
||||
// from the beginning on each attempt. (see #6259, #7546, #7713)
|
||||
var bufferedReqBody *bytes.Buffer
|
||||
if clonedReq.Body != nil && h.LoadBalancing != nil &&
|
||||
(h.LoadBalancing.Retries > 0 || h.LoadBalancing.TryDuration > 0) {
|
||||
if clonedReq.Body != nil {
|
||||
if reqBodyBuf, ok := clonedReq.Body.(bodyReadCloser); ok && reqBodyBuf.body == nil && reqBodyBuf.buf != nil {
|
||||
bufferedReqBody = reqBodyBuf.buf
|
||||
reqBodyBuf.buf = nil
|
||||
|
||||
@@ -664,10 +664,12 @@ func (s CookieHashSelection) Select(pool UpstreamPool, req *http.Request, w http
|
||||
return upstream
|
||||
}
|
||||
cookie := &http.Cookie{
|
||||
Name: s.Name,
|
||||
Value: sha,
|
||||
Path: "/",
|
||||
Secure: false,
|
||||
Name: s.Name,
|
||||
Value: sha,
|
||||
Path: "/",
|
||||
Secure: false,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
isProxyHttps := false
|
||||
if trusted, ok := caddyhttp.GetVar(req.Context(), caddyhttp.TrustedProxyVarKey).(bool); ok && trusted {
|
||||
|
||||
@@ -211,12 +211,7 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
|
||||
var newPath, newQuery, newFrag string
|
||||
|
||||
if path != "" {
|
||||
// replace the `path` placeholder to escaped path
|
||||
pathPlaceholder := "{http.request.uri.path}"
|
||||
if strings.Contains(path, pathPlaceholder) {
|
||||
path = strings.ReplaceAll(path, pathPlaceholder, r.URL.EscapedPath())
|
||||
}
|
||||
|
||||
path = escapePathPlaceholders(path, r, repl)
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
// performs replacements on each component, returning
|
||||
// the resulting query string. This function appends
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
@@ -241,8 +242,8 @@ func (routes RouteList) Compile(next Handler) Handler {
|
||||
mid = append(mid, wrapRoute(route))
|
||||
}
|
||||
stack := next
|
||||
for i := len(mid) - 1; i >= 0; i-- {
|
||||
stack = mid[i](stack)
|
||||
for _, middleware := range slices.Backward(mid) {
|
||||
stack = middleware(stack)
|
||||
}
|
||||
return stack
|
||||
}
|
||||
@@ -305,8 +306,8 @@ func wrapRoute(route Route) Middleware {
|
||||
}
|
||||
|
||||
// compile this route's handler stack
|
||||
for i := len(route.middleware) - 1; i >= 0; i-- {
|
||||
nextCopy = route.middleware[i](nextCopy)
|
||||
for _, middleware := range slices.Backward(route.middleware) {
|
||||
nextCopy = middleware(nextCopy)
|
||||
}
|
||||
|
||||
// Apply metrics instrumentation once for the entire route,
|
||||
|
||||
+50
-12
@@ -300,6 +300,8 @@ type Server struct {
|
||||
onStopFuncs []func(context.Context) error // TODO: Experimental (Nov. 2023)
|
||||
}
|
||||
|
||||
var defaultProtocols = []string{"h1", "h2", "h3"}
|
||||
|
||||
var (
|
||||
ServerHeader = "Caddy"
|
||||
serverHeader = []string{ServerHeader}
|
||||
@@ -899,22 +901,58 @@ func (s *Server) logRequest(
|
||||
// protocol returns true if the protocol proto is configured/enabled.
|
||||
func (s *Server) protocol(proto string) bool {
|
||||
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
|
||||
}
|
||||
} else {
|
||||
for _, lnProtocols := range s.ListenProtocols {
|
||||
for _, lnProtocol := range lnProtocols {
|
||||
if lnProtocol == "" && slices.Contains(s.Protocols, proto) || lnProtocol == proto {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
// so calling Accept() or Close() on them will probably break things.
|
||||
// They are made available here for read-only purposes (e.g. Addr())
|
||||
@@ -1085,11 +1123,11 @@ func strictUntrustedClientIp(r *http.Request, headers []string, trusted []netip.
|
||||
for _, headerName := range headers {
|
||||
parts := strings.Split(strings.Join(r.Header.Values(headerName), ","), ",")
|
||||
|
||||
for i := len(parts) - 1; i >= 0; i-- {
|
||||
for _, part := range slices.Backward(parts) {
|
||||
// Some proxies may retain the port number, so split if possible
|
||||
host, _, err := net.SplitHostPort(parts[i])
|
||||
host, _, err := net.SplitHostPort(part)
|
||||
if err != nil {
|
||||
host = parts[i]
|
||||
host = part
|
||||
}
|
||||
|
||||
// Remove any zone identifier from the IP address
|
||||
|
||||
@@ -36,13 +36,22 @@ func init() {
|
||||
// Templates is a middleware which executes response bodies as Go templates.
|
||||
// The syntax is documented in the Go standard library's
|
||||
// [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.
|
||||
//
|
||||
// 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
|
||||
// extra functions and data that are available to a template:
|
||||
//
|
||||
@@ -162,6 +171,25 @@ func init() {
|
||||
// {{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`
|
||||
//
|
||||
// Renders the given Markdown text as HTML and returns it. This uses the
|
||||
|
||||
@@ -140,6 +140,42 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error {
|
||||
iss.Email = email
|
||||
}
|
||||
|
||||
// expand CA endpoint, if non-empty
|
||||
if iss.CA != "" {
|
||||
ca, err := repl.ReplaceOrErr(iss.CA, true, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("expanding CA endpoint '%s': %v", iss.CA, err)
|
||||
}
|
||||
iss.CA = ca
|
||||
}
|
||||
|
||||
// expand TestCA endpoint, if non-empty
|
||||
if iss.TestCA != "" {
|
||||
testca, err := repl.ReplaceOrErr(iss.TestCA, true, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("expanding TestCA endpoint '%s': %v", iss.TestCA, err)
|
||||
}
|
||||
iss.TestCA = testca
|
||||
}
|
||||
|
||||
// expand EAB credentials, if non-empty
|
||||
if iss.ExternalAccount != nil {
|
||||
if iss.ExternalAccount.KeyID != "" {
|
||||
keyID, err := repl.ReplaceOrErr(iss.ExternalAccount.KeyID, true, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("expanding EAB key ID '%s': %v", iss.ExternalAccount.KeyID, err)
|
||||
}
|
||||
iss.ExternalAccount.KeyID = keyID
|
||||
}
|
||||
if iss.ExternalAccount.MACKey != "" {
|
||||
macKey, err := repl.ReplaceOrErr(iss.ExternalAccount.MACKey, true, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("expanding EAB MAC key (redacted): %v", err)
|
||||
}
|
||||
iss.ExternalAccount.MACKey = macKey
|
||||
}
|
||||
}
|
||||
|
||||
// expand account key, if non-empty
|
||||
if iss.AccountKey != "" {
|
||||
accountKey, err := repl.ReplaceOrErr(iss.AccountKey, true, true)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package caddytls
|
||||
|
||||
import (
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/mholt/acmez/v3/acme"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestACMEIssuerExpandPlaceholders(t *testing.T) {
|
||||
t.Setenv("CADDY_TEST_CA_URL", "https://acme.example.com/directory")
|
||||
t.Setenv("CADDY_TEST_TEST_CA_URL", "https://acme2.example.com/directory")
|
||||
t.Setenv("CADDY_TEST_EAB_KEY_ID", "example-key-id")
|
||||
t.Setenv("CADDY_TEST_EAB_MAC_KEY", "example-mac-key")
|
||||
|
||||
caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: t.Context()})
|
||||
defer cancel()
|
||||
|
||||
iss := &ACMEIssuer{
|
||||
CA: "{env.CADDY_TEST_CA_URL}",
|
||||
TestCA: "{env.CADDY_TEST_TEST_CA_URL}",
|
||||
ExternalAccount: &acme.EAB{
|
||||
KeyID: "{env.CADDY_TEST_EAB_KEY_ID}",
|
||||
MACKey: "{env.CADDY_TEST_EAB_MAC_KEY}",
|
||||
},
|
||||
}
|
||||
|
||||
if err := iss.Provision(caddyCtx); err != nil {
|
||||
t.Fatalf("Provision() returned unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if want := "https://acme.example.com/directory"; iss.CA != want {
|
||||
t.Errorf("CA: got %q, want %q", iss.CA, want)
|
||||
}
|
||||
if want := "https://acme2.example.com/directory"; iss.TestCA != want {
|
||||
t.Errorf("TestCA: got %q, want %q", iss.TestCA, want)
|
||||
}
|
||||
if want := "example-key-id"; iss.ExternalAccount.KeyID != want {
|
||||
t.Errorf("ExternalAccount.KeyID: got %q, want %q", iss.ExternalAccount.KeyID, want)
|
||||
}
|
||||
if want := "example-mac-key"; iss.ExternalAccount.MACKey != want {
|
||||
t.Errorf("ExternalAccount.MACKey: got %q, want %q", iss.ExternalAccount.MACKey, want)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
// run TLS servers) -- we skip names with placeholders
|
||||
if tlsApp.EncryptedClientHello.Publication == nil {
|
||||
var echNames []string
|
||||
repl := caddy.NewReplacer()
|
||||
for _, p := range cp {
|
||||
var echNames []string
|
||||
for _, m := range p.matchers {
|
||||
if sni, ok := m.(MatchServerName); ok {
|
||||
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) {
|
||||
|
||||
+26
-7
@@ -440,6 +440,10 @@ func (t *TLS) publishECHConfigs(logger *zap.Logger) error {
|
||||
zap.Strings("domains", dnsNamesToPublish),
|
||||
zap.Uint8s("config_ids", configIDs))
|
||||
|
||||
if dnsPublisher, ok := publisher.(*ECHDNSPublisher); ok {
|
||||
dnsPublisher.alpnByDomain = t.alpnValuesForServerNames(dnsNamesToPublish)
|
||||
}
|
||||
|
||||
// publish this ECH config list with this publisher
|
||||
pubTime := time.Now()
|
||||
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"`
|
||||
provider ECHDNSProvider
|
||||
|
||||
logger *zap.Logger
|
||||
alpnByDomain map[string][]string
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
@@ -872,12 +877,7 @@ nextName:
|
||||
continue
|
||||
}
|
||||
params := httpsRec.Params
|
||||
if params == nil {
|
||||
params = make(libdns.SvcParams)
|
||||
}
|
||||
|
||||
// overwrite only the "ech" SvcParamKey
|
||||
params["ech"] = []string{base64.StdEncoding.EncodeToString(configListBin)}
|
||||
params = dnsPub.publishedSvcParams(domain, params, configListBin)
|
||||
|
||||
// publish record
|
||||
_, err = dnsPub.provider.SetRecords(ctx, zone, []libdns.Record{
|
||||
@@ -903,6 +903,25 @@ nextName:
|
||||
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,
|
||||
// [draft-ietf-tls-esni-22](https://www.ietf.org/archive/id/draft-ietf-tls-esni-22.html).
|
||||
type echConfig struct {
|
||||
|
||||
@@ -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
@@ -23,6 +23,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -140,7 +141,7 @@ type TLS struct {
|
||||
logger *zap.Logger
|
||||
events *caddyevents.App
|
||||
|
||||
serverNames map[string]struct{}
|
||||
serverNames map[string]serverNameRegistration
|
||||
serverNamesMu *sync.Mutex
|
||||
|
||||
// set of subjects with managed certificates,
|
||||
@@ -168,7 +169,7 @@ func (t *TLS) Provision(ctx caddy.Context) error {
|
||||
t.logger = ctx.Logger()
|
||||
repl := caddy.NewReplacer()
|
||||
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)
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
// names that it IS managing certs for, from the otherSubjsToManage which includes names
|
||||
// that WILL be managed, or from names configured in the 'automate' loader.
|
||||
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
|
||||
@@ -629,7 +630,9 @@ func (t *TLS) managingWildcardFor(subj string, otherSubjsToManage map[string]str
|
||||
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, ".")
|
||||
for i := range labels {
|
||||
if labels[i] == "*" {
|
||||
@@ -643,32 +646,117 @@ func (t *TLS) managingWildcardFor(subj string, otherSubjsToManage map[string]str
|
||||
if _, ok := otherSubjsToManage[candidate]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := t.automateNames[candidate]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// RegisterServerNames registers the provided DNS names with the TLS app.
|
||||
// This is currently used to auto-publish Encrypted ClientHello (ECH)
|
||||
// configurations, if enabled. Use of this function by apps using the TLS
|
||||
// app removes the need for the user to redundantly specify domain names
|
||||
// in their configuration. This function separates hostname and port
|
||||
// (keeping only the hotsname) and filters IP addresses, which can't be
|
||||
// used with ECH.
|
||||
// RegisterServerNames registers the provided DNS names with the TLS app and
|
||||
// associates them with the given HTTPS RR ALPN values, if any. This is
|
||||
// currently used to auto-publish Encrypted ClientHello (ECH) configurations,
|
||||
// if enabled. Use of this function by apps using the TLS app removes the need
|
||||
// for the user to redundantly specify domain names in their configuration.
|
||||
// This function separates hostname and port, keeping only the hostname, and
|
||||
// filters IP addresses which can't be used with ECH.
|
||||
//
|
||||
// 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()
|
||||
defer t.serverNamesMu.Unlock()
|
||||
|
||||
for _, name := range dnsNames {
|
||||
host, _, err := net.SplitHostPort(name)
|
||||
if err != nil {
|
||||
host = name
|
||||
}
|
||||
if strings.TrimSpace(host) != "" && !certmagic.SubjectIsIP(host) {
|
||||
t.serverNames[strings.ToLower(host)] = struct{}{}
|
||||
host = strings.ToLower(strings.TrimSpace(host))
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user