diff --git a/admin.go b/admin.go index 0a9bfc49b..8cbf8fb22 100644 --- a/admin.go +++ b/admin.go @@ -34,6 +34,7 @@ import ( "net/url" "os" "path" + "reflect" "regexp" "strconv" "strings" @@ -55,6 +56,9 @@ func init() { if env, exists := os.LookupEnv("CADDY_ADMIN"); exists { DefaultAdminListen = env } + RegisterType("caddy.config_loaders", []reflect.Type{ + reflect.TypeOf((*ConfigLoader)(nil)).Elem(), + }) } // AdminConfig configures Caddy's API endpoint, which is used diff --git a/caddy.go b/caddy.go index 4a7bd8b6d..d011b4010 100644 --- a/caddy.go +++ b/caddy.go @@ -27,6 +27,7 @@ import ( "os" "path" "path/filepath" + "reflect" "runtime/debug" "strconv" "strings" @@ -41,6 +42,15 @@ import ( "github.com/caddyserver/caddy/v2/notify" ) +func init() { + RegisterType("", []reflect.Type{ + reflect.TypeOf((*App)(nil)).Elem(), + }) + RegisterType("caddy.storage", []reflect.Type{ + reflect.TypeOf((*StorageConverter)(nil)).Elem(), + }) +} + // Config is the top (or beginning) of the Caddy configuration structure. // Caddy config is expressed natively as a JSON document. If you prefer // not to work with JSON directly, there are [many config adapters](/docs/config-adapters) @@ -72,11 +82,15 @@ type Config struct { // module is `caddy.storage.file_system` (the local file system), // and the default path // [depends on the OS and environment](/docs/conventions#data-directory). + // A storage `module` should implement the following interfaces: + // - [StorageConverter](https://pkg.go.dev/github.com/caddyserver/caddy/v2#StorageConverter) StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"` // AppsRaw are the apps that Caddy will load and run. The // app module name is the key, and the app's config is the // associated value. + // An `app` should implement the following interfaces: + // - [caddy.App](https://pkg.go.dev/github.com/caddyserver/caddy/v2?tab=doc#App) AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="` apps map[string]App diff --git a/caddytest/integration/types_test.go b/caddytest/integration/types_test.go new file mode 100644 index 000000000..4ea28e98d --- /dev/null +++ b/caddytest/integration/types_test.go @@ -0,0 +1,22 @@ +package integration + +import ( + "testing" + + "github.com/caddyserver/caddy/v2" + _ "github.com/caddyserver/caddy/v2/modules/standard" +) + +// Validates Caddy's the registered internal types implement the necessary interfaces of their +// namespaces +func TestTypes(t *testing.T) { + var i int + for _, v := range caddy.Modules() { + mod, _ := caddy.GetModule(v) + if ok, err := caddy.ConformsToNamespace(mod.New(), mod.ID.Namespace()); !ok { + t.Errorf("%s", err) + } + i++ + } + t.Logf("Passed through %d modules", i) +} diff --git a/listeners.go b/listeners.go index 84a32e45a..7165e932c 100644 --- a/listeners.go +++ b/listeners.go @@ -24,6 +24,7 @@ import ( "net" "net/netip" "os" + "reflect" "strconv" "strings" "sync" @@ -37,6 +38,12 @@ import ( "github.com/caddyserver/caddy/v2/internal" ) +func init() { + RegisterType("caddy.listeners", []reflect.Type{ + reflect.TypeOf((*ListenerWrapper)(nil)).Elem(), + }) +} + // NetworkAddress represents one or more network addresses. // It contains the individual components for a parsed network // address of the form accepted by ParseNetworkAddress(). diff --git a/logging.go b/logging.go index 98b031c6a..2ddb34bed 100644 --- a/logging.go +++ b/logging.go @@ -20,6 +20,7 @@ import ( "io" "log" "os" + "reflect" "strings" "sync" "time" @@ -33,6 +34,12 @@ func init() { RegisterModule(StdoutWriter{}) RegisterModule(StderrWriter{}) RegisterModule(DiscardWriter{}) + RegisterType("caddy.logging.encoders", []reflect.Type{ + reflect.TypeOf((*zapcore.Encoder)(nil)).Elem(), + }) + RegisterType("caddy.logging.writers", []reflect.Type{ + reflect.TypeOf((*WriterOpener)(nil)).Elem(), + }) } // Logging facilitates logging within Caddy. The default log is @@ -265,6 +272,8 @@ type BaseLog struct { WriterRaw json.RawMessage `json:"writer,omitempty" caddy:"namespace=caddy.logging.writers inline_key=output"` // The encoder is how the log entries are formatted or encoded. + // An `encoder` should implement the following interfaces: + // - [zapcore.Encoder](https://pkg.go.dev/go.uber.org/zap/zapcore#Encoder) EncoderRaw json.RawMessage `json:"encoder,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"` // Level is the minimum level to emit, and is inclusive. diff --git a/modules/caddyhttp/caddyauth/basicauth.go b/modules/caddyhttp/caddyauth/basicauth.go index f30a8691a..15381ac02 100644 --- a/modules/caddyhttp/caddyauth/basicauth.go +++ b/modules/caddyhttp/caddyauth/basicauth.go @@ -21,8 +21,10 @@ import ( "fmt" weakrand "math/rand" "net/http" + "reflect" "strings" "sync" + "time" "golang.org/x/sync/singleflight" @@ -31,6 +33,15 @@ import ( func init() { caddy.RegisterModule(HTTPBasicAuth{}) + + caddy.RegisterType("http.authentication.hashes", []reflect.Type{ + reflect.TypeOf((*Comparer)(nil)).Elem(), + }) + caddy.RegisterType("http.authentication.providers", []reflect.Type{ + reflect.TypeOf((*Authenticator)(nil)).Elem(), + }) + + weakrand.Seed(time.Now().UnixNano()) } // HTTPBasicAuth facilitates HTTP basic authentication. diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go index dc35fa245..da97ec9bc 100644 --- a/modules/caddyhttp/encode/encode.go +++ b/modules/caddyhttp/encode/encode.go @@ -26,6 +26,7 @@ import ( "math" "net" "net/http" + "reflect" "sort" "strconv" "strings" @@ -37,6 +38,9 @@ import ( func init() { caddy.RegisterModule(Encode{}) + caddy.RegisterType("http.encoders", []reflect.Type{ + reflect.TypeOf((*Encoding)(nil)).Elem(), + }) } // Encode is a middleware which can encode responses. diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index 0ed558e8b..548f062ec 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -26,9 +26,11 @@ import ( "os" "path" "path/filepath" + "reflect" "runtime" "strconv" "strings" + "time" "go.uber.org/zap" @@ -38,6 +40,12 @@ import ( ) func init() { + weakrand.Seed(time.Now().UnixNano()) + + caddy.RegisterType("http.precompressed", []reflect.Type{ + reflect.TypeOf((*encode.Precompressed)(nil)).Elem(), + }) + caddy.RegisterModule(FileServer{}) } diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index b38597970..c6d8eaa0e 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -197,6 +197,8 @@ type ( // where each of the array elements is a matcher set, i.e. an // object keyed by matcher name. MatchNot struct { + // A `matcher` should implement the following interfaces: + // - [caddyhttp.RequestMatcher](https://pkg.go.dev/github.com/caddyserver/caddy/v2/modules/caddyhttp?tab=doc#RequestMatcher) MatcherSetsRaw []caddy.ModuleMap `json:"-" caddy:"namespace=http.matchers"` MatcherSets []MatcherSet `json:"-"` } @@ -212,6 +214,9 @@ func init() { caddy.RegisterModule(MatchHeaderRE{}) caddy.RegisterModule(new(MatchProtocol)) caddy.RegisterModule(MatchNot{}) + caddy.RegisterType("http.matchers", []reflect.Type{ + reflect.TypeOf((*RequestMatcher)(nil)).Elem(), + }) } // CaddyModule returns the Caddy module information. diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go index 041975d80..3471c40c8 100644 --- a/modules/caddyhttp/matchers_test.go +++ b/modules/caddyhttp/matchers_test.go @@ -1145,3 +1145,9 @@ func BenchmarkHostMatcherWithPlaceholder(b *testing.B) { match.Match(req) } } + +func TestConformsToNamespace(t *testing.T) { + if ok, err := caddy.ConformsToNamespace(new(StaticResponse), "http.matchers"); !ok || err != nil { + t.Errorf("%s", err) + } +} diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 1a76aef4c..b232247e9 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -27,6 +27,7 @@ import ( "net/netip" "net/textproto" "net/url" + "reflect" "strconv" "strings" "sync" @@ -45,6 +46,19 @@ import ( func init() { caddy.RegisterModule(Handler{}) + + caddy.RegisterType("http.reverse_proxy.circuit_breakers", []reflect.Type{ + reflect.TypeOf((*CircuitBreaker)(nil)).Elem(), + }) + caddy.RegisterType("http.reverse_proxy.selection_policies", []reflect.Type{ + reflect.TypeOf((*Selector)(nil)).Elem(), + }) + caddy.RegisterType("http.reverse_proxy.transport", []reflect.Type{ + reflect.TypeOf((*http.RoundTripper)(nil)).Elem(), + }) + caddy.RegisterType("http.reverse_proxy.upstreams", []reflect.Type{ + reflect.TypeOf((*UpstreamSource)(nil)).Elem(), + }) } // Handler implements a highly configurable and production-ready reverse proxy. diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go index 9be3d01a4..fbbac9fb8 100644 --- a/modules/caddyhttp/routes.go +++ b/modules/caddyhttp/routes.go @@ -18,10 +18,17 @@ import ( "encoding/json" "fmt" "net/http" + "reflect" "github.com/caddyserver/caddy/v2" ) +func init() { + caddy.RegisterType("http.handlers", []reflect.Type{ + reflect.TypeOf((*MiddlewareHandler)(nil)).Elem(), + }) +} + // Route consists of a set of rules for matching HTTP requests, // a list of handlers to execute, and optional flow control // parameters which customize the handling of HTTP requests diff --git a/modules/caddytls/automation.go b/modules/caddytls/automation.go index 6d085ee3f..1c331389c 100644 --- a/modules/caddytls/automation.go +++ b/modules/caddytls/automation.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "net/http" + "reflect" "strings" "time" @@ -30,6 +31,18 @@ import ( "github.com/caddyserver/caddy/v2" ) +func init() { + caddy.RegisterType("dns.provider", []reflect.Type{ + reflect.TypeOf((*acmez.Solver)(nil)).Elem(), + }) + caddy.RegisterType("tls.get_certificate", []reflect.Type{ + reflect.TypeOf((*certmagic.Manager)(nil)).Elem(), + }) + caddy.RegisterType("tls.issuance", []reflect.Type{ + reflect.TypeOf((*certmagic.Issuer)(nil)).Elem(), + }) +} + // AutomationConfig governs the automated management of TLS certificates. type AutomationConfig struct { // The list of automation policies. The first policy matching diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index 64fdd5138..811fbb032 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -23,6 +23,7 @@ import ( "io" "os" "path/filepath" + "reflect" "strings" "github.com/mholt/acmez" @@ -33,6 +34,9 @@ import ( func init() { caddy.RegisterModule(LeafCertClientAuth{}) + caddy.RegisterType("tls.handshake_match", []reflect.Type{ + reflect.TypeOf((*ConnectionMatcher)(nil)).Elem(), + }) } // ConnectionPolicies govern the establishment of TLS connections. It is diff --git a/modules/caddytls/sessiontickets.go b/modules/caddytls/sessiontickets.go index bfc5628ac..312f072d4 100644 --- a/modules/caddytls/sessiontickets.go +++ b/modules/caddytls/sessiontickets.go @@ -21,6 +21,7 @@ import ( "fmt" "io" "log" + "reflect" "runtime/debug" "sync" "time" @@ -28,6 +29,12 @@ import ( "github.com/caddyserver/caddy/v2" ) +func init() { + caddy.RegisterType("tls.stek", []reflect.Type{ + reflect.TypeOf((*STEKProvider)(nil)).Elem(), + }) +} + // SessionTicketService configures and manages TLS session tickets. type SessionTicketService struct { // KeySource is the method by which Caddy produces or obtains diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index b66b09c4d..0db080b9c 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -21,6 +21,7 @@ import ( "fmt" "log" "net/http" + "reflect" "runtime/debug" "sync" "time" @@ -35,6 +36,10 @@ import ( func init() { caddy.RegisterModule(TLS{}) caddy.RegisterModule(AutomateLoader{}) + + caddy.RegisterType("tls.certificates", []reflect.Type{ + reflect.TypeOf((*CertificateLoader)(nil)).Elem(), + }) } var ( @@ -652,6 +657,11 @@ func (AutomateLoader) CaddyModule() caddy.ModuleInfo { } } +// LoadCertificates is a stub so AutomateLoader can implement CertificateLoader +func (AutomateLoader) LoadCertificates() ([]Certificate, error) { + return nil, nil +} + // CertCacheOptions configures the certificate cache. type CertCacheOptions struct { // Maximum number of certificates to allow in the diff --git a/types.go b/types.go new file mode 100644 index 000000000..22758e9db --- /dev/null +++ b/types.go @@ -0,0 +1,35 @@ +package caddy + +import ( + "fmt" + "reflect" +) + +var namespaceTypes map[string][]reflect.Type = make(map[string][]reflect.Type) + +func RegisterType(namespace string, types []reflect.Type) { + if _, ok := namespaceTypes[namespace]; ok { + panic("namespace is already registered") + } + namespaceTypes[namespace] = types +} + +// NamespaceTypes returns a copy of Caddy's namespace->type registry +func NamespaceTypes() map[string][]reflect.Type { + copy := make(map[string][]reflect.Type) + for namespace, typeSlice := range namespaceTypes { + copy[namespace] = typeSlice + } + return copy +} + +// ConformsToNamespace validates the given module implements all the mandatory types of a given namespace +func ConformsToNamespace(mod Module, namespace string) (bool, error) { + modType := reflect.TypeOf(mod) + for _, t := range namespaceTypes[namespace] { + if !modType.Implements(t) { + return false, fmt.Errorf("%s does not implement %s", modType, t) + } + } + return true, nil +}