v2: Module documentation; refactor LoadModule(); new caddy struct tags (#2924)

This commit goes a long way toward making automated documentation of
Caddy config and Caddy modules possible. It's a broad, sweeping change,
but mostly internal. It allows us to automatically generate docs for all
Caddy modules (including future third-party ones) and make them viewable
on a web page; it also doubles as godoc comments.

As such, this commit makes significant progress in migrating the docs
from our temporary wiki page toward our new website which is still under
construction.

With this change, all host modules will use ctx.LoadModule() and pass in
both the struct pointer and the field name as a string. This allows the
reflect package to read the struct tag from that field so that it can
get the necessary information like the module namespace and the inline
key.

This has the nice side-effect of unifying the code and documentation. It
also simplifies module loading, and handles several variations on field
types for raw module fields (i.e. variations on json.RawMessage, such as
arrays and maps).

I also renamed ModuleInfo.Name -> ModuleInfo.ID, to make it clear that
the ID is the "full name" which includes both the module namespace and
the name. This clarity is helpful when describing module hierarchy.

As of this change, Caddy modules are no longer an experimental design.
I think the architecture is good enough to go forward.
This commit is contained in:
Matt Holt 2019-12-10 13:36:46 -07:00 committed by GitHub
parent a8533e5630
commit 3c90e370a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 1355 additions and 623 deletions

View file

@ -39,11 +39,32 @@ import (
// TODO: is there a way to make the admin endpoint so that it can be plugged into the HTTP app? see issue #2833 // TODO: is there a way to make the admin endpoint so that it can be plugged into the HTTP app? see issue #2833
// AdminConfig configures the admin endpoint. // AdminConfig configures Caddy's API endpoint, which is used
// to manage Caddy while it is running.
type AdminConfig struct { type AdminConfig struct {
// If true, the admin endpoint will be completely disabled.
// Note that this makes any runtime changes to the config
// impossible, since the interface to do so is through the
// admin endpoint.
Disabled bool `json:"disabled,omitempty"` Disabled bool `json:"disabled,omitempty"`
// The address to which the admin endpoint's listener should
// bind itself. Can be any single network address that can be
// parsed by Caddy.
Listen string `json:"listen,omitempty"` Listen string `json:"listen,omitempty"`
// If true, CORS headers will be emitted, and requests to the
// API will be rejected if their `Host` and `Origin` headers
// do not match the expected value(s). Use `origins` to
// customize which origins/hosts are allowed.If `origins` is
// not set, the listen address is the only value allowed by
// default.
EnforceOrigin bool `json:"enforce_origin,omitempty"` EnforceOrigin bool `json:"enforce_origin,omitempty"`
// The list of allowed origins for API requests. Only used if
// `enforce_origin` is true. If not set, the listener address
// will be the default value. If set but empty, no origins will
// be allowed.
Origins []string `json:"origins,omitempty"` Origins []string `json:"origins,omitempty"`
} }
@ -706,7 +727,7 @@ traverseLoop:
ptr = v[partInt] ptr = v[partInt]
default: default:
return fmt.Errorf("invalid path: %s", parts[:i+1]) return fmt.Errorf("invalid traversal path at: %s", strings.Join(parts[:i+1], "/"))
} }
} }

View file

