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

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

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

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

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

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

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

View File

@ -39,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], "/"))
}
}

View File

@ -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.

View File

@ -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

View File

@ -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)},
},
},
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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)
}

View File

@ -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) },
}
}

View File

@ -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: <namespace>.<id>
//
// An ID with no dot has the empty namespace, which is appropriate
// for app modules (these are "top-level" modules that Caddy core
// loads and runs).
//
// Module IDs should be lowercase and use underscore (_) instead of
// spaces.
//
// Example valid names:
// - http
// - http.handlers.file_server
// - caddy.logging.encoders.json
type ModuleID string
// Namespace returns the namespace (or scope) portion of a module ID,
// which is all but the last label of the ID. If the ID has only one
// label, then
func (id ModuleID) Namespace() string {
lastDot := strings.LastIndex(string(id), ".")
if lastDot < 0 {
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
)

View File

@ -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")

View File

@ -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
}

View File

@ -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

View File

@ -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) },
}
}

View File

@ -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.

View File

@ -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) },
}
}

View File

@ -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)
}

View File

@ -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

View File

@ -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) },
}
}

View File

@ -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) },
}
}

View File

@ -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),

View File

@ -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),
},
}

View File

@ -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) },
}
}

View File

@ -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) },
}
}

View File

@ -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) },
}
}

View File

@ -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) },
}
}

View File

@ -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) },
}
}

View File

@ -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) },
}
}

View File

@ -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) },
}
}

View File

@ -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)

View File

@ -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) },
}
}

View File

@ -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),
},
}

View File

@ -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)},
},
},

View File

@ -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) },
}
}

View File

@ -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) },
}
}

View File

@ -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) },
}
}

View File

@ -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:"-"`

View File

@ -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) },
}
}

View File

@ -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) },
}
}

View File

@ -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
}

View File

@ -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"`

View File

@ -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
}

View File

@ -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) },
}
}

View File

@ -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) },
}
}

View File

@ -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) },
}
}

View File

@ -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) },
}
}

View File

@ -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) },
}
}

View File

@ -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) },
}
}

View File

@ -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

View File

@ -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) },
}
}

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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) },
}
}

View File

@ -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) },
}
}

View File

@ -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.

View File

@ -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

View File

@ -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) },
}
}

View File

@ -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)

View File

@ -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) },
}
}

View File

@ -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 {

View File

@ -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) },
}
}

View File

@ -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
}

View File

@ -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) },
}
}

View File

@ -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"},
},
},
{