diff --git a/admin.go b/admin.go index 981f2dc1..fcbb62e5 100644 --- a/admin.go +++ b/admin.go @@ -39,12 +39,33 @@ import ( // 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 { - Disabled bool `json:"disabled,omitempty"` - Listen string `json:"listen,omitempty"` - EnforceOrigin bool `json:"enforce_origin,omitempty"` - Origins []string `json:"origins,omitempty"` + // 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"` + + // 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"` + + // 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"` + + // 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"` } // listenAddr extracts a singular listen address from ac.Listen, @@ -706,7 +727,7 @@ traverseLoop: ptr = v[partInt] default: - return fmt.Errorf("invalid path: %s", parts[:i+1]) + return fmt.Errorf("invalid traversal path at: %s", strings.Join(parts[:i+1], "/")) } } diff --git a/caddy.go b/caddy.go index d8609d2b..9dcd8836 100644 --- a/caddy.go +++ b/caddy.go @@ -32,23 +32,44 @@ import ( "github.com/mholt/certmagic" ) -// Config represents a Caddy configuration. It is the -// top of the module structure: all Caddy modules will -// be loaded, starting with this struct. In order to -// be loaded and run successfully, a Config and all its -// modules must be JSON-encodable; i.e. when filling a -// Config struct manually, its JSON-encodable fields -// (the ones with JSON struct tags, usually ending with -// "Raw" if they decode into a separate field) must be -// set so that they can be unmarshaled and provisioned. -// Setting the fields for the decoded values instead -// will result in those values being overwritten at -// unmarshaling/provisioning. +// Config is the top (or beginning) of the Caddy configuration structure. +// Caddy config is expressed natively as a JSON document. If you prefer +// not to work with JSON directly, there are [many config adapters](/docs/config-adapters) +// available that can convert various inputs into Caddy JSON. +// +// Many parts of this config are extensible through the use of Caddy modules. +// Fields which have a json.RawMessage type and which appear as dots (•••) in +// the online docs can be fulfilled by modules in a certain module +// namespace. The docs show which modules can be used in a given place. +// +// Whenever a module is used, its name must be given either inline as part of +// 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 { - Admin *AdminConfig `json:"admin,omitempty"` - Logging *Logging `json:"logging,omitempty"` - StorageRaw json.RawMessage `json:"storage,omitempty"` - AppsRaw map[string]json.RawMessage `json:"apps,omitempty"` + Admin *AdminConfig `json:"admin,omitempty"` + Logging *Logging `json:"logging,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 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 err = func() error { if newCfg.StorageRaw != nil { - val, err := ctx.LoadModuleInline("module", "caddy.storage", newCfg.StorageRaw) + val, err := ctx.LoadModule(newCfg, "StorageRaw") if err != nil { 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) } newCfg.storage = stor - newCfg.StorageRaw = nil // allow GC to deallocate } + if newCfg.storage == nil { 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 err = func() error { - for modName, rawMsg := range newCfg.AppsRaw { - val, err := ctx.LoadModule(modName, rawMsg) - if err != nil { - return fmt.Errorf("loading app module '%s': %v", modName, err) - } - newCfg.apps[modName] = val.(App) + appsIface, err := ctx.LoadModule(newCfg, "AppsRaw") + if err != nil { + return fmt.Errorf("loading app modules: %v", err) + } + for appName, appIface := range appsIface.(map[string]interface{}) { + newCfg.apps[appName] = appIface.(App) } return nil }() @@ -447,7 +468,10 @@ func Validate(cfg *Config) error { 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 // UnmarshalJSON satisfies json.Unmarshaler. diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go index ec3740c4..e92aa9da 100644 --- a/caddyconfig/httpcaddyfile/builtins.go +++ b/caddyconfig/httpcaddyfile/builtins.go @@ -21,6 +21,7 @@ import ( "net/http" "reflect" + "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddytls" @@ -71,7 +72,7 @@ func parseRoot(h Helper) ([]ConfigValue, error) { }, } if matcherSet != nil { - route.MatcherSetsRaw = []map[string]json.RawMessage{matcherSet} + route.MatcherSetsRaw = []caddy.ModuleMap{matcherSet} } return h.NewVarsRoute(route), nil diff --git a/caddyconfig/httpcaddyfile/directives.go b/caddyconfig/httpcaddyfile/directives.go index acdca20a..58aff98a 100644 --- a/caddyconfig/httpcaddyfile/directives.go +++ b/caddyconfig/httpcaddyfile/directives.go @@ -88,7 +88,7 @@ type Helper struct { *caddyfile.Dispenser options map[string]interface{} warnings *[]caddyconfig.Warning - matcherDefs map[string]map[string]json.RawMessage + matcherDefs map[string]caddy.ModuleMap 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 // 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. -func (h Helper) MatcherToken() (map[string]json.RawMessage, bool, error) { +func (h Helper) MatcherToken() (caddy.ModuleMap, bool, error) { if !h.NextArg() { 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. -func (h Helper) NewRoute(matcherSet map[string]json.RawMessage, +func (h Helper) NewRoute(matcherSet caddy.ModuleMap, handler caddyhttp.MiddlewareHandler) []ConfigValue { mod, err := caddy.GetModule(caddy.GetModuleName(handler)) if err != nil { // TODO: append to warnings } - var matcherSetsRaw []map[string]json.RawMessage + var matcherSetsRaw []caddy.ModuleMap if matcherSet != nil { matcherSetsRaw = append(matcherSetsRaw, matcherSet) } @@ -148,7 +148,7 @@ func (h Helper) NewRoute(matcherSet map[string]json.RawMessage, Class: "route", Value: caddyhttp.Route{ 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)}, }, }, } diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index 6ea32a28..2db3e7ca 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -172,7 +172,7 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock, } // 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 i, sblock := range p.serverBlocks { // tls automation policies @@ -189,7 +189,7 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock, } tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, caddytls.AutomationPolicy{ 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 { warnings = append(warnings, caddyconfig.Warning{ @@ -204,7 +204,7 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock, for _, clVal := range clVals { loader := clVal.Value.(caddytls.CertificateLoader) 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! - cfg := &caddy.Config{AppsRaw: make(map[string]json.RawMessage)} + cfg := &caddy.Config{AppsRaw: make(caddy.ModuleMap)} if !reflect.DeepEqual(httpApp, caddyhttp.App{}) { 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) } if storageCvtr, ok := options["storage"].(caddy.StorageConverter); ok { cfg.StorageRaw = caddyconfig.JSONModuleObject(storageCvtr, "module", - storageCvtr.(caddy.Module).CaddyModule().ID(), + storageCvtr.(caddy.Module).CaddyModule().ID.Name(), &warnings) } 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? - 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 } srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp) @@ -469,9 +469,9 @@ func consolidateAutomationPolicies(aps []caddytls.AutomationPolicy) []caddytls.A func matcherSetFromMatcherToken( tkn caddyfile.Token, - matcherDefs map[string]map[string]json.RawMessage, + matcherDefs map[string]caddy.ModuleMap, warnings *[]caddyconfig.Warning, -) (map[string]json.RawMessage, bool, error) { +) (caddy.ModuleMap, bool, error) { // matcher tokens can be wildcards, simple path matchers, // or refer to a pre-defined matcher by some name if tkn.Text == "*" { @@ -479,7 +479,7 @@ func matcherSetFromMatcherToken( return nil, true, nil } else if strings.HasPrefix(tkn.Text, "/") || strings.HasPrefix(tkn.Text, "=/") { // convenient way to specify a single path match - return map[string]json.RawMessage{ + return caddy.ModuleMap{ "path": caddyconfig.JSON(caddyhttp.MatchPath{tkn.Text}, warnings), }, true, nil } else if strings.HasPrefix(tkn.Text, "match:") { @@ -495,7 +495,7 @@ func matcherSetFromMatcherToken( 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 { hostm caddyhttp.MatchHost pathm caddyhttp.MatchPath @@ -562,7 +562,7 @@ func (st *ServerType) compileEncodedMatcherSets(sblock caddyfile.ServerBlock) ([ } // finally, encode each of the matcher sets - var matcherSetsEnc []map[string]json.RawMessage + var matcherSetsEnc []caddy.ModuleMap for _, ms := range matcherSets { msEncoded, err := encodeMatcherSet(ms) if err != nil { @@ -574,8 +574,8 @@ func (st *ServerType) compileEncodedMatcherSets(sblock caddyfile.ServerBlock) ([ return matcherSetsEnc, nil } -func parseMatcherDefinitions(d *caddyfile.Dispenser) (map[string]map[string]json.RawMessage, error) { - matchers := make(map[string]map[string]json.RawMessage) +func parseMatcherDefinitions(d *caddyfile.Dispenser) (map[string]caddy.ModuleMap, error) { + matchers := make(map[string]caddy.ModuleMap) for d.Next() { definitionName := d.Val() 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) } if _, ok := matchers[definitionName]; !ok { - matchers[definitionName] = make(map[string]json.RawMessage) + matchers[definitionName] = make(caddy.ModuleMap) } matchers[definitionName][matcherName] = caddyconfig.JSON(rm, nil) } @@ -605,8 +605,8 @@ func parseMatcherDefinitions(d *caddyfile.Dispenser) (map[string]map[string]json return matchers, nil } -func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (map[string]json.RawMessage, error) { - msEncoded := make(map[string]json.RawMessage) +func encodeMatcherSet(matchers map[string]caddyhttp.RequestMatcher) (caddy.ModuleMap, error) { + msEncoded := make(caddy.ModuleMap) for matcherName, val := range matchers { jsonBytes, err := json.Marshal(val) if err != nil { @@ -628,7 +628,7 @@ func tryInt(val interface{}, warnings *[]caddyconfig.Warning) int { } type matcherSetAndTokens struct { - matcherSet map[string]json.RawMessage + matcherSet caddy.ModuleMap tokens []caddyfile.Token } diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go index 74ec5076..e87b30f8 100644 --- a/caddyconfig/httpcaddyfile/options.go +++ b/caddyconfig/httpcaddyfile/options.go @@ -96,7 +96,7 @@ func parseOptStorage(d *caddyfile.Dispenser) (caddy.StorageConverter, error) { } unm, ok := mod.New().(caddyfile.Unmarshaler) 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()) if err != nil { @@ -104,7 +104,7 @@ func parseOptStorage(d *caddyfile.Dispenser) (caddy.StorageConverter, error) { } storage, ok := unm.(caddy.StorageConverter) 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 } diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go index d0886c5f..8526372d 100644 --- a/cmd/commandfuncs.go +++ b/cmd/commandfuncs.go @@ -321,11 +321,11 @@ func cmdListModules(fl Flags) (int, error) { return caddy.ExitCodeSuccess, nil } - for _, modName := range caddy.Modules() { - modInfo, err := caddy.GetModule(modName) + for _, modID := range caddy.Modules() { + modInfo, err := caddy.GetModule(modID) if err != nil { // that's weird - fmt.Println(modName) + fmt.Println(modID) continue } @@ -354,13 +354,13 @@ func cmdListModules(fl Flags) (int, error) { } // if we could find no matching module, just print out - // the module name instead + // the module ID instead if matched == nil { - fmt.Println(modName) + fmt.Println(modID) continue } - fmt.Printf("%s %s\n", modName, matched.Version) + fmt.Printf("%s %s\n", modID, matched.Version) } return caddy.ExitCodeSuccess, nil diff --git a/context.go b/context.go index 32368a9a..c95b08f5 100644 --- a/context.go +++ b/context.go @@ -80,24 +80,211 @@ func (ctx *Context) OnCancel(f func()) { ctx.cleanupFuncs = append(ctx.cleanupFuncs, f) } -// LoadModule decodes rawMsg into a new instance of mod and -// returns the value. If mod.New() does not return a pointer -// value, it is converted to one so that it is unmarshaled -// into the underlying concrete type. 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. -func (ctx Context) LoadModule(name string, rawMsg json.RawMessage) (interface{}, error) { - modulesMu.Lock() - mod, ok := modules[name] - modulesMu.Unlock() +// LoadModule loads the Caddy module(s) from the specified field of the parent struct +// pointer and returns the loaded module(s). The struct pointer and its field name as +// a string are necessary so that reflection can be used to read the struct tag on the +// field to get the module namespace and inline module name key (if specified). +// +// The field can be any one of the supported raw module types: json.RawMessage, +// []json.RawMessage, map[string]json.RawMessage, or []map[string]json.RawMessage. +// ModuleMap may be used in place of map[string]json.RawMessage. The return value's +// underlying type mirrors the input field's type: +// +// json.RawMessage => interface{} +// []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 { - 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 { - 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{}) @@ -108,7 +295,7 @@ func (ctx Context) LoadModule(name string, rawMsg json.RawMessage) (interface{}, if rv := reflect.ValueOf(val); rv.Kind() != reflect.Ptr { log.Printf("[WARNING] ModuleInfo.New() for module '%s' did not return a pointer,"+ " so we are using reflection to make a pointer instead; please fix this by"+ - " using new(Type) or &Type notation in your module's New() function.", name) + " using new(Type) or &Type notation in your module's New() function.", id) 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 { err := strictUnmarshalJSON(rawMsg, &val) 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 // before being used, so a nil value would panic; and there // is no good reason to explicitly declare null modules in - // a config; it might be because the user is trying to - // achieve a result they aren't expecting, which is a smell + // a config; it might be because the user is trying to achieve + // a result the developer isn't expecting, which is a smell 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) } } - 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) } } - 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 } -// LoadModuleInline loads a module from a JSON raw message which decodes -// to a map[string]interface{}, where one of the keys is moduleNameKey -// and the corresponding value is the module name as a string, which -// can be found in the given scope. +// loadModuleInline loads a module from a JSON raw message which decodes to +// a map[string]interface{}, where one of the object keys is moduleNameKey +// and the corresponding value is the module name (as a string) which can +// 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 -// used when their names cannot be the unique key in a map, such as -// when there are multiple instances in the map or it appears in an -// array (where there are no custom keys). In other words, the key -// containing the module name is treated special/separate from all -// the other keys. -func (ctx Context) LoadModuleInline(moduleNameKey, moduleScope string, raw json.RawMessage) (interface{}, error) { +// This allows modules to be decoded into their concrete types and used when +// their names cannot be the unique key in a map, such as when there are +// multiple instances in the map or it appears in an array (where there are +// no custom keys). In other words, the key containing the module name is +// treated special/separate from all the other keys in the object. +func (ctx Context) loadModuleInline(moduleNameKey, moduleScope string, raw json.RawMessage) (interface{}, error) { moduleName, raw, err := getModuleNameInline(moduleNameKey, raw) if err != nil { return nil, err } - val, err := ctx.LoadModule(moduleScope+"."+moduleName, raw) + val, err := ctx.LoadModuleByID(moduleScope+"."+moduleName, raw) if err != nil { 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 { return app, nil } - modVal, err := ctx.LoadModule(name, nil) + modVal, err := ctx.LoadModuleByID(name, nil) if err != nil { return nil, fmt.Errorf("instantiating new module %s: %v", name, err) } diff --git a/logging.go b/logging.go index 97c05138..3fdd644a 100644 --- a/logging.go +++ b/logging.go @@ -36,8 +36,36 @@ func init() { } // 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 { - Sink *StandardLibLog `json:"sink,omitempty"` + // 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"` + + // 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"` // 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. func (logging *Logging) Logger(mod Module) *zap.Logger { - modName := mod.CaddyModule().Name + modID := string(mod.CaddyModule().ID) var cores []zapcore.Core if logging != nil { for _, l := range logging.Logs { - if l.matchesModule(modName) { + if l.matchesModule(modID) { if len(l.Include) == 0 && len(l.Exclude) == 0 { cores = append(cores, l.core) continue @@ -186,7 +214,7 @@ func (logging *Logging) Logger(mod Module) *zap.Logger { 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 @@ -231,26 +259,27 @@ func (wdest writerDestructor) Destruct() error { // StandardLibLog configures the default Go standard library // global logger in the log package. This is necessary because // 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 { - 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 } func (sll *StandardLibLog) provision(ctx Context, logging *Logging) error { if sll.WriterRaw != nil { - val, err := ctx.LoadModuleInline("output", "caddy.logging.writers", sll.WriterRaw) + mod, err := ctx.LoadModule(sll, "WriterRaw") if err != nil { return fmt.Errorf("loading sink log writer module: %v", err) } - wo := val.(WriterOpener) - sll.WriterRaw = nil // allow GC to deallocate + wo := mod.(WriterOpener) var isNew bool sll.writer, isNew, err = logging.openWriter(wo) 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 { @@ -264,13 +293,40 @@ func (sll *StandardLibLog) provision(ctx Context, logging *Logging) error { } // 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 { - WriterRaw json.RawMessage `json:"writer,omitempty"` - EncoderRaw json.RawMessage `json:"encoder,omitempty"` - Level string `json:"level,omitempty"` - Sampling *LogSampling `json:"sampling,omitempty"` - Include []string `json:"include,omitempty"` - Exclude []string `json:"exclude,omitempty"` + // The writer defines where log entries are emitted. + 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"` + + // 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"` + + // 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"` + + // 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"` writerOpener WriterOpener writer io.WriteCloser @@ -336,24 +392,22 @@ func (cl *CustomLog) provision(ctx Context, logging *Logging) error { } if cl.EncoderRaw != nil { - val, err := ctx.LoadModuleInline("format", "caddy.logging.encoders", cl.EncoderRaw) + mod, err := ctx.LoadModule(cl, "EncoderRaw") if err != nil { return fmt.Errorf("loading log encoder module: %v", err) } - cl.EncoderRaw = nil // allow GC to deallocate - cl.encoder = val.(zapcore.Encoder) + cl.encoder = mod.(zapcore.Encoder) } if cl.encoder == nil { cl.encoder = newDefaultProductionLogEncoder() } if cl.WriterRaw != nil { - val, err := ctx.LoadModuleInline("output", "caddy.logging.writers", cl.WriterRaw) + mod, err := ctx.LoadModule(cl, "WriterRaw") if err != nil { return fmt.Errorf("loading log writer module: %v", err) } - cl.WriterRaw = nil // allow GC to deallocate - cl.writerOpener = val.(WriterOpener) + cl.writerOpener = mod.(WriterOpener) } if cl.writerOpener == nil { cl.writerOpener = StderrWriter{} @@ -398,8 +452,8 @@ func (cl *CustomLog) buildCore() { cl.core = c } -func (cl *CustomLog) matchesModule(moduleName string) bool { - return cl.loggerAllowed(moduleName, true) +func (cl *CustomLog) matchesModule(moduleID string) bool { + return cl.loggerAllowed(string(moduleID), true) } // loggerAllowed returns true if name is allowed to emit @@ -493,9 +547,17 @@ func (fc *filteringCore) Check(e zapcore.Entry, ce *zapcore.CheckedEntry) *zapco // LogSampling configures log entry sampling. type LogSampling struct { - Interval time.Duration `json:"interval,omitempty"` - First int `json:"first,omitempty"` - Thereafter int `json:"thereafter,omitempty"` + // The window over which to conduct sampling. + Interval time.Duration `json:"interval,omitempty"` + + // Log this many entries within a given level and + // message for each interval. + 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"` } type ( @@ -512,24 +574,24 @@ type ( // CaddyModule returns the Caddy module information. func (StdoutWriter) CaddyModule() ModuleInfo { return ModuleInfo{ - Name: "caddy.logging.writers.stdout", - New: func() Module { return new(StdoutWriter) }, + ID: "caddy.logging.writers.stdout", + New: func() Module { return new(StdoutWriter) }, } } // CaddyModule returns the Caddy module information. func (StderrWriter) CaddyModule() ModuleInfo { return ModuleInfo{ - Name: "caddy.logging.writers.stderr", - New: func() Module { return new(StderrWriter) }, + ID: "caddy.logging.writers.stderr", + New: func() Module { return new(StderrWriter) }, } } // CaddyModule returns the Caddy module information. func (DiscardWriter) CaddyModule() ModuleInfo { return ModuleInfo{ - Name: "caddy.logging.writers.discard", - New: func() Module { return new(DiscardWriter) }, + ID: "caddy.logging.writers.discard", + New: func() Module { return new(DiscardWriter) }, } } diff --git a/modules.go b/modules.go index 2d01eb3b..b036bea1 100644 --- a/modules.go +++ b/modules.go @@ -18,58 +18,107 @@ import ( "bytes" "encoding/json" "fmt" + "reflect" "sort" "strings" "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 { - // This method indicates the type is a Caddy - // module. The returned ModuleInfo must have - // both a name and a constructor function. - // This method must not have any side-effects. + // This method indicates that the type is a Caddy + // module. The returned ModuleInfo must have both + // a name and a constructor function. This method + // must not have any side-effects. CaddyModule() ModuleInfo } // ModuleInfo represents a registered Caddy module. 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. - Name string + ID ModuleID // New returns a pointer to a new, empty - // instance of the module's type. The host - // module which instantiates this module will - // likely type-assert and invoke methods on - // the returned value. This function must not - // have any side-effects. + // instance of the module's type. This + // function must not have any side-effects. New func() Module } -// Namespace returns the module's namespace (scope) -// which is all but the last element of its name. -// If there is no explicit namespace in the name, -// the whole name is considered the namespace. -func (mi ModuleInfo) Namespace() string { - lastDot := strings.LastIndex(mi.Name, ".") +// ModuleID is a string that uniquely identifies a Caddy module. A +// module ID is lightly structured. It consists of dot-separated +// labels which form a simple hierarchy from left to right. The last +// label is the module name, and the labels before that constitute +// the namespace (or scope). +// +// Thus, a module ID has the form: . +// +// 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 { - return mi.Name + return string(id) } - return mi.Name[:lastDot] + return string(id)[:lastDot] } -// ID returns a module's ID, which is the -// last element of its name. -func (mi ModuleInfo) ID() string { - if mi.Name == "" { +// Name returns the Name (last element) of a module name. +func (id ModuleID) Name() string { + if id == "" { return "" } - parts := strings.Split(mi.Name, ".") + parts := strings.Split(string(id), ".") 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 // 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 { mod := instance.CaddyModule() - if mod.Name == "" { - return fmt.Errorf("missing ModuleInfo.Name") + if mod.ID == "" { + return fmt.Errorf("module ID missing") } - if mod.Name == "caddy" || mod.Name == "admin" { - return fmt.Errorf("module name '%s' is reserved", mod.Name) + if mod.ID == "caddy" || mod.ID == "admin" { + return fmt.Errorf("module ID '%s' is reserved", mod.ID) } if mod.New == nil { return fmt.Errorf("missing ModuleInfo.New") @@ -98,17 +147,17 @@ func RegisterModule(instance Module) error { modulesMu.Lock() defer modulesMu.Unlock() - if _, ok := modules[mod.Name]; ok { - return fmt.Errorf("module already registered: %s", mod.Name) + if _, ok := modules[string(mod.ID)]; ok { + return fmt.Errorf("module already registered: %s", mod.ID) } - modules[mod.Name] = mod + modules[string(mod.ID)] = mod 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) { - modulesMu.Lock() - defer modulesMu.Unlock() + modulesMu.RLock() + defer modulesMu.RUnlock() m, ok := modules[name] if !ok { return ModuleInfo{}, fmt.Errorf("module not registered: %s", name) @@ -116,25 +165,25 @@ func GetModule(name string) (ModuleInfo, error) { return m, nil } -// GetModuleName returns a module's name from an instance of its value. -// If the value is not a module, an empty name will be returned. +// GetModuleName returns a module's name (the last label of its ID) +// from an instance of its value. If the value is not a module, an +// empty string will be returned. func GetModuleName(instance interface{}) string { var name string if mod, ok := instance.(Module); ok { - name = mod.CaddyModule().Name + name = mod.CaddyModule().ID.Name() } return name } -// GetModuleID returns a module's ID (the last element of its name) -// from an instance of its value. If the value is not a module, -// an empty string will be returned. +// GetModuleID returns a module's ID from an instance of its value. +// If the value is not a module, an empty string will be returned. func GetModuleID(instance interface{}) string { - var name string + var id string 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. @@ -144,11 +193,11 @@ func GetModuleID(instance interface{}) string { // scopes are not matched (i.e. scope "foo.ba" does not match // name "foo.bar"). // -// Because modules are registered to a map, the returned slice -// will be sorted to keep it deterministic. +// Because modules are registered to a map under the hood, the +// returned slice will be sorted to keep it deterministic. func GetModules(scope string) []ModuleInfo { - modulesMu.Lock() - defer modulesMu.Unlock() + modulesMu.RLock() + defer modulesMu.RUnlock() scopeParts := strings.Split(scope, ".") @@ -160,8 +209,8 @@ func GetModules(scope string) []ModuleInfo { var mods []ModuleInfo iterateModules: - for name, m := range modules { - modParts := strings.Split(name, ".") + for id, m := range modules { + modParts := strings.Split(string(id), ".") // match only the next level of nesting if len(modParts) != len(scopeParts)+1 { @@ -180,7 +229,7 @@ iterateModules: // make return value deterministic sort.Slice(mods, func(i, j int) bool { - return mods[i].Name < mods[j].Name + return mods[i].ID < mods[j].ID }) return mods @@ -189,12 +238,12 @@ iterateModules: // Modules returns the names of all registered modules // in ascending lexicographical order. func Modules() []string { - modulesMu.Lock() - defer modulesMu.Unlock() + modulesMu.RLock() + defer modulesMu.RUnlock() var names []string for name := range modules { - names = append(names, name) + names = append(names, string(name)) } sort.Strings(names) @@ -261,6 +310,25 @@ type CleanerUpper interface { 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 // if any of the fields are unrecognized. Useful when decoding // 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) } +// 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 ( modules = make(map[string]ModuleInfo) - modulesMu sync.Mutex + modulesMu sync.RWMutex ) diff --git a/modules/caddyhttp/caddyauth/basicauth.go b/modules/caddyhttp/caddyauth/basicauth.go index 6412d36e..8aa44f16 100644 --- a/modules/caddyhttp/caddyauth/basicauth.go +++ b/modules/caddyhttp/caddyauth/basicauth.go @@ -28,7 +28,7 @@ func init() { // HTTPBasicAuth facilitates HTTP basic authentication. 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"` Realm string `json:"realm,omitempty"` @@ -39,8 +39,8 @@ type HTTPBasicAuth struct { // CaddyModule returns the Caddy module information. func (HTTPBasicAuth) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.authentication.providers.http_basic", - New: func() caddy.Module { return new(HTTPBasicAuth) }, + ID: "http.authentication.providers.http_basic", + New: func() caddy.Module { return new(HTTPBasicAuth) }, } } @@ -51,12 +51,11 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error { } // load password hasher - hashIface, err := ctx.LoadModuleInline("algorithm", "http.handlers.authentication.hashes", hba.HashRaw) + hasherIface, err := ctx.LoadModule(hba, "HashRaw") if err != nil { return fmt.Errorf("loading password hasher module: %v", err) } - hba.Hash = hashIface.(Comparer) - hba.HashRaw = nil // allow GC to deallocate + hba.Hash = hasherIface.(Comparer) if hba.Hash == nil { return fmt.Errorf("hash is required") diff --git a/modules/caddyhttp/caddyauth/caddyauth.go b/modules/caddyhttp/caddyauth/caddyauth.go index 48d4fba1..c79d080b 100644 --- a/modules/caddyhttp/caddyauth/caddyauth.go +++ b/modules/caddyhttp/caddyauth/caddyauth.go @@ -15,7 +15,6 @@ package caddyauth import ( - "encoding/json" "fmt" "log" "net/http" @@ -30,7 +29,7 @@ func init() { // Authentication is a middleware which provides user authentication. 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:"-"` } @@ -38,23 +37,21 @@ type Authentication struct { // CaddyModule returns the Caddy module information. func (Authentication) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.authentication", - New: func() caddy.Module { return new(Authentication) }, + ID: "http.handlers.authentication", + New: func() caddy.Module { return new(Authentication) }, } } // Provision sets up a. func (a *Authentication) Provision(ctx caddy.Context) error { a.Providers = make(map[string]Authenticator) - for modName, rawMsg := range a.ProvidersRaw { - val, err := ctx.LoadModule("http.handlers.authentication.providers."+modName, rawMsg) - if err != nil { - return fmt.Errorf("loading authentication provider module '%s': %v", modName, err) - } - a.Providers[modName] = val.(Authenticator) + mods, err := ctx.LoadModule(a, "ProvidersRaw") + if err != nil { + return fmt.Errorf("loading authentication providers: %v", err) + } + for modName, modIface := range mods.(map[string]interface{}) { + a.Providers[modName] = modIface.(Authenticator) } - a.ProvidersRaw = nil // allow GC to deallocate - return nil } diff --git a/modules/caddyhttp/caddyauth/caddyfile.go b/modules/caddyhttp/caddyauth/caddyfile.go index 36003246..8a33e6f2 100644 --- a/modules/caddyhttp/caddyauth/caddyfile.go +++ b/modules/caddyhttp/caddyauth/caddyfile.go @@ -16,8 +16,8 @@ package caddyauth import ( "encoding/base64" - "encoding/json" + "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" @@ -97,7 +97,7 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) } return Authentication{ - ProvidersRaw: map[string]json.RawMessage{ + ProvidersRaw: caddy.ModuleMap{ "http_basic": caddyconfig.JSON(ba, nil), }, }, nil diff --git a/modules/caddyhttp/caddyauth/hashes.go b/modules/caddyhttp/caddyauth/hashes.go index 13010db7..3ca51168 100644 --- a/modules/caddyhttp/caddyauth/hashes.go +++ b/modules/caddyhttp/caddyauth/hashes.go @@ -33,8 +33,8 @@ type BcryptHash struct{} // CaddyModule returns the Caddy module information. func (BcryptHash) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.authentication.hashes.bcrypt", - New: func() caddy.Module { return new(BcryptHash) }, + ID: "http.authentication.hashes.bcrypt", + New: func() caddy.Module { return new(BcryptHash) }, } } @@ -61,8 +61,8 @@ type ScryptHash struct { // CaddyModule returns the Caddy module information. func (ScryptHash) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.authentication.hashes.scrypt", - New: func() caddy.Module { return new(ScryptHash) }, + ID: "http.authentication.hashes.scrypt", + New: func() caddy.Module { return new(ScryptHash) }, } } diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go index 064a963a..756a6c30 100644 --- a/modules/caddyhttp/caddyhttp.go +++ b/modules/caddyhttp/caddyhttp.go @@ -44,12 +44,27 @@ func init() { } } -// App is the HTTP app for Caddy. +// App is a robust, flexible HTTP server for Caddy. type App struct { - HTTPPort int `json:"http_port,omitempty"` - HTTPSPort int `json:"https_port,omitempty"` - GracePeriod caddy.Duration `json:"grace_period,omitempty"` - Servers map[string]*Server `json:"servers,omitempty"` + // 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"` + + // 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"` + + // 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"` + + // 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 []*http.Server h3servers []*http3.Server @@ -62,8 +77,8 @@ type App struct { // CaddyModule returns the Caddy module information. func (App) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http", - New: func() caddy.Module { return new(App) }, + ID: "http", + 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 // 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 // primitive types. However, using this type removes any // type safety as far as deserializing JSON is concerned. diff --git a/modules/caddyhttp/encode/brotli/brotli.go b/modules/caddyhttp/encode/brotli/brotli.go index cf055aaf..52bb205e 100644 --- a/modules/caddyhttp/encode/brotli/brotli.go +++ b/modules/caddyhttp/encode/brotli/brotli.go @@ -37,8 +37,8 @@ type Brotli struct { // CaddyModule returns the Caddy module information. func (Brotli) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.encoders.brotli", - New: func() caddy.Module { return new(Brotli) }, + ID: "http.encoders.brotli", + New: func() caddy.Module { return new(Brotli) }, } } diff --git a/modules/caddyhttp/encode/caddyfile.go b/modules/caddyhttp/encode/caddyfile.go index 4764e9b9..dd12de28 100644 --- a/modules/caddyhttp/encode/caddyfile.go +++ b/modules/caddyhttp/encode/caddyfile.go @@ -15,7 +15,6 @@ package encode import ( - "encoding/json" "fmt" "github.com/caddyserver/caddy/v2" @@ -52,14 +51,14 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { for _, arg := range d.RemainingArgs() { mod, err := caddy.GetModule("http.encoders." + arg) 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) 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 { - enc.EncodingsRaw = make(map[string]json.RawMessage) + enc.EncodingsRaw = make(caddy.ModuleMap) } 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) 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()) if err != nil { @@ -80,10 +79,10 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } encoding, ok := unm.(Encoding) 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 { - enc.EncodingsRaw = make(map[string]json.RawMessage) + enc.EncodingsRaw = make(caddy.ModuleMap) } enc.EncodingsRaw[name] = caddyconfig.JSON(encoding, nil) } diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go index 3716fc69..c68f507d 100644 --- a/modules/caddyhttp/encode/encode.go +++ b/modules/caddyhttp/encode/encode.go @@ -21,7 +21,6 @@ package encode import ( "bytes" - "encoding/json" "fmt" "io" "net/http" @@ -40,9 +39,9 @@ func init() { // Encode is a middleware which can encode responses. type Encode struct { - EncodingsRaw map[string]json.RawMessage `json:"encodings,omitempty"` - Prefer []string `json:"prefer,omitempty"` - MinLength int `json:"minimum_length,omitempty"` + EncodingsRaw caddy.ModuleMap `json:"encodings,omitempty" caddy:"namespace=http.encoders"` + Prefer []string `json:"prefer,omitempty"` + MinLength int `json:"minimum_length,omitempty"` writerPools map[string]*sync.Pool // TODO: these pools do not get reused through config reloads... } @@ -50,25 +49,23 @@ type Encode struct { // CaddyModule returns the Caddy module information. func (Encode) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.encode", - New: func() caddy.Module { return new(Encode) }, + ID: "http.handlers.encode", + New: func() caddy.Module { return new(Encode) }, } } // Provision provisions enc. func (enc *Encode) Provision(ctx caddy.Context) error { - for modName, rawMsg := range enc.EncodingsRaw { - val, err := ctx.LoadModule("http.encoders."+modName, rawMsg) + mods, err := ctx.LoadModule(enc, "EncodingsRaw") + if err != nil { + return fmt.Errorf("loading encoder modules: %v", err) + } + for modName, modIface := range mods.(map[string]interface{}) { + err = enc.addEncoding(modIface.(Encoding)) if err != nil { - return fmt.Errorf("loading encoder module '%s': %v", modName, err) - } - encoding := val.(Encoding) - err = enc.addEncoding(encoding) - 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 { enc.MinLength = defaultMinLength diff --git a/modules/caddyhttp/encode/gzip/gzip.go b/modules/caddyhttp/encode/gzip/gzip.go index d6d67f71..590f7089 100644 --- a/modules/caddyhttp/encode/gzip/gzip.go +++ b/modules/caddyhttp/encode/gzip/gzip.go @@ -37,8 +37,8 @@ type Gzip struct { // CaddyModule returns the Caddy module information. func (Gzip) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.encoders.gzip", - New: func() caddy.Module { return new(Gzip) }, + ID: "http.encoders.gzip", + New: func() caddy.Module { return new(Gzip) }, } } diff --git a/modules/caddyhttp/encode/zstd/zstd.go b/modules/caddyhttp/encode/zstd/zstd.go index f2b4e858..5182fc4e 100644 --- a/modules/caddyhttp/encode/zstd/zstd.go +++ b/modules/caddyhttp/encode/zstd/zstd.go @@ -31,8 +31,8 @@ type Zstd struct{} // CaddyModule returns the Caddy module information. func (Zstd) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.encoders.zstd", - New: func() caddy.Module { return new(Zstd) }, + ID: "http.encoders.zstd", + New: func() caddy.Module { return new(Zstd) }, } } diff --git a/modules/caddyhttp/fileserver/caddyfile.go b/modules/caddyhttp/fileserver/caddyfile.go index 46bc5b70..fb931a1d 100644 --- a/modules/caddyhttp/fileserver/caddyfile.go +++ b/modules/caddyhttp/fileserver/caddyfile.go @@ -15,8 +15,7 @@ package fileserver import ( - "encoding/json" - + "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "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}", } - matcherSet := map[string]json.RawMessage{ + matcherSet := caddy.ModuleMap{ "file": h.JSON(MatchFile{ TryFiles: try, }, nil), diff --git a/modules/caddyhttp/fileserver/command.go b/modules/caddyhttp/fileserver/command.go index b861a999..e7a0ee38 100644 --- a/modules/caddyhttp/fileserver/command.go +++ b/modules/caddyhttp/fileserver/command.go @@ -75,8 +75,8 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) { }, } if domain != "" { - route.MatcherSetsRaw = []map[string]json.RawMessage{ - map[string]json.RawMessage{ + route.MatcherSetsRaw = []caddy.ModuleMap{ + caddy.ModuleMap{ "host": caddyconfig.JSON(caddyhttp.MatchHost{domain}, nil), }, } @@ -100,7 +100,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) { cfg := &caddy.Config{ Admin: &caddy.AdminConfig{Disabled: true}, - AppsRaw: map[string]json.RawMessage{ + AppsRaw: caddy.ModuleMap{ "http": caddyconfig.JSON(httpApp, nil), }, } diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go index 4a7f6570..13cb60aa 100644 --- a/modules/caddyhttp/fileserver/matcher.go +++ b/modules/caddyhttp/fileserver/matcher.go @@ -54,8 +54,8 @@ type MatchFile struct { // CaddyModule returns the Caddy module information. func (MatchFile) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.matchers.file", - New: func() caddy.Module { return new(MatchFile) }, + ID: "http.matchers.file", + New: func() caddy.Module { return new(MatchFile) }, } } diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index 732894da..a9e6e1cc 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -53,8 +53,8 @@ type FileServer struct { // CaddyModule returns the Caddy module information. func (FileServer) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.file_server", - New: func() caddy.Module { return new(FileServer) }, + ID: "http.handlers.file_server", + New: func() caddy.Module { return new(FileServer) }, } } diff --git a/modules/caddyhttp/headers/headers.go b/modules/caddyhttp/headers/headers.go index 813b9fec..f53e859c 100644 --- a/modules/caddyhttp/headers/headers.go +++ b/modules/caddyhttp/headers/headers.go @@ -37,8 +37,8 @@ type Handler struct { // CaddyModule returns the Caddy module information. func (Handler) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.headers", - New: func() caddy.Module { return new(Handler) }, + ID: "http.handlers.headers", + New: func() caddy.Module { return new(Handler) }, } } diff --git a/modules/caddyhttp/httpcache/httpcache.go b/modules/caddyhttp/httpcache/httpcache.go index b5bc0448..81f58161 100644 --- a/modules/caddyhttp/httpcache/httpcache.go +++ b/modules/caddyhttp/httpcache/httpcache.go @@ -43,8 +43,8 @@ type Cache struct { // CaddyModule returns the Caddy module information. func (Cache) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.cache", - New: func() caddy.Module { return new(Cache) }, + ID: "http.handlers.cache", + New: func() caddy.Module { return new(Cache) }, } } diff --git a/modules/caddyhttp/markdown/markdown.go b/modules/caddyhttp/markdown/markdown.go index 5ff18b88..acaa0c3c 100644 --- a/modules/caddyhttp/markdown/markdown.go +++ b/modules/caddyhttp/markdown/markdown.go @@ -38,8 +38,8 @@ type Markdown struct { // CaddyModule returns the Caddy module information. func (Markdown) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.markdown", - New: func() caddy.Module { return new(Markdown) }, + ID: "http.handlers.markdown", + New: func() caddy.Module { return new(Markdown) }, } } diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index 82fb04a3..ea715c57 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -66,7 +66,7 @@ type ( // MatchNegate matches requests by negating its matchers' results. MatchNegate struct { - MatchersRaw map[string]json.RawMessage `json:"-"` + MatchersRaw caddy.ModuleMap `json:"-" caddy:"namespace=http.matchers"` Matchers MatcherSet `json:"-"` } @@ -95,8 +95,8 @@ func init() { // CaddyModule returns the Caddy module information. func (MatchHost) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.matchers.host", - New: func() caddy.Module { return new(MatchHost) }, + ID: "http.matchers.host", + New: func() caddy.Module { return new(MatchHost) }, } } @@ -149,8 +149,8 @@ outer: // CaddyModule returns the Caddy module information. func (MatchPath) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.matchers.path", - New: func() caddy.Module { return new(MatchPath) }, + ID: "http.matchers.path", + New: func() caddy.Module { return new(MatchPath) }, } } @@ -208,8 +208,8 @@ func (m *MatchPath) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // CaddyModule returns the Caddy module information. func (MatchPathRE) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.matchers.path_regexp", - New: func() caddy.Module { return new(MatchPathRE) }, + ID: "http.matchers.path_regexp", + New: func() caddy.Module { return new(MatchPathRE) }, } } @@ -222,8 +222,8 @@ func (m MatchPathRE) Match(r *http.Request) bool { // CaddyModule returns the Caddy module information. func (MatchMethod) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.matchers.method", - New: func() caddy.Module { return new(MatchMethod) }, + ID: "http.matchers.method", + New: func() caddy.Module { return new(MatchMethod) }, } } @@ -248,8 +248,8 @@ func (m MatchMethod) Match(r *http.Request) bool { // CaddyModule returns the Caddy module information. func (MatchQuery) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.matchers.query", - New: func() caddy.Module { return new(MatchQuery) }, + ID: "http.matchers.query", + New: func() caddy.Module { return new(MatchQuery) }, } } @@ -291,8 +291,8 @@ func (m MatchQuery) Match(r *http.Request) bool { // CaddyModule returns the Caddy module information. func (MatchHeader) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.matchers.header", - New: func() caddy.Module { return new(MatchHeader) }, + ID: "http.matchers.header", + New: func() caddy.Module { return new(MatchHeader) }, } } @@ -349,8 +349,8 @@ func (m MatchHeader) Match(r *http.Request) bool { // CaddyModule returns the Caddy module information. func (MatchHeaderRE) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.matchers.header_regexp", - New: func() caddy.Module { return new(MatchHeaderRE) }, + ID: "http.matchers.header_regexp", + New: func() caddy.Module { return new(MatchHeaderRE) }, } } @@ -406,8 +406,8 @@ func (m MatchHeaderRE) Validate() error { // CaddyModule returns the Caddy module information. func (MatchProtocol) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.matchers.protocol", - New: func() caddy.Module { return new(MatchProtocol) }, + ID: "http.matchers.protocol", + New: func() caddy.Module { return new(MatchProtocol) }, } } @@ -439,8 +439,8 @@ func (m *MatchProtocol) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // CaddyModule returns the Caddy module information. func (MatchNegate) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.matchers.not", - New: func() caddy.Module { return new(MatchNegate) }, + ID: "http.matchers.not", + 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 // to be able to marshal as JSON, otherwise config // adaptation won't work properly - m.MatchersRaw = make(map[string]json.RawMessage) + m.MatchersRaw = make(caddy.ModuleMap) for name, matchers := range matcherMap { jsonBytes, err := json.Marshal(matchers) if err != nil { @@ -500,14 +500,13 @@ func (m *MatchNegate) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // Provision loads the matcher modules to be negated. func (m *MatchNegate) Provision(ctx caddy.Context) error { - for modName, rawMsg := range m.MatchersRaw { - val, err := ctx.LoadModule("http.matchers."+modName, rawMsg) - if err != nil { - return fmt.Errorf("loading matcher module '%s': %v", modName, err) - } - m.Matchers = append(m.Matchers, val.(RequestMatcher)) + mods, err := ctx.LoadModule(m, "MatchersRaw") + if err != nil { + return fmt.Errorf("loading matchers: %v", err) + } + for _, modIface := range mods.(map[string]interface{}) { + m.Matchers = append(m.Matchers, modIface.(RequestMatcher)) } - m.MatchersRaw = nil // allow GC to deallocate return nil } @@ -520,8 +519,8 @@ func (m MatchNegate) Match(r *http.Request) bool { // CaddyModule returns the Caddy module information. func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.matchers.remote_ip", - New: func() caddy.Module { return new(MatchRemoteIP) }, + ID: "http.matchers.remote_ip", + New: func() caddy.Module { return new(MatchRemoteIP) }, } } @@ -597,8 +596,8 @@ func (m MatchRemoteIP) Match(r *http.Request) bool { // CaddyModule returns the Caddy module information. func (MatchStarlarkExpr) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.matchers.starlark_expr", // TODO: Rename to 'starlark'? - New: func() caddy.Module { return new(MatchStarlarkExpr) }, + ID: "http.matchers.starlark_expr", // TODO: Rename to 'starlark'? + New: func() caddy.Module { return new(MatchStarlarkExpr) }, } } diff --git a/modules/caddyhttp/requestbody/requestbody.go b/modules/caddyhttp/requestbody/requestbody.go index 9b162501..dd3f2569 100644 --- a/modules/caddyhttp/requestbody/requestbody.go +++ b/modules/caddyhttp/requestbody/requestbody.go @@ -33,8 +33,8 @@ type RequestBody struct { // CaddyModule returns the Caddy module information. func (RequestBody) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.request_body", // TODO: better name for this? - New: func() caddy.Module { return new(RequestBody) }, + ID: "http.handlers.request_body", // TODO: better name for this? + New: func() caddy.Module { return new(RequestBody) }, } } diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go index c8cf26e7..9dba7691 100644 --- a/modules/caddyhttp/reverseproxy/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/caddyfile.go @@ -108,11 +108,11 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { name := d.Val() mod, err := caddy.GetModule("http.handlers.reverse_proxy.selection_policies." + name) 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) 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()) if err != nil { @@ -120,7 +120,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } sel, ok := unm.(Selector) 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 { h.LoadBalancing = new(LoadBalancing) @@ -391,11 +391,11 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { name := d.Val() mod, err := caddy.GetModule("http.handlers.reverse_proxy.transport." + name) 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) 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()) if err != nil { @@ -403,7 +403,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } rt, ok := unm.(http.RoundTripper) 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) diff --git a/modules/caddyhttp/reverseproxy/circuitbreaker.go b/modules/caddyhttp/reverseproxy/circuitbreaker.go index de2a6f94..474f1c63 100644 --- a/modules/caddyhttp/reverseproxy/circuitbreaker.go +++ b/modules/caddyhttp/reverseproxy/circuitbreaker.go @@ -41,8 +41,8 @@ type localCircuitBreaker struct { // CaddyModule returns the Caddy module information. func (localCircuitBreaker) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.reverse_proxy.circuit_breakers.local", - New: func() caddy.Module { return new(localCircuitBreaker) }, + ID: "http.reverse_proxy.circuit_breakers.local", + New: func() caddy.Module { return new(localCircuitBreaker) }, } } diff --git a/modules/caddyhttp/reverseproxy/command.go b/modules/caddyhttp/reverseproxy/command.go index 0ddb8f2f..e16c4f52 100644 --- a/modules/caddyhttp/reverseproxy/command.go +++ b/modules/caddyhttp/reverseproxy/command.go @@ -113,8 +113,8 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) { } urlHost := fromURL.Hostname() if urlHost != "" { - route.MatcherSetsRaw = []map[string]json.RawMessage{ - map[string]json.RawMessage{ + route.MatcherSetsRaw = []caddy.ModuleMap{ + caddy.ModuleMap{ "host": caddyconfig.JSON(caddyhttp.MatchHost{urlHost}, nil), }, } @@ -138,7 +138,7 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) { cfg := &caddy.Config{ Admin: &caddy.AdminConfig{Disabled: true}, - AppsRaw: map[string]json.RawMessage{ + AppsRaw: caddy.ModuleMap{ "http": caddyconfig.JSON(httpApp, nil), }, } diff --git a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go index ed97342d..8e723b2c 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go @@ -18,6 +18,7 @@ import ( "encoding/json" "net/http" + "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "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 - redirMatcherSet := map[string]json.RawMessage{ + redirMatcherSet := caddy.ModuleMap{ "file": h.JSON(fileserver.MatchFile{ TryFiles: []string{"{http.request.uri.path}/index.php"}, }, nil), "not": h.JSON(caddyhttp.MatchNegate{ - MatchersRaw: map[string]json.RawMessage{ + MatchersRaw: caddy.ModuleMap{ "path": h.JSON(caddyhttp.MatchPath{"*/"}, nil), }, }, nil), @@ -136,12 +137,12 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error Headers: http.Header{"Location": []string{"{http.request.uri.path}/"}}, } redirRoute := caddyhttp.Route{ - MatcherSetsRaw: []map[string]json.RawMessage{redirMatcherSet}, + MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet}, HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)}, } // route to rewrite to PHP index file - rewriteMatcherSet := map[string]json.RawMessage{ + rewriteMatcherSet := caddy.ModuleMap{ "file": h.JSON(fileserver.MatchFile{ TryFiles: []string{"{http.request.uri.path}", "{http.request.uri.path}/index.php", "index.php"}, }, nil), @@ -151,13 +152,13 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error Rehandle: true, } rewriteRoute := caddyhttp.Route{ - MatcherSetsRaw: []map[string]json.RawMessage{rewriteMatcherSet}, + MatcherSetsRaw: []caddy.ModuleMap{rewriteMatcherSet}, HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, "handler", "rewrite", nil)}, } // route to actually reverse proxy requests to PHP files; // match only requests that are for PHP files - rpMatcherSet := map[string]json.RawMessage{ + rpMatcherSet := caddy.ModuleMap{ "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 // conditional on matching PHP files rpRoute := caddyhttp.Route{ - MatcherSetsRaw: []map[string]json.RawMessage{rpMatcherSet}, + MatcherSetsRaw: []caddy.ModuleMap{rpMatcherSet}, HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rpHandler, "handler", "reverse_proxy", nil)}, } @@ -207,7 +208,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error { Class: "route", Value: caddyhttp.Route{ - MatcherSetsRaw: []map[string]json.RawMessage{userMatcherSet}, + MatcherSetsRaw: []caddy.ModuleMap{userMatcherSet}, HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(subroute, "handler", "subroute", nil)}, }, }, diff --git a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go index 21aeb175..aff9a6ef 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go @@ -73,8 +73,8 @@ type Transport struct { // CaddyModule returns the Caddy module information. func (Transport) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.reverse_proxy.transport.fastcgi", - New: func() caddy.Module { return new(Transport) }, + ID: "http.reverse_proxy.transport.fastcgi", + New: func() caddy.Module { return new(Transport) }, } } diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index 38a904e2..1dd1d142 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -40,6 +40,7 @@ type HTTPTransport struct { // TODO: It's possible that other transports (like fastcgi) might be // able to borrow/use at least some of these config fields; if so, // maybe move them into a type called CommonTransport and embed it? + TLS *TLSConfig `json:"tls,omitempty"` KeepAlive *KeepAlive `json:"keep_alive,omitempty"` Compression *bool `json:"compression,omitempty"` @@ -59,8 +60,8 @@ type HTTPTransport struct { // CaddyModule returns the Caddy module information. func (HTTPTransport) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.reverse_proxy.transport.http", - New: func() caddy.Module { return new(HTTPTransport) }, + ID: "http.reverse_proxy.transport.http", + New: func() caddy.Module { return new(HTTPTransport) }, } } diff --git a/modules/caddyhttp/reverseproxy/ntlm.go b/modules/caddyhttp/reverseproxy/ntlm.go index e2d46b43..ea2bb858 100644 --- a/modules/caddyhttp/reverseproxy/ntlm.go +++ b/modules/caddyhttp/reverseproxy/ntlm.go @@ -30,12 +30,12 @@ func init() { 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 // NTLM's HTTP-hostile requirements. Specifically, it will use // HTTPTransport's single, default *http.Transport for all requests // (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 // maps the client's connection (by its address, req.RemoteAddr) // to a new transport that is used only by that downstream conn. @@ -56,8 +56,8 @@ type NTLMTransport struct { // CaddyModule returns the Caddy module information. func (NTLMTransport) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.reverse_proxy.transport.http_ntlm", - New: func() caddy.Module { return new(NTLMTransport) }, + ID: "http.reverse_proxy.transport.http_ntlm", + New: func() caddy.Module { return new(NTLMTransport) }, } } diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 87895e21..132f2220 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -42,8 +42,8 @@ func init() { // Handler implements a highly configurable and production-ready reverse proxy. type Handler struct { - TransportRaw json.RawMessage `json:"transport,omitempty"` - CBRaw json.RawMessage `json:"circuit_breaker,omitempty"` + TransportRaw json.RawMessage `json:"transport,omitempty" caddy:"namespace=http.reverse_proxy.transport inline_key=protocol"` + CBRaw json.RawMessage `json:"circuit_breaker,omitempty" caddy:"namespace=http.reverse_proxy.circuit_breakers inline_key=type"` LoadBalancing *LoadBalancing `json:"load_balancing,omitempty"` HealthChecks *HealthChecks `json:"health_checks,omitempty"` Upstreams UpstreamPool `json:"upstreams,omitempty"` @@ -60,8 +60,8 @@ type Handler struct { // CaddyModule returns the Caddy module information. func (Handler) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.reverse_proxy", - New: func() caddy.Module { return new(Handler) }, + ID: "http.handlers.reverse_proxy", + New: func() caddy.Module { return new(Handler) }, } } @@ -71,30 +71,25 @@ func (h *Handler) Provision(ctx caddy.Context) error { // start by loading modules 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 { - return fmt.Errorf("loading transport module: %s", err) + return fmt.Errorf("loading transport: %v", err) } - h.Transport = val.(http.RoundTripper) - h.TransportRaw = nil // allow GC to deallocate + h.Transport = mod.(http.RoundTripper) } if h.LoadBalancing != nil && h.LoadBalancing.SelectionPolicyRaw != nil { - val, err := ctx.LoadModuleInline("policy", - "http.handlers.reverse_proxy.selection_policies", - h.LoadBalancing.SelectionPolicyRaw) + mod, err := ctx.LoadModule(h.LoadBalancing, "SelectionPolicyRaw") 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.SelectionPolicyRaw = nil // allow GC to deallocate + h.LoadBalancing.SelectionPolicy = mod.(Selector) } 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 { - return fmt.Errorf("loading circuit breaker module: %s", err) + return fmt.Errorf("loading circuit breaker: %s", err) } - h.CB = val.(CircuitBreaker) - h.CBRaw = nil // allow GC to deallocate + h.CB = mod.(CircuitBreaker) } // set up transport @@ -128,12 +123,14 @@ func (h *Handler) Provision(ctx caddy.Context) error { // defaulting to a sane wait period between attempts 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 { 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 h.HealthChecks != nil && @@ -407,7 +404,7 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, di Dia // do the round-trip start := time.Now() res, err := h.Transport.RoundTrip(req) - latency := time.Since(start) + duration := time.Since(start) if err != nil { return err } @@ -415,12 +412,13 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, di Dia h.logger.Debug("upstream roundtrip", zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: req}), zap.Object("headers", caddyhttp.LoggableHTTPHeader(res.Header)), + zap.Duration("duration", duration), zap.Int("status", res.StatusCode), ) // update circuit breaker on current conditions 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) @@ -434,7 +432,7 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, di Dia // strike if the roundtrip took too long if h.HealthChecks.Passive.UnhealthyLatency > 0 && - latency >= time.Duration(h.HealthChecks.Passive.UnhealthyLatency) { + duration >= time.Duration(h.HealthChecks.Passive.UnhealthyLatency) { h.countFailure(di.Upstream) } } @@ -651,10 +649,10 @@ func removeConnectionHeaders(h http.Header) { // LoadBalancing has parameters related to load balancing. 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"` 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:"-"` RetryMatch caddyhttp.MatcherSets `json:"-"` diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies.go b/modules/caddyhttp/reverseproxy/selectionpolicies.go index a21e44c6..937ae37c 100644 --- a/modules/caddyhttp/reverseproxy/selectionpolicies.go +++ b/modules/caddyhttp/reverseproxy/selectionpolicies.go @@ -48,8 +48,8 @@ type RandomSelection struct{} // CaddyModule returns the Caddy module information. func (RandomSelection) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.reverse_proxy.selection_policies.random", - New: func() caddy.Module { return new(RandomSelection) }, + ID: "http.reverse_proxy.selection_policies.random", + New: func() caddy.Module { return new(RandomSelection) }, } } @@ -84,8 +84,8 @@ type RandomChoiceSelection struct { // CaddyModule returns the Caddy module information. func (RandomChoiceSelection) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.reverse_proxy.selection_policies.random_choose", - New: func() caddy.Module { return new(RandomChoiceSelection) }, + ID: "http.reverse_proxy.selection_policies.random_choose", + New: func() caddy.Module { return new(RandomChoiceSelection) }, } } @@ -154,8 +154,8 @@ type LeastConnSelection struct{} // CaddyModule returns the Caddy module information. func (LeastConnSelection) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.reverse_proxy.selection_policies.least_conn", - New: func() caddy.Module { return new(LeastConnSelection) }, + ID: "http.reverse_proxy.selection_policies.least_conn", + New: func() caddy.Module { return new(LeastConnSelection) }, } } @@ -199,8 +199,8 @@ type RoundRobinSelection struct { // CaddyModule returns the Caddy module information. func (RoundRobinSelection) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.reverse_proxy.selection_policies.round_robin", - New: func() caddy.Module { return new(RoundRobinSelection) }, + ID: "http.reverse_proxy.selection_policies.round_robin", + New: func() caddy.Module { return new(RoundRobinSelection) }, } } @@ -227,8 +227,8 @@ type FirstSelection struct{} // CaddyModule returns the Caddy module information. func (FirstSelection) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.reverse_proxy.selection_policies.first", - New: func() caddy.Module { return new(FirstSelection) }, + ID: "http.reverse_proxy.selection_policies.first", + New: func() caddy.Module { return new(FirstSelection) }, } } @@ -249,8 +249,8 @@ type IPHashSelection struct{} // CaddyModule returns the Caddy module information. func (IPHashSelection) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.reverse_proxy.selection_policies.ip_hash", - New: func() caddy.Module { return new(IPHashSelection) }, + ID: "http.reverse_proxy.selection_policies.ip_hash", + New: func() caddy.Module { return new(IPHashSelection) }, } } @@ -270,8 +270,8 @@ type URIHashSelection struct{} // CaddyModule returns the Caddy module information. func (URIHashSelection) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.reverse_proxy.selection_policies.uri_hash", - New: func() caddy.Module { return new(URIHashSelection) }, + ID: "http.reverse_proxy.selection_policies.uri_hash", + New: func() caddy.Module { return new(URIHashSelection) }, } } @@ -289,8 +289,8 @@ type HeaderHashSelection struct { // CaddyModule returns the Caddy module information. func (HeaderHashSelection) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.reverse_proxy.selection_policies.header", - New: func() caddy.Module { return new(HeaderHashSelection) }, + ID: "http.reverse_proxy.selection_policies.header", + New: func() caddy.Module { return new(HeaderHashSelection) }, } } diff --git a/modules/caddyhttp/rewrite/rewrite.go b/modules/caddyhttp/rewrite/rewrite.go index 5a84a331..f6106582 100644 --- a/modules/caddyhttp/rewrite/rewrite.go +++ b/modules/caddyhttp/rewrite/rewrite.go @@ -47,8 +47,8 @@ type Rewrite struct { // CaddyModule returns the Caddy module information. func (Rewrite) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.rewrite", - New: func() caddy.Module { return new(Rewrite) }, + ID: "http.handlers.rewrite", + New: func() caddy.Module { return new(Rewrite) }, } } diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go index 550b14e5..0dce990d 100644 --- a/modules/caddyhttp/routes.go +++ b/modules/caddyhttp/routes.go @@ -22,14 +22,73 @@ import ( "github.com/caddyserver/caddy/v2" ) -// Route represents a set of matching rules, -// middlewares, and a responder for handling HTTP -// requests. +// Route consists of a set of rules for matching HTTP requests, +// a list of handlers to execute, and optional flow control +// parameters which customize the handling of HTTP requests +// in a highly flexible and performant manner. type Route struct { - Group string `json:"group,omitempty"` - MatcherSetsRaw RawMatcherSets `json:"match,omitempty"` - HandlersRaw []json.RawMessage `json:"handle,omitempty"` - Terminal bool `json:"terminal,omitempty"` + // 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"` + + // 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"` // decoded values MatcherSets MatcherSets `json:"-"` @@ -54,22 +113,23 @@ type RouteList []Route func (routes RouteList) Provision(ctx caddy.Context) error { for i, route := range routes { // matchers - matcherSets, err := route.MatcherSetsRaw.Setup(ctx) + matchersIface, err := ctx.LoadModule(&route, "MatcherSetsRaw") 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 - for j, rawMsg := range route.HandlersRaw { - mh, err := ctx.LoadModuleInline("handler", "http.handlers", rawMsg) - if err != nil { - return fmt.Errorf("loading handler module in position %d: %v", j, err) - } - routes[i].Handlers = append(routes[i].Handlers, mh.(MiddlewareHandler)) + handlersIface, err := ctx.LoadModule(&route, "HandlersRaw") + if err != nil { + return fmt.Errorf("loading handler modules in route %d: %v", i, err) + } + for _, handler := range handlersIface.([]interface{}) { + routes[i].Handlers = append(routes[i].Handlers, handler.(MiddlewareHandler)) } - routes[i].HandlersRaw = nil // allow GC to deallocate } return nil } @@ -171,28 +231,7 @@ func (mset MatcherSet) Match(r *http.Request) bool { // RawMatcherSets is a group of matcher sets // in their raw, JSON form. -type RawMatcherSets []map[string]json.RawMessage - -// 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 -} +type RawMatcherSets []caddy.ModuleMap // MatcherSets is a group of matcher sets capable // of checking whether a request matches any of @@ -202,11 +241,27 @@ type MatcherSets []MatcherSet // AnyMatch returns true if req matches any of the // matcher sets in mss or if there are no matchers, // in which case the request always matches. -func (mss MatcherSets) AnyMatch(req *http.Request) bool { - for _, ms := range mss { - if ms.Match(req) { +func (ms MatcherSets) AnyMatch(req *http.Request) bool { + for _, m := range ms { + if m.Match(req) { 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 } diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index fef887d8..c34444ea 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -31,23 +31,101 @@ import ( "go.uber.org/zap/zapcore" ) -// Server is an HTTP server. +// Server describes an HTTP server. type Server struct { - Listen []string `json:"listen,omitempty"` - ReadTimeout caddy.Duration `json:"read_timeout,omitempty"` - ReadHeaderTimeout caddy.Duration `json:"read_header_timeout,omitempty"` - WriteTimeout caddy.Duration `json:"write_timeout,omitempty"` - IdleTimeout caddy.Duration `json:"idle_timeout,omitempty"` - MaxHeaderBytes int `json:"max_header_bytes,omitempty"` - Routes RouteList `json:"routes,omitempty"` - Errors *HTTPErrorConfig `json:"errors,omitempty"` - TLSConnPolicies caddytls.ConnectionPolicies `json:"tls_connection_policies,omitempty"` - AutoHTTPS *AutoHTTPSConfig `json:"automatic_https,omitempty"` - MaxRehandles *int `json:"max_rehandles,omitempty"` - StrictSNIHost *bool `json:"strict_sni_host,omitempty"` - Logs *ServerLogConfig `json:"logs,omitempty"` + // 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"` - // This field is not subject to compatibility promises + // 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"` + + // ReadHeaderTimeout is like ReadTimeout but for request headers. + 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"` + + // 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"` + + // MaxHeaderBytes is the maximum size to parse from a client's + // HTTP request headers. + 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"` + + // Errors is how this server will handle errors returned from + // any of the handlers in the primary routes. + Errors *HTTPErrorConfig `json:"errors,omitempty"` + + // How to handle TLS connections. + 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"` + + // 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"` + + // 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"` + + // Logs customizes how access logs are handled in this server. + Logs *ServerLogConfig `json:"logs,omitempty"` + + // 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"` tlsApp *caddytls.TLS @@ -296,6 +374,8 @@ func (s *Server) hasTLSClientAuth() bool { // AutoHTTPSConfig is used to disable automatic HTTPS // 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 { // If true, automatic HTTPS will be entirely disabled. Disabled bool `json:"disable,omitempty"` diff --git a/modules/caddyhttp/starlarkmw/internal/lib/module.go b/modules/caddyhttp/starlarkmw/internal/lib/module.go index a7164cd7..a75aedf1 100644 --- a/modules/caddyhttp/starlarkmw/internal/lib/module.go +++ b/modules/caddyhttp/starlarkmw/internal/lib/module.go @@ -64,7 +64,7 @@ func (r *LoadMiddleware) Run(thread *starlark.Thread, fn *starlark.Builtin, args name = fmt.Sprintf("http.handlers.%s", name) } - inst, err := r.Ctx.LoadModule(name, js) + inst, err := r.Ctx.LoadModuleByID(name, js) if err != nil { 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) } - inst, err := r.Ctx.LoadModule(name, js) + inst, err := r.Ctx.LoadModuleByID(name, js) if err != nil { return starlark.None, err } diff --git a/modules/caddyhttp/starlarkmw/starlarkmw.go b/modules/caddyhttp/starlarkmw/starlarkmw.go index 007ddb4e..47e335db 100644 --- a/modules/caddyhttp/starlarkmw/starlarkmw.go +++ b/modules/caddyhttp/starlarkmw/starlarkmw.go @@ -7,8 +7,8 @@ import ( "github.com/caddyserver/caddy/v2" "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" + caddyscript "github.com/caddyserver/caddy/v2/pkg/caddyscript/lib" "github.com/starlight-go/starlight/convert" "go.starlark.net/starlark" ) @@ -34,8 +34,8 @@ type StarlarkMW struct { // CaddyModule returns the Caddy module information. func (StarlarkMW) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.starlark", - New: func() caddy.Module { return new(StarlarkMW) }, + ID: "http.handlers.starlark", + New: func() caddy.Module { return new(StarlarkMW) }, } } diff --git a/modules/caddyhttp/staticerror.go b/modules/caddyhttp/staticerror.go index 3a45366c..fd1490d2 100644 --- a/modules/caddyhttp/staticerror.go +++ b/modules/caddyhttp/staticerror.go @@ -35,8 +35,8 @@ type StaticError struct { // CaddyModule returns the Caddy module information. func (StaticError) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.error", - New: func() caddy.Module { return new(StaticError) }, + ID: "http.handlers.error", + New: func() caddy.Module { return new(StaticError) }, } } diff --git a/modules/caddyhttp/staticresp.go b/modules/caddyhttp/staticresp.go index 732a3fb4..44b045ee 100644 --- a/modules/caddyhttp/staticresp.go +++ b/modules/caddyhttp/staticresp.go @@ -38,8 +38,8 @@ type StaticResponse struct { // CaddyModule returns the Caddy module information. func (StaticResponse) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.static_response", - New: func() caddy.Module { return new(StaticResponse) }, + ID: "http.handlers.static_response", + New: func() caddy.Module { return new(StaticResponse) }, } } diff --git a/modules/caddyhttp/subroute.go b/modules/caddyhttp/subroute.go index 57fb80af..a60eaf7d 100644 --- a/modules/caddyhttp/subroute.go +++ b/modules/caddyhttp/subroute.go @@ -44,8 +44,8 @@ type Subroute struct { // CaddyModule returns the Caddy module information. func (Subroute) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.subroute", - New: func() caddy.Module { return new(Subroute) }, + ID: "http.handlers.subroute", + New: func() caddy.Module { return new(Subroute) }, } } diff --git a/modules/caddyhttp/templates/templates.go b/modules/caddyhttp/templates/templates.go index e9c1da81..ac37e9d2 100644 --- a/modules/caddyhttp/templates/templates.go +++ b/modules/caddyhttp/templates/templates.go @@ -39,8 +39,8 @@ type Templates struct { // CaddyModule returns the Caddy module information. func (Templates) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.templates", - New: func() caddy.Module { return new(Templates) }, + ID: "http.handlers.templates", + New: func() caddy.Module { return new(Templates) }, } } diff --git a/modules/caddyhttp/vars.go b/modules/caddyhttp/vars.go index 3fb8fa36..791203be 100644 --- a/modules/caddyhttp/vars.go +++ b/modules/caddyhttp/vars.go @@ -32,8 +32,8 @@ type VarsMiddleware map[string]string // CaddyModule returns the Caddy module information. func (VarsMiddleware) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.handlers.vars", - New: func() caddy.Module { return new(VarsMiddleware) }, + ID: "http.handlers.vars", + New: func() caddy.Module { return new(VarsMiddleware) }, } } @@ -55,8 +55,8 @@ type VarsMatcher map[string]string // CaddyModule returns the Caddy module information. func (VarsMatcher) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "http.matchers.vars", - New: func() caddy.Module { return new(VarsMatcher) }, + ID: "http.matchers.vars", + New: func() caddy.Module { return new(VarsMatcher) }, } } diff --git a/modules/caddytls/acmemanager.go b/modules/caddytls/acmemanager.go index 9f312150..31c954ff 100644 --- a/modules/caddytls/acmemanager.go +++ b/modules/caddytls/acmemanager.go @@ -40,16 +40,50 @@ func init() { // after you have configured this struct // to your liking. type ACMEManagerMaker struct { - CA string `json:"ca,omitempty"` - Email string `json:"email,omitempty"` - RenewAhead caddy.Duration `json:"renew_ahead,omitempty"` - KeyType string `json:"key_type,omitempty"` - ACMETimeout caddy.Duration `json:"acme_timeout,omitempty"` - MustStaple bool `json:"must_staple,omitempty"` - Challenges *ChallengesConfig `json:"challenges,omitempty"` - OnDemand bool `json:"on_demand,omitempty"` - Storage json.RawMessage `json:"storage,omitempty"` - TrustedRootsPEMFiles []string `json:"trusted_roots_pem_files,omitempty"` + // The URL to the CA's ACME directory endpoint. + 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"` + + // 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"` + + // The type of key to generate for the certificate. + // Supported values: `rsa2048`, `rsa4096`, `p256`, `p384`. + KeyType string `json:"key_type,omitempty"` + + // Time to wait before timing out an ACME operation. + 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"` + + // Configures the various ACME challenge types. + 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"` + + // 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"` + + // 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"` storage certmagic.Storage rootPool *x509.CertPool @@ -58,8 +92,8 @@ type ACMEManagerMaker struct { // CaddyModule returns the Caddy module information. func (ACMEManagerMaker) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "tls.management.acme", - New: func() caddy.Module { return new(ACMEManagerMaker) }, + ID: "tls.management.acme", + 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 { // DNS providers 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 { - 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.DNSRaw = nil // allow GC to deallocate } // policy-specific storage implementation if m.Storage != nil { - val, err := ctx.LoadModuleInline("module", "caddy.storage", m.Storage) + val, err := ctx.LoadModule(m, "Storage") 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() if err != nil { return fmt.Errorf("creating TLS storage configuration: %v", err) } m.storage = cmStorage - m.Storage = nil // allow GC to deallocate } // add any custom CAs to trust store diff --git a/modules/caddytls/certselection.go b/modules/caddytls/certselection.go index b56185ac..eb01605c 100644 --- a/modules/caddytls/certselection.go +++ b/modules/caddytls/certselection.go @@ -28,8 +28,8 @@ type Policy struct { // CaddyModule returns the Caddy module information. func (Policy) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "tls.certificate_selection.custom", - New: func() caddy.Module { return new(Policy) }, + ID: "tls.certificate_selection.custom", + New: func() caddy.Module { return new(Policy) }, } } diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index c82337d7..6ce6b9e6 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -39,23 +39,21 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) (*tls.Config, error) { // set up each of the connection policies for i, pol := range cp { // matchers - for modName, rawMsg := range pol.Matchers { - val, err := ctx.LoadModule("tls.handshake_match."+modName, rawMsg) - if err != nil { - return nil, fmt.Errorf("loading handshake matcher module '%s': %s", modName, err) - } - cp[i].matchers = append(cp[i].matchers, val.(ConnectionMatcher)) + mods, err := ctx.LoadModule(pol, "MatchersRaw") + if err != nil { + return nil, fmt.Errorf("loading handshake matchers: %v", err) + } + 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 if pol.CertSelection != nil { - val, err := ctx.LoadModuleInline("policy", "tls.certificate_selection", pol.CertSelection) + val, err := ctx.LoadModule(pol, "CertSelection") if err != nil { return nil, fmt.Errorf("loading certificate selection module: %s", err) } 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. type ConnectionPolicy struct { - Matchers map[string]json.RawMessage `json:"match,omitempty"` - CertSelection json.RawMessage `json:"certificate_selection,omitempty"` + // How to match this policy with a TLS ClientHello. If + // this policy is the first to match, it will be used. + MatchersRaw caddy.ModuleMap `json:"match,omitempty" caddy:"namespace=tls.handshake_match"` - CipherSuites []string `json:"cipher_suites,omitempty"` - Curves []string `json:"curves,omitempty"` - ALPN []string `json:"alpn,omitempty"` - ProtocolMin string `json:"protocol_min,omitempty"` - ProtocolMax string `json:"protocol_max,omitempty"` + // 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"` + + // The list of elliptic curves to support. Caddy's + // defaults are modern and secure. + Curves []string `json:"curves,omitempty"` + + // Protocols to use for Application-Layer Protocol + // Negotiation (ALPN) during the handshake. + ALPN []string `json:"alpn,omitempty"` + + // Minimum TLS protocol version to allow. Default: `tls1.2` + ProtocolMin string `json:"protocol_min,omitempty"` + + // Maximum TLS protocol version to allow. Default: `tls1.3` + ProtocolMax string `json:"protocol_max,omitempty"` + + // Enables and configures TLS client authentication. ClientAuthentication *ClientAuthentication `json:"client_authentication,omitempty"` matchers []ConnectionMatcher diff --git a/modules/caddytls/distributedstek/distributedstek.go b/modules/caddytls/distributedstek/distributedstek.go index a0c4cd2b..cef3733f 100644 --- a/modules/caddytls/distributedstek/distributedstek.go +++ b/modules/caddytls/distributedstek/distributedstek.go @@ -39,9 +39,15 @@ func init() { 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 { - 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 stekConfig *caddytls.SessionTicketService @@ -51,8 +57,8 @@ type Provider struct { // CaddyModule returns the Caddy module information. func (Provider) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "tls.stek.distributed", - New: func() caddy.Module { return new(Provider) }, + ID: "tls.stek.distributed", + New: func() caddy.Module { return new(Provider) }, } } @@ -60,7 +66,7 @@ func (Provider) CaddyModule() caddy.ModuleInfo { func (s *Provider) Provision(ctx caddy.Context) error { // unpack the storage module to use, if different from the default if s.Storage != nil { - val, err := ctx.LoadModuleInline("module", "caddy.storage", s.Storage) + val, err := ctx.LoadModule(s, "Storage") if err != nil { 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) } s.storage = cmStorage - s.Storage = nil // allow GC to deallocate } // otherwise, use default storage diff --git a/modules/caddytls/fileloader.go b/modules/caddytls/fileloader.go index b2cc1325..6d6ff99c 100644 --- a/modules/caddytls/fileloader.go +++ b/modules/caddytls/fileloader.go @@ -32,18 +32,27 @@ type FileLoader []CertKeyFilePair // CaddyModule returns the Caddy module information. func (FileLoader) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "tls.certificates.load_files", - New: func() caddy.Module { return new(FileLoader) }, + ID: "tls.certificates.load_files", + New: func() caddy.Module { return new(FileLoader) }, } } // CertKeyFilePair pairs certificate and key file names along with their // encoding format so that they can be loaded from disk. type CertKeyFilePair struct { - Certificate string `json:"certificate"` - Key string `json:"key"` - Format string `json:"format,omitempty"` // "pem" is default - Tags []string `json:"tags,omitempty"` + // Path to the certificate (public key) file. + Certificate string `json:"certificate"` + + // Path to the private key file. + Key string `json:"key"` + + // 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"` } // LoadCertificates returns the certificates to be loaded by fl. diff --git a/modules/caddytls/folderloader.go b/modules/caddytls/folderloader.go index da1dff09..f1a742df 100644 --- a/modules/caddytls/folderloader.go +++ b/modules/caddytls/folderloader.go @@ -39,8 +39,8 @@ type FolderLoader []string // CaddyModule returns the Caddy module information. func (FolderLoader) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "tls.certificates.load_folders", - New: func() caddy.Module { return new(FolderLoader) }, + ID: "tls.certificates.load_folders", + New: func() caddy.Module { return new(FolderLoader) }, } } diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go index 47fb2964..9e2dfc5a 100644 --- a/modules/caddytls/matchers.go +++ b/modules/caddytls/matchers.go @@ -30,8 +30,8 @@ type MatchServerName []string // CaddyModule returns the Caddy module information. func (MatchServerName) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "tls.handshake_match.sni", - New: func() caddy.Module { return new(MatchServerName) }, + ID: "tls.handshake_match.sni", + New: func() caddy.Module { return new(MatchServerName) }, } } diff --git a/modules/caddytls/pemloader.go b/modules/caddytls/pemloader.go index 30a491cd..46d06a87 100644 --- a/modules/caddytls/pemloader.go +++ b/modules/caddytls/pemloader.go @@ -33,16 +33,23 @@ type PEMLoader []CertKeyPEMPair // CaddyModule returns the Caddy module information. func (PEMLoader) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "tls.certificates.load_pem", - New: func() caddy.Module { return PEMLoader{} }, + ID: "tls.certificates.load_pem", + New: func() caddy.Module { return PEMLoader{} }, } } // CertKeyPEMPair pairs certificate and key PEM blocks. type CertKeyPEMPair struct { - CertificatePEM string `json:"certificate"` - KeyPEM string `json:"key"` - Tags []string `json:"tags,omitempty"` + // The certificate (public key) in PEM format. + CertificatePEM string `json:"certificate"` + + // The private key in PEM format. + 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"` } // LoadCertificates returns the certificates contained in pl. diff --git a/modules/caddytls/sessiontickets.go b/modules/caddytls/sessiontickets.go index 6ca921d9..258c1357 100644 --- a/modules/caddytls/sessiontickets.go +++ b/modules/caddytls/sessiontickets.go @@ -28,11 +28,22 @@ import ( // SessionTicketService configures and manages TLS session tickets. type SessionTicketService struct { - KeySource json.RawMessage `json:"key_source,omitempty"` - RotationInterval caddy.Duration `json:"rotation_interval,omitempty"` - MaxKeys int `json:"max_keys,omitempty"` - DisableRotation bool `json:"disable_rotation,omitempty"` - Disabled bool `json:"disabled,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"` + + // The maximum number of keys to keep in rotation. Default: 4. + MaxKeys int `json:"max_keys,omitempty"` + + // Disables STEK rotation. + DisableRotation bool `json:"disable_rotation,omitempty"` + + // Disables TLS session resumption by tickets. + Disabled bool `json:"disabled,omitempty"` keySource STEKProvider configs map[*tls.Config]struct{} @@ -57,12 +68,11 @@ func (s *SessionTicketService) provision(ctx caddy.Context) error { } // 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 { return fmt.Errorf("loading TLS session ticket ephemeral keys provider module: %s", err) } s.keySource = val.(STEKProvider) - s.KeySource = nil // allow GC to deallocate // if session tickets or just rotation are // disabled, no need to start service diff --git a/modules/caddytls/standardstek/stek.go b/modules/caddytls/standardstek/stek.go index 6d10c76c..eb609ca9 100644 --- a/modules/caddytls/standardstek/stek.go +++ b/modules/caddytls/standardstek/stek.go @@ -35,8 +35,8 @@ type standardSTEKProvider struct { // CaddyModule returns the Caddy module information. func (standardSTEKProvider) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "tls.stek.standard", - New: func() caddy.Module { return new(standardSTEKProvider) }, + ID: "tls.stek.standard", + New: func() caddy.Module { return new(standardSTEKProvider) }, } } diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 5dfe0639..1b155b0e 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -30,15 +30,30 @@ import ( func init() { 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 { - Certificates map[string]json.RawMessage `json:"certificates,omitempty"` - Automation *AutomationConfig `json:"automation,omitempty"` - SessionTickets *SessionTicketService `json:"session_tickets,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"` + + // Configures session ticket ephemeral keys (STEKs). + SessionTickets *SessionTicketService `json:"session_tickets,omitempty"` certificateLoaders []CertificateLoader + automateNames []string certCache *certmagic.Cache ctx caddy.Context storageCleanTicker *time.Ticker @@ -49,8 +64,8 @@ type TLS struct { // CaddyModule returns the Caddy module information. func (TLS) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "tls", - New: func() caddy.Module { return new(TLS) }, + ID: "tls", + New: func() caddy.Module { return new(TLS) }, } } @@ -74,25 +89,32 @@ func (t *TLS) Provision(ctx caddy.Context) error { // automation/management policies if t.Automation != nil { 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 { return fmt.Errorf("loading TLS automation management module: %s", err) } t.Automation.Policies[i].Management = val.(ManagerMaker) - t.Automation.Policies[i].ManagementRaw = nil // allow GC to deallocate } } // certificate loaders - for modName, rawMsg := range t.Certificates { - if modName == automateKey { - continue // special case; these will be loaded in later + val, err := ctx.LoadModule(t, "CertificatesRaw") + if err != nil { + return fmt.Errorf("loading TLS automation management module: %s", err) + } + 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 } - val, err := ctx.LoadModule("tls.certificates."+modName, rawMsg) - if err != nil { - return fmt.Errorf("loading certificate module '%s': %s", modName, err) - } - t.certificateLoaders = append(t.certificateLoaders, val.(CertificateLoader)) + t.certificateLoaders = append(t.certificateLoaders, modIface.(CertificateLoader)) } // 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 // 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{ Storage: ctx.Storage(), }) @@ -137,19 +160,12 @@ func (t *TLS) Provision(ctx caddy.Context) error { // Start activates the TLS module. func (t *TLS) Start() error { - // load automated (managed) certificates - if automatedRawMsg, ok := t.Certificates[automateKey]; ok { - var names []string - err := json.Unmarshal(automatedRawMsg, &names) - if err != nil { - return fmt.Errorf("automate: decoding names: %v", err) - } - err = t.Manage(names) - if err != nil { - return fmt.Errorf("automate: managing %v: %v", names, err) - } + // now that we are running, and all manual certificates have + // been loaded, time to load the automated/managed certificates + err := t.Manage(t.automateNames) + if err != nil { + return fmt.Errorf("automate: managing %v: %v", t.automateNames, err) } - t.Certificates = nil // allow GC to deallocate t.keepStorageClean() @@ -311,18 +327,48 @@ type Certificate struct { // AutomationConfig designates configuration for the // construction and use of ACME clients. type AutomationConfig struct { - Policies []AutomationPolicy `json:"policies,omitempty"` - OnDemand *OnDemandConfig `json:"on_demand,omitempty"` - OCSPCheckInterval caddy.Duration `json:"ocsp_interval,omitempty"` - RenewCheckInterval caddy.Duration `json:"renew_interval,omitempty"` + // The list of automation policies. The first matching + // policy will be applied for a given certificate/name. + 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"` + + // 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"` + + // 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"` } // AutomationPolicy designates the policy for automating the -// management of managed TLS certificates. +// management (obtaining, renewal, and revocation) of managed +// TLS certificates. type AutomationPolicy struct { - Hosts []string `json:"hosts,omitempty"` - ManagementRaw json.RawMessage `json:"management,omitempty"` - ManageSync bool `json:"manage_sync,omitempty"` + // Which hostnames this policy applies to. + Hosts []string `json:"hosts,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"` Management ManagerMaker `json:"-"` } @@ -345,36 +391,84 @@ func (ap AutomationPolicy) makeCertMagicConfig(ctx caddy.Context) certmagic.Conf // ChallengesConfig configures the ACME challenges. type ChallengesConfig struct { - HTTP *HTTPChallengeConfig `json:"http,omitempty"` + // HTTP configures the ACME HTTP challenge. This + // challenge is enabled and used automatically + // and by default. + 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"` - 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:"-"` } // HTTPChallengeConfig configures the ACME HTTP challenge. type HTTPChallengeConfig struct { - Disabled bool `json:"disabled,omitempty"` - AlternatePort int `json:"alternate_port,omitempty"` + // If true, the HTTP challenge will be disabled. + 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"` } // TLSALPNChallengeConfig configures the ACME TLS-ALPN challenge. type TLSALPNChallengeConfig struct { - Disabled bool `json:"disabled,omitempty"` - AlternatePort int `json:"alternate_port,omitempty"` + // If true, the TLS-ALPN challenge will be disabled. + 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"` } // 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 { + // An optional rate limit to throttle the + // issuance of certificates from handshakes. RateLimit *RateLimit `json:"rate_limit,omitempty"` - Ask string `json:"ask,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"` } // RateLimit specifies an interval with optional burst size. type RateLimit struct { + // A duration value. A certificate may be obtained 'burst' + // times during this interval. Interval caddy.Duration `json:"interval,omitempty"` - Burst int `json:"burst,omitempty"` + + // How many times during an interval a certificate can be obtained. + Burst int `json:"burst,omitempty"` } // ManagerMaker makes a certificate manager. @@ -382,6 +476,21 @@ type ManagerMaker interface { 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. var ( onDemandRateLimiter = certmagic.NewRateLimiter(0, 0) diff --git a/modules/filestorage/filestorage.go b/modules/filestorage/filestorage.go index 6217a071..55607baf 100644 --- a/modules/filestorage/filestorage.go +++ b/modules/filestorage/filestorage.go @@ -32,8 +32,8 @@ type FileStorage struct { // CaddyModule returns the Caddy module information. func (FileStorage) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "caddy.storage.file_system", - New: func() caddy.Module { return new(FileStorage) }, + ID: "caddy.storage.file_system", + New: func() caddy.Module { return new(FileStorage) }, } } diff --git a/modules/logging/encoders.go b/modules/logging/encoders.go index c3c8aba9..28cda559 100644 --- a/modules/logging/encoders.go +++ b/modules/logging/encoders.go @@ -43,8 +43,8 @@ type ConsoleEncoder struct { // CaddyModule returns the Caddy module information. func (ConsoleEncoder) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "caddy.logging.encoders.console", - New: func() caddy.Module { return new(ConsoleEncoder) }, + ID: "caddy.logging.encoders.console", + New: func() caddy.Module { return new(ConsoleEncoder) }, } } @@ -63,8 +63,8 @@ type JSONEncoder struct { // CaddyModule returns the Caddy module information. func (JSONEncoder) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "caddy.logging.encoders.json", - New: func() caddy.Module { return new(JSONEncoder) }, + ID: "caddy.logging.encoders.json", + New: func() caddy.Module { return new(JSONEncoder) }, } } @@ -84,8 +84,8 @@ type LogfmtEncoder struct { // CaddyModule returns the Caddy module information. func (LogfmtEncoder) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "caddy.logging.encoders.logfmt", - New: func() caddy.Module { return new(LogfmtEncoder) }, + ID: "caddy.logging.encoders.logfmt", + New: func() caddy.Module { return new(LogfmtEncoder) }, } } @@ -102,25 +102,24 @@ func (lfe *LogfmtEncoder) Provision(_ caddy.Context) error { type StringEncoder struct { zapcore.Encoder 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. func (StringEncoder) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "caddy.logging.encoders.string", - New: func() caddy.Module { return new(StringEncoder) }, + ID: "caddy.logging.encoders.string", + New: func() caddy.Module { return new(StringEncoder) }, } } // Provision sets up the encoder. func (se *StringEncoder) Provision(ctx caddy.Context) error { if se.FallbackRaw != nil { - val, err := ctx.LoadModuleInline("format", "caddy.logging.encoders", se.FallbackRaw) + val, err := ctx.LoadModule(se, "FallbackRaw") if err != nil { return fmt.Errorf("loading fallback encoder module: %v", err) } - se.FallbackRaw = nil // allow GC to deallocate se.Encoder = val.(zapcore.Encoder) } if se.Encoder == nil { diff --git a/modules/logging/filewriter.go b/modules/logging/filewriter.go index 6957b6a5..cc60c649 100644 --- a/modules/logging/filewriter.go +++ b/modules/logging/filewriter.go @@ -28,22 +28,42 @@ func init() { 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 { - Filename string `json:"filename,omitempty"` - Roll *bool `json:"roll,omitempty"` - RollSizeMB int `json:"roll_size_mb,omitempty"` - RollCompress *bool `json:"roll_gzip,omitempty"` - RollLocalTime bool `json:"roll_local_time,omitempty"` - RollKeep int `json:"roll_keep,omitempty"` - RollKeepDays int `json:"roll_keep_days,omitempty"` + // Filename is the name of the file to write. + Filename string `json:"filename,omitempty"` + + // Roll toggles log rolling or rotation, which is + // enabled by default. + Roll *bool `json:"roll,omitempty"` + + // When a log file reaches approximately this size, + // it will be rotated. + RollSizeMB int `json:"roll_size_mb,omitempty"` + + // Whether to compress rolled files. Default: true + RollCompress *bool `json:"roll_gzip,omitempty"` + + // Whether to use local timestamps in rolled filenames. + // Default: false + RollLocalTime bool `json:"roll_local_time,omitempty"` + + // The maximum number of rolled log files to keep. + // Default: 10 + RollKeep int `json:"roll_keep,omitempty"` + + // How many days to keep rolled log files. Default: 90 + RollKeepDays int `json:"roll_keep_days,omitempty"` } // CaddyModule returns the Caddy module information. func (FileWriter) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "caddy.logging.writers.file", - New: func() caddy.Module { return new(FileWriter) }, + ID: "caddy.logging.writers.file", + New: func() caddy.Module { return new(FileWriter) }, } } diff --git a/modules/logging/filterencoder.go b/modules/logging/filterencoder.go index eff0279d..6680019e 100644 --- a/modules/logging/filterencoder.go +++ b/modules/logging/filterencoder.go @@ -29,13 +29,17 @@ func init() { caddy.RegisterModule(FilterEncoder{}) } -// FilterEncoder wraps an underlying encoder. It does -// not do any encoding itself, but it can manipulate -// (filter) fields before they are actually encoded. -// A wrapped encoder is required. +// FilterEncoder can filter (manipulate) fields on +// log entries before they are actually encoded by +// an underlying encoder. type FilterEncoder struct { - WrappedRaw json.RawMessage `json:"wrap,omitempty"` - FieldsRaw map[string]json.RawMessage `json:"fields,omitempty"` + // The underlying encoder that actually + // 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 Fields map[string]LogFieldFilter `json:"-"` @@ -47,8 +51,8 @@ type FilterEncoder struct { // CaddyModule returns the Caddy module information. func (FilterEncoder) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "caddy.logging.encoders.filter", - New: func() caddy.Module { return new(FilterEncoder) }, + ID: "caddy.logging.encoders.filter", + New: func() caddy.Module { return new(FilterEncoder) }, } } @@ -59,28 +63,23 @@ func (fe *FilterEncoder) Provision(ctx caddy.Context) error { } // set up wrapped encoder (required) - val, err := ctx.LoadModuleInline("format", "caddy.logging.encoders", fe.WrappedRaw) + val, err := ctx.LoadModule(fe, "WrappedRaw") if err != nil { return fmt.Errorf("loading fallback encoder module: %v", err) } - fe.WrappedRaw = nil // allow GC to deallocate fe.wrapped = val.(zapcore.Encoder) // set up each field filter if fe.Fields == nil { fe.Fields = make(map[string]LogFieldFilter) } - for field, filterRaw := range fe.FieldsRaw { - if filterRaw == nil { - continue - } - val, err := ctx.LoadModuleInline("filter", "caddy.logging.encoders.filter", filterRaw) - if err != nil { - return fmt.Errorf("loading log filter module: %v", err) - } - fe.Fields[field] = val.(LogFieldFilter) + vals, err := ctx.LoadModule(fe, "FieldsRaw") + if err != nil { + return fmt.Errorf("loading log filter modules: %v", err) + } + for fieldName, modIface := range vals.(map[string]interface{}) { + fe.Fields[fieldName] = modIface.(LogFieldFilter) } - fe.FieldsRaw = nil // allow GC to deallocate return nil } diff --git a/modules/logging/filters.go b/modules/logging/filters.go index b44e0842..52db2fec 100644 --- a/modules/logging/filters.go +++ b/modules/logging/filters.go @@ -41,8 +41,8 @@ type DeleteFilter struct{} // CaddyModule returns the Caddy module information. func (DeleteFilter) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "caddy.logging.encoders.filter.delete", - New: func() caddy.Module { return new(DeleteFilter) }, + ID: "caddy.logging.encoders.filter.delete", + New: func() caddy.Module { return new(DeleteFilter) }, } } @@ -55,15 +55,18 @@ func (DeleteFilter) Filter(in zapcore.Field) zapcore.Field { // IPMaskFilter is a Caddy log field filter that // masks IP addresses. type IPMaskFilter struct { + // The IPv4 range in CIDR notation. IPv4CIDR int `json:"ipv4_cidr,omitempty"` + + // The IPv6 range in CIDR notation. IPv6CIDR int `json:"ipv6_cidr,omitempty"` } // CaddyModule returns the Caddy module information. func (IPMaskFilter) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - Name: "caddy.logging.encoders.filter.ip_mask", - New: func() caddy.Module { return new(IPMaskFilter) }, + ID: "caddy.logging.encoders.filter.ip_mask", + New: func() caddy.Module { return new(IPMaskFilter) }, } } diff --git a/modules_test.go b/modules_test.go index ef7edf77..c561c6f4 100644 --- a/modules_test.go +++ b/modules_test.go @@ -22,17 +22,17 @@ import ( func TestGetModules(t *testing.T) { modulesMu.Lock() modules = map[string]ModuleInfo{ - "a": {Name: "a"}, - "a.b": {Name: "a.b"}, - "a.b.c": {Name: "a.b.c"}, - "a.b.cd": {Name: "a.b.cd"}, - "a.c": {Name: "a.c"}, - "a.d": {Name: "a.d"}, - "b": {Name: "b"}, - "b.a": {Name: "b.a"}, - "b.b": {Name: "b.b"}, - "b.a.c": {Name: "b.a.c"}, - "c": {Name: "c"}, + "a": {ID: "a"}, + "a.b": {ID: "a.b"}, + "a.b.c": {ID: "a.b.c"}, + "a.b.cd": {ID: "a.b.cd"}, + "a.c": {ID: "a.c"}, + "a.d": {ID: "a.d"}, + "b": {ID: "b"}, + "b.a": {ID: "b.a"}, + "b.b": {ID: "b.b"}, + "b.a.c": {ID: "b.a.c"}, + "c": {ID: "c"}, } modulesMu.Unlock() @@ -43,24 +43,24 @@ func TestGetModules(t *testing.T) { { input: "", expect: []ModuleInfo{ - {Name: "a"}, - {Name: "b"}, - {Name: "c"}, + {ID: "a"}, + {ID: "b"}, + {ID: "c"}, }, }, { input: "a", expect: []ModuleInfo{ - {Name: "a.b"}, - {Name: "a.c"}, - {Name: "a.d"}, + {ID: "a.b"}, + {ID: "a.c"}, + {ID: "a.d"}, }, }, { input: "a.b", expect: []ModuleInfo{ - {Name: "a.b.c"}, - {Name: "a.b.cd"}, + {ID: "a.b.c"}, + {ID: "a.b.cd"}, }, }, { @@ -69,8 +69,8 @@ func TestGetModules(t *testing.T) { { input: "b", expect: []ModuleInfo{ - {Name: "b.a"}, - {Name: "b.b"}, + {ID: "b.a"}, + {ID: "b.b"}, }, }, {