@ -32,23 +32,44 @@ import (
"github.com/mholt/certmagic" "github.com/mholt/certmagic"
) )
// Config represents a Caddy configuration. It is the // Config is the top (or beginning) of the Caddy configuration structure.
// top of the module structure: all Caddy modules will // Caddy config is expressed natively as a JSON document. If you prefer
// be loaded, starting with this struct. In order to // not to work with JSON directly, there are [many config adapters](/docs/config-adapters)
// be loaded and run successfully, a Config and all its // available that can convert various inputs into Caddy JSON.
// modules must be JSON-encodable; i.e. when filling a //
// Config struct manually, its JSON-encodable fields // Many parts of this config are extensible through the use of Caddy modules.
// (the ones with JSON struct tags, usually ending with // Fields which have a json.RawMessage type and which appear as dots (•••) in
// "Raw" if they decode into a separate field) must be // the online docs can be fulfilled by modules in a certain module
// set so that they can be unmarshaled and provisioned. // namespace. The docs show which modules can be used in a given place.
// Setting the fields for the decoded values instead //
// will result in those values being overwritten at // Whenever a module is used, its name must be given either inline as part of
// unmarshaling/provisioning. // the module, or as the key to the module's value. The docs will make it clear
// which to use.
//
// Generally, all config settings are optional, as it is Caddy convention to
// have good, documented default values. If a parameter is required, the docs
// should say so.
//
// Go programs which are directly building a Config struct value should take
// care to populate the JSON-encodable fields of the struct (i.e. the fields
// with `json` struct tags) if employing the module lifecycle (e.g. Provision
// method calls).
type Config struct { type Config struct {
Admin *AdminConfig `json:"admin,omitempty"` Admin *AdminConfig `json:"admin,omitempty"`
Logging *Logging `json:"logging,omitempty"` Logging *Logging `json:"logging,omitempty"`
StorageRaw json.RawMessage `json:"storage,omitempty"`
AppsRaw map[string]json.RawMessage `json:"apps,omitempty"` // StorageRaw is a storage module that defines how/where Caddy
// stores assets (such as TLS certificates). By default, this is
// the local file system (`caddy.storage.file_system` module).
// If the `XDG_DATA_HOME` environment variable is set, then
// `$XDG_DATA_HOME/caddy` is the default folder. Otherwise,
// `$HOME/.local/share/caddy` is the default folder.
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.
AppsRaw ModuleMap `json:"apps,omitempty" caddy:"namespace="`
apps map[string]App apps map[string]App
storage certmagic.Storage storage certmagic.Storage
@ -322,7 +343,7 @@ func run(newCfg *Config, start bool) error {
// set up global storage and make it CertMagic's default storage, too // set up global storage and make it CertMagic's default storage, too
err = func() error { err = func() error {
if newCfg.StorageRaw != nil { if newCfg.StorageRaw != nil {
val, err := ctx.LoadModuleInline("module", "caddy.storage", newCfg.StorageRaw) val, err := ctx.LoadModule(newCfg, "StorageRaw")
if err != nil { if err != nil {
return fmt.Errorf("loading storage module: %v", err) return fmt.Errorf("loading storage module: %v", err)
} }
@ -331,8 +352,8 @@ func run(newCfg *Config, start bool) error {
return fmt.Errorf("creating storage value: %v", err) return fmt.Errorf("creating storage value: %v", err)
} }
newCfg.storage = stor newCfg.storage = stor
newCfg.StorageRaw = nil // allow GC to deallocate
} }
if newCfg.storage == nil { if newCfg.storage == nil {
newCfg.storage = &certmagic.FileStorage{Path: dataDir()} newCfg.storage = &certmagic.FileStorage{Path: dataDir()}
} }
@ -346,12 +367,12 @@ func run(newCfg *Config, start bool) error {
// Load, Provision, Validate each app and their submodules // Load, Provision, Validate each app and their submodules
err = func() error { err = func() error {
for modName, rawMsg := range newCfg.AppsRaw { appsIface, err := ctx.LoadModule(newCfg, "AppsRaw")
val, err := ctx.LoadModule(modName, rawMsg)
if err != nil { if err != nil {
return fmt.Errorf("loading app module '%s': %v", modName, err) return fmt.Errorf("loading app modules: %v", err)
} }
newCfg.apps[modName] = val.(App) for appName, appIface := range appsIface.(map[string]interface{}) {
newCfg.apps[appName] = appIface.(App)
} }
return nil return nil
}() }()
@ -447,7 +468,10 @@ func Validate(cfg *Config) error {
return err return err
} }
// Duration is a JSON-string-unmarshable duration type. // Duration can be an integer or a string. An integer is
// interpreted as nanoseconds. If a string, it is a Go
// time.Duration value such as `300ms`, `1.5h`, or `2h45m`;
// valid units are `ns`, `us`/`µs`, `ms`, `s`, `m`, and `h`.
type Duration time.Duration type Duration time.Duration
// UnmarshalJSON satisfies json.Unmarshaler. // UnmarshalJSON satisfies json.Unmarshaler.

View file

@ -21,6 +21,7 @@ import (
"net/http" "net/http"
"reflect" "reflect"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/caddy/v2/modules/caddytls"
@ -71,7 +72,7 @@ func parseRoot(h Helper) ([]ConfigValue, error) {
}, },
} }
if matcherSet != nil { if matcherSet != nil {
route.MatcherSetsRaw = []map[string]json.RawMessage{matcherSet} route.MatcherSetsRaw = []caddy.ModuleMap{matcherSet}
} }
return h.NewVarsRoute(route), nil return h.NewVarsRoute(route), nil

View file

@ -88,7 +88,7 @@ type Helper struct {
*caddyfile.Dispenser *caddyfile.Dispenser
options map[string]interface{} options map[string]interface{}
warnings *[]caddyconfig.Warning warnings *[]caddyconfig.Warning
matcherDefs map[string]map[string]json.RawMessage matcherDefs map[string]caddy.ModuleMap
parentBlock caddyfile.ServerBlock parentBlock caddyfile.ServerBlock
} }
@ -125,7 +125,7 @@ func (h Helper) JSON(val interface{}, warnings *[]caddyconfig.Warning) json.RawM
// if so, returns the matcher set along with a true value. If the current // if so, returns the matcher set along with a true value. If the current
// token is not a matcher, nil and false is returned. Note that a true // token is not a matcher, nil and false is returned. Note that a true
// value may be returned with a nil matcher set if it is a catch-all. // value may be returned with a nil matcher set if it is a catch-all.
func (h Helper) MatcherToken() (map[string]json.RawMessage, bool, error) { func (h Helper) MatcherToken() (caddy.ModuleMap, bool, error) {
if !h.NextArg() { if !h.NextArg() {
return nil, false, nil return nil, false, nil
} }
@ -133,13 +133,13 @@ func (h Helper) MatcherToken() (map[string]json.RawMessage, bool, error) {
} }
// NewRoute returns config values relevant to creating a new HTTP route. // NewRoute returns config values relevant to creating a new HTTP route.
func (h Helper) NewRoute(matcherSet map[string]json.RawMessage, func (h Helper) NewRoute(matcherSet caddy.ModuleMap,
handler caddyhttp.MiddlewareHandler) []ConfigValue { handler caddyhttp.MiddlewareHandler) []ConfigValue {
mod, err := caddy.GetModule(caddy.GetModuleName(handler)) mod, err := caddy.GetModule(caddy.GetModuleName(handler))
if err != nil { if err != nil {
// TODO: append to warnings // TODO: append to warnings
} }
var matcherSetsRaw []map[string]json.RawMessage var matcherSetsRaw []caddy.ModuleMap
if matcherSet != nil { if matcherSet != nil {
matcherSetsRaw = append(matcherSetsRaw, matcherSet) matcherSetsRaw = append(matcherSetsRaw, matcherSet)
} }
@ -148,7 +148,7 @@ func (h Helper) NewRoute(matcherSet map[string]json.RawMessage,
Class: "route", Class: "route",
Value: caddyhttp.Route{ Value: caddyhttp.Route{
MatcherSetsRaw: matcherSetsRaw, MatcherSetsRaw: matcherSetsRaw,
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", mod.ID(), h.warnings)}, HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(handler, "handler", mod.ID.Name(), h.warnings)},
}, },
}, },
} }

View file

@ -172,7 +172,7 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
} }
// now for the TLS app! (TODO: refactor into own func) // now for the TLS app! (TODO: refactor into own func)
tlsApp := caddytls.TLS{Certificates: make(map[string]json.RawMessage)} tlsApp := caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}
for _, p := range pairings { for _, p := range pairings {
for i, sblock := range p.serverBlocks { for i, sblock := range p.serverBlocks {
// tls automation policies // tls automation policies
@ -189,7 +189,7 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
} }
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, caddytls.AutomationPolicy{ tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, caddytls.AutomationPolicy{
Hosts: sblockHosts, Hosts: sblockHosts,
ManagementRaw: caddyconfig.JSONModuleObject(mm, "module", mm.(caddy.Module).CaddyModule().ID(), &warnings), ManagementRaw: caddyconfig.JSONModuleObject(mm, "module", mm.(caddy.Module).CaddyModule().ID.Name(), &warnings),
}) })
} else { } else {
warnings = append(warnings, caddyconfig.Warning{ warnings = append(warnings, caddyconfig.Warning{
@ -204,7 +204,7 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
for _, clVal := range clVals { for _, clVal := range clVals {
loader := clVal.Value.(caddytls.CertificateLoader) loader := clVal.Value.(caddytls.CertificateLoader)
loaderName := caddy.GetModuleID(loader) loaderName := caddy.GetModuleID(loader)
tlsApp.Certificates[loaderName] = caddyconfig.JSON(loader, &warnings) tlsApp.CertificatesRaw[loaderName] = caddyconfig.JSON(loader, &warnings)
} }
} }
} }
@ -243,17 +243,17 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
} }
// annnd the top-level config, then we're done! // annnd the top-level config, then we're done!
cfg := &caddy.Config{AppsRaw: make(map[string]json.RawMessage)} cfg := &caddy.Config{AppsRaw: make(caddy.ModuleMap)}
if !reflect.DeepEqual(httpApp, caddyhttp.App{}) { if !reflect.DeepEqual(httpApp, caddyhttp.App{}) {
cfg.AppsRaw["http"] = caddyconfig.JSON(httpApp, &warnings) cfg.AppsRaw["http"] = caddyconfig.JSON(httpApp, &warnings)
} }
if !reflect.DeepEqual(tlsApp, caddytls.TLS{Certificates: make(map[string]json.RawMessage)}) { if !reflect.DeepEqual(tlsApp, caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}) {
cfg.AppsRaw["tls"] = caddyconfig.JSON(tlsApp, &warnings) cfg.AppsRaw["tls"] = caddyconfig.JSON(tlsApp, &warnings)
} }
if storageCvtr, ok := options["storage"].(caddy.StorageConverter); ok { if storageCvtr, ok := options["storage"].(caddy.StorageConverter); ok {
cfg.StorageRaw = caddyconfig.JSONModuleObject(storageCvtr, cfg.StorageRaw = caddyconfig.JSONModuleObject(storageCvtr,
"module", "module",
storageCvtr.(caddy.Module).CaddyModule().ID(), storageCvtr.(caddy.Module).CaddyModule().ID.Name(),
&warnings) &warnings)
} }
if adminConfig, ok := options["admin"].(string); ok && adminConfig != "" { if adminConfig, ok := options["admin"].(string); ok && adminConfig != "" {
@ -337,7 +337,7 @@ func (st *ServerType) serversFromPairings(
} }
// TODO: are matchers needed if every hostname of the config is matched? // TODO: are matchers needed if every hostname of the config is matched?
cp.Matchers = map[string]json.RawMessage{ cp.MatchersRaw = caddy.ModuleMap{
"sni": caddyconfig.JSON(hosts, warnings), // make sure to match all hosts, not just auto-HTTPS-qualified ones "sni": caddyconfig.JSON(hosts, warnings), // make sure to match all hosts, not just auto-HTTPS-qualified ones
} }
srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp) srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp)
@ -469,9 +469,9 @@ func consolidateAutomationPolicies(aps []caddytls.AutomationPolicy) []caddytls.A
func matcherSetFromMatcherToken( func matcherSetFromMatcherToken(
tkn caddyfile.Token, tkn caddyfile.Token,
matcherDefs map[string]map[string]json.RawMessage, matcherDefs map[string]caddy.ModuleMap,
warnings *[]caddyconfig.Warning, warnings *[]caddyconfig.Warning,
) (map[string]json.RawMessage, bool, error) { ) (caddy.ModuleMap, bool, error) {
// matcher tokens can be wildcards, simple path matchers, // matcher tokens can be wildcards, simple path matchers,
// or refer to a pre-defined matcher by some name // or refer to a pre-defined matcher by some name
if tkn.Text == "*" { if tkn.Text == "*" {
@ -479,7 +479,7 @@ func matcherSetFromMatcherToken(
return nil, true, nil return nil, true, nil
} else if strings.HasPrefix(tkn.Text, "/") || strings.HasPrefix(tkn.Text, "=/") { } else if strings.HasPrefix(tkn.Text, "/") || strings.HasPrefix(tkn.Text, "=/") {
// convenient way to specify a single path match // convenient way to specify a single path match
return map[string]json.RawMessage{ return caddy.ModuleMap{
"path": caddyconfig.JSON(caddyhttp.MatchPath{tkn.Text}, warnings), "path": caddyconfig.JSON(caddyhttp.MatchPath{tkn.Text}, warnings),
}, true, nil }, true, nil
} else if strings.HasPrefix(tkn.Text, "match:") { } else if strings.HasPrefix(tkn.Text, "match:") {
@ -495,7 +495,7 @@ func matcherSetFromMatcherToken(
return nil, false, nil return nil, false, nil
} }
func (st *ServerType) compileEncodedMatcherSets(sblock caddyfile.ServerBlock) ([]map[string]json.RawMessage, error) { func (st *ServerType) compileEncodedMatcherSets(sblock caddyfile.ServerBlock) ([]caddy.ModuleMap, error) {
type hostPathPair struct { type hostPathPair struct {
hostm caddyhttp.MatchHost hostm caddyhttp.MatchHost
pathm caddyhttp.MatchPath pathm caddyhttp.MatchPath
@ -562,7 +562,7 @@ func (st *ServerType) compileEncodedMatcherSets(sblock caddyfile.ServerBlock) ([
} }
// finally, encode each of the matcher sets // finally, encode each of the matcher sets
var matcherSetsEnc []map[string]json.RawMessage var matcherSetsEnc []caddy.ModuleMap
for _, ms := range matcherSets { for _, ms := range matcherSets {
msEncoded, err := encodeMatcherSet(ms) msEncoded, err := encodeMatcherSet(ms)
if err != nil { if err != nil {
@ -574,8 +574,8 @@ func (st *ServerType) compileEncodedMatcherSets(sblock caddyfile.ServerBlock) ([
return matcherSetsEnc, nil return matcherSetsEnc, nil
} }
func parseMatcherDefinitions(d *caddyfile.Dispenser) (map[string]map[string]json.RawMessage, error) { func parseMatcherDefinitions(d *caddyfile.Dispenser) (map[string]caddy.ModuleMap, error) {
matchers := make(map[string]map[string]json.RawMessage) matchers := make(map[string]caddy.ModuleMap)
for d.Next() { for d.Next() {
definitionName := d.Val() definitionName := d.Val()
for nesting := d.Nesting(); d.NextBlock(nesting); { for nesting := d.Nesting(); d.NextBlock(nesting); {
@ -597,7 +597,7 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser) (map[string]map[string]json
return nil, fmt.Errorf("matcher module '%s' is not a request matcher", matcherName) return nil, fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
} }
if _, ok := matchers[definitionName]; !ok { if _, ok := matchers[definitionName]; !ok {
matchers[definitionName] = make(map[string]json.RawMessage) matchers[definitionName] = make(caddy.ModuleMap)
} }
matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil) matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil)
} }
@ -605,8 +605,8 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser) (map[string]map[string]json
return matchers, nil return matchers, nil
} }
func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (map[string]json.RawMessage, error) { func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (caddy.ModuleMap, error) {
msEncoded := make(map[string]json.RawMessage) msEncoded := make(caddy.ModuleMap)
for matcherName, val := range matchers { for matcherName, val := range matchers {
jsonBytes, err := json.Marshal(val) jsonBytes, err := json.Marshal(val)
if err != nil { if err != nil {
@ -628,7 +628,7 @@ func tryInt(val interface{}, warnings *[]caddyconfig.Warning) int {
} }
type matcherSetAndTokens struct { type matcherSetAndTokens struct {
matcherSet map[string]json.RawMessage matcherSet caddy.ModuleMap
tokens []caddyfile.Token tokens []caddyfile.Token
} }

View file

@ -96,7 +96,7 @@ func parseOptStorage(d *caddyfile.Dispenser) (caddy.StorageConverter, error) {
} }
unm, ok := mod.New().(caddyfile.Unmarshaler) unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok { if !ok {
return nil, fmt.Errorf("storage module '%s' is not a Caddyfile unmarshaler", mod.Name) return nil, fmt.Errorf("storage module '%s' is not a Caddyfile unmarshaler", mod.ID)
} }
err = unm.UnmarshalCaddyfile(d.NewFromNextTokens()) err = unm.UnmarshalCaddyfile(d.NewFromNextTokens())
if err != nil { if err != nil {
@ -104,7 +104,7 @@ func parseOptStorage(d *caddyfile.Dispenser) (caddy.StorageConverter, error) {
} }
storage, ok := unm.(caddy.StorageConverter) storage, ok := unm.(caddy.StorageConverter)
if !ok { if !ok {
return nil, fmt.Errorf("module %s is not a StorageConverter", mod.Name) return nil, fmt.Errorf("module %s is not a StorageConverter", mod.ID)
} }
return storage, nil return storage, nil
} }

View file

@ -321,11 +321,11 @@ func cmdListModules(fl Flags) (int, error) {
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil
} }
for _, modName := range caddy.Modules() { for _, modID := range caddy.Modules() {
modInfo, err := caddy.GetModule(modName) modInfo, err := caddy.GetModule(modID)
if err != nil { if err != nil {
// that's weird // that's weird
fmt.Println(modName) fmt.Println(modID)
continue continue
} }
@ -354,13 +354,13 @@ func cmdListModules(fl Flags) (int, error) {
} }
// if we could find no matching module, just print out // if we could find no matching module, just print out
// the module name instead // the module ID instead
if matched == nil { if matched == nil {
fmt.Println(modName) fmt.Println(modID)
continue continue
} }
fmt.Printf("%s %s\n", modName, matched.Version) fmt.Printf("%s %s\n", modID, matched.Version)
} }
return caddy.ExitCodeSuccess, nil return caddy.ExitCodeSuccess, nil

View file

@ -80,24 +80,211 @@ func (ctx *Context) OnCancel(f func()) {
ctx.cleanupFuncs = append(ctx.cleanupFuncs, f) ctx.cleanupFuncs = append(ctx.cleanupFuncs, f)
} }
// LoadModule decodes rawMsg into a new instance of mod and // LoadModule loads the Caddy module(s) from the specified field of the parent struct
// returns the value. If mod.New() does not return a pointer // pointer and returns the loaded module(s). The struct pointer and its field name as
// value, it is converted to one so that it is unmarshaled // a string are necessary so that reflection can be used to read the struct tag on the
// into the underlying concrete type. If mod.New is nil, an // field to get the module namespace and inline module name key (if specified).
// error is returned. If the module implements Validator or //
// Provisioner interfaces, those methods are invoked to // The field can be any one of the supported raw module types: json.RawMessage,
// ensure the module is fully configured and valid before // []json.RawMessage, map[string]json.RawMessage, or []map[string]json.RawMessage.
// being used. // ModuleMap may be used in place of map[string]json.RawMessage. The return value's
func (ctx Context) LoadModule(name string, rawMsg json.RawMessage) (interface{}, error) { // underlying type mirrors the input field's type:
modulesMu.Lock() //
mod, ok := modules[name] // json.RawMessage => interface{}
modulesMu.Unlock() // []json.RawMessage => []interface{}
// map[string]json.RawMessage => map[string]interface{}
// []map[string]json.RawMessage => []map[string]interface{}
//
// The field must have a "caddy" struct tag in this format:
//
// caddy:"key1=val1 key2=val2"
//
// To load modules, a "namespace" key is required. For example, to load modules
// in the "http.handlers" namespace, you'd put: `namespace=http.handlers` in the
// Caddy struct tag.
//
// The module name must also be available. If the field type is a map or slice of maps,
// then key is assumed to be the module name if an "inline_key" is NOT specified in the
// caddy struct tag. In this case, the module name does NOT need to be specified in-line
// with the module itself.
//
// If not a map, or if inline_key is non-empty, then the module name must be embedded
// into the values, which must be objects; then there must be a key in those objects
// where its associated value is the module name. This is called the "inline key",
// meaning the key containing the module's name that is defined inline with the module
// itself. You must specify the inline key in a struct tag, along with the namespace:
//
// caddy:"namespace=http.handlers inline_key=handler"
//
// This will look for a key/value pair like `"handler": "..."` in the json.RawMessage
// in order to know the module name.
//
// To make use of the loaded module(s) (the return value), you will probably want
// to type-assert each interface{} value(s) to the types that are useful to you
// and store them on the same struct. Storing them on the same struct makes for
// easy garbage collection when your host module is no longer needed.
//
// Loaded modules have already been provisioned and validated.
func (ctx Context) LoadModule(structPointer interface{}, fieldName string) (interface{}, error) {
val := reflect.ValueOf(structPointer).Elem().FieldByName(fieldName)
typ := val.Type()
field, ok := reflect.TypeOf(structPointer).Elem().FieldByName(fieldName)
if !ok { if !ok {
return nil, fmt.Errorf("unknown module: %s", name) panic(fmt.Sprintf("field %s does not exist in %#v", fieldName, structPointer))
}
opts, err := ParseStructTag(field.Tag.Get("caddy"))
if err != nil {
panic(fmt.Sprintf("malformed tag on field %s: %v", fieldName, err))
}
moduleNamespace, ok := opts["namespace"]
if !ok {
panic(fmt.Sprintf("missing 'namespace' key in struct tag on field %s", fieldName))
}
inlineModuleKey := opts["inline_key"]
var result interface{}
switch val.Kind() {
case reflect.Slice:
if isJSONRawMessage(typ) {
// val is `json.RawMessage` ([]uint8 under the hood)
if inlineModuleKey == "" {
panic("unable to determine module name without inline_key when type is not a ModuleMap")
}
val, err := ctx.loadModuleInline(inlineModuleKey, moduleNamespace, val.Interface().(json.RawMessage))
if err != nil {
return nil, err
}
result = val
} else if isJSONRawMessage(typ.Elem()) {
// val is `[]json.RawMessage`
if inlineModuleKey == "" {
panic("unable to determine module name without inline_key because type is not a ModuleMap")
}
var all []interface{}
for i := 0; i < val.Len(); i++ {
val, err := ctx.loadModuleInline(inlineModuleKey, moduleNamespace, val.Index(i).Interface().(json.RawMessage))
if err != nil {
return nil, fmt.Errorf("position %d: %v", i, err)
}
all = append(all, val)
}
result = all
} else if isModuleMapType(typ.Elem()) {
// val is `[]map[string]json.RawMessage`
var all []map[string]interface{}
for i := 0; i < val.Len(); i++ {
thisSet, err := ctx.loadModulesFromSomeMap(moduleNamespace, inlineModuleKey, val.Index(i))
if err != nil {
return nil, err
}
all = append(all, thisSet)
}
result = all
}
case reflect.Map:
// val is a ModuleMap or some other kind of map
result, err = ctx.loadModulesFromSomeMap(moduleNamespace, inlineModuleKey, val)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unrecognized type for module: %s", typ)
}
// we're done with the raw bytes; allow GC to deallocate
val.Set(reflect.Zero(typ))
return result, nil
}
// loadModulesFromSomeMap loads modules from val, which must be a type of map[string]interface{}.
// Depending on inlineModuleKey, it will be interpeted as either a ModuleMap (key is the module
// name) or as a regular map (key is not the module name, and module name is defined inline).
func (ctx Context) loadModulesFromSomeMap(namespace, inlineModuleKey string, val reflect.Value) (map[string]interface{}, error) {
// if no inline_key is specified, then val must be a ModuleMap,
// where the key is the module name
if inlineModuleKey == "" {
if !isModuleMapType(val.Type()) {
panic(fmt.Sprintf("expected ModuleMap because inline_key is empty; but we do not recognize this type: %s", val.Type()))
}
return ctx.loadModuleMap(namespace, val)
}
// otherwise, val is a map with modules, but the module name is
// inline with each value (the key means something else)
return ctx.loadModulesFromRegularMap(namespace, inlineModuleKey, val)
}
// loadModulesFromRegularMap loads modules from val, where val is a map[string]json.RawMessage.
// Map keys are NOT interpreted as module names, so module names are still expected to appear
// inline with the objects.
func (ctx Context) loadModulesFromRegularMap(namespace, inlineModuleKey string, val reflect.Value) (map[string]interface{}, error) {
mods := make(map[string]interface{})
iter := val.MapRange()
for iter.Next() {
k := iter.Key()
v := iter.Value()
mod, err := ctx.loadModuleInline(inlineModuleKey, namespace, v.Interface().(json.RawMessage))
if err != nil {
return nil, fmt.Errorf("key %s: %v", k, err)
}
mods[k.String()] = mod
}
return mods, nil
}
// loadModuleMap loads modules from a ModuleMap, i.e. map[string]interface{}, where the key is the
// module name. With a module map, module names do not need to be defined inline with their values.
func (ctx Context) loadModuleMap(namespace string, val reflect.Value) (map[string]interface{}, error) {
all := make(map[string]interface{})
iter := val.MapRange()
for iter.Next() {
k := iter.Key().Interface().(string)
v := iter.Value().Interface().(json.RawMessage)
moduleName := namespace + "." + k
if namespace == "" {
moduleName = k
}
val, err := ctx.LoadModuleByID(moduleName, v)
if err != nil {
return nil, fmt.Errorf("module name '%s': %v", k, err)
}
all[k] = val
}
return all, nil
}
// LoadModuleByID decodes rawMsg into a new instance of mod and
// returns the value. If mod.New is nil, an error is returned.
// If the module implements Validator or Provisioner interfaces,
// those methods are invoked to ensure the module is fully
// configured and valid before being used.
//
// This is a lower-level method and will usually not be called
// directly by most modules. However, this method is useful when
// dynamically loading/unloading modules in their own context,
// like from embedded scripts, etc.
func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{}, error) {
modulesMu.RLock()
mod, ok := modules[id]
modulesMu.RUnlock()
if !ok {
return nil, fmt.Errorf("unknown module: %s", id)
} }
if mod.New == nil { if mod.New == nil {
return nil, fmt.Errorf("module '%s' has no constructor", mod.Name) return nil, fmt.Errorf("module '%s' has no constructor", mod.ID)
} }
val := mod.New().(interface{}) val := mod.New().(interface{})
@ -108,7 +295,7 @@ func (ctx Context) LoadModule(name string, rawMsg json.RawMessage) (interface{},
if rv := reflect.ValueOf(val); rv.Kind() != reflect.Ptr { if rv := reflect.ValueOf(val); rv.Kind() != reflect.Ptr {
log.Printf("[WARNING] ModuleInfo.New() for module '%s' did not return a 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"+ " 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.", name) " using new(Type) or &Type notation in your module's New() function.", id)
val = reflect.New(rv.Type()).Elem().Addr().Interface().(Module) val = reflect.New(rv.Type()).Elem().Addr().Interface().(Module)
} }
@ -116,7 +303,7 @@ func (ctx Context) LoadModule(name string, rawMsg json.RawMessage) (interface{},
if len(rawMsg) > 0 { if len(rawMsg) > 0 {
err := strictUnmarshalJSON(rawMsg, &val) err := strictUnmarshalJSON(rawMsg, &val)
if err != nil { if err != nil {
return nil, fmt.Errorf("decoding module config: %s: %v", mod.Name, err) return nil, fmt.Errorf("decoding module config: %s: %v", mod, err)
} }
} }
@ -124,8 +311,8 @@ func (ctx Context) LoadModule(name string, rawMsg json.RawMessage) (interface{},
// returned module values are almost always type-asserted // returned module values are almost always type-asserted
// before being used, so a nil value would panic; and there // before being used, so a nil value would panic; and there
// is no good reason to explicitly declare null modules in // is no good reason to explicitly declare null modules in
// a config; it might be because the user is trying to // a config; it might be because the user is trying to achieve
// achieve a result they aren't expecting, which is a smell // a result the developer isn't expecting, which is a smell
return nil, fmt.Errorf("module value cannot be null") return nil, fmt.Errorf("module value cannot be null")
} }
@ -140,7 +327,7 @@ func (ctx Context) LoadModule(name string, rawMsg json.RawMessage) (interface{},
err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2) err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2)
} }
} }
return nil, fmt.Errorf("provision %s: %v", mod.Name, err) return nil, fmt.Errorf("provision %s: %v", mod, err)
} }
} }
@ -154,33 +341,33 @@ func (ctx Context) LoadModule(name string, rawMsg json.RawMessage) (interface{},
err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2) err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2)
} }
} }
return nil, fmt.Errorf("%s: invalid configuration: %v", mod.Name, err) return nil, fmt.Errorf("%s: invalid configuration: %v", mod, err)
} }
} }
ctx.moduleInstances[name] = append(ctx.moduleInstances[name], val) ctx.moduleInstances[id] = append(ctx.moduleInstances[id], val)
return val, nil return val, nil
} }
// LoadModuleInline loads a module from a JSON raw message which decodes // loadModuleInline loads a module from a JSON raw message which decodes to
// to a map[string]interface{}, where one of the keys is moduleNameKey // a map[string]interface{}, where one of the object keys is moduleNameKey
// and the corresponding value is the module name as a string, which // and the corresponding value is the module name (as a string) which can
// can be found in the given scope. // be found in the given scope. In other words, the module name is declared
// in-line with the module itself.
// //
// This allows modules to be decoded into their concrete types and // This allows modules to be decoded into their concrete types and used when
// used when their names cannot be the unique key in a map, such as // their names cannot be the unique key in a map, such as when there are
// when there are multiple instances in the map or it appears in an // multiple instances in the map or it appears in an array (where there are
// array (where there are no custom keys). In other words, the key // no custom keys). In other words, the key containing the module name is
// containing the module name is treated special/separate from all // treated special/separate from all the other keys in the object.
// the other keys. func (ctx Context) loadModuleInline(moduleNameKey, moduleScope string, raw json.RawMessage) (interface{}, error) {
func (ctx Context) LoadModuleInline(moduleNameKey, moduleScope string, raw json.RawMessage) (interface{}, error) {
moduleName, raw, err := getModuleNameInline(moduleNameKey, raw) moduleName, raw, err := getModuleNameInline(moduleNameKey, raw)
if err != nil { if err != nil {
return nil, err return nil, err
} }
val, err := ctx.LoadModule(moduleScope+"."+moduleName, raw) val, err := ctx.LoadModuleByID(moduleScope+"."+moduleName, raw)
if err != nil { if err != nil {
return nil, fmt.Errorf("loading module '%s': %v", moduleName, err) return nil, fmt.Errorf("loading module '%s': %v", moduleName, err)
} }
@ -195,7 +382,7 @@ func (ctx Context) App(name string) (interface{}, error) {
if app, ok := ctx.cfg.apps[name]; ok { if app, ok := ctx.cfg.apps[name]; ok {
return app, nil return app, nil
} }
modVal, err := ctx.LoadModule(name, nil) modVal, err := ctx.LoadModuleByID(name, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("instantiating new module %s: %v", name, err) return nil, fmt.Errorf("instantiating new module %s: %v", name, err)
} }

View file

@ -36,8 +36,36 @@ func init() {
} }
// Logging facilitates logging within Caddy. // Logging facilitates logging within Caddy.
//
// By default, all logs at INFO level and higher are written to
// standard error ("stderr" writer) in a human-readable format
// ("console" encoder). The default log is called "default" and
// you can customize it. You can also define additional logs.
//
// All defined logs accept all log entries by default, but you
// can filter by level and module/logger names. A logger's name
// is the same as the module's name, but a module may append to
// logger names for more specificity. For example, you can
// filter logs emitted only by HTTP handlers using the name
// "http.handlers", because all HTTP handler module names have
// that prefix.
//
// Caddy logs (except the sink) are mostly zero-allocation, so
// they are very high-performing in terms of memory and CPU time.
// Enabling sampling can further increase throughput on extremely
// high-load servers.
type Logging struct { type Logging struct {
// Sink is the destination for all unstructured logs emitted
// from Go's standard library logger. These logs are common
// in dependencies that are not designed specifically for use
// in Caddy. Because it is global and unstructured, the sink
// lacks most advanced features and customizations.
Sink *StandardLibLog `json:"sink,omitempty"` Sink *StandardLibLog `json:"sink,omitempty"`
// Logs are your logs, keyed by an arbitrary name of your
// choosing. The default log can be customized by defining
// a log called "default". You can further define other logs
// and filter what kinds of entries they accept.
Logs map[string]*CustomLog `json:"logs,omitempty"` Logs map[string]*CustomLog `json:"logs,omitempty"`
// a list of all keys for open writers; all writers // a list of all keys for open writers; all writers
@ -169,12 +197,12 @@ func (logging *Logging) closeLogs() error {
// Logger returns a logger that is ready for the module to use. // Logger returns a logger that is ready for the module to use.
func (logging *Logging) Logger(mod Module) *zap.Logger { func (logging *Logging) Logger(mod Module) *zap.Logger {
modName := mod.CaddyModule().Name modID := string(mod.CaddyModule().ID)
var cores []zapcore.Core var cores []zapcore.Core
if logging != nil { if logging != nil {
for _, l := range logging.Logs { for _, l := range logging.Logs {
if l.matchesModule(modName) { if l.matchesModule(modID) {
if len(l.Include) == 0 && len(l.Exclude) == 0 { if len(l.Include) == 0 && len(l.Exclude) == 0 {
cores = append(cores, l.core) cores = append(cores, l.core)
continue continue
@ -186,7 +214,7 @@ func (logging *Logging) Logger(mod Module) *zap.Logger {
multiCore := zapcore.NewTee(cores...) multiCore := zapcore.NewTee(cores...)
return zap.New(multiCore).Named(modName) return zap.New(multiCore).Named(string(modID))
} }
// openWriter opens a writer using opener, and returns true if // openWriter opens a writer using opener, and returns true if
@ -231,26 +259,27 @@ func (wdest writerDestructor) Destruct() error {
// StandardLibLog configures the default Go standard library // StandardLibLog configures the default Go standard library
// global logger in the log package. This is necessary because // global logger in the log package. This is necessary because
// module dependencies which are not built specifically for // module dependencies which are not built specifically for
// Caddy will use the standard logger. // Caddy will use the standard logger. This is also known as
// the "sink" logger.
type StandardLibLog struct { type StandardLibLog struct {
WriterRaw json.RawMessage `json:"writer,omitempty"` // The module that writes out log entries for the sink.
WriterRaw json.RawMessage `json:"writer,omitempty" caddy:"namespace=caddy.logging.writers inline_key=output"`
writer io.WriteCloser writer io.WriteCloser
} }
func (sll *StandardLibLog) provision(ctx Context, logging *Logging) error { func (sll *StandardLibLog) provision(ctx Context, logging *Logging) error {
if sll.WriterRaw != nil { if sll.WriterRaw != nil {
val, err := ctx.LoadModuleInline("output", "caddy.logging.writers", sll.WriterRaw) mod, err := ctx.LoadModule(sll, "WriterRaw")
if err != nil { if err != nil {
return fmt.Errorf("loading sink log writer module: %v", err) return fmt.Errorf("loading sink log writer module: %v", err)
} }
wo := val.(WriterOpener) wo := mod.(WriterOpener)
sll.WriterRaw = nil // allow GC to deallocate
var isNew bool var isNew bool
sll.writer, isNew, err = logging.openWriter(wo) sll.writer, isNew, err = logging.openWriter(wo)
if err != nil { if err != nil {
return fmt.Errorf("opening sink log writer %#v: %v", val, err) return fmt.Errorf("opening sink log writer %#v: %v", mod, err)
} }
if isNew { if isNew {
@ -264,12 +293,39 @@ func (sll *StandardLibLog) provision(ctx Context, logging *Logging) error {
} }
// CustomLog represents a custom logger configuration. // CustomLog represents a custom logger configuration.
//
// By default, a log will emit all log entries. Some entries
// will be skipped if sampling is enabled. Further, the Include
// and Exclude parameters define which loggers (by name) are
// allowed or rejected from emitting in this log. If both Include
// and Exclude are populated, their values must be mutually
// exclusive, and longer namespaces have priority. If neither
// are populated, all logs are emitted.
type CustomLog struct { type CustomLog struct {
WriterRaw json.RawMessage `json:"writer,omitempty"` // The writer defines where log entries are emitted.
EncoderRaw json.RawMessage `json:"encoder,omitempty"` 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.
EncoderRaw json.RawMessage `json:"encoder,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"`
// Level is the minimum level to emit, and is inclusive.
// Possible levels: DEBUG, INFO, WARN, ERROR, PANIC, and FATAL
Level string `json:"level,omitempty"` Level string `json:"level,omitempty"`
// Sampling configures log entry sampling. If enabled,
// only some log entries will be emitted. This is useful
// for improving performance on extremely high-pressure
// servers.
Sampling *LogSampling `json:"sampling,omitempty"` Sampling *LogSampling `json:"sampling,omitempty"`
// Include defines the names of loggers to emit in this
// log. For example, to include only logs emitted by the
// admin API, you would include "admin.api".
Include []string `json:"include,omitempty"` Include []string `json:"include,omitempty"`
// Exclude defines the names of loggers that should be
// skipped by this log. For example, to exclude only
// HTTP access logs, you would exclude "http.log.access".
Exclude []string `json:"exclude,omitempty"` Exclude []string `json:"exclude,omitempty"`
writerOpener WriterOpener writerOpener WriterOpener
@ -336,24 +392,22 @@ func (cl *CustomLog) provision(ctx Context, logging *Logging) error {
} }
if cl.EncoderRaw != nil { if cl.EncoderRaw != nil {
val, err := ctx.LoadModuleInline("format", "caddy.logging.encoders", cl.EncoderRaw) mod, err := ctx.LoadModule(cl, "EncoderRaw")
if err != nil { if err != nil {
return fmt.Errorf("loading log encoder module: %v", err) return fmt.Errorf("loading log encoder module: %v", err)
} }
cl.EncoderRaw = nil // allow GC to deallocate cl.encoder = mod.(zapcore.Encoder)
cl.encoder = val.(zapcore.Encoder)
} }
if cl.encoder == nil { if cl.encoder == nil {
cl.encoder = newDefaultProductionLogEncoder() cl.encoder = newDefaultProductionLogEncoder()
} }
if cl.WriterRaw != nil { if cl.WriterRaw != nil {
val, err := ctx.LoadModuleInline("output", "caddy.logging.writers", cl.WriterRaw) mod, err := ctx.LoadModule(cl, "WriterRaw")
if err != nil { if err != nil {
return fmt.Errorf("loading log writer module: %v", err) return fmt.Errorf("loading log writer module: %v", err)
} }
cl.WriterRaw = nil // allow GC to deallocate cl.writerOpener = mod.(WriterOpener)
cl.writerOpener = val.(WriterOpener)
} }
if cl.writerOpener == nil { if cl.writerOpener == nil {
cl.writerOpener = StderrWriter{} cl.writerOpener = StderrWriter{}
@ -398,8 +452,8 @@ func (cl *CustomLog) buildCore() {
cl.core = c cl.core = c
} }
func (cl *CustomLog) matchesModule(moduleName string) bool { func (cl *CustomLog) matchesModule(moduleID string) bool {
return cl.loggerAllowed(moduleName, true) return cl.loggerAllowed(string(moduleID), true)
} }
// loggerAllowed returns true if name is allowed to emit // loggerAllowed returns true if name is allowed to emit
@ -493,8 +547,16 @@ func (fc *filteringCore) Check(e zapcore.Entry, ce *zapcore.CheckedEntry) *zapco
// LogSampling configures log entry sampling. // LogSampling configures log entry sampling.
type LogSampling struct { type LogSampling struct {
// The window over which to conduct sampling.
Interval time.Duration `json:"interval,omitempty"` Interval time.Duration `json:"interval,omitempty"`
// Log this many entries within a given level and
// message for each interval.
First int `json:"first,omitempty"` First int `json:"first,omitempty"`
// If more entries with the same level and message
// are seen during the same interval, keep one in
// this many entries until the end of the interval.
Thereafter int `json:"thereafter,omitempty"` Thereafter int `json:"thereafter,omitempty"`
} }
@ -512,7 +574,7 @@ type (
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (StdoutWriter) CaddyModule() ModuleInfo { func (StdoutWriter) CaddyModule() ModuleInfo {
return ModuleInfo{ return ModuleInfo{
Name: "caddy.logging.writers.stdout", ID: "caddy.logging.writers.stdout",
New: func() Module { return new(StdoutWriter) }, New: func() Module { return new(StdoutWriter) },
} }
} }
@ -520,7 +582,7 @@ func (StdoutWriter) CaddyModule() ModuleInfo {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (StderrWriter) CaddyModule() ModuleInfo { func (StderrWriter) CaddyModule() ModuleInfo {
return ModuleInfo{ return ModuleInfo{
Name: "caddy.logging.writers.stderr", ID: "caddy.logging.writers.stderr",
New: func() Module { return new(StderrWriter) }, New: func() Module { return new(StderrWriter) },
} }
} }
@ -528,7 +590,7 @@ func (StderrWriter) CaddyModule() ModuleInfo {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (DiscardWriter) CaddyModule() ModuleInfo { func (DiscardWriter) CaddyModule() ModuleInfo {
return ModuleInfo{ return ModuleInfo{
Name: "caddy.logging.writers.discard", ID: "caddy.logging.writers.discard",
New: func() Module { return new(DiscardWriter) }, New: func() Module { return new(DiscardWriter) },
} }
} }

View file

@ -18,58 +18,107 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"reflect"
"sort" "sort"
"strings" "strings"
"sync" "sync"
) )
// Module is a type that is used as a Caddy module. // Module is a type that is used as a Caddy module. In
// addition to this interface, most modules will implement
// some interface expected by their host module in order
// to be useful. To learn which interface(s) to implement,
// see the documentation for the host module. At a bare
// minimum, this interface, when implemented, only provides
// the module's ID and constructor function.
//
// Modules will often implement additional interfaces
// including Provisioner, Validator, and CleanerUpper.
// If a module implements these interfaces, their
// methods are called during the module's lifespan.
//
// When a module is loaded by a host module, the following
// happens: 1) ModuleInfo.New() is called to get a new
// instance of the module. 2) The module's configuration is
// unmarshaled into that instance. 3) If the module is a
// Provisioner, the Provision() method is called. 4) If the
// module is a Validator, the Validate() method is called.
// 5) The module will probably be type-asserted from
// interface{} to some other, more useful interface expected
// by the host module. For example, HTTP handler modules are
// type-asserted as caddyhttp.MiddlewareHandler values.
// 6) When a module's containing Context is canceled, if it is
// a CleanerUpper, its Cleanup() method is called.
type Module interface { type Module interface {
// This method indicates the type is a Caddy // This method indicates that the type is a Caddy
// module. The returned ModuleInfo must have // module. The returned ModuleInfo must have both
// both a name and a constructor function. // a name and a constructor function. This method
// This method must not have any side-effects. // must not have any side-effects.
CaddyModule() ModuleInfo CaddyModule() ModuleInfo
} }
// ModuleInfo represents a registered Caddy module. // ModuleInfo represents a registered Caddy module.
type ModuleInfo struct { type ModuleInfo struct {
// Name is the full name of the module. It // ID is the "full name" of the module. It
// must be unique and properly namespaced. // must be unique and properly namespaced.
Name string ID ModuleID
// New returns a pointer to a new, empty // New returns a pointer to a new, empty
// instance of the module's type. The host // instance of the module's type. This
// module which instantiates this module will // function must not have any side-effects.
// likely type-assert and invoke methods on
// the returned value. This function must not
// have any side-effects.
New func() Module New func() Module
} }
// Namespace returns the module's namespace (scope) // ModuleID is a string that uniquely identifies a Caddy module. A
// which is all but the last element of its name. // module ID is lightly structured. It consists of dot-separated
// If there is no explicit namespace in the name, // labels which form a simple hierarchy from left to right. The last
// the whole name is considered the namespace. // label is the module name, and the labels before that constitute
func (mi ModuleInfo) Namespace() string { // the namespace (or scope).
lastDot := strings.LastIndex(mi.Name, ".") //
// Thus, a module ID has the form: <namespace>.<id>
//
// An ID with no dot has the empty namespace, which is appropriate
// for app modules (these are "top-level" modules that Caddy core
// loads and runs).
//
// Module IDs should be lowercase and use underscore (_) instead of
// spaces.
//
// Example valid names:
// - http
// - http.handlers.file_server
// - caddy.logging.encoders.json
type ModuleID string
// Namespace returns the namespace (or scope) portion of a module ID,
// which is all but the last label of the ID. If the ID has only one
// label, then
func (id ModuleID) Namespace() string {
lastDot := strings.LastIndex(string(id), ".")
if lastDot < 0 { if lastDot < 0 {
return mi.Name return string(id)
} }
return mi.Name[:lastDot] return string(id)[:lastDot]
} }
// ID returns a module's ID, which is the // Name returns the Name (last element) of a module name.
// last element of its name. func (id ModuleID) Name() string {
func (mi ModuleInfo) ID() string { if id == "" {
if mi.Name == "" {
return "" return ""
} }
parts := strings.Split(mi.Name, ".") parts := strings.Split(string(id), ".")
return parts[len(parts)-1] return parts[len(parts)-1]
} }
func (mi ModuleInfo) String() string { return mi.Name } func (mi ModuleInfo) String() string { return string(mi.ID) }
// ModuleMap is a map that can contain multiple modules,
// where the map key is the module's name. (The namespace
// is usually read from an associated field's struct tag.)
// Because the module's name is given as the key in a
// module map, the name does not have to be given in the
// json.RawMessage.
type ModuleMap map[string]json.RawMessage
// RegisterModule registers a module by receiving a // RegisterModule registers a module by receiving a
// plain/empty value of the module. For registration to // plain/empty value of the module. For registration to
@ -82,11 +131,11 @@ func (mi ModuleInfo) String() string { return mi.Name }
func RegisterModule(instance Module) error { func RegisterModule(instance Module) error {
mod := instance.CaddyModule() mod := instance.CaddyModule()
if mod.Name == "" { if mod.ID == "" {
return fmt.Errorf("missing ModuleInfo.Name") return fmt.Errorf("module ID missing")
} }
if mod.Name == "caddy" || mod.Name == "admin" { if mod.ID == "caddy" || mod.ID == "admin" {
return fmt.Errorf("module name '%s' is reserved", mod.Name) return fmt.Errorf("module ID '%s' is reserved", mod.ID)
} }
if mod.New == nil { if mod.New == nil {
return fmt.Errorf("missing ModuleInfo.New") return fmt.Errorf("missing ModuleInfo.New")
@ -98,17 +147,17 @@ func RegisterModule(instance Module) error {
modulesMu.Lock() modulesMu.Lock()
defer modulesMu.Unlock() defer modulesMu.Unlock()
if _, ok := modules[mod.Name]; ok { if _, ok := modules[string(mod.ID)]; ok {
return fmt.Errorf("module already registered: %s", mod.Name) return fmt.Errorf("module already registered: %s", mod.ID)
} }
modules[mod.Name] = mod modules[string(mod.ID)] = mod
return nil return nil
} }
// GetModule returns module information from its full name. // GetModule returns module information from its ID (full name).
func GetModule(name string) (ModuleInfo, error) { func GetModule(name string) (ModuleInfo, error) {
modulesMu.Lock() modulesMu.RLock()
defer modulesMu.Unlock() defer modulesMu.RUnlock()
m, ok := modules[name] m, ok := modules[name]
if !ok { if !ok {
return ModuleInfo{}, fmt.Errorf("module not registered: %s", name) return ModuleInfo{}, fmt.Errorf("module not registered: %s", name)
@ -116,25 +165,25 @@ func GetModule(name string) (ModuleInfo, error) {
return m, nil return m, nil
} }
// GetModuleName returns a module's name from an instance of its value. // GetModuleName returns a module's name (the last label of its ID)
// If the value is not a module, an empty name will be returned. // from an instance of its value. If the value is not a module, an
// empty string will be returned.
func GetModuleName(instance interface{}) string { func GetModuleName(instance interface{}) string {
var name string var name string
if mod, ok := instance.(Module); ok { if mod, ok := instance.(Module); ok {
name = mod.CaddyModule().Name name = mod.CaddyModule().ID.Name()
} }
return name return name
} }
// GetModuleID returns a module's ID (the last element of its name) // GetModuleID returns a module's ID from an instance of its value.
// from an instance of its value. If the value is not a module, // If the value is not a module, an empty string will be returned.
// an empty string will be returned.
func GetModuleID(instance interface{}) string { func GetModuleID(instance interface{}) string {
var name string var id string
if mod, ok := instance.(Module); ok { if mod, ok := instance.(Module); ok {
name = mod.CaddyModule().ID() id = string(mod.CaddyModule().ID)
} }
return name return id
} }
// GetModules returns all modules in the given scope/namespace. // GetModules returns all modules in the given scope/namespace.
@ -144,11 +193,11 @@ func GetModuleID(instance interface{}) string {
// scopes are not matched (i.e. scope "foo.ba" does not match // scopes are not matched (i.e. scope "foo.ba" does not match
// name "foo.bar"). // name "foo.bar").
// //
// Because modules are registered to a map, the returned slice // Because modules are registered to a map under the hood, the
// will be sorted to keep it deterministic. // returned slice will be sorted to keep it deterministic.
func GetModules(scope string) []ModuleInfo { func GetModules(scope string) []ModuleInfo {
modulesMu.Lock() modulesMu.RLock()
defer modulesMu.Unlock() defer modulesMu.RUnlock()
scopeParts := strings.Split(scope, ".") scopeParts := strings.Split(scope, ".")
@ -160,8 +209,8 @@ func GetModules(scope string) []ModuleInfo {
var mods []ModuleInfo var mods []ModuleInfo
iterateModules: iterateModules:
for name, m := range modules { for id, m := range modules {
modParts := strings.Split(name, ".") modParts := strings.Split(string(id), ".")
// match only the next level of nesting // match only the next level of nesting
if len(modParts) != len(scopeParts)+1 { if len(modParts) != len(scopeParts)+1 {
@ -180,7 +229,7 @@ iterateModules:
// make return value deterministic // make return value deterministic
sort.Slice(mods, func(i, j int) bool { sort.Slice(mods, func(i, j int) bool {
return mods[i].Name < mods[j].Name return mods[i].ID < mods[j].ID
}) })
return mods return mods
@ -189,12 +238,12 @@ iterateModules:
// Modules returns the names of all registered modules // Modules returns the names of all registered modules
// in ascending lexicographical order. // in ascending lexicographical order.
func Modules() []string { func Modules() []string {
modulesMu.Lock() modulesMu.RLock()
defer modulesMu.Unlock() defer modulesMu.RUnlock()
var names []string var names []string
for name := range modules { for name := range modules {
names = append(names, name) names = append(names, string(name))
} }
sort.Strings(names) sort.Strings(names)
@ -261,6 +310,25 @@ type CleanerUpper interface {
Cleanup() error Cleanup() error
} }
// ParseStructTag parses a caddy struct tag into its keys and values.
// It is very simple. The expected syntax is:
// `caddy:"key1=val1 key2=val2 ..."`
func ParseStructTag(tag string) (map[string]string, error) {
results := make(map[string]string)
pairs := strings.Split(tag, " ")
for i, pair := range pairs {
if pair == "" {
continue
}
parts := strings.SplitN(pair, "=", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("missing key in '%s' (pair %d)", pair, i)
}
results[parts[0]] = parts[1]
}
return results, nil
}
// strictUnmarshalJSON is like json.Unmarshal but returns an error // strictUnmarshalJSON is like json.Unmarshal but returns an error
// if any of the fields are unrecognized. Useful when decoding // if any of the fields are unrecognized. Useful when decoding
// module configurations, where you want to be more sure they're // module configurations, where you want to be more sure they're
@ -271,7 +339,24 @@ func strictUnmarshalJSON(data []byte, v interface{}) error {
return dec.Decode(v) return dec.Decode(v)
} }
// isJSONRawMessage returns true if the type is encoding/json.RawMessage.
func isJSONRawMessage(typ reflect.Type) bool {
return typ.PkgPath() == "encoding/json" && typ.Name() == "RawMessage"
}
// isModuleMapType returns true if the type is map[string]json.RawMessage.
// It assumes that the string key is the module name, but this is not
// always the case. To know for sure, this function must return true, but
// also the struct tag where this type appears must NOT define an inline_key
// attribute, which would mean that the module names appear inline with the
// values, not in the key.
func isModuleMapType(typ reflect.Type) bool {
return typ.Kind() == reflect.Map &&
typ.Key().Kind() == reflect.String &&
isJSONRawMessage(typ.Elem())
}
var ( var (
modules = make(map[string]ModuleInfo) modules = make(map[string]ModuleInfo)
modulesMu sync.Mutex modulesMu sync.RWMutex
) )

View file

@ -28,7 +28,7 @@ func init() {
// HTTPBasicAuth facilitates HTTP basic authentication. // HTTPBasicAuth facilitates HTTP basic authentication.
type HTTPBasicAuth struct { type HTTPBasicAuth struct {
HashRaw json.RawMessage `json:"hash,omitempty"` HashRaw json.RawMessage `json:"hash,omitempty" caddy:"namespace=http.authentication.hashes inline_key=algorithm"`
AccountList []Account `json:"accounts,omitempty"` AccountList []Account `json:"accounts,omitempty"`
Realm string `json:"realm,omitempty"` Realm string `json:"realm,omitempty"`
@ -39,7 +39,7 @@ type HTTPBasicAuth struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (HTTPBasicAuth) CaddyModule() caddy.ModuleInfo { func (HTTPBasicAuth) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.authentication.providers.http_basic", ID: "http.authentication.providers.http_basic",
New: func() caddy.Module { return new(HTTPBasicAuth) }, New: func() caddy.Module { return new(HTTPBasicAuth) },
} }
} }
@ -51,12 +51,11 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
} }
// load password hasher // load password hasher
hashIface, err := ctx.LoadModuleInline("algorithm", "http.handlers.authentication.hashes", hba.HashRaw) hasherIface, err := ctx.LoadModule(hba, "HashRaw")
if err != nil { if err != nil {
return fmt.Errorf("loading password hasher module: %v", err) return fmt.Errorf("loading password hasher module: %v", err)
} }
hba.Hash = hashIface.(Comparer) hba.Hash = hasherIface.(Comparer)
hba.HashRaw = nil // allow GC to deallocate
if hba.Hash == nil { if hba.Hash == nil {
return fmt.Errorf("hash is required") return fmt.Errorf("hash is required")

View file

@ -15,7 +15,6 @@
package caddyauth package caddyauth
import ( import (
"encoding/json"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
@ -30,7 +29,7 @@ func init() {
// Authentication is a middleware which provides user authentication. // Authentication is a middleware which provides user authentication.
type Authentication struct { type Authentication struct {
ProvidersRaw map[string]json.RawMessage `json:"providers,omitempty"` ProvidersRaw caddy.ModuleMap `json:"providers,omitempty" caddy:"namespace=http.authentication.providers"`
Providers map[string]Authenticator `json:"-"` Providers map[string]Authenticator `json:"-"`
} }
@ -38,7 +37,7 @@ type Authentication struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (Authentication) CaddyModule() caddy.ModuleInfo { func (Authentication) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.authentication", ID: "http.handlers.authentication",
New: func() caddy.Module { return new(Authentication) }, New: func() caddy.Module { return new(Authentication) },
} }
} }
@ -46,15 +45,13 @@ func (Authentication) CaddyModule() caddy.ModuleInfo {
// Provision sets up a. // Provision sets up a.
func (a *Authentication) Provision(ctx caddy.Context) error { func (a *Authentication) Provision(ctx caddy.Context) error {
a.Providers = make(map[string]Authenticator) a.Providers = make(map[string]Authenticator)
for modName, rawMsg := range a.ProvidersRaw { mods, err := ctx.LoadModule(a, "ProvidersRaw")
val, err := ctx.LoadModule("http.handlers.authentication.providers."+modName, rawMsg)
if err != nil { if err != nil {
return fmt.Errorf("loading authentication provider module '%s': %v", modName, err) return fmt.Errorf("loading authentication providers: %v", err)
} }
a.Providers[modName] = val.(Authenticator) for modName, modIface := range mods.(map[string]interface{}) {
a.Providers[modName] = modIface.(Authenticator)
} }
a.ProvidersRaw = nil // allow GC to deallocate
return nil return nil
} }

View file

@ -16,8 +16,8 @@ package caddyauth
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
@ -97,7 +97,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
} }
return Authentication{ return Authentication{
ProvidersRaw: map[string]json.RawMessage{ ProvidersRaw: caddy.ModuleMap{
"http_basic": caddyconfig.JSON(ba, nil), "http_basic": caddyconfig.JSON(ba, nil),
}, },
}, nil }, nil

View file

@ -33,7 +33,7 @@ type BcryptHash struct{}
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (BcryptHash) CaddyModule() caddy.ModuleInfo { func (BcryptHash) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.authentication.hashes.bcrypt", ID: "http.authentication.hashes.bcrypt",
New: func() caddy.Module { return new(BcryptHash) }, New: func() caddy.Module { return new(BcryptHash) },
} }
} }
@ -61,7 +61,7 @@ type ScryptHash struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (ScryptHash) CaddyModule() caddy.ModuleInfo { func (ScryptHash) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.authentication.hashes.scrypt", ID: "http.authentication.hashes.scrypt",
New: func() caddy.Module { return new(ScryptHash) }, New: func() caddy.Module { return new(ScryptHash) },
} }
} }

View file

@ -44,11 +44,26 @@ func init() {
} }
} }
// App is the HTTP app for Caddy. // App is a robust, flexible HTTP server for Caddy.
type App struct { type App struct {
// HTTPPort specifies the port to use for HTTP (as opposed to HTTPS),
// which is used when setting up HTTP->HTTPS redirects or ACME HTTP
// challenge solvers. Default: 80.
HTTPPort int `json:"http_port,omitempty"` HTTPPort int `json:"http_port,omitempty"`
// HTTPSPort specifies the port to use for HTTPS, which is used when
// solving the ACME TLS-ALPN challenges, or whenever HTTPS is needed
// but no specific port number is given. Default: 443.
HTTPSPort int `json:"https_port,omitempty"` HTTPSPort int `json:"https_port,omitempty"`
// GracePeriod is how long to wait for active connections when shutting
// down the server. Once the grace period is over, connections will
// be forcefully closed.
GracePeriod caddy.Duration `json:"grace_period,omitempty"` GracePeriod caddy.Duration `json:"grace_period,omitempty"`
// Servers is the list of servers, keyed by arbitrary names chosen
// at your discretion for your own convenience; the keys do not
// affect functionality.
Servers map[string]*Server `json:"servers,omitempty"` Servers map[string]*Server `json:"servers,omitempty"`
servers []*http.Server servers []*http.Server
@ -62,7 +77,7 @@ type App struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (App) CaddyModule() caddy.ModuleInfo { func (App) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http", ID: "http",
New: func() caddy.Module { return new(App) }, New: func() caddy.Module { return new(App) },
} }
} }
@ -561,8 +576,10 @@ var emptyHandler HandlerFunc = func(http.ResponseWriter, *http.Request) error {
// WeakString is a type that unmarshals any JSON value // WeakString is a type that unmarshals any JSON value
// as a string literal, with the following exceptions: // as a string literal, with the following exceptions:
// 1) actual string values are decoded as strings; and //
// 2) null is decoded as empty string; // 1. actual string values are decoded as strings; and
// 2. null is decoded as empty string;
//
// and provides methods for getting the value as various // and provides methods for getting the value as various
// primitive types. However, using this type removes any // primitive types. However, using this type removes any
// type safety as far as deserializing JSON is concerned. // type safety as far as deserializing JSON is concerned.

View file

@ -37,7 +37,7 @@ type Brotli struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (Brotli) CaddyModule() caddy.ModuleInfo { func (Brotli) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.encoders.brotli", ID: "http.encoders.brotli",
New: func() caddy.Module { return new(Brotli) }, New: func() caddy.Module { return new(Brotli) },
} }
} }

View file

@ -15,7 +15,6 @@
package encode package encode
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
@ -52,14 +51,14 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
for _, arg := range d.RemainingArgs() { for _, arg := range d.RemainingArgs() {
mod, err := caddy.GetModule("http.encoders." + arg) mod, err := caddy.GetModule("http.encoders." + arg)
if err != nil { if err != nil {
return fmt.Errorf("finding encoder module '%s': %v", mod.Name, err) return fmt.Errorf("finding encoder module '%s': %v", mod, err)
} }
encoding, ok := mod.New().(Encoding) encoding, ok := mod.New().(Encoding)
if !ok { if !ok {
return fmt.Errorf("module %s is not an HTTP encoding", mod.Name) return fmt.Errorf("module %s is not an HTTP encoding", mod)
} }
if enc.EncodingsRaw == nil { if enc.EncodingsRaw == nil {
enc.EncodingsRaw = make(map[string]json.RawMessage) enc.EncodingsRaw = make(caddy.ModuleMap)
} }
enc.EncodingsRaw[arg] = caddyconfig.JSON(encoding, nil) enc.EncodingsRaw[arg] = caddyconfig.JSON(encoding, nil)
} }
@ -72,7 +71,7 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
} }
unm, ok := mod.New().(caddyfile.Unmarshaler) unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok { if !ok {
return fmt.Errorf("encoder module '%s' is not a Caddyfile unmarshaler", mod.Name) return fmt.Errorf("encoder module '%s' is not a Caddyfile unmarshaler", mod)
} }
err = unm.UnmarshalCaddyfile(d.NewFromNextTokens()) err = unm.UnmarshalCaddyfile(d.NewFromNextTokens())
if err != nil { if err != nil {
@ -80,10 +79,10 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
} }
encoding, ok := unm.(Encoding) encoding, ok := unm.(Encoding)
if !ok { if !ok {
return fmt.Errorf("module %s is not an HTTP encoding", mod.Name) return fmt.Errorf("module %s is not an HTTP encoding", mod)
} }
if enc.EncodingsRaw == nil { if enc.EncodingsRaw == nil {
enc.EncodingsRaw = make(map[string]json.RawMessage) enc.EncodingsRaw = make(caddy.ModuleMap)
} }
enc.EncodingsRaw[name] = caddyconfig.JSON(encoding, nil) enc.EncodingsRaw[name] = caddyconfig.JSON(encoding, nil)
} }

View file

@ -21,7 +21,6 @@ package encode
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -40,7 +39,7 @@ func init() {
// Encode is a middleware which can encode responses. // Encode is a middleware which can encode responses.
type Encode struct { type Encode struct {
EncodingsRaw map[string]json.RawMessage `json:"encodings,omitempty"` EncodingsRaw caddy.ModuleMap `json:"encodings,omitempty" caddy:"namespace=http.encoders"`
Prefer []string `json:"prefer,omitempty"` Prefer []string `json:"prefer,omitempty"`
MinLength int `json:"minimum_length,omitempty"` MinLength int `json:"minimum_length,omitempty"`
@ -50,25 +49,23 @@ type Encode struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (Encode) CaddyModule() caddy.ModuleInfo { func (Encode) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.encode", ID: "http.handlers.encode",
New: func() caddy.Module { return new(Encode) }, New: func() caddy.Module { return new(Encode) },
} }
} }
// Provision provisions enc. // Provision provisions enc.
func (enc *Encode) Provision(ctx caddy.Context) error { func (enc *Encode) Provision(ctx caddy.Context) error {
for modName, rawMsg := range enc.EncodingsRaw { mods, err := ctx.LoadModule(enc, "EncodingsRaw")
val, err := ctx.LoadModule("http.encoders."+modName, rawMsg)
if err != nil { if err != nil {
return fmt.Errorf("loading encoder module '%s': %v", modName, err) return fmt.Errorf("loading encoder modules: %v", err)
} }
encoding := val.(Encoding) for modName, modIface := range mods.(map[string]interface{}) {
err = enc.addEncoding(encoding) err = enc.addEncoding(modIface.(Encoding))
if err != nil { if err != nil {
return err return fmt.Errorf("adding encoding %s: %v", modName, err)
} }
} }
enc.EncodingsRaw = nil // allow GC to deallocate
if enc.MinLength == 0 { if enc.MinLength == 0 {
enc.MinLength = defaultMinLength enc.MinLength = defaultMinLength

View file

@ -37,7 +37,7 @@ type Gzip struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (Gzip) CaddyModule() caddy.ModuleInfo { func (Gzip) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.encoders.gzip", ID: "http.encoders.gzip",
New: func() caddy.Module { return new(Gzip) }, New: func() caddy.Module { return new(Gzip) },
} }
} }

View file

@ -31,7 +31,7 @@ type Zstd struct{}
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (Zstd) CaddyModule() caddy.ModuleInfo { func (Zstd) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.encoders.zstd", ID: "http.encoders.zstd",
New: func() caddy.Module { return new(Zstd) }, New: func() caddy.Module { return new(Zstd) },
} }
} }

View file

@ -15,8 +15,7 @@
package fileserver package fileserver
import ( import (
"encoding/json" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite" "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
@ -122,7 +121,7 @@ func parseTryFiles(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
URI: "{http.matchers.file.relative}{http.request.uri.query_string}", URI: "{http.matchers.file.relative}{http.request.uri.query_string}",
} }
matcherSet := map[string]json.RawMessage{ matcherSet := caddy.ModuleMap{
"file": h.JSON(MatchFile{ "file": h.JSON(MatchFile{
TryFiles: try, TryFiles: try,
}, nil), }, nil),

View file

@ -75,8 +75,8 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
}, },
} }
if domain != "" { if domain != "" {
route.MatcherSetsRaw = []map[string]json.RawMessage{ route.MatcherSetsRaw = []caddy.ModuleMap{
map[string]json.RawMessage{ caddy.ModuleMap{
"host": caddyconfig.JSON(caddyhttp.MatchHost{domain}, nil), "host": caddyconfig.JSON(caddyhttp.MatchHost{domain}, nil),
}, },
} }
@ -100,7 +100,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
cfg := &caddy.Config{ cfg := &caddy.Config{
Admin: &caddy.AdminConfig{Disabled: true}, Admin: &caddy.AdminConfig{Disabled: true},
AppsRaw: map[string]json.RawMessage{ AppsRaw: caddy.ModuleMap{
"http": caddyconfig.JSON(httpApp, nil), "http": caddyconfig.JSON(httpApp, nil),
}, },
} }

View file

@ -54,7 +54,7 @@ type MatchFile struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (MatchFile) CaddyModule() caddy.ModuleInfo { func (MatchFile) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.matchers.file", ID: "http.matchers.file",
New: func() caddy.Module { return new(MatchFile) }, New: func() caddy.Module { return new(MatchFile) },
} }
} }

View file

@ -53,7 +53,7 @@ type FileServer struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (FileServer) CaddyModule() caddy.ModuleInfo { func (FileServer) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.file_server", ID: "http.handlers.file_server",
New: func() caddy.Module { return new(FileServer) }, New: func() caddy.Module { return new(FileServer) },
} }
} }

View file

@ -37,7 +37,7 @@ type Handler struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (Handler) CaddyModule() caddy.ModuleInfo { func (Handler) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.headers", ID: "http.handlers.headers",
New: func() caddy.Module { return new(Handler) }, New: func() caddy.Module { return new(Handler) },
} }
} }

View file

@ -43,7 +43,7 @@ type Cache struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (Cache) CaddyModule() caddy.ModuleInfo { func (Cache) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.cache", ID: "http.handlers.cache",
New: func() caddy.Module { return new(Cache) }, New: func() caddy.Module { return new(Cache) },
} }
} }

View file

@ -38,7 +38,7 @@ type Markdown struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (Markdown) CaddyModule() caddy.ModuleInfo { func (Markdown) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.markdown", ID: "http.handlers.markdown",
New: func() caddy.Module { return new(Markdown) }, New: func() caddy.Module { return new(Markdown) },
} }
} }

View file

@ -66,7 +66,7 @@ type (
// MatchNegate matches requests by negating its matchers' results. // MatchNegate matches requests by negating its matchers' results.
MatchNegate struct { MatchNegate struct {
MatchersRaw map[string]json.RawMessage `json:"-"` MatchersRaw caddy.ModuleMap `json:"-" caddy:"namespace=http.matchers"`
Matchers MatcherSet `json:"-"` Matchers MatcherSet `json:"-"`
} }
@ -95,7 +95,7 @@ func init() {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (MatchHost) CaddyModule() caddy.ModuleInfo { func (MatchHost) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.matchers.host", ID: "http.matchers.host",
New: func() caddy.Module { return new(MatchHost) }, New: func() caddy.Module { return new(MatchHost) },
} }
} }
@ -149,7 +149,7 @@ outer:
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (MatchPath) CaddyModule() caddy.ModuleInfo { func (MatchPath) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.matchers.path", ID: "http.matchers.path",
New: func() caddy.Module { return new(MatchPath) }, New: func() caddy.Module { return new(MatchPath) },
} }
} }
@ -208,7 +208,7 @@ func (m *MatchPath) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (MatchPathRE) CaddyModule() caddy.ModuleInfo { func (MatchPathRE) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.matchers.path_regexp", ID: "http.matchers.path_regexp",
New: func() caddy.Module { return new(MatchPathRE) }, New: func() caddy.Module { return new(MatchPathRE) },
} }
} }
@ -222,7 +222,7 @@ func (m MatchPathRE) Match(r *http.Request) bool {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (MatchMethod) CaddyModule() caddy.ModuleInfo { func (MatchMethod) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.matchers.method", ID: "http.matchers.method",
New: func() caddy.Module { return new(MatchMethod) }, New: func() caddy.Module { return new(MatchMethod) },
} }
} }
@ -248,7 +248,7 @@ func (m MatchMethod) Match(r *http.Request) bool {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (MatchQuery) CaddyModule() caddy.ModuleInfo { func (MatchQuery) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.matchers.query", ID: "http.matchers.query",
New: func() caddy.Module { return new(MatchQuery) }, New: func() caddy.Module { return new(MatchQuery) },
} }
} }
@ -291,7 +291,7 @@ func (m MatchQuery) Match(r *http.Request) bool {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (MatchHeader) CaddyModule() caddy.ModuleInfo { func (MatchHeader) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.matchers.header", ID: "http.matchers.header",
New: func() caddy.Module { return new(MatchHeader) }, New: func() caddy.Module { return new(MatchHeader) },
} }
} }
@ -349,7 +349,7 @@ func (m MatchHeader) Match(r *http.Request) bool {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (MatchHeaderRE) CaddyModule() caddy.ModuleInfo { func (MatchHeaderRE) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.matchers.header_regexp", ID: "http.matchers.header_regexp",
New: func() caddy.Module { return new(MatchHeaderRE) }, New: func() caddy.Module { return new(MatchHeaderRE) },
} }
} }
@ -406,7 +406,7 @@ func (m MatchHeaderRE) Validate() error {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (MatchProtocol) CaddyModule() caddy.ModuleInfo { func (MatchProtocol) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.matchers.protocol", ID: "http.matchers.protocol",
New: func() caddy.Module { return new(MatchProtocol) }, New: func() caddy.Module { return new(MatchProtocol) },
} }
} }
@ -439,7 +439,7 @@ func (m *MatchProtocol) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (MatchNegate) CaddyModule() caddy.ModuleInfo { func (MatchNegate) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.matchers.not", ID: "http.matchers.not",
New: func() caddy.Module { return new(MatchNegate) }, New: func() caddy.Module { return new(MatchNegate) },
} }
} }
@ -486,7 +486,7 @@ func (m *MatchNegate) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// we should now be functional, but we also need // we should now be functional, but we also need
// to be able to marshal as JSON, otherwise config // to be able to marshal as JSON, otherwise config
// adaptation won't work properly // adaptation won't work properly
m.MatchersRaw = make(map[string]json.RawMessage) m.MatchersRaw = make(caddy.ModuleMap)
for name, matchers := range matcherMap { for name, matchers := range matcherMap {
jsonBytes, err := json.Marshal(matchers) jsonBytes, err := json.Marshal(matchers)
if err != nil { if err != nil {
@ -500,14 +500,13 @@ func (m *MatchNegate) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Provision loads the matcher modules to be negated. // Provision loads the matcher modules to be negated.
func (m *MatchNegate) Provision(ctx caddy.Context) error { func (m *MatchNegate) Provision(ctx caddy.Context) error {
for modName, rawMsg := range m.MatchersRaw { mods, err := ctx.LoadModule(m, "MatchersRaw")
val, err := ctx.LoadModule("http.matchers."+modName, rawMsg)
if err != nil { if err != nil {
return fmt.Errorf("loading matcher module '%s': %v", modName, err) return fmt.Errorf("loading matchers: %v", err)
} }
m.Matchers = append(m.Matchers, val.(RequestMatcher)) for _, modIface := range mods.(map[string]interface{}) {
m.Matchers = append(m.Matchers, modIface.(RequestMatcher))
} }
m.MatchersRaw = nil // allow GC to deallocate
return nil return nil
} }
@ -520,7 +519,7 @@ func (m MatchNegate) Match(r *http.Request) bool {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo { func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.matchers.remote_ip", ID: "http.matchers.remote_ip",
New: func() caddy.Module { return new(MatchRemoteIP) }, New: func() caddy.Module { return new(MatchRemoteIP) },
} }
} }
@ -597,7 +596,7 @@ func (m MatchRemoteIP) Match(r *http.Request) bool {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (MatchStarlarkExpr) CaddyModule() caddy.ModuleInfo { func (MatchStarlarkExpr) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.matchers.starlark_expr", // TODO: Rename to 'starlark'? ID: "http.matchers.starlark_expr", // TODO: Rename to 'starlark'?
New: func() caddy.Module { return new(MatchStarlarkExpr) }, New: func() caddy.Module { return new(MatchStarlarkExpr) },
} }
} }

