mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-26 08:42:31 -04:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cc58caa109 | |||
| d80774cb3f | |||
| a4a38c3e88 | |||
| 761347aa63 | |||
| 4ba16fe82c | |||
| 0fab9f0f7d | |||
| 5e76b5ee43 |
@@ -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
@@ -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))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
"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
|
||||||
|
|||||||
@@ -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