summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMatthew Holt <[email protected]>2019-04-25 13:54:48 -0600
committerMatthew Holt <[email protected]>2019-04-25 13:54:48 -0600
commit2d056fbe66849f041a233a0d961639fae3835cbb (patch)
treedc78505933861e01f615470ffc1dd56a852da0b8
parent545f28008e0175491af030f8689cab2112fda9ed (diff)
downloadcaddy-2d056fbe66849f041a233a0d961639fae3835cbb.tar.gz
caddy-2d056fbe66849f041a233a0d961639fae3835cbb.zip
Initial commit of Storage, TLS, and automatic HTTPS implementations
-rw-r--r--admin.go6
-rw-r--r--caddy.go257
-rw-r--r--cmd/caddy2/main.go1
-rw-r--r--listeners.go4
-rw-r--r--modules.go66
-rw-r--r--modules/caddyhttp/caddyhttp.go109
-rw-r--r--modules/caddyhttp/caddylog/log.go2
-rw-r--r--modules/caddyhttp/routes.go19
-rw-r--r--modules/caddyhttp/staticfiles/staticfiles.go4
-rw-r--r--modules/caddytls/acmemanager.go84
-rw-r--r--modules/caddytls/connpolicy.go149
-rw-r--r--modules/caddytls/fileloader.go61
-rw-r--r--modules/caddytls/folderloader.go122
-rw-r--r--modules/caddytls/matchers.go79
-rw-r--r--modules/caddytls/tls.go359
-rw-r--r--storage.go74
16 files changed, 1278 insertions, 118 deletions
diff --git a/admin.go b/admin.go
index 487569b17..a123a64f1 100644
--- a/admin.go
+++ b/admin.go
@@ -119,15 +119,15 @@ func Load(r io.Reader) error {
return err
}
- var cfg Config
+ var cfg *Config
err = json.Unmarshal(buf.Bytes(), &cfg)
if err != nil {
return fmt.Errorf("decoding config: %v", err)
}
- err = Start(cfg)
+ err = Run(cfg)
if err != nil {
- return fmt.Errorf("starting: %v", err)
+ return fmt.Errorf("running: %v", err)
}
return nil
diff --git a/caddy.go b/caddy.go
index 36c9239fc..62b12b636 100644
--- a/caddy.go
+++ b/caddy.go
@@ -8,27 +8,36 @@ import (
"sync"
"sync/atomic"
"time"
+
+ "github.com/mholt/certmagic"
)
-// Start runs Caddy with the given config.
-func Start(cfg Config) error {
+// Run runs Caddy with the given config.
+func Run(cfg *Config) error {
// allow only one call to Start at a time,
// since various calls to LoadModule()
// access shared map moduleInstances
startMu.Lock()
defer startMu.Unlock()
- // prepare the config for use
- cfg.runners = make(map[string]Runner)
+ // because we will need to roll back any state
+ // 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{})
// reset the shared moduleInstances map; but
// keep a temporary reference to the current
// one so we can transfer over any necessary
- // state to the new modules; or in case this
- // function returns an error, we need to put
- // the "old" one back where we found it
- var err error
+ // state to the new modules or to roll back
+ // if necessary
oldModuleInstances := moduleInstances
defer func() {
if err != nil {
@@ -37,109 +46,183 @@ func Start(cfg Config) error {
}()
moduleInstances = make(map[string][]interface{})
- // load (decode) each runner module
- for modName, rawMsg := range cfg.Modules {
- val, err := LoadModule(modName, rawMsg)
- if err != nil {
- return fmt.Errorf("loading module '%s': %v", modName, err)
+ // set up storage and make it CertMagic's default storage, too
+ err = func() error {
+ if cfg.StorageRaw != nil {
+ val, err := LoadModuleInline("system", "caddy.storage", cfg.StorageRaw)
+ 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)
- }
-
- // start the new runners
- for name, r := range cfg.runners {
- err := r.Run()
- if err != nil {
- // TODO: If any one has an error, stop the others
- return fmt.Errorf("%s module: %v", name, err)
+ if cfg.storage == nil {
+ cfg.storage = &certmagic.FileStorage{Path: dataDir()}
}
+ certmagic.Default.Storage = cfg.storage
+
+ return nil
+ }()
+ if err != nil {
+ return err
}
- // shut down down the old runners
- currentCfgMu.Lock()
- if currentCfg != nil {
- for name, r := range currentCfg.runners {
- err := r.Cancel()
+ // Load, Provision, Validate
+ err = func() error {
+ for modName, rawMsg := range cfg.AppsRaw {
+ val, err := LoadModule(modName, rawMsg)
if err != nil {
- log.Printf("[ERROR] cancel %s: %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
}
+
+ // swap old config with the new one, and
+ // roll back this change if anything fails
+ currentCfgMu.Lock()
oldCfg := currentCfg
- currentCfg = &cfg
+ currentCfg = cfg
currentCfgMu.Unlock()
-
- // invoke unload callbacks on old configuration
- for modName := range oldModuleInstances {
- mod, err := GetModule(modName)
+ defer func() {
if err != nil {
- return err
+ currentCfgMu.Lock()
+ currentCfg = oldCfg
+ currentCfgMu.Unlock()
}
- if mod.OnUnload != nil {
- var unloadingState interface{}
- if oldCfg != nil {
- unloadingState = oldCfg.moduleStates[modName]
- }
- err := mod.OnUnload(unloadingState)
+ }()
+
+ // OnLoad
+ err = func() error {
+ for modName, instances := range moduleInstances {
+ mod, err := GetModule(modName)
if err != nil {
- log.Printf("[ERROR] module OnUnload: %s: %v", modName, err)
- continue
+ 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 {
+ return fmt.Errorf("module OnLoad: %s: %v", modName, err)
+ }
+ if modState != nil {
+ cfg.moduleStates[modName] = modState
+ }
}
}
+ return nil
+ }()
+ if err != nil {
+ return err
}
- // invoke load callbacks on new configuration
- for modName, instances := range moduleInstances {
- mod, err := GetModule(modName)
- 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)
+ }
}
- if mod.OnLoad != nil {
- var priorState interface{}
- if oldCfg != nil {
- priorState = oldCfg.moduleStates[modName]
+ 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)
}
- modState, err := mod.OnLoad(instances, priorState)
+ }
+ }
+
+ // OnUnload
+ err = func() error {
+ for modName := range oldModuleInstances {
+ mod, err := GetModule(modName)
if err != nil {
- return fmt.Errorf("module OnLoad: %s: %v", modName, err)
+ return err
}
- if modState != nil {
- cfg.moduleStates[modName] = modState
+ if mod.OnUnload != nil {
+ 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
- listenersMu.Lock()
- for key, info := range listeners {
- if atomic.LoadInt32(&info.usage) == 0 {
- err := info.ln.Close()
- if err != nil {
- log.Printf("[ERROR] closing listener %s: %v", info.ln.Addr(), err)
- continue
+ err = func() error {
+ listenersMu.Lock()
+ for key, info := range listeners {
+ if atomic.LoadInt32(&info.usage) == 0 {
+ err := info.ln.Close()
+ if err != nil {
+ 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
}
-// Runner is a thing that Caddy runs.
-type Runner interface {
- Run() error
- Cancel() error
+// App is a thing that Caddy runs.
+type App interface {
+ Start(Handle) error
+ Stop() error
}
// Config represents a Caddy configuration.
type Config struct {
+ StorageRaw json.RawMessage `json:"storage"`
+ storage certmagic.Storage
+
TestVal string `json:"testval"`
- Modules map[string]json.RawMessage `json:"modules"`
+ AppsRaw map[string]json.RawMessage `json:"apps"`
- // runners stores the decoded Modules values,
+ // apps stores the decoded Apps values,
// keyed by module name.
- runners map[string]Runner
+ apps map[string]App
// moduleStates stores the optional "global" state
// values of every module used by this configuration,
@@ -147,6 +230,34 @@ type Config struct {
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.
type Duration time.Duration
@@ -167,7 +278,7 @@ type CtxKey string
// currentCfg is the currently-loaded configuration.
var (
currentCfg *Config
- currentCfgMu sync.Mutex
+ currentCfgMu sync.RWMutex
)
// moduleInstances stores the individual instantiated
@@ -181,5 +292,5 @@ var (
var moduleInstances = make(map[string][]interface{})
// 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
diff --git a/cmd/caddy2/main.go b/cmd/caddy2/main.go
index 41fff6e8b..4be632ccf 100644
--- a/cmd/caddy2/main.go
+++ b/cmd/caddy2/main.go
@@ -12,6 +12,7 @@ import (
_ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp"
_ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/caddylog"
_ "bitbucket.org/lightcodelabs/caddy2/modules/caddyhttp/staticfiles"
+ _ "bitbucket.org/lightcodelabs/caddy2/modules/caddytls"
_ "bitbucket.org/lightcodelabs/dynamicconfig"
_ "bitbucket.org/lightcodelabs/proxy"
)
diff --git a/listeners.go b/listeners.go
index 7102e7665..db2ebaf56 100644
--- a/listeners.go
+++ b/listeners.go
@@ -111,7 +111,7 @@ func (fcl *fakeCloseListener) fakeClosedErr() error {
Op: "accept",
Net: fcl.Listener.Addr().Network(),
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
// server using it can terminate, while the underlying
// socket is actually left open.
-var ErrFakeClosed = fmt.Errorf("listener 'closed' 😉")
+var errFakeClosed = fmt.Errorf("listener 'closed' 😉")
// listenerUsage pairs a net.Listener with a
// count of how many servers are using it.
diff --git a/modules.go b/modules.go
index ac41f16ae..0fd898e9d 100644
--- a/modules.go
+++ b/modules.go
@@ -9,7 +9,7 @@ import (
"sync"
)
-// Module is a module.
+// Module represents a Caddy module.
type Module struct {
Name string
New func() (interface{}, error)
@@ -21,6 +21,10 @@ func (m Module) String() string { return m.Name }
// RegisterModule registers a module.
func RegisterModule(mod Module) error {
+ if mod.Name == "caddy" {
+ return fmt.Errorf("modules cannot be named 'caddy'")
+ }
+
modulesMu.Lock()
defer modulesMu.Unlock()
@@ -45,7 +49,7 @@ func GetModule(name string) (Module, error) {
// GetModules returns all modules in the given scope/namespace.
// 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
// scopes are not matched (i.e. scope "foo.ba" does not match
// name "foo.bar").
@@ -112,7 +116,10 @@ func Modules() []string {
// 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.
+// 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) {
modulesMu.Lock()
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)
}
+ 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 {
err := validator.Validate()
if err != nil {
@@ -152,27 +166,23 @@ func LoadModule(name string, rawMsg json.RawMessage) (interface{}, error) {
return val, nil
}
-// LoadModuleInlineName loads a module from a JSON raw message which
-// decodes to a map[string]interface{}, and where one of the keys is
-// "_module", which indicates the module name and which 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 keys is moduleNameKey
+// and the corresponding value is the module name as a string, which
+// can be found in the given scope.
//
// 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).
-func LoadModuleInlineName(moduleScope string, raw json.RawMessage) (interface{}, error) {
- var tmp map[string]interface{}
- err := json.Unmarshal(raw, &tmp)
+// 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 LoadModuleInline(moduleNameKey, moduleScope string, raw json.RawMessage) (interface{}, error) {
+ moduleName, err := getModuleNameInline(moduleNameKey, raw)
if err != nil {
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)
if err != nil {
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
}
+// 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
// configurations are valid. This method will be called after New()
// instantiations of modules (if implemented). Validation should
@@ -190,6 +217,13 @@ type Validator interface {
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 (
modules = make(map[string]Module)
modulesMu sync.Mutex
diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go
index 5f1587df5..437e48f77 100644
--- a/modules/caddyhttp/caddyhttp.go
+++ b/modules/caddyhttp/caddyhttp.go
@@ -2,6 +2,7 @@ package caddyhttp
import (
"context"
+ "crypto/tls"
"fmt"
"log"
mathrand "math/rand"
@@ -12,9 +13,13 @@ import (
"time"
"bitbucket.org/lightcodelabs/caddy2"
+ "bitbucket.org/lightcodelabs/caddy2/modules/caddytls"
+ "github.com/mholt/certmagic"
)
func init() {
+ mathrand.Seed(time.Now().UnixNano())
+
err := caddy2.RegisterModule(caddy2.Module{
Name: "http",
New: func() (interface{}, error) { return new(httpModuleConfig), nil },
@@ -22,17 +27,15 @@ func init() {
if err != nil {
log.Fatal(err)
}
-
- mathrand.Seed(time.Now().UnixNano())
}
type httpModuleConfig struct {
- Servers map[string]httpServerConfig `json:"servers"`
+ Servers map[string]*httpServerConfig `json:"servers"`
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
for _, srv := range hc.Servers {
err := srv.Routes.setup()
@@ -43,7 +46,18 @@ func (hc *httpModuleConfig) Run() error {
if err != nil {
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{
ReadTimeout: time.Duration(srv.ReadTimeout),
ReadHeaderTimeout: time.Duration(srv.ReadHeaderTimeout),
@@ -53,13 +67,30 @@ func (hc *httpModuleConfig) Run() error {
for _, lnAddr := range srv.Listen {
network, addrs, err := parseListenAddr(lnAddr)
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 {
ln, err := caddy2.Listen(network, addr)
if err != nil {
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)
hc.servers = append(hc.servers, s)
}
@@ -69,7 +100,7 @@ func (hc *httpModuleConfig) Run() error {
return nil
}
-func (hc *httpModuleConfig) Cancel() error {
+func (hc *httpModuleConfig) Stop() error {
for _, s := range hc.servers {
err := s.Shutdown(context.Background()) // TODO
if err != nil {
@@ -79,13 +110,63 @@ func (hc *httpModuleConfig) Cancel() error {
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 {
- Listen []string `json:"listen"`
- ReadTimeout caddy2.Duration `json:"read_timeout"`
- ReadHeaderTimeout caddy2.Duration `json:"read_header_timeout"`
- HiddenFiles []string `json:"hidden_files"` // TODO:... experimenting with shared/common state
- Routes routeList `json:"routes"`
- Errors httpErrorConfig `json:"errors"`
+ Listen []string `json:"listen"`
+ ReadTimeout caddy2.Duration `json:"read_timeout"`
+ ReadHeaderTimeout caddy2.Duration `json:"read_header_timeout"`
+ HiddenFiles []string `json:"hidden_files"` // TODO:... experimenting with shared/common state
+ Routes routeList `json:"routes"`
+ Errors httpErrorConfig `json:"errors"`
+ TLSConnPolicies caddytls.ConnectionPolicies `json:"tls_connection_policies"`
+ DisableAutoHTTPS bool `json:"disable_auto_https"`
+
+ tlsApp *caddytls.TLS
}
type httpErrorConfig struct {
@@ -95,6 +176,10 @@ type httpErrorConfig struct {
// ServeHTTP is the entry point for all HTTP requests.
func (s httpServerConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ if s.tlsApp.HandleHTTPChallenge(w, r) {
+ return
+ }
+
stack := s.Routes.buildMiddlewareChain(w, r)
err := executeMiddlewareChain(w, r, stack)
if err != nil {
diff --git a/modules/caddyhttp/caddylog/log.go b/modules/caddyhttp/caddylog/log.go
index dc940b3cf..dfc9da58b 100644
--- a/modules/caddyhttp/caddylog/log.go
+++ b/modules/caddyhttp/caddylog/log.go
@@ -64,4 +64,4 @@ func (l *Log) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.H
}
// Interface guard
-var _ caddyhttp.MiddlewareHandler = &Log{}
+var _ caddyhttp.MiddlewareHandler = (*Log)(nil)
diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go
index 95b6ee821..cc26436e6 100644
--- a/modules/caddyhttp/routes.go
+++ b/modules/caddyhttp/routes.go
@@ -32,17 +32,13 @@ func (routes routeList) buildMiddlewareChain(w http.ResponseWriter, r *http.Requ
var responder Handler
mrw := &middlewareResponseWriter{ResponseWriterWrapper: &ResponseWriterWrapper{w}}
+routeLoop:
for _, route := range routes {
- matched := len(route.matchers) == 0
for _, m := range route.matchers {
- if m.Match(r) {
- matched = true
- break
+ if !m.Match(r) {
+ continue routeLoop
}
}
- if !matched {
- continue
- }
for _, m := range route.middleware {
mid = append(mid, func(next HandlerFunc) HandlerFunc {
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 {
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 {
break
}
@@ -83,24 +81,27 @@ func (routes routeList) setup() error {
}
routes[i].matchers = append(routes[i].matchers, val.(RouteMatcher))
}
+ routes[i].Matchers = nil // allow GC to deallocate - TODO: Does this help?
// middleware
for j, rawMsg := range route.Apply {
- mid, err := caddy2.LoadModuleInlineName("http.middleware", rawMsg)
+ mid, err := caddy2.LoadModuleInline("middleware", "http.middleware", rawMsg)
if err != nil {
return fmt.Errorf("loading middleware module in position %d: %v", j, err)
}
routes[i].middleware = append(routes[i].middleware, mid.(MiddlewareHandler))
}
+ routes[i].Apply = nil // allow GC to deallocate - TODO: Does this help?
// responder
if route.Respond != nil {
- resp, err := caddy2.LoadModuleInlineName("http.responders", route.Respond)
+ resp, err := caddy2.LoadModuleInline("responder", "http.responders", route.Respond)
if err != nil {
return fmt.Errorf("loading responder module: %v", err)
}
routes[i].responder = resp.(Handler)
}
+ routes[i].Respond = nil // allow GC to deallocate - TODO: Does this help?
}
return nil
}
diff --git a/modules/caddyhttp/staticfiles/staticfiles.go b/modules/caddyhttp/staticfiles/staticfiles.go
index d1a7a7e90..2a6fe37fc 100644
--- a/modules/caddyhttp/staticfiles/staticfiles.go
+++ b/modules/caddyhttp/staticfiles/staticfiles.go
@@ -10,7 +10,7 @@ import (
func init() {
caddy2.RegisterModule(caddy2.Module{
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
-var _ caddyhttp.Handler = StaticFiles{}
+var _ caddyhttp.Handler = (*StaticFiles)(nil)
diff --git a/modules/caddytls/acmemanager.go b/modules/caddytls/acmemanager.go
new file mode 100644
index 000000000..a7a460a26
--- /dev/null
+++ b/modules/caddytls/acmemanager.go
@@ -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
+}
diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go
new file mode 100644
index 000000000..94000345f
--- /dev/null
+++ b/modules/caddytls/connpolicy.go
@@ -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
+}
diff --git a/modules/caddytls/fileloader.go b/modules/caddytls/fileloader.go
new file mode 100644
index 000000000..fae2275a8
--- /dev/null
+++ b/modules/caddytls/fileloader.go
@@ -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)
diff --git a/modules/caddytls/folderloader.go b/modules/caddytls/folderloader.go
new file mode 100644
index 000000000..9d46502c5
--- /dev/null
+++ b/modules/caddytls/folderloader.go
@@ -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
+}
diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go
new file mode 100644
index 000000000..c376f8744
--- /dev/null
+++ b/modules/caddytls/matchers.go
@@ -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)
+)
diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go
new file mode 100644
index 000000000..43ad957f1
--- /dev/null
+++ b/modules/caddytls/tls.go
@@ -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"
diff --git a/storage.go b/storage.go
new file mode 100644
index 000000000..cb93f5981
--- /dev/null
+++ b/storage.go
@@ -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{}