View file

@ -33,7 +33,7 @@ type RequestBody struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (RequestBody) CaddyModule() caddy.ModuleInfo { func (RequestBody) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.request_body", // TODO: better name for this? ID: "http.handlers.request_body", // TODO: better name for this?
New: func() caddy.Module { return new(RequestBody) }, New: func() caddy.Module { return new(RequestBody) },
} }
} }

View file

@ -108,11 +108,11 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
name := d.Val() name := d.Val()
mod, err := caddy.GetModule("http.handlers.reverse_proxy.selection_policies." + name) mod, err := caddy.GetModule("http.handlers.reverse_proxy.selection_policies." + name)
if err != nil { if err != nil {
return d.Errf("getting load balancing policy module '%s': %v", mod.Name, err) return d.Errf("getting load balancing policy module '%s': %v", mod, err)
} }
unm, ok := mod.New().(caddyfile.Unmarshaler) unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok { if !ok {
return d.Errf("load balancing policy module '%s' is not a Caddyfile unmarshaler", mod.Name) return d.Errf("load balancing policy module '%s' is not a Caddyfile unmarshaler", mod)
} }
err = unm.UnmarshalCaddyfile(d.NewFromNextTokens()) err = unm.UnmarshalCaddyfile(d.NewFromNextTokens())
if err != nil { if err != nil {
@ -120,7 +120,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
} }
sel, ok := unm.(Selector) sel, ok := unm.(Selector)
if !ok { if !ok {
return d.Errf("module %s is not a Selector", mod.Name) return d.Errf("module %s is not a Selector", mod)
} }
if h.LoadBalancing == nil { if h.LoadBalancing == nil {
h.LoadBalancing = new(LoadBalancing) h.LoadBalancing = new(LoadBalancing)
@ -391,11 +391,11 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
name := d.Val() name := d.Val()
mod, err := caddy.GetModule("http.handlers.reverse_proxy.transport." + name) mod, err := caddy.GetModule("http.handlers.reverse_proxy.transport." + name)
if err != nil { if err != nil {
return d.Errf("getting transport module '%s': %v", mod.Name, err) return d.Errf("getting transport module '%s': %v", mod, err)
} }
unm, ok := mod.New().(caddyfile.Unmarshaler) unm, ok := mod.New().(caddyfile.Unmarshaler)
if !ok { if !ok {
return d.Errf("transport module '%s' is not a Caddyfile unmarshaler", mod.Name) return d.Errf("transport module '%s' is not a Caddyfile unmarshaler", mod)
} }
err = unm.UnmarshalCaddyfile(d.NewFromNextTokens()) err = unm.UnmarshalCaddyfile(d.NewFromNextTokens())
if err != nil { if err != nil {
@ -403,7 +403,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
} }
rt, ok := unm.(http.RoundTripper) rt, ok := unm.(http.RoundTripper)
if !ok { if !ok {
return d.Errf("module %s is not a RoundTripper", mod.Name) return d.Errf("module %s is not a RoundTripper", mod)
} }
h.TransportRaw = caddyconfig.JSONModuleObject(rt, "protocol", name, nil) h.TransportRaw = caddyconfig.JSONModuleObject(rt, "protocol", name, nil)

View file

@ -41,7 +41,7 @@ type localCircuitBreaker struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (localCircuitBreaker) CaddyModule() caddy.ModuleInfo { func (localCircuitBreaker) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.reverse_proxy.circuit_breakers.local", ID: "http.reverse_proxy.circuit_breakers.local",
New: func() caddy.Module { return new(localCircuitBreaker) }, New: func() caddy.Module { return new(localCircuitBreaker) },
} }
} }

