mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-26 13:43:47 +03:00
Initial commit of Storage, TLS, and automatic HTTPS implementations
This commit is contained in:
parent
545f28008e
commit
2d056fbe66
16 changed files with 1282 additions and 122 deletions
6
admin.go
6
admin.go
|
@ -119,15 +119,15 @@ func Load(r io.Reader) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg Config
|
var cfg *Config
|
||||||
err = json.Unmarshal(buf.Bytes(), &cfg)
|
err = json.Unmarshal(buf.Bytes(), &cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("decoding config: %v", err)
|
return fmt.Errorf("decoding config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = Start(cfg)
|
err = Run(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("starting: %v", err)
|
return fmt.Errorf("running: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
265
caddy.go
265
caddy.go
|
@ -8,27 +8,36 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mholt/certmagic"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Start runs Caddy with the given config.
|
// Run runs Caddy with the given config.
|
||||||
func Start(cfg Config) error {
|
func Run(cfg *Config) error {
|
||||||
// allow only one call to Start at a time,
|
// allow only one call to Start at a time,
|
||||||
// since various calls to LoadModule()
|
// since various calls to LoadModule()
|
||||||
// access shared map moduleInstances
|
// access shared map moduleInstances
|
||||||
startMu.Lock()
|
startMu.Lock()
|
||||||
defer startMu.Unlock()
|
defer startMu.Unlock()
|
||||||
|
|
||||||
// prepare the config for use
|
// because we will need to roll back any state
|
||||||
cfg.runners = make(map[string]Runner)
|
// modifications if this function errors, we
|
||||||
|
// keep a single error value and scope all
|
||||||
|
// sub-operations to their own functions to
|
||||||
|
// ensure this error value does not get
|
||||||
|
// overridden or missed when it should have
|
||||||
|
// been set by a short assignment
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// prepare the new config for use
|
||||||
|
cfg.apps = make(map[string]App)
|
||||||
cfg.moduleStates = make(map[string]interface{})
|
cfg.moduleStates = make(map[string]interface{})
|
||||||
|
|
||||||
// reset the shared moduleInstances map; but
|
// reset the shared moduleInstances map; but
|
||||||
// keep a temporary reference to the current
|
// keep a temporary reference to the current
|
||||||
// one so we can transfer over any necessary
|
// one so we can transfer over any necessary
|
||||||
// state to the new modules; or in case this
|
// state to the new modules or to roll back
|
||||||
// function returns an error, we need to put
|
// if necessary
|
||||||
// the "old" one back where we found it
|
|
||||||
var err error
|
|
||||||
oldModuleInstances := moduleInstances
|
oldModuleInstances := moduleInstances
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -37,109 +46,183 @@ func Start(cfg Config) error {
|
||||||
}()
|
}()
|
||||||
moduleInstances = make(map[string][]interface{})
|
moduleInstances = make(map[string][]interface{})
|
||||||
|
|
||||||
// load (decode) each runner module
|
// set up storage and make it CertMagic's default storage, too
|
||||||
for modName, rawMsg := range cfg.Modules {
|
err = func() error {
|
||||||
val, err := LoadModule(modName, rawMsg)
|
if cfg.StorageRaw != nil {
|
||||||
if err != nil {
|
val, err := LoadModuleInline("system", "caddy.storage", cfg.StorageRaw)
|
||||||
return fmt.Errorf("loading module '%s': %v", modName, err)
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading storage module: %v", err)
|
||||||
|
}
|
||||||
|
stor, err := val.(StorageConverter).CertMagicStorage()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating storage value: %v", err)
|
||||||
|
}
|
||||||
|
cfg.storage = stor
|
||||||
|
cfg.StorageRaw = nil // allow GC to deallocate - TODO: Does this help?
|
||||||
}
|
}
|
||||||
cfg.runners[modName] = val.(Runner)
|
if cfg.storage == nil {
|
||||||
|
cfg.storage = &certmagic.FileStorage{Path: dataDir()}
|
||||||
|
}
|
||||||
|
certmagic.Default.Storage = cfg.storage
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// start the new runners
|
// Load, Provision, Validate
|
||||||
for name, r := range cfg.runners {
|
err = func() error {
|
||||||
err := r.Run()
|
for modName, rawMsg := range cfg.AppsRaw {
|
||||||
if err != nil {
|
val, err := LoadModule(modName, rawMsg)
|
||||||
// TODO: If any one has an error, stop the others
|
if err != nil {
|
||||||
return fmt.Errorf("%s module: %v", name, err)
|
return fmt.Errorf("loading app module '%s': %v", modName, err)
|
||||||
|
}
|
||||||
|
cfg.apps[modName] = val.(App)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// shut down down the old runners
|
// swap old config with the new one, and
|
||||||
|
// roll back this change if anything fails
|
||||||
currentCfgMu.Lock()
|
currentCfgMu.Lock()
|
||||||
if currentCfg != nil {
|
|
||||||
for name, r := range currentCfg.runners {
|
|
||||||
err := r.Cancel()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[ERROR] cancel %s: %v", name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
oldCfg := currentCfg
|
oldCfg := currentCfg
|
||||||
currentCfg = &cfg
|
currentCfg = cfg
|
||||||
currentCfgMu.Unlock()
|
currentCfgMu.Unlock()
|
||||||
|
defer func() {
|
||||||
// invoke unload callbacks on old configuration
|
|
||||||
for modName := range oldModuleInstances {
|
|
||||||
mod, err := GetModule(modName)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
currentCfgMu.Lock()
|
||||||
|
currentCfg = oldCfg
|
||||||
|
currentCfgMu.Unlock()
|
||||||
}
|
}
|
||||||
if mod.OnUnload != nil {
|
}()
|
||||||
var unloadingState interface{}
|
|
||||||
if oldCfg != nil {
|
// OnLoad
|
||||||
unloadingState = oldCfg.moduleStates[modName]
|
err = func() error {
|
||||||
}
|
for modName, instances := range moduleInstances {
|
||||||
err := mod.OnUnload(unloadingState)
|
mod, err := GetModule(modName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] module OnUnload: %s: %v", modName, err)
|
return err
|
||||||
continue
|
}
|
||||||
|
if mod.OnLoad != nil {
|
||||||
|
var priorState interface{}
|
||||||
|
if oldCfg != nil {
|
||||||
|
priorState = oldCfg.moduleStates[modName]
|
||||||
|
}
|
||||||
|
modState, err := mod.OnLoad(instances, priorState)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("module OnLoad: %s: %v", modName, err)
|
||||||
|
}
|
||||||
|
if modState != nil {
|
||||||
|
cfg.moduleStates[modName] = modState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start
|
||||||
|
err = func() error {
|
||||||
|
h := Handle{cfg}
|
||||||
|
for name, a := range cfg.apps {
|
||||||
|
err := a.Start(h)
|
||||||
|
if err != nil {
|
||||||
|
for otherAppName, otherApp := range cfg.apps {
|
||||||
|
err := otherApp.Stop()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("aborting app %s: %v", otherAppName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s app module: start: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop
|
||||||
|
if oldCfg != nil {
|
||||||
|
for name, a := range oldCfg.apps {
|
||||||
|
err := a.Stop()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] stop %s: %v", name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoke load callbacks on new configuration
|
// OnUnload
|
||||||
for modName, instances := range moduleInstances {
|
err = func() error {
|
||||||
mod, err := GetModule(modName)
|
for modName := range oldModuleInstances {
|
||||||
if err != nil {
|
mod, err := GetModule(modName)
|
||||||
return err
|
|
||||||
}
|
|
||||||
if mod.OnLoad != nil {
|
|
||||||
var priorState interface{}
|
|
||||||
if oldCfg != nil {
|
|
||||||
priorState = oldCfg.moduleStates[modName]
|
|
||||||
}
|
|
||||||
modState, err := mod.OnLoad(instances, priorState)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("module OnLoad: %s: %v", modName, err)
|
return err
|
||||||
}
|
}
|
||||||
if modState != nil {
|
if mod.OnUnload != nil {
|
||||||
cfg.moduleStates[modName] = modState
|
var unloadingState interface{}
|
||||||
|
if oldCfg != nil {
|
||||||
|
unloadingState = oldCfg.moduleStates[modName]
|
||||||
|
}
|
||||||
|
err := mod.OnUnload(unloadingState)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] module OnUnload: %s: %v", modName, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// shut down listeners that are no longer being used
|
// shut down listeners that are no longer being used
|
||||||
listenersMu.Lock()
|
err = func() error {
|
||||||
for key, info := range listeners {
|
listenersMu.Lock()
|
||||||
if atomic.LoadInt32(&info.usage) == 0 {
|
for key, info := range listeners {
|
||||||
err := info.ln.Close()
|
if atomic.LoadInt32(&info.usage) == 0 {
|
||||||
if err != nil {
|
err := info.ln.Close()
|
||||||
log.Printf("[ERROR] closing listener %s: %v", info.ln.Addr(), err)
|
if err != nil {
|
||||||
continue
|
log.Printf("[ERROR] closing listener %s: %v", info.ln.Addr(), err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
delete(listeners, key)
|
||||||
}
|
}
|
||||||
delete(listeners, key)
|
|
||||||
}
|
}
|
||||||
|
listenersMu.Unlock()
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
listenersMu.Unlock()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runner is a thing that Caddy runs.
|
// App is a thing that Caddy runs.
|
||||||
type Runner interface {
|
type App interface {
|
||||||
Run() error
|
Start(Handle) error
|
||||||
Cancel() error
|
Stop() error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config represents a Caddy configuration.
|
// Config represents a Caddy configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
TestVal string `json:"testval"`
|
StorageRaw json.RawMessage `json:"storage"`
|
||||||
Modules map[string]json.RawMessage `json:"modules"`
|
storage certmagic.Storage
|
||||||
|
|
||||||
// runners stores the decoded Modules values,
|
TestVal string `json:"testval"`
|
||||||
|
AppsRaw map[string]json.RawMessage `json:"apps"`
|
||||||
|
|
||||||
|
// apps stores the decoded Apps values,
|
||||||
// keyed by module name.
|
// keyed by module name.
|
||||||
runners map[string]Runner
|
apps map[string]App
|
||||||
|
|
||||||
// moduleStates stores the optional "global" state
|
// moduleStates stores the optional "global" state
|
||||||
// values of every module used by this configuration,
|
// values of every module used by this configuration,
|
||||||
|
@ -147,6 +230,34 @@ type Config struct {
|
||||||
moduleStates map[string]interface{}
|
moduleStates map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle allows app modules to access
|
||||||
|
// the top-level Config in a controlled
|
||||||
|
// manner without needing to rely on
|
||||||
|
// global state.
|
||||||
|
type Handle struct {
|
||||||
|
current *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// App returns the configured app named name.
|
||||||
|
// A nil value is returned if no app with that
|
||||||
|
// name is currently configured.
|
||||||
|
func (h Handle) App(name string) interface{} {
|
||||||
|
return h.current.apps[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStorage returns the configured Caddy storage implementation.
|
||||||
|
// If no storage implementation is explicitly configured, the
|
||||||
|
// default one is returned instead, as long as there is a current
|
||||||
|
// configuration loaded.
|
||||||
|
func GetStorage() certmagic.Storage {
|
||||||
|
currentCfgMu.RLock()
|
||||||
|
defer currentCfgMu.RUnlock()
|
||||||
|
if currentCfg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return currentCfg.storage
|
||||||
|
}
|
||||||
|
|
||||||
// Duration is a JSON-string-unmarshable duration type.
|
// Duration is a JSON-string-unmarshable duration type.
|
||||||
type Duration time.Duration
|
type Duration time.Duration
|
||||||
|
|
||||||
|
@ -167,7 +278,7 @@ type CtxKey string
|
||||||
// currentCfg is the currently-loaded configuration.
|
// currentCfg is the currently-loaded configuration.
|
||||||
var (
|
var (
|
||||||
currentCfg *Config
|
currentCfg *Config
|
||||||
currentCfgMu sync.Mutex
|
currentCfgMu sync.RWMutex
|
||||||
)
|
)
|
||||||
|
|
||||||
// moduleInstances stores the individual instantiated
|
// moduleInstances stores the individual instantiated
|
||||||
|
@ -181,5 +292,5 @@ var (
|
||||||
var moduleInstances = make(map[string][]interface{})
|
var moduleInstances = make(map[string][]interface{})
|
||||||
|
|
||||||
// startMu ensures that only one Start() happens at a time.
|
// startMu ensures that only one Start() happens at a time.
|
||||||
// This is important since
|
// This is important since moduleInstances is shared state.
|
||||||
var startMu sync.Mutex
|
var startMu sync.Mutex
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
_ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp"
|
_ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp"
|
||||||
_ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/caddylog"
|
_ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/caddylog"
|
||||||
_ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/staticfiles"
|
_ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/staticfiles"
|
||||||
|
_ "bitbucket.org/lightcodelabs/caddy2/modules/caddytls"
|
||||||
_ "bitbucket.org/lightcodelabs/dynamicconfig"
|
_ "bitbucket.org/lightcodelabs/dynamicconfig"
|
||||||
_ "bitbucket.org/lightcodelabs/proxy"
|
_ "bitbucket.org/lightcodelabs/proxy"
|
||||||
)
|
)
|
||||||
|
|
|
@ -111,7 +111,7 @@ func (fcl *fakeCloseListener) fakeClosedErr() error {
|
||||||
Op: "accept",
|
Op: "accept",
|
||||||
Net: fcl.Listener.Addr().Network(),
|
Net: fcl.Listener.Addr().Network(),
|
||||||
Addr: fcl.Listener.Addr(),
|
Addr: fcl.Listener.Addr(),
|
||||||
Err: ErrFakeClosed,
|
Err: errFakeClosed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ func (fcl *fakeCloseListener) fakeClosedErr() error {
|
||||||
// indicating that it is pretending to be closed so that the
|
// indicating that it is pretending to be closed so that the
|
||||||
// server using it can terminate, while the underlying
|
// server using it can terminate, while the underlying
|
||||||
// socket is actually left open.
|
// socket is actually left open.
|
||||||
var ErrFakeClosed = fmt.Errorf("listener 'closed' 😉")
|
var errFakeClosed = fmt.Errorf("listener 'closed' 😉")
|
||||||
|
|
||||||
// listenerUsage pairs a net.Listener with a
|
// listenerUsage pairs a net.Listener with a
|
||||||
// count of how many servers are using it.
|
// count of how many servers are using it.
|
||||||
|
|
66
modules.go
66
modules.go
|
@ -9,7 +9,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Module is a module.
|
// Module represents a Caddy module.
|
||||||
type Module struct {
|
type Module struct {
|
||||||
Name string
|
Name string
|
||||||
New func() (interface{}, error)
|
New func() (interface{}, error)
|
||||||
|
@ -21,6 +21,10 @@ func (m Module) String() string { return m.Name }
|
||||||
|
|
||||||
// RegisterModule registers a module.
|
// RegisterModule registers a module.
|
||||||
func RegisterModule(mod Module) error {
|
func RegisterModule(mod Module) error {
|
||||||
|
if mod.Name == "caddy" {
|
||||||
|
return fmt.Errorf("modules cannot be named 'caddy'")
|
||||||
|
}
|
||||||
|
|
||||||
modulesMu.Lock()
|
modulesMu.Lock()
|
||||||
defer modulesMu.Unlock()
|
defer modulesMu.Unlock()
|
||||||
|
|
||||||
|
@ -45,7 +49,7 @@ func GetModule(name string) (Module, error) {
|
||||||
|
|
||||||
// GetModules returns all modules in the given scope/namespace.
|
// GetModules returns all modules in the given scope/namespace.
|
||||||
// For example, a scope of "foo" returns modules named "foo.bar",
|
// For example, a scope of "foo" returns modules named "foo.bar",
|
||||||
// "foo.lee", but not "bar", "foo.bar.lee", etc. An empty scope
|
// "foo.loo", but not "bar", "foo.bar.loo", etc. An empty scope
|
||||||
// returns top-level modules, for example "foo" or "bar". Partial
|
// returns top-level modules, for example "foo" or "bar". Partial
|
||||||
// scopes are not matched (i.e. scope "foo.ba" does not match
|
// scopes are not matched (i.e. scope "foo.ba" does not match
|
||||||
// name "foo.bar").
|
// name "foo.bar").
|
||||||
|
@ -112,7 +116,10 @@ func Modules() []string {
|
||||||
// returns the value. If mod.New() does not return a pointer
|
// returns the value. If mod.New() does not return a pointer
|
||||||
// value, it is converted to one so that it is unmarshaled
|
// value, it is converted to one so that it is unmarshaled
|
||||||
// into the underlying concrete type. If mod.New is nil, an
|
// into the underlying concrete type. If mod.New is nil, an
|
||||||
// error is returned.
|
// 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 LoadModule(name string, rawMsg json.RawMessage) (interface{}, error) {
|
func LoadModule(name string, rawMsg json.RawMessage) (interface{}, error) {
|
||||||
modulesMu.Lock()
|
modulesMu.Lock()
|
||||||
mod, ok := modules[name]
|
mod, ok := modules[name]
|
||||||
|
@ -140,6 +147,13 @@ func LoadModule(name string, rawMsg json.RawMessage) (interface{}, error) {
|
||||||
return nil, fmt.Errorf("decoding module config: %s: %v", mod.Name, err)
|
return nil, fmt.Errorf("decoding module config: %s: %v", mod.Name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if prov, ok := val.(Provisioner); ok {
|
||||||
|
err := prov.Provision()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("provision %s: %v", mod.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if validator, ok := val.(Validator); ok {
|
if validator, ok := val.(Validator); ok {
|
||||||
err := validator.Validate()
|
err := validator.Validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -152,27 +166,23 @@ func LoadModule(name string, rawMsg json.RawMessage) (interface{}, error) {
|
||||||
return val, nil
|
return val, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadModuleInlineName loads a module from a JSON raw message which
|
// LoadModuleInline loads a module from a JSON raw message which decodes
|
||||||
// decodes to a map[string]interface{}, and where one of the keys is
|
// to a map[string]interface{}, where one of the keys is moduleNameKey
|
||||||
// "_module", which indicates the module name and which be found in
|
// and the corresponding value is the module name as a string, which
|
||||||
// the given scope.
|
// can be found in the given scope.
|
||||||
//
|
//
|
||||||
// This allows modules to be decoded into their concrete types and
|
// 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
|
// 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
|
// when there are multiple instances in the map or it appears in an
|
||||||
// array (where there are no custom keys).
|
// array (where there are no custom keys). In other words, the key
|
||||||
func LoadModuleInlineName(moduleScope string, raw json.RawMessage) (interface{}, error) {
|
// containing the module name is treated special/separate from all
|
||||||
var tmp map[string]interface{}
|
// the other keys.
|
||||||
err := json.Unmarshal(raw, &tmp)
|
func LoadModuleInline(moduleNameKey, moduleScope string, raw json.RawMessage) (interface{}, error) {
|
||||||
|
moduleName, err := getModuleNameInline(moduleNameKey, raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
moduleName, ok := tmp["_module"].(string)
|
|
||||||
if !ok || moduleName == "" {
|
|
||||||
return nil, fmt.Errorf("module name not specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
val, err := LoadModule(moduleScope+"."+moduleName, raw)
|
val, err := LoadModule(moduleScope+"."+moduleName, raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("loading module '%s': %v", moduleName, err)
|
return nil, fmt.Errorf("loading module '%s': %v", moduleName, err)
|
||||||
|
@ -181,6 +191,23 @@ func LoadModuleInlineName(moduleScope string, raw json.RawMessage) (interface{},
|
||||||
return val, nil
|
return val, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getModuleNameInline loads the string value from raw of moduleNameKey,
|
||||||
|
// where raw must be a JSON encoding of a map.
|
||||||
|
func getModuleNameInline(moduleNameKey string, raw json.RawMessage) (string, error) {
|
||||||
|
var tmp map[string]interface{}
|
||||||
|
err := json.Unmarshal(raw, &tmp)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
moduleName, ok := tmp[moduleNameKey].(string)
|
||||||
|
if !ok || moduleName == "" {
|
||||||
|
return "", fmt.Errorf("module name not specified with key '%s' in %+v", moduleNameKey, tmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return moduleName, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Validator is implemented by modules which can verify that their
|
// Validator is implemented by modules which can verify that their
|
||||||
// configurations are valid. This method will be called after New()
|
// configurations are valid. This method will be called after New()
|
||||||
// instantiations of modules (if implemented). Validation should
|
// instantiations of modules (if implemented). Validation should
|
||||||
|
@ -190,6 +217,13 @@ type Validator interface {
|
||||||
Validate() error
|
Validate() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Provisioner is implemented by modules which may need to perform
|
||||||
|
// some additional "setup" steps immediately after being loaded.
|
||||||
|
// This method will be called after Validate() (if implemented).
|
||||||
|
type Provisioner interface {
|
||||||
|
Provision() error
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
modules = make(map[string]Module)
|
modules = make(map[string]Module)
|
||||||
modulesMu sync.Mutex
|
modulesMu sync.Mutex
|
||||||
|
|
|
@ -2,6 +2,7 @@ package caddyhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
mathrand "math/rand"
|
mathrand "math/rand"
|
||||||
|
@ -12,9 +13,13 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"bitbucket.org/lightcodelabs/caddy2"
|
"bitbucket.org/lightcodelabs/caddy2"
|
||||||
|
"bitbucket.org/lightcodelabs/caddy2/modules/caddytls"
|
||||||
|
"github.com/mholt/certmagic"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
mathrand.Seed(time.Now().UnixNano())
|
||||||
|
|
||||||
err := caddy2.RegisterModule(caddy2.Module{
|
err := caddy2.RegisterModule(caddy2.Module{
|
||||||
Name: "http",
|
Name: "http",
|
||||||
New: func() (interface{}, error) { return new(httpModuleConfig), nil },
|
New: func() (interface{}, error) { return new(httpModuleConfig), nil },
|
||||||
|
@ -22,17 +27,15 @@ func init() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mathrand.Seed(time.Now().UnixNano())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type httpModuleConfig struct {
|
type httpModuleConfig struct {
|
||||||
Servers map[string]httpServerConfig `json:"servers"`
|
Servers map[string]*httpServerConfig `json:"servers"`
|
||||||
|
|
||||||
servers []*http.Server
|
servers []*http.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hc *httpModuleConfig) Run() error {
|
func (hc *httpModuleConfig) Provision() error {
|
||||||
// TODO: Either prevent overlapping listeners on different servers, or combine them into one
|
// TODO: Either prevent overlapping listeners on different servers, or combine them into one
|
||||||
for _, srv := range hc.Servers {
|
for _, srv := range hc.Servers {
|
||||||
err := srv.Routes.setup()
|
err := srv.Routes.setup()
|
||||||
|
@ -43,7 +46,18 @@ func (hc *httpModuleConfig) Run() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("setting up server error handling routes: %v", err)
|
return fmt.Errorf("setting up server error handling routes: %v", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *httpModuleConfig) Start(handle caddy2.Handle) error {
|
||||||
|
err := hc.automaticHTTPS(handle)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("enabling automatic HTTPS: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for srvName, srv := range hc.Servers {
|
||||||
s := &http.Server{
|
s := &http.Server{
|
||||||
ReadTimeout: time.Duration(srv.ReadTimeout),
|
ReadTimeout: time.Duration(srv.ReadTimeout),
|
||||||
ReadHeaderTimeout: time.Duration(srv.ReadHeaderTimeout),
|
ReadHeaderTimeout: time.Duration(srv.ReadHeaderTimeout),
|
||||||
|
@ -53,13 +67,30 @@ func (hc *httpModuleConfig) Run() error {
|
||||||
for _, lnAddr := range srv.Listen {
|
for _, lnAddr := range srv.Listen {
|
||||||
network, addrs, err := parseListenAddr(lnAddr)
|
network, addrs, err := parseListenAddr(lnAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parsing listen address '%s': %v", lnAddr, err)
|
return fmt.Errorf("%s: parsing listen address '%s': %v", srvName, lnAddr, err)
|
||||||
}
|
}
|
||||||
for _, addr := range addrs {
|
for _, addr := range addrs {
|
||||||
ln, err := caddy2.Listen(network, addr)
|
ln, err := caddy2.Listen(network, addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: listening on %s: %v", network, addr, err)
|
return fmt.Errorf("%s: listening on %s: %v", network, addr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// enable HTTP/2 by default
|
||||||
|
for _, pol := range srv.TLSConnPolicies {
|
||||||
|
if len(pol.ALPN) == 0 {
|
||||||
|
pol.ALPN = append(pol.ALPN, defaultALPN...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// enable TLS
|
||||||
|
if len(srv.TLSConnPolicies) > 0 {
|
||||||
|
tlsCfg, err := srv.TLSConnPolicies.TLSConfig(handle)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s/%s: making TLS configuration: %v", network, addr, err)
|
||||||
|
}
|
||||||
|
ln = tls.NewListener(ln, tlsCfg)
|
||||||
|
}
|
||||||
|
|
||||||
go s.Serve(ln)
|
go s.Serve(ln)
|
||||||
hc.servers = append(hc.servers, s)
|
hc.servers = append(hc.servers, s)
|
||||||
}
|
}
|
||||||
|
@ -69,7 +100,7 @@ func (hc *httpModuleConfig) Run() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hc *httpModuleConfig) Cancel() error {
|
func (hc *httpModuleConfig) Stop() error {
|
||||||
for _, s := range hc.servers {
|
for _, s := range hc.servers {
|
||||||
err := s.Shutdown(context.Background()) // TODO
|
err := s.Shutdown(context.Background()) // TODO
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -79,13 +110,63 @@ func (hc *httpModuleConfig) Cancel() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (hc *httpModuleConfig) automaticHTTPS(handle caddy2.Handle) error {
|
||||||
|
tlsApp := handle.App("tls").(*caddytls.TLS)
|
||||||
|
|
||||||
|
for srvName, srv := range hc.Servers {
|
||||||
|
srv.tlsApp = tlsApp
|
||||||
|
|
||||||
|
if srv.DisableAutoHTTPS {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
domainSet := make(map[string]struct{})
|
||||||
|
for _, route := range srv.Routes {
|
||||||
|
for _, m := range route.matchers {
|
||||||
|
if hm, ok := m.(*matchHost); ok {
|
||||||
|
for _, d := range *hm {
|
||||||
|
if !certmagic.HostQualifies(d) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
domainSet[d] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var domains []string
|
||||||
|
for d := range domainSet {
|
||||||
|
domains = append(domains, d)
|
||||||
|
}
|
||||||
|
if len(domains) > 0 {
|
||||||
|
err := tlsApp.Manage(domains)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: managing certificate for %s: %s", srvName, domains, err)
|
||||||
|
}
|
||||||
|
// TODO: Connection policies... redirects... man...
|
||||||
|
srv.TLSConnPolicies = caddytls.ConnectionPolicies{
|
||||||
|
{
|
||||||
|
ALPN: defaultALPN,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultALPN = []string{"h2", "http/1.1"}
|
||||||
|
|
||||||
type httpServerConfig struct {
|
type httpServerConfig struct {
|
||||||
Listen []string `json:"listen"`
|
Listen []string `json:"listen"`
|
||||||
ReadTimeout caddy2.Duration `json:"read_timeout"`
|
ReadTimeout caddy2.Duration `json:"read_timeout"`
|
||||||
ReadHeaderTimeout caddy2.Duration `json:"read_header_timeout"`
|
ReadHeaderTimeout caddy2.Duration `json:"read_header_timeout"`
|
||||||
HiddenFiles []string `json:"hidden_files"` // TODO:... experimenting with shared/common state
|
HiddenFiles []string `json:"hidden_files"` // TODO:... experimenting with shared/common state
|
||||||
Routes routeList `json:"routes"`
|
Routes routeList `json:"routes"`
|
||||||
Errors httpErrorConfig `json:"errors"`
|
Errors httpErrorConfig `json:"errors"`
|
||||||
|
TLSConnPolicies caddytls.ConnectionPolicies `json:"tls_connection_policies"`
|
||||||
|
DisableAutoHTTPS bool `json:"disable_auto_https"`
|
||||||
|
|
||||||
|
tlsApp *caddytls.TLS
|
||||||
}
|
}
|
||||||
|
|
||||||
type httpErrorConfig struct {
|
type httpErrorConfig struct {
|
||||||
|
@ -95,6 +176,10 @@ type httpErrorConfig struct {
|
||||||
|
|
||||||
// ServeHTTP is the entry point for all HTTP requests.
|
// ServeHTTP is the entry point for all HTTP requests.
|
||||||
func (s httpServerConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (s httpServerConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if s.tlsApp.HandleHTTPChallenge(w, r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
stack := s.Routes.buildMiddlewareChain(w, r)
|
stack := s.Routes.buildMiddlewareChain(w, r)
|
||||||
err := executeMiddlewareChain(w, r, stack)
|
err := executeMiddlewareChain(w, r, stack)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -64,4 +64,4 @@ func (l *Log) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.H
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guard
|
// Interface guard
|
||||||
var _ caddyhttp.MiddlewareHandler = &Log{}
|
var _ caddyhttp.MiddlewareHandler = (*Log)(nil)
|
||||||
|
|
|
@ -32,17 +32,13 @@ func (routes routeList) buildMiddlewareChain(w http.ResponseWriter, r *http.Requ
|
||||||
var responder Handler
|
var responder Handler
|
||||||
mrw := &middlewareResponseWriter{ResponseWriterWrapper: &ResponseWriterWrapper{w}}
|
mrw := &middlewareResponseWriter{ResponseWriterWrapper: &ResponseWriterWrapper{w}}
|
||||||
|
|
||||||
|
routeLoop:
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
matched := len(route.matchers) == 0
|
|
||||||
for _, m := range route.matchers {
|
for _, m := range route.matchers {
|
||||||
if m.Match(r) {
|
if !m.Match(r) {
|
||||||
matched = true
|
continue routeLoop
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !matched {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, m := range route.middleware {
|
for _, m := range route.middleware {
|
||||||
mid = append(mid, func(next HandlerFunc) HandlerFunc {
|
mid = append(mid, func(next HandlerFunc) HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) error {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
@ -53,6 +49,8 @@ func (routes routeList) buildMiddlewareChain(w http.ResponseWriter, r *http.Requ
|
||||||
if responder == nil {
|
if responder == nil {
|
||||||
responder = route.responder
|
responder = route.responder
|
||||||
}
|
}
|
||||||
|
// TODO: Should exclusive apply to only middlewares, or responder too?
|
||||||
|
// i.e. what if they haven't set a responder yet, but the first middleware chain is exclusive...
|
||||||
if route.Exclusive {
|
if route.Exclusive {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -83,24 +81,27 @@ func (routes routeList) setup() error {
|
||||||
}
|
}
|
||||||
routes[i].matchers = append(routes[i].matchers, val.(RouteMatcher))
|
routes[i].matchers = append(routes[i].matchers, val.(RouteMatcher))
|
||||||
}
|
}
|
||||||
|
routes[i].Matchers = nil // allow GC to deallocate - TODO: Does this help?
|
||||||
|
|
||||||
// middleware
|
// middleware
|
||||||
for j, rawMsg := range route.Apply {
|
for j, rawMsg := range route.Apply {
|
||||||
mid, err := caddy2.LoadModuleInlineName("http.middleware", rawMsg)
|
mid, err := caddy2.LoadModuleInline("middleware", "http.middleware", rawMsg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("loading middleware module in position %d: %v", j, err)
|
return fmt.Errorf("loading middleware module in position %d: %v", j, err)
|
||||||
}
|
}
|
||||||
routes[i].middleware = append(routes[i].middleware, mid.(MiddlewareHandler))
|
routes[i].middleware = append(routes[i].middleware, mid.(MiddlewareHandler))
|
||||||
}
|
}
|
||||||
|
routes[i].Apply = nil // allow GC to deallocate - TODO: Does this help?
|
||||||
|
|
||||||
// responder
|
// responder
|
||||||
if route.Respond != nil {
|
if route.Respond != nil {
|
||||||
resp, err := caddy2.LoadModuleInlineName("http.responders", route.Respond)
|
resp, err := caddy2.LoadModuleInline("responder", "http.responders", route.Respond)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("loading responder module: %v", err)
|
return fmt.Errorf("loading responder module: %v", err)
|
||||||
}
|
}
|
||||||
routes[i].responder = resp.(Handler)
|
routes[i].responder = resp.(Handler)
|
||||||
}
|
}
|
||||||
|
routes[i].Respond = nil // allow GC to deallocate - TODO: Does this help?
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
func init() {
|
func init() {
|
||||||
caddy2.RegisterModule(caddy2.Module{
|
caddy2.RegisterModule(caddy2.Module{
|
||||||
Name: "http.responders.static_files",
|
Name: "http.responders.static_files",
|
||||||
New: func() (interface{}, error) { return &StaticFiles{}, nil },
|
New: func() (interface{}, error) { return new(StaticFiles), nil },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,4 +25,4 @@ func (sf StaticFiles) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guard
|
// Interface guard
|
||||||
var _ caddyhttp.Handler = StaticFiles{}
|
var _ caddyhttp.Handler = (*StaticFiles)(nil)
|
||||||
|
|
84
modules/caddytls/acmemanager.go
Normal file
84
modules/caddytls/acmemanager.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
package caddytls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-acme/lego/certcrypto"
|
||||||
|
|
||||||
|
"bitbucket.org/lightcodelabs/caddy2"
|
||||||
|
"github.com/go-acme/lego/challenge"
|
||||||
|
"github.com/mholt/certmagic"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
caddy2.RegisterModule(caddy2.Module{
|
||||||
|
Name: "tls.management.acme",
|
||||||
|
New: func() (interface{}, error) { return new(acmeManagerMaker), nil },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ManagerMaker TODO: WIP...
|
||||||
|
type ManagerMaker interface {
|
||||||
|
newManager(interactive bool) (certmagic.Manager, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// acmeManagerMaker makes an ACME manager
|
||||||
|
// for managinig certificates using ACME.
|
||||||
|
type acmeManagerMaker struct {
|
||||||
|
CA string `json:"ca,omitempty"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
RenewAhead caddy2.Duration `json:"renew_ahead,omitempty"`
|
||||||
|
KeyType string `json:"key_type,omitempty"`
|
||||||
|
ACMETimeout caddy2.Duration `json:"acme_timeout,omitempty"`
|
||||||
|
MustStaple bool `json:"must_staple,omitempty"`
|
||||||
|
Challenges ChallengesConfig `json:"challenges"`
|
||||||
|
OnDemand *OnDemandConfig `json:"on_demand,omitempty"`
|
||||||
|
Storage json.RawMessage `json:"storage,omitempty"`
|
||||||
|
|
||||||
|
storage certmagic.Storage
|
||||||
|
keyType certcrypto.KeyType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *acmeManagerMaker) Provision() error {
|
||||||
|
m.setDefaults()
|
||||||
|
|
||||||
|
// DNS providers
|
||||||
|
if m.Challenges.DNS != nil {
|
||||||
|
val, err := caddy2.LoadModuleInline("provider", "tls.dns", m.Challenges.DNS)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading TLS storage module: %s", err)
|
||||||
|
}
|
||||||
|
m.Challenges.dns = val.(challenge.Provider)
|
||||||
|
m.Challenges.DNS = nil // allow GC to deallocate - TODO: Does this help?
|
||||||
|
}
|
||||||
|
|
||||||
|
// policy-specific storage implementation
|
||||||
|
if m.Storage != nil {
|
||||||
|
val, err := caddy2.LoadModuleInline("system", "caddy.storage", m.Storage)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading TLS storage module: %s", err)
|
||||||
|
}
|
||||||
|
cmStorage, err := val.(caddy2.StorageConverter).CertMagicStorage()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating TLS storage configuration: %v", err)
|
||||||
|
}
|
||||||
|
m.storage = cmStorage
|
||||||
|
m.Storage = nil // allow GC to deallocate - TODO: Does this help?
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setDefaults indiscriminately sets all the default values in m.
|
||||||
|
func (m *acmeManagerMaker) setDefaults() {
|
||||||
|
m.CA = certmagic.LetsEncryptStagingCA // certmagic.Default.CA // TODO: When not testing, switch to production CA
|
||||||
|
m.Email = certmagic.Default.Email
|
||||||
|
m.RenewAhead = caddy2.Duration(certmagic.Default.RenewDurationBefore)
|
||||||
|
m.keyType = certmagic.Default.KeyType
|
||||||
|
m.storage = certmagic.Default.Storage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *acmeManagerMaker) newManager(interactive bool) (certmagic.Manager, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
149
modules/caddytls/connpolicy.go
Normal file
149
modules/caddytls/connpolicy.go
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
package caddytls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"bitbucket.org/lightcodelabs/caddy2"
|
||||||
|
"github.com/go-acme/lego/challenge/tlsalpn01"
|
||||||
|
"github.com/mholt/certmagic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConnectionPolicies is an ordered group of connection policies;
|
||||||
|
// the first matching policy will be used to configure TLS
|
||||||
|
// connections at handshake-time.
|
||||||
|
type ConnectionPolicies []*ConnectionPolicy
|
||||||
|
|
||||||
|
// TLSConfig converts the group of policies to a standard-lib-compatible
|
||||||
|
// TLS configuration which selects the first matching policy based on
|
||||||
|
// the ClientHello.
|
||||||
|
func (cp ConnectionPolicies) TLSConfig(handle caddy2.Handle) (*tls.Config, error) {
|
||||||
|
// connection policy matchers
|
||||||
|
for i, pol := range cp {
|
||||||
|
for modName, rawMsg := range pol.MatchersRaw {
|
||||||
|
val, err := caddy2.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))
|
||||||
|
}
|
||||||
|
cp[i].MatchersRaw = nil // allow GC to deallocate - TODO: Does this help?
|
||||||
|
}
|
||||||
|
|
||||||
|
// pre-build standard TLS configs so we don't have to at handshake-time
|
||||||
|
for i := range cp {
|
||||||
|
err := cp[i].buildStandardTLSConfig(handle)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("connection policy %d: building standard TLS config: %s", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tls.Config{
|
||||||
|
GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||||
|
policyLoop:
|
||||||
|
for _, pol := range cp {
|
||||||
|
for _, matcher := range pol.Matchers {
|
||||||
|
if !matcher.Match(hello) {
|
||||||
|
continue policyLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pol.stdTLSConfig, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no server TLS configuration available for ClientHello: %+v", hello)
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectionPolicy specifies the logic for handling a TLS handshake.
|
||||||
|
type ConnectionPolicy struct {
|
||||||
|
MatchersRaw map[string]json.RawMessage `json:"match,omitempty"`
|
||||||
|
|
||||||
|
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"`
|
||||||
|
|
||||||
|
// TODO: Client auth
|
||||||
|
|
||||||
|
// TODO: see if starlark could be useful here - enterprise only
|
||||||
|
StarlarkHandshake string `json:"starlark_handshake,omitempty"`
|
||||||
|
|
||||||
|
Matchers []ConnectionMatcher
|
||||||
|
stdTLSConfig *tls.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cp *ConnectionPolicy) buildStandardTLSConfig(handle caddy2.Handle) error {
|
||||||
|
tlsApp := handle.App("tls").(*TLS)
|
||||||
|
|
||||||
|
cfg := &tls.Config{
|
||||||
|
NextProtos: cp.ALPN,
|
||||||
|
PreferServerCipherSuites: true,
|
||||||
|
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
cfgTpl, err := tlsApp.getConfigForName(hello.ServerName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("getting config for name %s: %v", hello.ServerName, err)
|
||||||
|
}
|
||||||
|
newCfg := certmagic.New(tlsApp.certCache, cfgTpl)
|
||||||
|
return newCfg.GetCertificate(hello)
|
||||||
|
},
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
MaxVersion: tls.VersionTLS13,
|
||||||
|
// TODO: Session ticket key rotation (use Storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add all the cipher suites in order, without duplicates
|
||||||
|
cipherSuitesAdded := make(map[uint16]struct{})
|
||||||
|
for _, csName := range cp.CipherSuites {
|
||||||
|
csID := supportedCipherSuites[csName]
|
||||||
|
if _, ok := cipherSuitesAdded[csID]; !ok {
|
||||||
|
cipherSuitesAdded[csID] = struct{}{}
|
||||||
|
cfg.CipherSuites = append(cfg.CipherSuites, csID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add all the curve preferences in order, without duplicates
|
||||||
|
curvesAdded := make(map[tls.CurveID]struct{})
|
||||||
|
for _, curveName := range cp.Curves {
|
||||||
|
curveID := supportedCurves[curveName]
|
||||||
|
if _, ok := curvesAdded[curveID]; !ok {
|
||||||
|
curvesAdded[curveID] = struct{}{}
|
||||||
|
cfg.CurvePreferences = append(cfg.CurvePreferences, curveID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure ALPN includes the ACME TLS-ALPN protocol
|
||||||
|
var alpnFound bool
|
||||||
|
for _, a := range cp.ALPN {
|
||||||
|
if a == tlsalpn01.ACMETLS1Protocol {
|
||||||
|
alpnFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !alpnFound {
|
||||||
|
cfg.NextProtos = append(cfg.NextProtos, tlsalpn01.ACMETLS1Protocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
// min and max protocol versions
|
||||||
|
if cp.ProtocolMin != "" {
|
||||||
|
cfg.MinVersion = supportedProtocols[cp.ProtocolMin]
|
||||||
|
}
|
||||||
|
if cp.ProtocolMax != "" {
|
||||||
|
cfg.MaxVersion = supportedProtocols[cp.ProtocolMax]
|
||||||
|
}
|
||||||
|
if cp.ProtocolMin > cp.ProtocolMax {
|
||||||
|
return fmt.Errorf("protocol min (%x) cannot be greater than protocol max (%x)", cp.ProtocolMin, cp.ProtocolMax)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: client auth, and other fields
|
||||||
|
|
||||||
|
cp.stdTLSConfig = cfg
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectionMatcher is a type which matches TLS handshakes.
|
||||||
|
type ConnectionMatcher interface {
|
||||||
|
Match(*tls.ClientHelloInfo) bool
|
||||||
|
}
|
61
modules/caddytls/fileloader.go
Normal file
61
modules/caddytls/fileloader.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package caddytls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"bitbucket.org/lightcodelabs/caddy2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
caddy2.RegisterModule(caddy2.Module{
|
||||||
|
Name: "tls.certificates.load_files",
|
||||||
|
New: func() (interface{}, error) { return fileLoader{}, nil },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// fileLoader loads certificates and their associated keys from disk.
|
||||||
|
type fileLoader []CertKeyFilePair
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadCertificates returns the certificates to be loaded by fl.
|
||||||
|
func (fl fileLoader) LoadCertificates() ([]tls.Certificate, error) {
|
||||||
|
var certs []tls.Certificate
|
||||||
|
for _, pair := range fl {
|
||||||
|
certData, err := ioutil.ReadFile(pair.Certificate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
keyData, err := ioutil.ReadFile(pair.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cert tls.Certificate
|
||||||
|
switch pair.Format {
|
||||||
|
case "":
|
||||||
|
fallthrough
|
||||||
|
case "pem":
|
||||||
|
cert, err = tls.X509KeyPair(certData, keyData)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unrecognized certificate/key encoding format: %s", pair.Format)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
certs = append(certs, cert)
|
||||||
|
}
|
||||||
|
return certs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guard
|
||||||
|
var _ CertificateLoader = (fileLoader)(nil)
|
122
modules/caddytls/folderloader.go
Normal file
122
modules/caddytls/folderloader.go
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
package caddytls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"bitbucket.org/lightcodelabs/caddy2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
caddy2.RegisterModule(caddy2.Module{
|
||||||
|
Name: "tls.certificates.load_folders",
|
||||||
|
New: func() (interface{}, error) { return folderLoader{}, nil },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// folderLoader loads certificates and their associated keys from disk
|
||||||
|
// by recursively walking the specified directories, looking for PEM
|
||||||
|
// files which contain both a certificate and a key.
|
||||||
|
type folderLoader []string
|
||||||
|
|
||||||
|
// LoadCertificates loads all the certificates+keys in the directories
|
||||||
|
// listed in fl from all files ending with .pem. This method of loading
|
||||||
|
// certificates expects the certificate and key to be bundled into the
|
||||||
|
// same file.
|
||||||
|
func (fl folderLoader) LoadCertificates() ([]tls.Certificate, error) {
|
||||||
|
var certs []tls.Certificate
|
||||||
|
for _, dir := range fl {
|
||||||
|
err := filepath.Walk(dir, func(fpath string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to traverse into path: %s", fpath)
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(strings.ToLower(info.Name()), ".pem") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := x509CertFromCertAndKeyPEMFile(fpath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
certs = append(certs, cert)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return certs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func x509CertFromCertAndKeyPEMFile(fpath string) (tls.Certificate, error) {
|
||||||
|
bundle, err := ioutil.ReadFile(fpath)
|
||||||
|
if err != nil {
|
||||||
|
return tls.Certificate{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
certBuilder, keyBuilder := new(bytes.Buffer), new(bytes.Buffer)
|
||||||
|
var foundKey bool // use only the first key in the file
|
||||||
|
|
||||||
|
for {
|
||||||
|
// Decode next block so we can see what type it is
|
||||||
|
var derBlock *pem.Block
|
||||||
|
derBlock, bundle = pem.Decode(bundle)
|
||||||
|
if derBlock == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if derBlock.Type == "CERTIFICATE" {
|
||||||
|
// Re-encode certificate as PEM, appending to certificate chain
|
||||||
|
pem.Encode(certBuilder, derBlock)
|
||||||
|
} else if derBlock.Type == "EC PARAMETERS" {
|
||||||
|
// EC keys generated from openssl can be composed of two blocks:
|
||||||
|
// parameters and key (parameter block should come first)
|
||||||
|
if !foundKey {
|
||||||
|
// Encode parameters
|
||||||
|
pem.Encode(keyBuilder, derBlock)
|
||||||
|
|
||||||
|
// Key must immediately follow
|
||||||
|
derBlock, bundle = pem.Decode(bundle)
|
||||||
|
if derBlock == nil || derBlock.Type != "EC PRIVATE KEY" {
|
||||||
|
return tls.Certificate{}, fmt.Errorf("%s: expected elliptic private key to immediately follow EC parameters", fpath)
|
||||||
|
}
|
||||||
|
pem.Encode(keyBuilder, derBlock)
|
||||||
|
foundKey = true
|
||||||
|
}
|
||||||
|
} else if derBlock.Type == "PRIVATE KEY" || strings.HasSuffix(derBlock.Type, " PRIVATE KEY") {
|
||||||
|
// RSA key
|
||||||
|
if !foundKey {
|
||||||
|
pem.Encode(keyBuilder, derBlock)
|
||||||
|
foundKey = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return tls.Certificate{}, fmt.Errorf("%s: unrecognized PEM block type: %s", fpath, derBlock.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
certPEMBytes, keyPEMBytes := certBuilder.Bytes(), keyBuilder.Bytes()
|
||||||
|
if len(certPEMBytes) == 0 {
|
||||||
|
return tls.Certificate{}, fmt.Errorf("%s: failed to parse PEM data", fpath)
|
||||||
|
}
|
||||||
|
if len(keyPEMBytes) == 0 {
|
||||||
|
return tls.Certificate{}, fmt.Errorf("%s: no private key block found", fpath)
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := tls.X509KeyPair(certPEMBytes, keyPEMBytes)
|
||||||
|
if err != nil {
|
||||||
|
return tls.Certificate{}, fmt.Errorf("%s: making X509 key pair: %v", fpath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cert, nil
|
||||||
|
}
|
79
modules/caddytls/matchers.go
Normal file
79
modules/caddytls/matchers.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package caddytls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
|
||||||
|
"bitbucket.org/lightcodelabs/caddy2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
MatchServerName []string
|
||||||
|
|
||||||
|
// TODO: these others should be enterprise-only, probably
|
||||||
|
MatchProtocol []string // TODO: version or protocol?
|
||||||
|
MatchClientCert struct{} // TODO: client certificate options
|
||||||
|
MatchRemote []string
|
||||||
|
MatchStarlark string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
caddy2.RegisterModule(caddy2.Module{
|
||||||
|
Name: "tls.handshake_match.host",
|
||||||
|
New: func() (interface{}, error) { return MatchServerName{}, nil },
|
||||||
|
})
|
||||||
|
caddy2.RegisterModule(caddy2.Module{
|
||||||
|
Name: "tls.handshake_match.protocol",
|
||||||
|
New: func() (interface{}, error) { return MatchProtocol{}, nil },
|
||||||
|
})
|
||||||
|
caddy2.RegisterModule(caddy2.Module{
|
||||||
|
Name: "tls.handshake_match.client_cert",
|
||||||
|
New: func() (interface{}, error) { return MatchClientCert{}, nil },
|
||||||
|
})
|
||||||
|
caddy2.RegisterModule(caddy2.Module{
|
||||||
|
Name: "tls.handshake_match.remote",
|
||||||
|
New: func() (interface{}, error) { return MatchRemote{}, nil },
|
||||||
|
})
|
||||||
|
caddy2.RegisterModule(caddy2.Module{
|
||||||
|
Name: "tls.handshake_match.starlark",
|
||||||
|
New: func() (interface{}, error) { return new(MatchStarlark), nil },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MatchServerName) Match(hello *tls.ClientHelloInfo) bool {
|
||||||
|
for _, name := range m {
|
||||||
|
// TODO: support wildcards (and regex?)
|
||||||
|
if hello.ServerName == name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MatchProtocol) Match(hello *tls.ClientHelloInfo) bool {
|
||||||
|
// TODO: not implemented
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MatchClientCert) Match(hello *tls.ClientHelloInfo) bool {
|
||||||
|
// TODO: not implemented
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MatchRemote) Match(hello *tls.ClientHelloInfo) bool {
|
||||||
|
// TODO: not implemented
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m MatchStarlark) Match(hello *tls.ClientHelloInfo) bool {
|
||||||
|
// TODO: not implemented
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards
|
||||||
|
var (
|
||||||
|
_ ConnectionMatcher = MatchServerName{}
|
||||||
|
_ ConnectionMatcher = MatchProtocol{}
|
||||||
|
_ ConnectionMatcher = MatchClientCert{}
|
||||||
|
_ ConnectionMatcher = MatchRemote{}
|
||||||
|
_ ConnectionMatcher = new(MatchStarlark)
|
||||||
|
)
|
359
modules/caddytls/tls.go
Normal file
359
modules/caddytls/tls.go
Normal file
|
@ -0,0 +1,359 @@
|
||||||
|
package caddytls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"bitbucket.org/lightcodelabs/caddy2"
|
||||||
|
"github.com/go-acme/lego/certcrypto"
|
||||||
|
"github.com/go-acme/lego/challenge"
|
||||||
|
"github.com/klauspost/cpuid"
|
||||||
|
"github.com/mholt/certmagic"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
caddy2.RegisterModule(caddy2.Module{
|
||||||
|
Name: "tls",
|
||||||
|
New: func() (interface{}, error) { return new(TLS), nil },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLS represents a process-wide TLS configuration.
|
||||||
|
type TLS struct {
|
||||||
|
Certificates map[string]json.RawMessage `json:"certificates"`
|
||||||
|
Automation AutomationConfig `json:"automation"`
|
||||||
|
|
||||||
|
certificateLoaders []CertificateLoader
|
||||||
|
certCache *certmagic.Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Finish stubbing out this two-phase setup process: prepare, then start...
|
||||||
|
|
||||||
|
func (t *TLS) Provision() error {
|
||||||
|
// set up the certificate cache
|
||||||
|
// TODO: this makes a new cache every time; better to only make a new
|
||||||
|
// cache (or even better, add/remove only what is necessary) if the
|
||||||
|
// certificates config has been updated
|
||||||
|
t.certCache = certmagic.NewCache(certmagic.CacheOptions{
|
||||||
|
GetConfigForCert: func(cert certmagic.Certificate) (certmagic.Config, error) {
|
||||||
|
return t.getConfigForName(cert.Names[0])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
for i, ap := range t.Automation.Policies {
|
||||||
|
val, err := caddy2.LoadModuleInline("module", "tls.management", ap.Management)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading TLS automation management module: %s", err)
|
||||||
|
}
|
||||||
|
t.Automation.Policies[i].management = val.(ManagerMaker)
|
||||||
|
t.Automation.Policies[i].Management = nil // allow GC to deallocate - TODO: Does this help?
|
||||||
|
}
|
||||||
|
|
||||||
|
// certificate loaders
|
||||||
|
for modName, rawMsg := range t.Certificates {
|
||||||
|
if modName == automateKey {
|
||||||
|
continue // special case; these will be loaded in later
|
||||||
|
}
|
||||||
|
val, err := caddy2.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))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start activates the TLS module.
|
||||||
|
func (t *TLS) Start(handle caddy2.Handle) error {
|
||||||
|
// load manual/static (unmanaged) certificates
|
||||||
|
for _, loader := range t.certificateLoaders {
|
||||||
|
certs, err := loader.LoadCertificates()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading certificates: %v", err)
|
||||||
|
}
|
||||||
|
magic := certmagic.New(t.certCache, certmagic.Config{
|
||||||
|
Storage: caddy2.GetStorage(),
|
||||||
|
})
|
||||||
|
for _, cert := range certs {
|
||||||
|
err := magic.CacheUnmanagedTLSCertificate(cert)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("caching unmanaged certificate: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
// for _, name := range names {
|
||||||
|
// t.Manage([]string{name)
|
||||||
|
// ap := t.getAutomationPolicyForName(name)
|
||||||
|
// magic := certmagic.New(t.certCache, ap.makeCertMagicConfig())
|
||||||
|
// err := magic.Manage([]string{name})
|
||||||
|
// if err != nil {
|
||||||
|
// return fmt.Errorf("automate: manage %s: %v", name, err)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
t.Certificates = nil // allow GC to deallocate - TODO: Does this help?
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the TLS module and cleans up any allocations.
|
||||||
|
func (t *TLS) Stop() error {
|
||||||
|
if t.certCache != nil {
|
||||||
|
// TODO: ensure locks are cleaned up too... maybe in certmagic though
|
||||||
|
t.certCache.Stop()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manage immediately begins managing names according to the
|
||||||
|
// matching automation policy.
|
||||||
|
func (t *TLS) Manage(names []string) error {
|
||||||
|
for _, name := range names {
|
||||||
|
ap := t.getAutomationPolicyForName(name)
|
||||||
|
magic := certmagic.New(t.certCache, ap.makeCertMagicConfig())
|
||||||
|
err := magic.Manage([]string{name})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("automate: manage %s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleHTTPChallenge ensures that the HTTP challenge is handled for the
|
||||||
|
// certificate named by r.Host, if it is an HTTP challenge request.
|
||||||
|
func (t *TLS) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool {
|
||||||
|
if !certmagic.LooksLikeHTTPChallenge(r) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ap := t.getAutomationPolicyForName(r.Host)
|
||||||
|
magic := certmagic.New(t.certCache, ap.makeCertMagicConfig())
|
||||||
|
return magic.HandleHTTPChallenge(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TLS) getConfigForName(name string) (certmagic.Config, error) {
|
||||||
|
ap := t.getAutomationPolicyForName(name)
|
||||||
|
return ap.makeCertMagicConfig(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TLS) getAutomationPolicyForName(name string) AutomationPolicy {
|
||||||
|
for _, ap := range t.Automation.Policies {
|
||||||
|
if len(ap.Hosts) == 0 {
|
||||||
|
// no host filter is an automatic match
|
||||||
|
return ap
|
||||||
|
}
|
||||||
|
for _, h := range ap.Hosts {
|
||||||
|
if h == name {
|
||||||
|
return ap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// default automation policy
|
||||||
|
mgmt := new(acmeManagerMaker)
|
||||||
|
mgmt.setDefaults()
|
||||||
|
return AutomationPolicy{management: mgmt}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CertificateLoader is a type that can load certificates.
|
||||||
|
type CertificateLoader interface {
|
||||||
|
LoadCertificates() ([]tls.Certificate, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutomationConfig designates configuration for the
|
||||||
|
// construction and use of ACME clients.
|
||||||
|
type AutomationConfig struct {
|
||||||
|
Policies []AutomationPolicy `json:"policies,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutomationPolicy designates the policy for automating the
|
||||||
|
// management of managed TLS certificates.
|
||||||
|
type AutomationPolicy struct {
|
||||||
|
Hosts []string `json:"hosts,omitempty"`
|
||||||
|
Management json.RawMessage `json:"management"`
|
||||||
|
|
||||||
|
management ManagerMaker
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap AutomationPolicy) makeCertMagicConfig() certmagic.Config {
|
||||||
|
if acmeMgmt, ok := ap.management.(*acmeManagerMaker); ok {
|
||||||
|
// default, which is management via ACME
|
||||||
|
|
||||||
|
storage := acmeMgmt.storage
|
||||||
|
if storage == nil {
|
||||||
|
storage = caddy2.GetStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
var ond *certmagic.OnDemandConfig
|
||||||
|
if acmeMgmt.OnDemand != nil {
|
||||||
|
ond = &certmagic.OnDemandConfig{
|
||||||
|
// TODO: fill this out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return certmagic.Config{
|
||||||
|
CA: certmagic.LetsEncryptStagingCA, //ap.CA, // TODO: Restore true value
|
||||||
|
Email: acmeMgmt.Email,
|
||||||
|
Agreed: true,
|
||||||
|
DisableHTTPChallenge: acmeMgmt.Challenges.HTTP.Disabled,
|
||||||
|
DisableTLSALPNChallenge: acmeMgmt.Challenges.TLSALPN.Disabled,
|
||||||
|
RenewDurationBefore: time.Duration(acmeMgmt.RenewAhead),
|
||||||
|
AltHTTPPort: acmeMgmt.Challenges.HTTP.AlternatePort,
|
||||||
|
AltTLSALPNPort: acmeMgmt.Challenges.TLSALPN.AlternatePort,
|
||||||
|
DNSProvider: acmeMgmt.Challenges.dns,
|
||||||
|
KeyType: supportedCertKeyTypes[acmeMgmt.KeyType],
|
||||||
|
CertObtainTimeout: time.Duration(acmeMgmt.ACMETimeout),
|
||||||
|
OnDemand: ond,
|
||||||
|
MustStaple: acmeMgmt.MustStaple,
|
||||||
|
Storage: storage,
|
||||||
|
// TODO: listenHost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return certmagic.Config{
|
||||||
|
NewManager: ap.management.newManager,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChallengesConfig configures the ACME challenges.
|
||||||
|
type ChallengesConfig struct {
|
||||||
|
HTTP HTTPChallengeConfig `json:"http"`
|
||||||
|
TLSALPN TLSALPNChallengeConfig `json:"tls-alpn"`
|
||||||
|
DNS json.RawMessage `json:"dns,omitempty"`
|
||||||
|
|
||||||
|
dns challenge.Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPChallengeConfig configures the ACME HTTP challenge.
|
||||||
|
type HTTPChallengeConfig struct {
|
||||||
|
Disabled bool `json:"disabled,omitempty"`
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnDemandConfig configures on-demand TLS, for obtaining
|
||||||
|
// needed certificates at handshake-time.
|
||||||
|
type OnDemandConfig struct {
|
||||||
|
// TODO: MaxCertificates state might not endure reloads...
|
||||||
|
// MaxCertificates int `json:"max_certificates,omitempty"`
|
||||||
|
AskURL string `json:"ask_url,omitempty"`
|
||||||
|
AskStarlark string `json:"ask_starlark,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// supportedCertKeyTypes is all the key types that are supported
|
||||||
|
// for certificates that are obtained through ACME.
|
||||||
|
var supportedCertKeyTypes = map[string]certcrypto.KeyType{
|
||||||
|
"RSA2048": certcrypto.RSA2048,
|
||||||
|
"RSA4096": certcrypto.RSA4096,
|
||||||
|
"P256": certcrypto.EC256,
|
||||||
|
"P384": certcrypto.EC384,
|
||||||
|
}
|
||||||
|
|
||||||
|
// supportedCipherSuites is the unordered map of cipher suite
|
||||||
|
// string names to their definition in crypto/tls.
|
||||||
|
// TODO: might not be needed much longer, see:
|
||||||
|
// https://github.com/golang/go/issues/30325
|
||||||
|
var supportedCipherSuites = map[string]uint16{
|
||||||
|
"ECDHE_ECDSA_AES256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||||
|
"ECDHE_RSA_AES256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||||
|
"ECDHE_ECDSA_AES128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
"ECDHE_RSA_AES128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
"ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||||
|
"ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||||
|
"ECDHE_RSA_AES256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||||
|
"ECDHE_RSA_AES128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||||
|
"ECDHE_ECDSA_AES256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||||
|
"ECDHE_ECDSA_AES128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||||
|
"RSA_AES256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||||
|
"RSA_AES128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||||
|
"ECDHE_RSA_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||||
|
"RSA_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultCipherSuites is the ordered list of all the cipher
|
||||||
|
// suites we want to support by default, assuming AES-NI
|
||||||
|
// (hardware acceleration for AES).
|
||||||
|
var defaultCipherSuitesWithAESNI = []uint16{
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultCipherSuites is the ordered list of all the cipher
|
||||||
|
// suites we want to support by default, assuming lack of
|
||||||
|
// AES-NI (NO hardware acceleration for AES).
|
||||||
|
var defaultCipherSuitesWithoutAESNI = []uint16{
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
}
|
||||||
|
|
||||||
|
// getOptimalDefaultCipherSuites returns an appropriate cipher
|
||||||
|
// suite to use depending on the hardware support for AES.
|
||||||
|
//
|
||||||
|
// See https://github.com/mholt/caddy/issues/1674
|
||||||
|
func getOptimalDefaultCipherSuites() []uint16 {
|
||||||
|
if cpuid.CPU.AesNi() {
|
||||||
|
return defaultCipherSuitesWithAESNI
|
||||||
|
}
|
||||||
|
return defaultCipherSuitesWithoutAESNI
|
||||||
|
}
|
||||||
|
|
||||||
|
// supportedCurves is the unordered map of supported curves.
|
||||||
|
// https://golang.org/pkg/crypto/tls/#CurveID
|
||||||
|
var supportedCurves = map[string]tls.CurveID{
|
||||||
|
"X25519": tls.X25519,
|
||||||
|
"P256": tls.CurveP256,
|
||||||
|
"P384": tls.CurveP384,
|
||||||
|
"P521": tls.CurveP521,
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultCurves is the list of only the curves we want to use
|
||||||
|
// by default, in descending order of preference.
|
||||||
|
//
|
||||||
|
// This list should only include curves which are fast by design
|
||||||
|
// (e.g. X25519) and those for which an optimized assembly
|
||||||
|
// implementation exists (e.g. P256). The latter ones can be
|
||||||
|
// found here:
|
||||||
|
// https://github.com/golang/go/tree/master/src/crypto/elliptic
|
||||||
|
var defaultCurves = []tls.CurveID{
|
||||||
|
tls.X25519,
|
||||||
|
tls.CurveP256,
|
||||||
|
}
|
||||||
|
|
||||||
|
// supportedProtocols is a map of supported protocols.
|
||||||
|
// HTTP/2 only supports TLS 1.2 and higher.
|
||||||
|
var supportedProtocols = map[string]uint16{
|
||||||
|
"tls1.0": tls.VersionTLS10,
|
||||||
|
"tls1.1": tls.VersionTLS11,
|
||||||
|
"tls1.2": tls.VersionTLS12,
|
||||||
|
"tls1.3": tls.VersionTLS13,
|
||||||
|
}
|
||||||
|
|
||||||
|
const automateKey = "automate"
|
74
storage.go
Normal file
74
storage.go
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
package caddy2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/mholt/certmagic"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterModule(Module{
|
||||||
|
Name: "caddy.storage.file_system",
|
||||||
|
New: func() (interface{}, error) { return new(fileStorage), nil },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// StorageConverter is a type that can convert itself
|
||||||
|
// to a valid, usable certmagic.Storage value. The
|
||||||
|
// value might be short-lived.
|
||||||
|
type StorageConverter interface {
|
||||||
|
CertMagicStorage() (certmagic.Storage, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Wrappers other than file_system should be enterprise-only.
|
||||||
|
|
||||||
|
// It may seem trivial to wrap these, but the benefits are:
|
||||||
|
// 1. We don't need to change the actual CertMagic storage implementions
|
||||||
|
// to a structure that is operable with Caddy's config (including JSON
|
||||||
|
// tags), and
|
||||||
|
// 2. We don't need to rely on rely on maintainers of third-party
|
||||||
|
// certmagic.Storage implementations. We can make any certmagic.Storage
|
||||||
|
// work with Caddy this way.
|
||||||
|
|
||||||
|
// fileStorage is a certmagic.Storage wrapper for certmagic.FileStorage.
|
||||||
|
type fileStorage struct {
|
||||||
|
Root string `json:"root"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s fileStorage) CertMagicStorage() (certmagic.Storage, error) {
|
||||||
|
return &certmagic.FileStorage{Path: s.Root}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// homeDir returns the best guess of the current user's home
|
||||||
|
// directory from environment variables. If unknown, "." (the
|
||||||
|
// current directory) is returned instead.
|
||||||
|
func homeDir() string {
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
if home == "" && runtime.GOOS == "windows" {
|
||||||
|
drive := os.Getenv("HOMEDRIVE")
|
||||||
|
path := os.Getenv("HOMEPATH")
|
||||||
|
home = drive + path
|
||||||
|
if drive == "" || path == "" {
|
||||||
|
home = os.Getenv("USERPROFILE")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if home == "" {
|
||||||
|
home = "."
|
||||||
|
}
|
||||||
|
return home
|
||||||
|
}
|
||||||
|
|
||||||
|
// dataDir returns a directory path that is suitable for storage.
|
||||||
|
// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html#variables
|
||||||
|
func dataDir() string {
|
||||||
|
baseDir := filepath.Join(homeDir(), ".local", "share")
|
||||||
|
if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" {
|
||||||
|
baseDir = xdgData
|
||||||
|
}
|
||||||
|
return filepath.Join(baseDir, "caddy")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guard
|
||||||
|
var _ StorageConverter = fileStorage{}
|
Loading…
Reference in a new issue