View file

@ -113,8 +113,8 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
} }
urlHost := fromURL.Hostname() urlHost := fromURL.Hostname()
if urlHost != "" { if urlHost != "" {
route.MatcherSetsRaw = []map[string]json.RawMessage{ route.MatcherSetsRaw = []caddy.ModuleMap{
map[string]json.RawMessage{ caddy.ModuleMap{
"host": caddyconfig.JSON(caddyhttp.MatchHost{urlHost}, nil), "host": caddyconfig.JSON(caddyhttp.MatchHost{urlHost}, nil),
}, },
} }
@ -138,7 +138,7 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
cfg := &caddy.Config{ cfg := &caddy.Config{
Admin: &caddy.AdminConfig{Disabled: true}, Admin: &caddy.AdminConfig{Disabled: true},
AppsRaw: map[string]json.RawMessage{ AppsRaw: caddy.ModuleMap{
"http": caddyconfig.JSON(httpApp, nil), "http": caddyconfig.JSON(httpApp, nil),
}, },
} }

View file

@ -18,6 +18,7 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
@ -121,12 +122,12 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
} }
// route to redirect to canonical path if index PHP file // route to redirect to canonical path if index PHP file
redirMatcherSet := map[string]json.RawMessage{ redirMatcherSet := caddy.ModuleMap{
"file": h.JSON(fileserver.MatchFile{ "file": h.JSON(fileserver.MatchFile{
TryFiles: []string{"{http.request.uri.path}/index.php"}, TryFiles: []string{"{http.request.uri.path}/index.php"},
}, nil), }, nil),
"not": h.JSON(caddyhttp.MatchNegate{ "not": h.JSON(caddyhttp.MatchNegate{
MatchersRaw: map[string]json.RawMessage{ MatchersRaw: caddy.ModuleMap{
"path": h.JSON(caddyhttp.MatchPath{"*/"}, nil), "path": h.JSON(caddyhttp.MatchPath{"*/"}, nil),
}, },
}, nil), }, nil),
@ -136,12 +137,12 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
Headers: http.Header{"Location": []string{"{http.request.uri.path}/"}}, Headers: http.Header{"Location": []string{"{http.request.uri.path}/"}},
} }
redirRoute := caddyhttp.Route{ redirRoute := caddyhttp.Route{
MatcherSetsRaw: []map[string]json.RawMessage{redirMatcherSet}, MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)}, HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)},
} }
// route to rewrite to PHP index file // route to rewrite to PHP index file
rewriteMatcherSet := map[string]json.RawMessage{ rewriteMatcherSet := caddy.ModuleMap{
"file": h.JSON(fileserver.MatchFile{ "file": h.JSON(fileserver.MatchFile{
TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php", "index.php"}, TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php", "index.php"},
}, nil), }, nil),
@ -151,13 +152,13 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
Rehandle: true, Rehandle: true,
} }
rewriteRoute := caddyhttp.Route{ rewriteRoute := caddyhttp.Route{
MatcherSetsRaw: []map[string]json.RawMessage{rewriteMatcherSet}, MatcherSetsRaw: []caddy.ModuleMap{rewriteMatcherSet},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, "handler", "rewrite", nil)}, HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, "handler", "rewrite", nil)},
} }
// route to actually reverse proxy requests to PHP files; // route to actually reverse proxy requests to PHP files;
// match only requests that are for PHP files // match only requests that are for PHP files
rpMatcherSet := map[string]json.RawMessage{ rpMatcherSet := caddy.ModuleMap{
"path": h.JSON([]string{"*.php"}, nil), "path": h.JSON([]string{"*.php"}, nil),
} }
@ -193,7 +194,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
// create the final reverse proxy route which is // create the final reverse proxy route which is
// conditional on matching PHP files // conditional on matching PHP files
rpRoute := caddyhttp.Route{ rpRoute := caddyhttp.Route{
MatcherSetsRaw: []map[string]json.RawMessage{rpMatcherSet}, MatcherSetsRaw: []caddy.ModuleMap{rpMatcherSet},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rpHandler, "handler", "reverse_proxy", nil)}, HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rpHandler, "handler", "reverse_proxy", nil)},
} }
@ -207,7 +208,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
{ {
Class: "route", Class: "route",
Value: caddyhttp.Route{ Value: caddyhttp.Route{
MatcherSetsRaw: []map[string]json.RawMessage{userMatcherSet}, MatcherSetsRaw: []caddy.ModuleMap{userMatcherSet},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(subroute, "handler", "subroute", nil)}, HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(subroute, "handler", "subroute", nil)},
}, },
}, },

View file

@ -73,7 +73,7 @@ type Transport struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (Transport) CaddyModule() caddy.ModuleInfo { func (Transport) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.reverse_proxy.transport.fastcgi", ID: "http.reverse_proxy.transport.fastcgi",
New: func() caddy.Module { return new(Transport) }, New: func() caddy.Module { return new(Transport) },
} }
} }

View file

@ -40,6 +40,7 @@ type HTTPTransport struct {
// TODO: It's possible that other transports (like fastcgi) might be // TODO: It's possible that other transports (like fastcgi) might be
// able to borrow/use at least some of these config fields; if so, // able to borrow/use at least some of these config fields; if so,
// maybe move them into a type called CommonTransport and embed it? // maybe move them into a type called CommonTransport and embed it?
TLS *TLSConfig `json:"tls,omitempty"` TLS *TLSConfig `json:"tls,omitempty"`
KeepAlive *KeepAlive `json:"keep_alive,omitempty"` KeepAlive *KeepAlive `json:"keep_alive,omitempty"`
Compression *bool `json:"compression,omitempty"` Compression *bool `json:"compression,omitempty"`
@ -59,7 +60,7 @@ type HTTPTransport struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (HTTPTransport) CaddyModule() caddy.ModuleInfo { func (HTTPTransport) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.reverse_proxy.transport.http", ID: "http.reverse_proxy.transport.http",
New: func() caddy.Module { return new(HTTPTransport) }, New: func() caddy.Module { return new(HTTPTransport) },
} }
} }

View file

@ -30,12 +30,12 @@ func init() {
caddy.RegisterModule(NTLMTransport{}) caddy.RegisterModule(NTLMTransport{})
} }
// NTLMTransport proxies HTTP+NTLM authentication is being used. // NTLMTransport proxies HTTP with NTLM authentication.
// It basically wraps HTTPTransport so that it is compatible with // It basically wraps HTTPTransport so that it is compatible with
// NTLM's HTTP-hostile requirements. Specifically, it will use // NTLM's HTTP-hostile requirements. Specifically, it will use
// HTTPTransport's single, default *http.Transport for all requests // HTTPTransport's single, default *http.Transport for all requests
// (unless the client's connection is already mapped to a different // (unless the client's connection is already mapped to a different
// transport) until a request comes in with Authorization header // transport) until a request comes in with an Authorization header
// that has "NTLM" or "Negotiate"; when that happens, NTLMTransport // that has "NTLM" or "Negotiate"; when that happens, NTLMTransport
// maps the client's connection (by its address, req.RemoteAddr) // maps the client's connection (by its address, req.RemoteAddr)
// to a new transport that is used only by that downstream conn. // to a new transport that is used only by that downstream conn.
@ -56,7 +56,7 @@ type NTLMTransport struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (NTLMTransport) CaddyModule() caddy.ModuleInfo { func (NTLMTransport) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.reverse_proxy.transport.http_ntlm", ID: "http.reverse_proxy.transport.http_ntlm",
New: func() caddy.Module { return new(NTLMTransport) }, New: func() caddy.Module { return new(NTLMTransport) },
} }
} }

View file

@ -42,8 +42,8 @@ func init() {
// Handler implements a highly configurable and production-ready reverse proxy. // Handler implements a highly configurable and production-ready reverse proxy.
type Handler struct { type Handler struct {
TransportRaw json.RawMessage `json:"transport,omitempty"` TransportRaw json.RawMessage `json:"transport,omitempty" caddy:"namespace=http.reverse_proxy.transport inline_key=protocol"`
CBRaw json.RawMessage `json:"circuit_breaker,omitempty"` CBRaw json.RawMessage `json:"circuit_breaker,omitempty" caddy:"namespace=http.reverse_proxy.circuit_breakers inline_key=type"`
LoadBalancing *LoadBalancing `json:"load_balancing,omitempty"` LoadBalancing *LoadBalancing `json:"load_balancing,omitempty"`
HealthChecks *HealthChecks `json:"health_checks,omitempty"` HealthChecks *HealthChecks `json:"health_checks,omitempty"`
Upstreams UpstreamPool `json:"upstreams,omitempty"` Upstreams UpstreamPool `json:"upstreams,omitempty"`
@ -60,7 +60,7 @@ type Handler struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (Handler) CaddyModule() caddy.ModuleInfo { func (Handler) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.reverse_proxy", ID: "http.handlers.reverse_proxy",
New: func() caddy.Module { return new(Handler) }, New: func() caddy.Module { return new(Handler) },
} }
} }
@ -71,30 +71,25 @@ func (h *Handler) Provision(ctx caddy.Context) error {
// start by loading modules // start by loading modules
if h.TransportRaw != nil { if h.TransportRaw != nil {
val, err := ctx.LoadModuleInline("protocol", "http.handlers.reverse_proxy.transport", h.TransportRaw) mod, err := ctx.LoadModule(h, "TransportRaw")
if err != nil { if err != nil {
return fmt.Errorf("loading transport module: %s", err) return fmt.Errorf("loading transport: %v", err)
} }
h.Transport = val.(http.RoundTripper) h.Transport = mod.(http.RoundTripper)
h.TransportRaw = nil // allow GC to deallocate
} }
if h.LoadBalancing != nil && h.LoadBalancing.SelectionPolicyRaw != nil { if h.LoadBalancing != nil && h.LoadBalancing.SelectionPolicyRaw != nil {
val, err := ctx.LoadModuleInline("policy", mod, err := ctx.LoadModule(h.LoadBalancing, "SelectionPolicyRaw")
"http.handlers.reverse_proxy.selection_policies",
h.LoadBalancing.SelectionPolicyRaw)
if err != nil { if err != nil {
return fmt.Errorf("loading load balancing selection module: %s", err) return fmt.Errorf("loading load balancing selection policy: %s", err)
} }
h.LoadBalancing.SelectionPolicy = val.(Selector) h.LoadBalancing.SelectionPolicy = mod.(Selector)
h.LoadBalancing.SelectionPolicyRaw = nil // allow GC to deallocate
} }
if h.CBRaw != nil { if h.CBRaw != nil {
val, err := ctx.LoadModuleInline("type", "http.handlers.reverse_proxy.circuit_breakers", h.CBRaw) mod, err := ctx.LoadModule(h, "CBRaw")
if err != nil { if err != nil {
return fmt.Errorf("loading circuit breaker module: %s", err) return fmt.Errorf("loading circuit breaker: %s", err)
} }
h.CB = val.(CircuitBreaker) h.CB = mod.(CircuitBreaker)
h.CBRaw = nil // allow GC to deallocate
} }
// set up transport // set up transport
@ -128,12 +123,14 @@ func (h *Handler) Provision(ctx caddy.Context) error {
// defaulting to a sane wait period between attempts // defaulting to a sane wait period between attempts
h.LoadBalancing.TryInterval = caddy.Duration(250 * time.Millisecond) h.LoadBalancing.TryInterval = caddy.Duration(250 * time.Millisecond)
} }
lbMatcherSets, err := h.LoadBalancing.RetryMatchRaw.Setup(ctx) lbMatcherSets, err := ctx.LoadModule(h.LoadBalancing, "RetryMatchRaw")
if err != nil {
return err
}
err = h.LoadBalancing.RetryMatch.FromInterface(lbMatcherSets)
if err != nil { if err != nil {
return err return err
} }
h.LoadBalancing.RetryMatch = lbMatcherSets
h.LoadBalancing.RetryMatchRaw = nil // allow GC to deallocate
// if active health checks are enabled, configure them and start a worker // if active health checks are enabled, configure them and start a worker
if h.HealthChecks != nil && if h.HealthChecks != nil &&
@ -407,7 +404,7 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, di Dia
// do the round-trip // do the round-trip
start := time.Now() start := time.Now()
res, err := h.Transport.RoundTrip(req) res, err := h.Transport.RoundTrip(req)
latency := time.Since(start) duration := time.Since(start)
if err != nil { if err != nil {
return err return err
} }
@ -415,12 +412,13 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, di Dia
h.logger.Debug("upstream roundtrip", h.logger.Debug("upstream roundtrip",
zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: req}), zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: req}),
zap.Object("headers", caddyhttp.LoggableHTTPHeader(res.Header)), zap.Object("headers", caddyhttp.LoggableHTTPHeader(res.Header)),
zap.Duration("duration", duration),
zap.Int("status", res.StatusCode), zap.Int("status", res.StatusCode),
) )
// update circuit breaker on current conditions // update circuit breaker on current conditions
if di.Upstream.cb != nil { if di.Upstream.cb != nil {
di.Upstream.cb.RecordMetric(res.StatusCode, latency) di.Upstream.cb.RecordMetric(res.StatusCode, duration)
} }
// perform passive health checks (if enabled) // perform passive health checks (if enabled)
@ -434,7 +432,7 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, di Dia
// strike if the roundtrip took too long // strike if the roundtrip took too long
if h.HealthChecks.Passive.UnhealthyLatency > 0 && if h.HealthChecks.Passive.UnhealthyLatency > 0 &&
latency >= time.Duration(h.HealthChecks.Passive.UnhealthyLatency) { duration >= time.Duration(h.HealthChecks.Passive.UnhealthyLatency) {
h.countFailure(di.Upstream) h.countFailure(di.Upstream)
} }
} }
@ -651,10 +649,10 @@ func removeConnectionHeaders(h http.Header) {
// LoadBalancing has parameters related to load balancing. // LoadBalancing has parameters related to load balancing.
type LoadBalancing struct { type LoadBalancing struct {
SelectionPolicyRaw json.RawMessage `json:"selection_policy,omitempty"` SelectionPolicyRaw json.RawMessage `json:"selection_policy,omitempty" caddy:"namespace=http.reverse_proxy.selection_policies inline_key=policy"`
TryDuration caddy.Duration `json:"try_duration,omitempty"` TryDuration caddy.Duration `json:"try_duration,omitempty"`
TryInterval caddy.Duration `json:"try_interval,omitempty"` TryInterval caddy.Duration `json:"try_interval,omitempty"`
RetryMatchRaw caddyhttp.RawMatcherSets `json:"retry_match,omitempty"` RetryMatchRaw caddyhttp.RawMatcherSets `json:"retry_match,omitempty" caddy:"namespace=http.matchers"`
SelectionPolicy Selector `json:"-"` SelectionPolicy Selector `json:"-"`
RetryMatch caddyhttp.MatcherSets `json:"-"` RetryMatch caddyhttp.MatcherSets `json:"-"`

View file

@ -48,7 +48,7 @@ type RandomSelection struct{}
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (RandomSelection) CaddyModule() caddy.ModuleInfo { func (RandomSelection) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.reverse_proxy.selection_policies.random", ID: "http.reverse_proxy.selection_policies.random",
New: func() caddy.Module { return new(RandomSelection) }, New: func() caddy.Module { return new(RandomSelection) },
} }
} }
@ -84,7 +84,7 @@ type RandomChoiceSelection struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (RandomChoiceSelection) CaddyModule() caddy.ModuleInfo { func (RandomChoiceSelection) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.reverse_proxy.selection_policies.random_choose", ID: "http.reverse_proxy.selection_policies.random_choose",
New: func() caddy.Module { return new(RandomChoiceSelection) }, New: func() caddy.Module { return new(RandomChoiceSelection) },
} }
} }
@ -154,7 +154,7 @@ type LeastConnSelection struct{}
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (LeastConnSelection) CaddyModule() caddy.ModuleInfo { func (LeastConnSelection) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.reverse_proxy.selection_policies.least_conn", ID: "http.reverse_proxy.selection_policies.least_conn",
New: func() caddy.Module { return new(LeastConnSelection) }, New: func() caddy.Module { return new(LeastConnSelection) },
} }
} }
@ -199,7 +199,7 @@ type RoundRobinSelection struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (RoundRobinSelection) CaddyModule() caddy.ModuleInfo { func (RoundRobinSelection) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.reverse_proxy.selection_policies.round_robin", ID: "http.reverse_proxy.selection_policies.round_robin",
New: func() caddy.Module { return new(RoundRobinSelection) }, New: func() caddy.Module { return new(RoundRobinSelection) },
} }
} }
@ -227,7 +227,7 @@ type FirstSelection struct{}
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (FirstSelection) CaddyModule() caddy.ModuleInfo { func (FirstSelection) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.reverse_proxy.selection_policies.first", ID: "http.reverse_proxy.selection_policies.first",
New: func() caddy.Module { return new(FirstSelection) }, New: func() caddy.Module { return new(FirstSelection) },
} }
} }
@ -249,7 +249,7 @@ type IPHashSelection struct{}
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (IPHashSelection) CaddyModule() caddy.ModuleInfo { func (IPHashSelection) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.reverse_proxy.selection_policies.ip_hash", ID: "http.reverse_proxy.selection_policies.ip_hash",
New: func() caddy.Module { return new(IPHashSelection) }, New: func() caddy.Module { return new(IPHashSelection) },
} }
} }
@ -270,7 +270,7 @@ type URIHashSelection struct{}
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (URIHashSelection) CaddyModule() caddy.ModuleInfo { func (URIHashSelection) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.reverse_proxy.selection_policies.uri_hash", ID: "http.reverse_proxy.selection_policies.uri_hash",
New: func() caddy.Module { return new(URIHashSelection) }, New: func() caddy.Module { return new(URIHashSelection) },
} }
} }
@ -289,7 +289,7 @@ type HeaderHashSelection struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (HeaderHashSelection) CaddyModule() caddy.ModuleInfo { func (HeaderHashSelection) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.reverse_proxy.selection_policies.header", ID: "http.reverse_proxy.selection_policies.header",
New: func() caddy.Module { return new(HeaderHashSelection) }, New: func() caddy.Module { return new(HeaderHashSelection) },
} }
} }

View file

@ -47,7 +47,7 @@ type Rewrite struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (Rewrite) CaddyModule() caddy.ModuleInfo { func (Rewrite) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.rewrite", ID: "http.handlers.rewrite",
New: func() caddy.Module { return new(Rewrite) }, New: func() caddy.Module { return new(Rewrite) },
} }
} }

View file

@ -22,13 +22,72 @@ import (
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
) )
// Route represents a set of matching rules, // Route consists of a set of rules for matching HTTP requests,
// middlewares, and a responder for handling HTTP // a list of handlers to execute, and optional flow control
// requests. // parameters which customize the handling of HTTP requests
// in a highly flexible and performant manner.
type Route struct { type Route struct {
// Group is an optional name for a group to which this
// route belongs. If a route belongs to a group, only
// the first matching route in the group will be used.
Group string `json:"group,omitempty"` Group string `json:"group,omitempty"`
MatcherSetsRaw RawMatcherSets `json:"match,omitempty"`
HandlersRaw []json.RawMessage `json:"handle,omitempty"` // The matcher sets which will be used to qualify this
// route for a request. Essentially the "if" statement
// of this route. Each matcher set is OR'ed, but matchers
// within a set are AND'ed together.
MatcherSetsRaw RawMatcherSets `json:"match,omitempty" caddy:"namespace=http.matchers"`
// The list of handlers for this route. Upon matching a request, they are chained
// together in a middleware fashion: requests flow from the first handler to the last
// (top of the list to the bottom), with the possibility that any handler could stop
// the chain and/or return an error. Responses flow back through the chain (bottom of
// the list to the top) as they are written out to the client.
//
// Not all handlers call the next handler in the chain. For example, the reverse_proxy
// handler always sends a request upstream or returns an error. Thus, configuring
// handlers after reverse_proxy in the same route is illogical, since they would never
// be executed. You will want to put handlers which originate the response at the very
// end of your route(s). The documentation for a module should state whether it invokes
// the next handler, but sometimes it is common sense.
//
// Some handlers manipulate the response. Remember that requests flow down the list, and
// responses flow up the list.
//
// For example, if you wanted to use both `templates` and `encode` handlers, you would
// need to put `templates` after `encode` in your route, because responses flow up.
// Thus, `templates` will be able to parse and execute the plain-text response as a
// template, and then return it up to the `encode` handler which will then compress it
// into a binary format.
//
// If `templates` came before `encode`, then `encode` would write a compressed,
// binary-encoded response to `templates` which would not be able to parse the response
// properly.
//
// The correct order, then, is this:
//
// [
// {"handler": "encode"},
// {"handler": "templates"},
// {"handler": "file_server"}
// ]
//
// The request flows ⬇️ DOWN (`encode` -> `templates` -> `file_server`).
//
// 1. First, `encode` will choose how to `encode` the response and wrap the response.
// 2. Then, `templates` will wrap the response with a buffer.
// 3. Finally, `file_server` will originate the content from a file.
//
// The response flows ⬆️ UP (`file_server` -> `templates` -> `encode`):
//
// 1. First, `file_server` will write the file to the response.
// 2. That write will be buffered and then executed by `templates`.
// 3. Lastly, the write from `templates` will flow into `encode` which will compress the stream.
//
// If you think of routes in this way, it will be easy and even fun to solve the puzzle of writing correct routes.
HandlersRaw []json.RawMessage `json:"handle,omitempty" caddy:"namespace=http.handlers inline_key=handler"`
// If true, no more routes will be executed after this one, even if they matched.
Terminal bool `json:"terminal,omitempty"` Terminal bool `json:"terminal,omitempty"`
// decoded values // decoded values
@ -54,22 +113,23 @@ type RouteList []Route
func (routes RouteList) Provision(ctx caddy.Context) error { func (routes RouteList) Provision(ctx caddy.Context) error {
for i, route := range routes { for i, route := range routes {
// matchers // matchers
matcherSets, err := route.MatcherSetsRaw.Setup(ctx) matchersIface, err := ctx.LoadModule(&route, "MatcherSetsRaw")
if err != nil { if err != nil {
return err return fmt.Errorf("loadng matchers in route %d: %v", i, err)
}
err = routes[i].MatcherSets.FromInterface(matchersIface)
if err != nil {
return fmt.Errorf("route %d: %v", i, err)
} }
routes[i].MatcherSets = matcherSets
routes[i].MatcherSetsRaw = nil // allow GC to deallocate
// handlers // handlers
for j, rawMsg := range route.HandlersRaw { handlersIface, err := ctx.LoadModule(&route, "HandlersRaw")
mh, err := ctx.LoadModuleInline("handler", "http.handlers", rawMsg)
if err != nil { if err != nil {
return fmt.Errorf("loading handler module in position %d: %v", j, err) return fmt.Errorf("loading handler modules in route %d: %v", i, err)
} }
routes[i].Handlers = append(routes[i].Handlers, mh.(MiddlewareHandler)) for _, handler := range handlersIface.([]interface{}) {
routes[i].Handlers = append(routes[i].Handlers, handler.(MiddlewareHandler))
} }
routes[i].HandlersRaw = nil // allow GC to deallocate
} }
return nil return nil
} }
@ -171,28 +231,7 @@ func (mset MatcherSet) Match(r *http.Request) bool {
// RawMatcherSets is a group of matcher sets // RawMatcherSets is a group of matcher sets
// in their raw, JSON form. // in their raw, JSON form.
type RawMatcherSets []map[string]json.RawMessage type RawMatcherSets []caddy.ModuleMap
// Setup sets up all matcher sets by loading each matcher module
// and returning the group of provisioned matcher sets.
func (rm RawMatcherSets) Setup(ctx caddy.Context) (MatcherSets, error) {
if rm == nil {
return nil, nil
}
var ms MatcherSets
for _, matcherSet := range rm {
var matchers MatcherSet
for modName, rawMsg := range matcherSet {
val, err := ctx.LoadModule("http.matchers."+modName, rawMsg)
if err != nil {
return nil, fmt.Errorf("loading matcher module '%s': %v", modName, err)
}
matchers = append(matchers, val.(RequestMatcher))
}
ms = append(ms, matchers)
}
return ms, nil
}
// MatcherSets is a group of matcher sets capable // MatcherSets is a group of matcher sets capable
// of checking whether a request matches any of // of checking whether a request matches any of
@ -202,11 +241,27 @@ type MatcherSets []MatcherSet
// AnyMatch returns true if req matches any of the // AnyMatch returns true if req matches any of the
// matcher sets in mss or if there are no matchers, // matcher sets in mss or if there are no matchers,
// in which case the request always matches. // in which case the request always matches.
func (mss MatcherSets) AnyMatch(req *http.Request) bool { func (ms MatcherSets) AnyMatch(req *http.Request) bool {
for _, ms := range mss { for _, m := range ms {
if ms.Match(req) { if m.Match(req) {
return true return true
} }
} }
return len(mss) == 0 return len(ms) == 0
}
// FromInterface fills ms from an interface{} value obtained from LoadModule.
func (ms *MatcherSets) FromInterface(matcherSets interface{}) error {
for _, matcherSetIfaces := range matcherSets.([]map[string]interface{}) {
var matcherSet MatcherSet
for _, matcher := range matcherSetIfaces {
reqMatcher, ok := matcher.(RequestMatcher)
if !ok {
return fmt.Errorf("decoded module is not a RequestMatcher: %#v", matcher)
}
matcherSet = append(matcherSet, reqMatcher)
}
*ms = append(*ms, matcherSet)
}
return nil
} }

View file

@ -31,23 +31,101 @@ import (
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
) )
// Server is an HTTP server. // Server describes an HTTP server.
type Server struct { type Server struct {
// Socket interfaces to which to bind listeners. Caddy network
// addresses have the following form:
//
// network/address
//
// The network part is anything that [Go's `net` package](https://golang.org/pkg/net/)
// recognizes, and is optional. The default network is `tcp`. If
// a network is specified, a single forward slash `/` is used to
// separate the network and address portions.
//
// The address part may be any of these forms:
//
// - `host`
// - `host:port`
// - `:port`
// - `/path/to/unix/socket`
//
// The host may be any hostname, resolvable domain name, or IP address.
// The port may be a single value (`:8080`) or a range (`:8080-8085`).
// A port range will be multiplied into singular addresses. Not all
// config parameters accept port ranges, but Listen does.
//
// Valid examples:
//
// :8080
// 127.0.0.1:8080
// localhost:8080
// localhost:8080-8085
// tcp/localhost:8080
// tcp/localhost:8080-8085
// udp/localhost:9005
// unix//path/to/socket
//
Listen []string `json:"listen,omitempty"` Listen []string `json:"listen,omitempty"`
// How long to allow a read from a client's upload. Setting this
// to a short, non-zero value can mitigate slowloris attacks, but
// may also affect legitimately slow clients.
ReadTimeout caddy.Duration `json:"read_timeout,omitempty"` ReadTimeout caddy.Duration `json:"read_timeout,omitempty"`
// ReadHeaderTimeout is like ReadTimeout but for request headers.
ReadHeaderTimeout caddy.Duration `json:"read_header_timeout,omitempty"` ReadHeaderTimeout caddy.Duration `json:"read_header_timeout,omitempty"`
// WriteTimeout is how long to allow a write to a client. Note
// that setting this to a small value when serving large files
// may negatively affect legitimately slow clients.
WriteTimeout caddy.Duration `json:"write_timeout,omitempty"` WriteTimeout caddy.Duration `json:"write_timeout,omitempty"`
// IdleTimeout is the maximum time to wait for the next request
// when keep-alives are enabled. If zero, ReadTimeout is used.
// If both are zero, there is no timeout.
IdleTimeout caddy.Duration `json:"idle_timeout,omitempty"` IdleTimeout caddy.Duration `json:"idle_timeout,omitempty"`
// MaxHeaderBytes is the maximum size to parse from a client's
// HTTP request headers.
MaxHeaderBytes int `json:"max_header_bytes,omitempty"` MaxHeaderBytes int `json:"max_header_bytes,omitempty"`
// Routes describes how this server will handle requests.
// When a request comes in, each route's matchers will
// be evaluated against the request, and matching routes
// will be compiled into a middleware chain in the order
// in which they appear in the list.
Routes RouteList `json:"routes,omitempty"` Routes RouteList `json:"routes,omitempty"`
// Errors is how this server will handle errors returned from
// any of the handlers in the primary routes.
Errors *HTTPErrorConfig `json:"errors,omitempty"` Errors *HTTPErrorConfig `json:"errors,omitempty"`
// How to handle TLS connections.
TLSConnPolicies caddytls.ConnectionPolicies `json:"tls_connection_policies,omitempty"` TLSConnPolicies caddytls.ConnectionPolicies `json:"tls_connection_policies,omitempty"`
// AutoHTTPS configures or disables automatic HTTPS within this server.
// HTTPS is enabled automatically and by default when qualifying names
// are present in a Host matcher.
AutoHTTPS *AutoHTTPSConfig `json:"automatic_https,omitempty"` AutoHTTPS *AutoHTTPSConfig `json:"automatic_https,omitempty"`
// MaxRehandles is the maximum number of times to allow a
// request to be rehandled, to prevent accidental infinite
// loops. Default: 1.
MaxRehandles *int `json:"max_rehandles,omitempty"` MaxRehandles *int `json:"max_rehandles,omitempty"`
// If true, will require that a request's Host header match
// the value of the ServerName sent by the client's TLS
// ClientHello; often a necessary safeguard when using TLS
// client authentication.
StrictSNIHost *bool `json:"strict_sni_host,omitempty"` StrictSNIHost *bool `json:"strict_sni_host,omitempty"`
// Logs customizes how access logs are handled in this server.
Logs *ServerLogConfig `json:"logs,omitempty"` Logs *ServerLogConfig `json:"logs,omitempty"`
// This field is not subject to compatibility promises // Enable experimental HTTP/3 support. Note that HTTP/3 is not a
// finished standard and has extremely limited client support.
// This field is not subject to compatibility promises.
ExperimentalHTTP3 bool `json:"experimental_http3,omitempty"` ExperimentalHTTP3 bool `json:"experimental_http3,omitempty"`
tlsApp *caddytls.TLS tlsApp *caddytls.TLS
@ -296,6 +374,8 @@ func (s *Server) hasTLSClientAuth() bool {
// AutoHTTPSConfig is used to disable automatic HTTPS // AutoHTTPSConfig is used to disable automatic HTTPS
// or certain aspects of it for a specific server. // or certain aspects of it for a specific server.
// HTTPS is enabled automatically and by default when
// qualifying hostnames are available from the config.
type AutoHTTPSConfig struct { type AutoHTTPSConfig struct {
// If true, automatic HTTPS will be entirely disabled. // If true, automatic HTTPS will be entirely disabled.
Disabled bool `json:"disable,omitempty"` Disabled bool `json:"disable,omitempty"`

View file

@ -64,7 +64,7 @@ func (r *LoadMiddleware) Run(thread *starlark.Thread, fn *starlark.Builtin, args
name = fmt.Sprintf("http.handlers.%s", name) name = fmt.Sprintf("http.handlers.%s", name)
} }
inst, err := r.Ctx.LoadModule(name, js) inst, err := r.Ctx.LoadModuleByID(name, js)
if err != nil { if err != nil {
return starlark.None, err return starlark.None, err
} }
@ -112,7 +112,7 @@ func (r *LoadResponder) Run(thread *starlark.Thread, fn *starlark.Builtin, args
name = fmt.Sprintf("http.handlers.%s", name) name = fmt.Sprintf("http.handlers.%s", name)
} }
inst, err := r.Ctx.LoadModule(name, js) inst, err := r.Ctx.LoadModuleByID(name, js)
if err != nil { if err != nil {
return starlark.None, err return starlark.None, err
} }

View file

@ -7,8 +7,8 @@ import (
"github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp"
caddyscript "github.com/caddyserver/caddy/v2/pkg/caddyscript/lib"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/starlarkmw/internal/lib" "github.com/caddyserver/caddy/v2/modules/caddyhttp/starlarkmw/internal/lib"
caddyscript "github.com/caddyserver/caddy/v2/pkg/caddyscript/lib"
"github.com/starlight-go/starlight/convert" "github.com/starlight-go/starlight/convert"
"go.starlark.net/starlark" "go.starlark.net/starlark"
) )
@ -34,7 +34,7 @@ type StarlarkMW struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (StarlarkMW) CaddyModule() caddy.ModuleInfo { func (StarlarkMW) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.starlark", ID: "http.handlers.starlark",
New: func() caddy.Module { return new(StarlarkMW) }, New: func() caddy.Module { return new(StarlarkMW) },
} }
} }

View file

@ -35,7 +35,7 @@ type StaticError struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (StaticError) CaddyModule() caddy.ModuleInfo { func (StaticError) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.error", ID: "http.handlers.error",
New: func() caddy.Module { return new(StaticError) }, New: func() caddy.Module { return new(StaticError) },
} }
} }

View file

@ -38,7 +38,7 @@ type StaticResponse struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (StaticResponse) CaddyModule() caddy.ModuleInfo { func (StaticResponse) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.static_response", ID: "http.handlers.static_response",
New: func() caddy.Module { return new(StaticResponse) }, New: func() caddy.Module { return new(StaticResponse) },
} }
} }

View file

@ -44,7 +44,7 @@ type Subroute struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (Subroute) CaddyModule() caddy.ModuleInfo { func (Subroute) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.subroute", ID: "http.handlers.subroute",
New: func() caddy.Module { return new(Subroute) }, New: func() caddy.Module { return new(Subroute) },
} }
} }

View file

@ -39,7 +39,7 @@ type Templates struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (Templates) CaddyModule() caddy.ModuleInfo { func (Templates) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.templates", ID: "http.handlers.templates",
New: func() caddy.Module { return new(Templates) }, New: func() caddy.Module { return new(Templates) },
} }
} }

View file

@ -32,7 +32,7 @@ type VarsMiddleware map[string]string
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (VarsMiddleware) CaddyModule() caddy.ModuleInfo { func (VarsMiddleware) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.handlers.vars", ID: "http.handlers.vars",
New: func() caddy.Module { return new(VarsMiddleware) }, New: func() caddy.Module { return new(VarsMiddleware) },
} }
} }
@ -55,7 +55,7 @@ type VarsMatcher map[string]string
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (VarsMatcher) CaddyModule() caddy.ModuleInfo { func (VarsMatcher) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "http.matchers.vars", ID: "http.matchers.vars",
New: func() caddy.Module { return new(VarsMatcher) }, New: func() caddy.Module { return new(VarsMatcher) },
} }
} }

View file

@ -40,15 +40,49 @@ func init() {
// after you have configured this struct // after you have configured this struct
// to your liking. // to your liking.
type ACMEManagerMaker struct { type ACMEManagerMaker struct {
// The URL to the CA's ACME directory endpoint.
CA string `json:"ca,omitempty"` CA string `json:"ca,omitempty"`
// Your email address, so the CA can contact you if necessary.
// Not required, but strongly recommended to provide one so
// you can be reached if there is a problem. Your email is
// not sent to any Caddy mothership or used for any purpose
// other than ACME transactions.
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
// How long before a certificate's expiration to try renewing it.
// Should usually be about 1/3 of certificate lifetime, but long
// enough to give yourself time to troubleshoot problems before
// expiration. Default: 30d
RenewAhead caddy.Duration `json:"renew_ahead,omitempty"` RenewAhead caddy.Duration `json:"renew_ahead,omitempty"`
// The type of key to generate for the certificate.
// Supported values: `rsa2048`, `rsa4096`, `p256`, `p384`.
KeyType string `json:"key_type,omitempty"` KeyType string `json:"key_type,omitempty"`
// Time to wait before timing out an ACME operation.
ACMETimeout caddy.Duration `json:"acme_timeout,omitempty"` ACMETimeout caddy.Duration `json:"acme_timeout,omitempty"`
// If true, certificates will be requested with MustStaple. Not all
// CAs support this, and there are potentially serious consequences
// of enabling this feature without proper threat modeling.
MustStaple bool `json:"must_staple,omitempty"` MustStaple bool `json:"must_staple,omitempty"`
// Configures the various ACME challenge types.
Challenges *ChallengesConfig `json:"challenges,omitempty"` Challenges *ChallengesConfig `json:"challenges,omitempty"`
// If true, certificates will be managed "on demand", that is, during
// TLS handshakes or when needed, as opposed to at startup or config
// load.
OnDemand bool `json:"on_demand,omitempty"` OnDemand bool `json:"on_demand,omitempty"`
// Optionally configure a separate storage module associated with this
// manager, instead of using Caddy's global/default-configured storage.
Storage json.RawMessage `json:"storage,omitempty"` Storage json.RawMessage `json:"storage,omitempty"`
// An array of files of CA certificates to accept when connecting to the
// ACME CA. Generally, you should only use this if the ACME CA endpoint
// is internal or for development/testing purposes.
TrustedRootsPEMFiles []string `json:"trusted_roots_pem_files,omitempty"` TrustedRootsPEMFiles []string `json:"trusted_roots_pem_files,omitempty"`
storage certmagic.Storage storage certmagic.Storage
@ -58,7 +92,7 @@ type ACMEManagerMaker struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (ACMEManagerMaker) CaddyModule() caddy.ModuleInfo { func (ACMEManagerMaker) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "tls.management.acme", ID: "tls.management.acme",
New: func() caddy.Module { return new(ACMEManagerMaker) }, New: func() caddy.Module { return new(ACMEManagerMaker) },
} }
} }
@ -73,26 +107,24 @@ func (m ACMEManagerMaker) NewManager(interactive bool) (certmagic.Manager, error
func (m *ACMEManagerMaker) Provision(ctx caddy.Context) error { func (m *ACMEManagerMaker) Provision(ctx caddy.Context) error {
// DNS providers // DNS providers
if m.Challenges != nil && m.Challenges.DNSRaw != nil { if m.Challenges != nil && m.Challenges.DNSRaw != nil {
val, err := ctx.LoadModuleInline("provider", "tls.dns", m.Challenges.DNSRaw) val, err := ctx.LoadModule(m.Challenges, "DNSRaw")
if err != nil { if err != nil {
return fmt.Errorf("loading DNS provider module: %s", err) return fmt.Errorf("loading DNS provider module: %v", err)
} }
m.Challenges.DNS = val.(challenge.Provider) m.Challenges.DNS = val.(challenge.Provider)
m.Challenges.DNSRaw = nil // allow GC to deallocate
} }
// policy-specific storage implementation // policy-specific storage implementation
if m.Storage != nil { if m.Storage != nil {
val, err := ctx.LoadModuleInline("module", "caddy.storage", m.Storage) val, err := ctx.LoadModule(m, "Storage")
if err != nil { if err != nil {
return fmt.Errorf("loading TLS storage module: %s", err) return fmt.Errorf("loading TLS storage module: %v", err)
} }
cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage() cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage()
if err != nil { if err != nil {
return fmt.Errorf("creating TLS storage configuration: %v", err) return fmt.Errorf("creating TLS storage configuration: %v", err)
} }
m.storage = cmStorage m.storage = cmStorage
m.Storage = nil // allow GC to deallocate
} }
// add any custom CAs to trust store // add any custom CAs to trust store

View file

@ -28,7 +28,7 @@ type Policy struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (Policy) CaddyModule() caddy.ModuleInfo { func (Policy) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "tls.certificate_selection.custom", ID: "tls.certificate_selection.custom",
New: func() caddy.Module { return new(Policy) }, New: func() caddy.Module { return new(Policy) },
} }
} }

View file

@ -39,23 +39,21 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) (*tls.Config, error) {
// set up each of the connection policies // set up each of the connection policies
for i, pol := range cp { for i, pol := range cp {
// matchers // matchers
for modName, rawMsg := range pol.Matchers { mods, err := ctx.LoadModule(pol, "MatchersRaw")
val, err := ctx.LoadModule("tls.handshake_match."+modName, rawMsg)
if err != nil { if err != nil {
return nil, fmt.Errorf("loading handshake matcher module '%s': %s", modName, err) return nil, fmt.Errorf("loading handshake matchers: %v", err)
} }
cp[i].matchers = append(cp[i].matchers, val.(ConnectionMatcher)) for _, modIface := range mods.(map[string]interface{}) {
cp[i].matchers = append(cp[i].matchers, modIface.(ConnectionMatcher))
} }
cp[i].Matchers = nil // allow GC to deallocate
// certificate selector // certificate selector
if pol.CertSelection != nil { if pol.CertSelection != nil {
val, err := ctx.LoadModuleInline("policy", "tls.certificate_selection", pol.CertSelection) val, err := ctx.LoadModule(pol, "CertSelection")
if err != nil { if err != nil {
return nil, fmt.Errorf("loading certificate selection module: %s", err) return nil, fmt.Errorf("loading certificate selection module: %s", err)
} }
cp[i].certSelector = val.(certmagic.CertificateSelector) cp[i].certSelector = val.(certmagic.CertificateSelector)
cp[i].CertSelection = nil // allow GC to deallocate
} }
} }
@ -109,14 +107,33 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) (*tls.Config, error) {
// ConnectionPolicy specifies the logic for handling a TLS handshake. // ConnectionPolicy specifies the logic for handling a TLS handshake.
type ConnectionPolicy struct { type ConnectionPolicy struct {
Matchers map[string]json.RawMessage `json:"match,omitempty"` // How to match this policy with a TLS ClientHello. If
CertSelection json.RawMessage `json:"certificate_selection,omitempty"` // this policy is the first to match, it will be used.
MatchersRaw caddy.ModuleMap `json:"match,omitempty" caddy:"namespace=tls.handshake_match"`
// How to choose a certificate if more than one matched
// the given ServerName (SNI) value.
CertSelection json.RawMessage `json:"certificate_selection,omitempty" caddy:"namespace=tls.certificate_selection inline_key=policy"`
// The list of cipher suites to support. Caddy's
// defaults are modern and secure.
CipherSuites []string `json:"cipher_suites,omitempty"` CipherSuites []string `json:"cipher_suites,omitempty"`
// The list of elliptic curves to support. Caddy's
// defaults are modern and secure.
Curves []string `json:"curves,omitempty"` Curves []string `json:"curves,omitempty"`
// Protocols to use for Application-Layer Protocol
// Negotiation (ALPN) during the handshake.
ALPN []string `json:"alpn,omitempty"` ALPN []string `json:"alpn,omitempty"`
// Minimum TLS protocol version to allow. Default: `tls1.2`
ProtocolMin string `json:"protocol_min,omitempty"` ProtocolMin string `json:"protocol_min,omitempty"`
// Maximum TLS protocol version to allow. Default: `tls1.3`
ProtocolMax string `json:"protocol_max,omitempty"` ProtocolMax string `json:"protocol_max,omitempty"`
// Enables and configures TLS client authentication.
ClientAuthentication *ClientAuthentication `json:"client_authentication,omitempty"` ClientAuthentication *ClientAuthentication `json:"client_authentication,omitempty"`
matchers []ConnectionMatcher matchers []ConnectionMatcher

View file

@ -39,9 +39,15 @@ func init() {
caddy.RegisterModule(Provider{}) caddy.RegisterModule(Provider{})
} }
// Provider implements a distributed STEK provider. // Provider implements a distributed STEK provider. This
// module will obtain STEKs from a storage module instead
// of generating STEKs internally. This allows STEKs to be
// coordinated, improving TLS session resumption in a cluster.
type Provider struct { type Provider struct {
Storage json.RawMessage `json:"storage,omitempty"` // The storage module wherein to store and obtain session
// ticket keys. If unset, Caddy's default/global-configured
// storage module will be used.
Storage json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`
storage certmagic.Storage storage certmagic.Storage
stekConfig *caddytls.SessionTicketService stekConfig *caddytls.SessionTicketService
@ -51,7 +57,7 @@ type Provider struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (Provider) CaddyModule() caddy.ModuleInfo { func (Provider) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "tls.stek.distributed", ID: "tls.stek.distributed",
New: func() caddy.Module { return new(Provider) }, New: func() caddy.Module { return new(Provider) },
} }
} }
@ -60,7 +66,7 @@ func (Provider) CaddyModule() caddy.ModuleInfo {
func (s *Provider) Provision(ctx caddy.Context) error { func (s *Provider) Provision(ctx caddy.Context) error {
// unpack the storage module to use, if different from the default // unpack the storage module to use, if different from the default
if s.Storage != nil { if s.Storage != nil {
val, err := ctx.LoadModuleInline("module", "caddy.storage", s.Storage) val, err := ctx.LoadModule(s, "Storage")
if err != nil { if err != nil {
return fmt.Errorf("loading TLS storage module: %s", err) return fmt.Errorf("loading TLS storage module: %s", err)
} }
@ -69,7 +75,6 @@ func (s *Provider) Provision(ctx caddy.Context) error {
return fmt.Errorf("creating TLS storage configuration: %v", err) return fmt.Errorf("creating TLS storage configuration: %v", err)
} }
s.storage = cmStorage s.storage = cmStorage
s.Storage = nil // allow GC to deallocate
} }
// otherwise, use default storage // otherwise, use default storage

View file

@ -32,7 +32,7 @@ type FileLoader []CertKeyFilePair
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (FileLoader) CaddyModule() caddy.ModuleInfo { func (FileLoader) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "tls.certificates.load_files", ID: "tls.certificates.load_files",
New: func() caddy.Module { return new(FileLoader) }, New: func() caddy.Module { return new(FileLoader) },
} }
} }
@ -40,9 +40,18 @@ func (FileLoader) CaddyModule() caddy.ModuleInfo {
// CertKeyFilePair pairs certificate and key file names along with their // CertKeyFilePair pairs certificate and key file names along with their
// encoding format so that they can be loaded from disk. // encoding format so that they can be loaded from disk.
type CertKeyFilePair struct { type CertKeyFilePair struct {
// Path to the certificate (public key) file.
Certificate string `json:"certificate"` Certificate string `json:"certificate"`
// Path to the private key file.
Key string `json:"key"` Key string `json:"key"`
Format string `json:"format,omitempty"` // "pem" is default
// The format of the cert and key. Can be "pem". Default: "pem"
Format string `json:"format,omitempty"`
// Arbitrary values to associate with this certificate.
// Can be useful when you want to select a particular
// certificate when there may be multiple valid candidates.
Tags []string `json:"tags,omitempty"` Tags []string `json:"tags,omitempty"`
} }

View file

@ -39,7 +39,7 @@ type FolderLoader []string
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (FolderLoader) CaddyModule() caddy.ModuleInfo { func (FolderLoader) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "tls.certificates.load_folders", ID: "tls.certificates.load_folders",
New: func() caddy.Module { return new(FolderLoader) }, New: func() caddy.Module { return new(FolderLoader) },
} }
} }

View file

@ -30,7 +30,7 @@ type MatchServerName []string
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (MatchServerName) CaddyModule() caddy.ModuleInfo { func (MatchServerName) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "tls.handshake_match.sni", ID: "tls.handshake_match.sni",
New: func() caddy.Module { return new(MatchServerName) }, New: func() caddy.Module { return new(MatchServerName) },
} }
} }

View file

@ -33,15 +33,22 @@ type PEMLoader []CertKeyPEMPair
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (PEMLoader) CaddyModule() caddy.ModuleInfo { func (PEMLoader) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "tls.certificates.load_pem", ID: "tls.certificates.load_pem",
New: func() caddy.Module { return PEMLoader{} }, New: func() caddy.Module { return PEMLoader{} },
} }
} }
// CertKeyPEMPair pairs certificate and key PEM blocks. // CertKeyPEMPair pairs certificate and key PEM blocks.
type CertKeyPEMPair struct { type CertKeyPEMPair struct {
// The certificate (public key) in PEM format.
CertificatePEM string `json:"certificate"` CertificatePEM string `json:"certificate"`
// The private key in PEM format.
KeyPEM string `json:"key"` KeyPEM string `json:"key"`
// Arbitrary values to associate with this certificate.
// Can be useful when you want to select a particular
// certificate when there may be multiple valid candidates.
Tags []string `json:"tags,omitempty"` Tags []string `json:"tags,omitempty"`
} }

View file

@ -28,10 +28,21 @@ import (
// SessionTicketService configures and manages TLS session tickets. // SessionTicketService configures and manages TLS session tickets.
type SessionTicketService struct { type SessionTicketService struct {
KeySource json.RawMessage `json:"key_source,omitempty"` // KeySource is the method by which Caddy produces or obtains
// TLS session ticket keys (STEKs). By default, Caddy generates
// them internally using a secure pseudorandom source.
KeySource json.RawMessage `json:"key_source,omitempty" caddy:"namespace=tls.stek inline_key=provider"`
// How often Caddy rotates STEKs. Default: 12h.
RotationInterval caddy.Duration `json:"rotation_interval,omitempty"` RotationInterval caddy.Duration `json:"rotation_interval,omitempty"`
// The maximum number of keys to keep in rotation. Default: 4.
MaxKeys int `json:"max_keys,omitempty"` MaxKeys int `json:"max_keys,omitempty"`
// Disables STEK rotation.
DisableRotation bool `json:"disable_rotation,omitempty"` DisableRotation bool `json:"disable_rotation,omitempty"`
// Disables TLS session resumption by tickets.
Disabled bool `json:"disabled,omitempty"` Disabled bool `json:"disabled,omitempty"`
keySource STEKProvider keySource STEKProvider
@ -57,12 +68,11 @@ func (s *SessionTicketService) provision(ctx caddy.Context) error {
} }
// load the STEK module, which will provide keys // load the STEK module, which will provide keys
val, err := ctx.LoadModuleInline("provider", "tls.stek", s.KeySource) val, err := ctx.LoadModule(s, "KeySource")
if err != nil { if err != nil {
return fmt.Errorf("loading TLS session ticket ephemeral keys provider module: %s", err) return fmt.Errorf("loading TLS session ticket ephemeral keys provider module: %s", err)
} }
s.keySource = val.(STEKProvider) s.keySource = val.(STEKProvider)
s.KeySource = nil // allow GC to deallocate
// if session tickets or just rotation are // if session tickets or just rotation are
// disabled, no need to start service // disabled, no need to start service

View file

@ -35,7 +35,7 @@ type standardSTEKProvider struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (standardSTEKProvider) CaddyModule() caddy.ModuleInfo { func (standardSTEKProvider) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "tls.stek.standard", ID: "tls.stek.standard",
New: func() caddy.Module { return new(standardSTEKProvider) }, New: func() caddy.Module { return new(standardSTEKProvider) },
} }
} }

View file

@ -30,15 +30,30 @@ import (
func init() { func init() {
caddy.RegisterModule(TLS{}) caddy.RegisterModule(TLS{})
caddy.RegisterModule(AutomateLoader{})
} }
// TLS represents a process-wide TLS configuration. // TLS provides TLS facilities including certificate
// loading and management, client auth, and more.
type TLS struct { type TLS struct {
Certificates map[string]json.RawMessage `json:"certificates,omitempty"` // Caches certificates in memory for quick use during
// TLS handshakes. Each key is the name of a certificate
// loader module. All loaded certificates get pooled
// into the same cache and may be used to complete TLS
// handshakes for the relevant server names (SNI).
// Certificates loaded manually (anything other than
// "automate") are not automatically managed and will
// have to be refreshed manually before they expire.
CertificatesRaw caddy.ModuleMap `json:"certificates,omitempty" caddy:"namespace=tls.certificates"`
// Configures the automation of certificate management.
Automation *AutomationConfig `json:"automation,omitempty"` Automation *AutomationConfig `json:"automation,omitempty"`
// Configures session ticket ephemeral keys (STEKs).
SessionTickets *SessionTicketService `json:"session_tickets,omitempty"` SessionTickets *SessionTicketService `json:"session_tickets,omitempty"`
certificateLoaders []CertificateLoader certificateLoaders []CertificateLoader
automateNames []string
certCache *certmagic.Cache certCache *certmagic.Cache
ctx caddy.Context ctx caddy.Context
storageCleanTicker *time.Ticker storageCleanTicker *time.Ticker
@ -49,7 +64,7 @@ type TLS struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (TLS) CaddyModule() caddy.ModuleInfo { func (TLS) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "tls", ID: "tls",
New: func() caddy.Module { return new(TLS) }, New: func() caddy.Module { return new(TLS) },
} }
} }
@ -74,25 +89,32 @@ func (t *TLS) Provision(ctx caddy.Context) error {
// automation/management policies // automation/management policies
if t.Automation != nil { if t.Automation != nil {
for i, ap := range t.Automation.Policies { for i, ap := range t.Automation.Policies {
val, err := ctx.LoadModuleInline("module", "tls.management", ap.ManagementRaw) val, err := ctx.LoadModule(&ap, "ManagementRaw")
if err != nil { if err != nil {
return fmt.Errorf("loading TLS automation management module: %s", err) return fmt.Errorf("loading TLS automation management module: %s", err)
} }
t.Automation.Policies[i].Management = val.(ManagerMaker) t.Automation.Policies[i].Management = val.(ManagerMaker)
t.Automation.Policies[i].ManagementRaw = nil // allow GC to deallocate
} }
} }
// certificate loaders // certificate loaders
for modName, rawMsg := range t.Certificates { val, err := ctx.LoadModule(t, "CertificatesRaw")
if modName == automateKey {
continue // special case; these will be loaded in later
}
val, err := ctx.LoadModule("tls.certificates."+modName, rawMsg)
if err != nil { if err != nil {
return fmt.Errorf("loading certificate module '%s': %s", modName, err) return fmt.Errorf("loading TLS automation management module: %s", err)
} }
t.certificateLoaders = append(t.certificateLoaders, val.(CertificateLoader)) for modName, modIface := range val.(map[string]interface{}) {
if modName == "automate" {
// special case; these will be loaded in later
// using our automation facilities, which we
// want to avoid during provisioning
var ok bool
t.automateNames, ok = modIface.([]string)
if !ok {
return fmt.Errorf("loading certificates with 'automate' requires []string, got: %#v", modIface)
}
continue
}
t.certificateLoaders = append(t.certificateLoaders, modIface.(CertificateLoader))
} }
// session ticket ephemeral keys (STEK) service and provider // session ticket ephemeral keys (STEK) service and provider
@ -115,7 +137,8 @@ func (t *TLS) Provision(ctx caddy.Context) error {
// load manual/static (unmanaged) certificates - we do this in // load manual/static (unmanaged) certificates - we do this in
// provision so that other apps (such as http) can know which // provision so that other apps (such as http) can know which
// certificates have been manually loaded // certificates have been manually loaded, and also so that
// commands like validate can be a better test
magic := certmagic.New(t.certCache, certmagic.Config{ magic := certmagic.New(t.certCache, certmagic.Config{
Storage: ctx.Storage(), Storage: ctx.Storage(),
}) })
@ -137,19 +160,12 @@ func (t *TLS) Provision(ctx caddy.Context) error {
// Start activates the TLS module. // Start activates the TLS module.
func (t *TLS) Start() error { func (t *TLS) Start() error {
// load automated (managed) certificates // now that we are running, and all manual certificates have
if automatedRawMsg, ok := t.Certificates[automateKey]; ok { // been loaded, time to load the automated/managed certificates
var names []string err := t.Manage(t.automateNames)
err := json.Unmarshal(automatedRawMsg, &names)
if err != nil { if err != nil {
return fmt.Errorf("automate: decoding names: %v", err) return fmt.Errorf("automate: managing %v: %v", t.automateNames, err)
} }
err = t.Manage(names)
if err != nil {
return fmt.Errorf("automate: managing %v: %v", names, err)
}
}
t.Certificates = nil // allow GC to deallocate
t.keepStorageClean() t.keepStorageClean()
@ -311,17 +327,47 @@ type Certificate struct {
// AutomationConfig designates configuration for the // AutomationConfig designates configuration for the
// construction and use of ACME clients. // construction and use of ACME clients.
type AutomationConfig struct { type AutomationConfig struct {
// The list of automation policies. The first matching
// policy will be applied for a given certificate/name.
Policies []AutomationPolicy `json:"policies,omitempty"` Policies []AutomationPolicy `json:"policies,omitempty"`
// On-Demand TLS defers certificate operations to the
// moment they are needed, e.g. during a TLS handshake.
// Useful when you don't know all the hostnames up front.
// Caddy was the first web server to deploy this technology.
OnDemand *OnDemandConfig `json:"on_demand,omitempty"` OnDemand *OnDemandConfig `json:"on_demand,omitempty"`
// Caddy staples OCSP (and caches the response) for all
// qualifying certificates by default. This setting
// changes how often it scans responses for freshness,
// and updates them if they are getting stale.
OCSPCheckInterval caddy.Duration `json:"ocsp_interval,omitempty"` OCSPCheckInterval caddy.Duration `json:"ocsp_interval,omitempty"`
// Every so often, Caddy will scan all loaded, managed
// certificates for expiration. Certificates which are
// about 2/3 into their valid lifetime are due for
// renewal. This setting changes how frequently the scan
// is performed. If your certificate lifetimes are very
// short (less than ~1 week), you should customize this.
RenewCheckInterval caddy.Duration `json:"renew_interval,omitempty"` RenewCheckInterval caddy.Duration `json:"renew_interval,omitempty"`
} }
// AutomationPolicy designates the policy for automating the // AutomationPolicy designates the policy for automating the
// management of managed TLS certificates. // management (obtaining, renewal, and revocation) of managed
// TLS certificates.
type AutomationPolicy struct { type AutomationPolicy struct {
// Which hostnames this policy applies to.
Hosts []string `json:"hosts,omitempty"` Hosts []string `json:"hosts,omitempty"`
ManagementRaw json.RawMessage `json:"management,omitempty"`
// How to manage certificates.
ManagementRaw json.RawMessage `json:"management,omitempty" caddy:"namespace=tls.management inline_key=module"`
// If true, certificate management will be conducted
// in the foreground; this will block config reloads
// and return errors if there were problems with
// obtaining or renewing certificates. This is often
// not desirable, especially when serving sites out
// of your control. Default: false
ManageSync bool `json:"manage_sync,omitempty"` ManageSync bool `json:"manage_sync,omitempty"`
Management ManagerMaker `json:"-"` Management ManagerMaker `json:"-"`
@ -345,35 +391,83 @@ func (ap AutomationPolicy) makeCertMagicConfig(ctx caddy.Context) certmagic.Conf
// ChallengesConfig configures the ACME challenges. // ChallengesConfig configures the ACME challenges.
type ChallengesConfig struct { type ChallengesConfig struct {
// HTTP configures the ACME HTTP challenge. This
// challenge is enabled and used automatically
// and by default.
HTTP *HTTPChallengeConfig `json:"http,omitempty"` HTTP *HTTPChallengeConfig `json:"http,omitempty"`
// TLSALPN configures the ACME TLS-ALPN challenge.
// This challenge is enabled and used automatically
// and by default.
TLSALPN *TLSALPNChallengeConfig `json:"tls-alpn,omitempty"` TLSALPN *TLSALPNChallengeConfig `json:"tls-alpn,omitempty"`
DNSRaw json.RawMessage `json:"dns,omitempty"`
// Configures the ACME DNS challenge. Because this
// challenge typically requires credentials for
// interfacing with a DNS provider, this challenge is
// not enabled by default. This is the only challenge
// type which does not require a direct connection
// to Caddy from an external server.
DNSRaw json.RawMessage `json:"dns,omitempty" caddy:"namespace=tls.dns inline_key=provider"`
DNS challenge.Provider `json:"-"` DNS challenge.Provider `json:"-"`
} }
// HTTPChallengeConfig configures the ACME HTTP challenge. // HTTPChallengeConfig configures the ACME HTTP challenge.
type HTTPChallengeConfig struct { type HTTPChallengeConfig struct {
// If true, the HTTP challenge will be disabled.
Disabled bool `json:"disabled,omitempty"` Disabled bool `json:"disabled,omitempty"`
// An alternate port on which to service this
// challenge. Note that the HTTP challenge port is
// hard-coded into the spec and cannot be changed,
// so you would have to forward packets from the
// standard HTTP challenge port to this one.
AlternatePort int `json:"alternate_port,omitempty"` AlternatePort int `json:"alternate_port,omitempty"`
} }
// TLSALPNChallengeConfig configures the ACME TLS-ALPN challenge. // TLSALPNChallengeConfig configures the ACME TLS-ALPN challenge.
type TLSALPNChallengeConfig struct { type TLSALPNChallengeConfig struct {
// If true, the TLS-ALPN challenge will be disabled.
Disabled bool `json:"disabled,omitempty"` Disabled bool `json:"disabled,omitempty"`
// An alternate port on which to service this
// challenge. Note that the TLS-ALPN challenge port
// is hard-coded into the spec and cannot be changed,
// so you would have to forward packets from the
// standard TLS-ALPN challenge port to this one.
AlternatePort int `json:"alternate_port,omitempty"` AlternatePort int `json:"alternate_port,omitempty"`
} }
// OnDemandConfig configures on-demand TLS, for obtaining // OnDemandConfig configures on-demand TLS, for obtaining
// needed certificates at handshake-time. // needed certificates at handshake-time. Because this
// feature can easily be abused, you should set up rate
// limits and/or an internal endpoint that Caddy can
// "ask" if it should be allowed to manage certificates
// for a given hostname.
type OnDemandConfig struct { type OnDemandConfig struct {
// An optional rate limit to throttle the
// issuance of certificates from handshakes.
RateLimit *RateLimit `json:"rate_limit,omitempty"` RateLimit *RateLimit `json:"rate_limit,omitempty"`
// If Caddy needs to obtain or renew a certificate
// during a TLS handshake, it will perform a quick
// HTTP request to this URL to check if it should be
// allowed to try to get a certificate for the name
// in the "domain" query string parameter, like so:
// `?domain=example.com`. The endpoint must return a
// 200 OK status if a certificate is allowed;
// anything else will cause it to be denied.
// Redirects are not followed.
Ask string `json:"ask,omitempty"` Ask string `json:"ask,omitempty"`
} }
// RateLimit specifies an interval with optional burst size. // RateLimit specifies an interval with optional burst size.
type RateLimit struct { type RateLimit struct {
// A duration value. A certificate may be obtained 'burst'
// times during this interval.
Interval caddy.Duration `json:"interval,omitempty"` Interval caddy.Duration `json:"interval,omitempty"`
// How many times during an interval a certificate can be obtained.
Burst int `json:"burst,omitempty"` Burst int `json:"burst,omitempty"`
} }
@ -382,6 +476,21 @@ type ManagerMaker interface {
NewManager(interactive bool) (certmagic.Manager, error) NewManager(interactive bool) (certmagic.Manager, error)
} }
// AutomateLoader is a no-op certificate loader module
// that is treated as a special case: it uses this app's
// automation features to load certificates for the
// list of hostnames, rather than loading certificates
// manually.
type AutomateLoader []string
// CaddyModule returns the Caddy module information.
func (AutomateLoader) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "tls.certificates.automate",
New: func() caddy.Module { return new(AutomateLoader) },
}
}
// These perpetual values are used for on-demand TLS. // These perpetual values are used for on-demand TLS.
var ( var (
onDemandRateLimiter = certmagic.NewRateLimiter(0, 0) onDemandRateLimiter = certmagic.NewRateLimiter(0, 0)

View file

@ -32,7 +32,7 @@ type FileStorage struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (FileStorage) CaddyModule() caddy.ModuleInfo { func (FileStorage) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "caddy.storage.file_system", ID: "caddy.storage.file_system",
New: func() caddy.Module { return new(FileStorage) }, New: func() caddy.Module { return new(FileStorage) },
} }
} }

View file

@ -43,7 +43,7 @@ type ConsoleEncoder struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (ConsoleEncoder) CaddyModule() caddy.ModuleInfo { func (ConsoleEncoder) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "caddy.logging.encoders.console", ID: "caddy.logging.encoders.console",
New: func() caddy.Module { return new(ConsoleEncoder) }, New: func() caddy.Module { return new(ConsoleEncoder) },
} }
} }
@ -63,7 +63,7 @@ type JSONEncoder struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (JSONEncoder) CaddyModule() caddy.ModuleInfo { func (JSONEncoder) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "caddy.logging.encoders.json", ID: "caddy.logging.encoders.json",
New: func() caddy.Module { return new(JSONEncoder) }, New: func() caddy.Module { return new(JSONEncoder) },
} }
} }
@ -84,7 +84,7 @@ type LogfmtEncoder struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (LogfmtEncoder) CaddyModule() caddy.ModuleInfo { func (LogfmtEncoder) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "caddy.logging.encoders.logfmt", ID: "caddy.logging.encoders.logfmt",
New: func() caddy.Module { return new(LogfmtEncoder) }, New: func() caddy.Module { return new(LogfmtEncoder) },
} }
} }
@ -102,13 +102,13 @@ func (lfe *LogfmtEncoder) Provision(_ caddy.Context) error {
type StringEncoder struct { type StringEncoder struct {
zapcore.Encoder zapcore.Encoder
FieldName string `json:"field,omitempty"` FieldName string `json:"field,omitempty"`
FallbackRaw json.RawMessage `json:"fallback,omitempty"` FallbackRaw json.RawMessage `json:"fallback,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"`
} }
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (StringEncoder) CaddyModule() caddy.ModuleInfo { func (StringEncoder) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "caddy.logging.encoders.string", ID: "caddy.logging.encoders.string",
New: func() caddy.Module { return new(StringEncoder) }, New: func() caddy.Module { return new(StringEncoder) },
} }
} }
@ -116,11 +116,10 @@ func (StringEncoder) CaddyModule() caddy.ModuleInfo {
// Provision sets up the encoder. // Provision sets up the encoder.
func (se *StringEncoder) Provision(ctx caddy.Context) error { func (se *StringEncoder) Provision(ctx caddy.Context) error {
if se.FallbackRaw != nil { if se.FallbackRaw != nil {
val, err := ctx.LoadModuleInline("format", "caddy.logging.encoders", se.FallbackRaw) val, err := ctx.LoadModule(se, "FallbackRaw")
if err != nil { if err != nil {
return fmt.Errorf("loading fallback encoder module: %v", err) return fmt.Errorf("loading fallback encoder module: %v", err)
} }
se.FallbackRaw = nil // allow GC to deallocate
se.Encoder = val.(zapcore.Encoder) se.Encoder = val.(zapcore.Encoder)
} }
if se.Encoder == nil { if se.Encoder == nil {

View file

@ -28,21 +28,41 @@ func init() {
caddy.RegisterModule(FileWriter{}) caddy.RegisterModule(FileWriter{})
} }
// FileWriter can write logs to files. // FileWriter can write logs to files. By default, log files
// are rotated ("rolled") when they get large, and old log
// files get deleted, to ensure that the process does not
// exhaust disk space.
type FileWriter struct { type FileWriter struct {
// Filename is the name of the file to write.
Filename string `json:"filename,omitempty"` Filename string `json:"filename,omitempty"`
// Roll toggles log rolling or rotation, which is
// enabled by default.
Roll *bool `json:"roll,omitempty"` Roll *bool `json:"roll,omitempty"`
// When a log file reaches approximately this size,
// it will be rotated.
RollSizeMB int `json:"roll_size_mb,omitempty"` RollSizeMB int `json:"roll_size_mb,omitempty"`
// Whether to compress rolled files. Default: true
RollCompress *bool `json:"roll_gzip,omitempty"` RollCompress *bool `json:"roll_gzip,omitempty"`
// Whether to use local timestamps in rolled filenames.
// Default: false
RollLocalTime bool `json:"roll_local_time,omitempty"` RollLocalTime bool `json:"roll_local_time,omitempty"`
// The maximum number of rolled log files to keep.
// Default: 10
RollKeep int `json:"roll_keep,omitempty"` RollKeep int `json:"roll_keep,omitempty"`
// How many days to keep rolled log files. Default: 90
RollKeepDays int `json:"roll_keep_days,omitempty"` RollKeepDays int `json:"roll_keep_days,omitempty"`
} }
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (FileWriter) CaddyModule() caddy.ModuleInfo { func (FileWriter) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "caddy.logging.writers.file", ID: "caddy.logging.writers.file",
New: func() caddy.Module { return new(FileWriter) }, New: func() caddy.Module { return new(FileWriter) },
} }
} }

View file

@ -29,13 +29,17 @@ func init() {
caddy.RegisterModule(FilterEncoder{}) caddy.RegisterModule(FilterEncoder{})
} }
// FilterEncoder wraps an underlying encoder. It does // FilterEncoder can filter (manipulate) fields on
// not do any encoding itself, but it can manipulate // log entries before they are actually encoded by
// (filter) fields before they are actually encoded. // an underlying encoder.
// A wrapped encoder is required.
type FilterEncoder struct { type FilterEncoder struct {
WrappedRaw json.RawMessage `json:"wrap,omitempty"` // The underlying encoder that actually
FieldsRaw map[string]json.RawMessage `json:"fields,omitempty"` // encodes the log entries. Required.
WrappedRaw json.RawMessage `json:"wrap,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"`
// A map of field names to their filters. Note that this
// is not a module map; the keys are field names.
FieldsRaw map[string]json.RawMessage `json:"fields,omitempty" caddy:"namespace=caddy.logging.encoders.filter inline_key=filter"`
wrapped zapcore.Encoder wrapped zapcore.Encoder
Fields map[string]LogFieldFilter `json:"-"` Fields map[string]LogFieldFilter `json:"-"`
@ -47,7 +51,7 @@ type FilterEncoder struct {
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (FilterEncoder) CaddyModule() caddy.ModuleInfo { func (FilterEncoder) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "caddy.logging.encoders.filter", ID: "caddy.logging.encoders.filter",
New: func() caddy.Module { return new(FilterEncoder) }, New: func() caddy.Module { return new(FilterEncoder) },
} }
} }
@ -59,28 +63,23 @@ func (fe *FilterEncoder) Provision(ctx caddy.Context) error {
} }
// set up wrapped encoder (required) // set up wrapped encoder (required)
val, err := ctx.LoadModuleInline("format", "caddy.logging.encoders", fe.WrappedRaw) val, err := ctx.LoadModule(fe, "WrappedRaw")
if err != nil { if err != nil {
return fmt.Errorf("loading fallback encoder module: %v", err) return fmt.Errorf("loading fallback encoder module: %v", err)
} }
fe.WrappedRaw = nil // allow GC to deallocate
fe.wrapped = val.(zapcore.Encoder) fe.wrapped = val.(zapcore.Encoder)
// set up each field filter // set up each field filter
if fe.Fields == nil { if fe.Fields == nil {
fe.Fields = make(map[string]LogFieldFilter) fe.Fields = make(map[string]LogFieldFilter)
} }
for field, filterRaw := range fe.FieldsRaw { vals, err := ctx.LoadModule(fe, "FieldsRaw")
if filterRaw == nil {
continue
}
val, err := ctx.LoadModuleInline("filter", "caddy.logging.encoders.filter", filterRaw)
if err != nil { if err != nil {
return fmt.Errorf("loading log filter module: %v", err) return fmt.Errorf("loading log filter modules: %v", err)
} }
fe.Fields[field] = val.(LogFieldFilter) for fieldName, modIface := range vals.(map[string]interface{}) {
fe.Fields[fieldName] = modIface.(LogFieldFilter)
} }
fe.FieldsRaw = nil // allow GC to deallocate
return nil return nil
} }

View file

@ -41,7 +41,7 @@ type DeleteFilter struct{}
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (DeleteFilter) CaddyModule() caddy.ModuleInfo { func (DeleteFilter) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "caddy.logging.encoders.filter.delete", ID: "caddy.logging.encoders.filter.delete",
New: func() caddy.Module { return new(DeleteFilter) }, New: func() caddy.Module { return new(DeleteFilter) },
} }
} }
@ -55,14 +55,17 @@ func (DeleteFilter) Filter(in zapcore.Field) zapcore.Field {
// IPMaskFilter is a Caddy log field filter that // IPMaskFilter is a Caddy log field filter that
// masks IP addresses. // masks IP addresses.
type IPMaskFilter struct { type IPMaskFilter struct {
// The IPv4 range in CIDR notation.
IPv4CIDR int `json:"ipv4_cidr,omitempty"` IPv4CIDR int `json:"ipv4_cidr,omitempty"`
// The IPv6 range in CIDR notation.
IPv6CIDR int `json:"ipv6_cidr,omitempty"` IPv6CIDR int `json:"ipv6_cidr,omitempty"`
} }
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
func (IPMaskFilter) CaddyModule() caddy.ModuleInfo { func (IPMaskFilter) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ return caddy.ModuleInfo{
Name: "caddy.logging.encoders.filter.ip_mask", ID: "caddy.logging.encoders.filter.ip_mask",
New: func() caddy.Module { return new(IPMaskFilter) }, New: func() caddy.Module { return new(IPMaskFilter) },
} }
} }

View file

@ -22,17 +22,17 @@ import (
func TestGetModules(t *testing.T) { func TestGetModules(t *testing.T) {
modulesMu.Lock() modulesMu.Lock()
modules = map[string]ModuleInfo{ modules = map[string]ModuleInfo{
"a": {Name: "a"}, "a": {ID: "a"},
"a.b": {Name: "a.b"}, "a.b": {ID: "a.b"},
"a.b.c": {Name: "a.b.c"}, "a.b.c": {ID: "a.b.c"},
"a.b.cd": {Name: "a.b.cd"}, "a.b.cd": {ID: "a.b.cd"},
"a.c": {Name: "a.c"}, "a.c": {ID: "a.c"},
"a.d": {Name: "a.d"}, "a.d": {ID: "a.d"},
"b": {Name: "b"}, "b": {ID: "b"},
"b.a": {Name: "b.a"}, "b.a": {ID: "b.a"},
"b.b": {Name: "b.b"}, "b.b": {ID: "b.b"},
"b.a.c": {Name: "b.a.c"}, "b.a.c": {ID: "b.a.c"},
"c": {Name: "c"}, "c": {ID: "c"},
} }
modulesMu.Unlock() modulesMu.Unlock()
@ -43,24 +43,24 @@ func TestGetModules(t *testing.T) {
{ {
input: "", input: "",
expect: []ModuleInfo{ expect: []ModuleInfo{
{Name: "a"}, {ID: "a"},
{Name: "b"}, {ID: "b"},
{Name: "c"}, {ID: "c"},
}, },
}, },
{ {
input: "a", input: "a",
expect: []ModuleInfo{ expect: []ModuleInfo{
{Name: "a.b"}, {ID: "a.b"},
{Name: "a.c"}, {ID: "a.c"},
{Name: "a.d"}, {ID: "a.d"},
}, },
}, },
{ {
input: "a.b", input: "a.b",
expect: []ModuleInfo{ expect: []ModuleInfo{
{Name: "a.b.c"}, {ID: "a.b.c"},
{Name: "a.b.cd"}, {ID: "a.b.cd"},
}, },
}, },
{ {
@ -69,8 +69,8 @@ func TestGetModules(t *testing.T) {
{ {
input: "b", input: "b",
expect: []ModuleInfo{ expect: []ModuleInfo{
{Name: "b.a"}, {ID: "b.a"},
{Name: "b.b"}, {ID: "b.b"},
}, },
}, },
{ {