diff options
| author | Matthew Holt <mholt@users.noreply.github.com> | 2019-04-25 13:54:48 -0600 | 
|---|---|---|
| committer | Matthew Holt <mholt@users.noreply.github.com> | 2019-04-25 13:54:48 -0600 | 
| commit | 2d056fbe66849f041a233a0d961639fae3835cbb (patch) | |
| tree | dc78505933861e01f615470ffc1dd56a852da0b8 | |
| parent | 545f28008e0175491af030f8689cab2112fda9ed (diff) | |
Initial commit of Storage, TLS, and automatic HTTPS implementations
| -rw-r--r-- | admin.go | 6 | ||||
| -rw-r--r-- | caddy.go | 257 | ||||
| -rw-r--r-- | cmd/caddy2/main.go | 1 | ||||
| -rw-r--r-- | listeners.go | 4 | ||||
| -rw-r--r-- | modules.go | 66 | ||||
| -rw-r--r-- | modules/caddyhttp/caddyhttp.go | 109 | ||||
| -rw-r--r-- | modules/caddyhttp/caddylog/log.go | 2 | ||||
| -rw-r--r-- | modules/caddyhttp/routes.go | 19 | ||||
| -rw-r--r-- | modules/caddyhttp/staticfiles/staticfiles.go | 4 | ||||
| -rw-r--r-- | modules/caddytls/acmemanager.go | 84 | ||||
| -rw-r--r-- | modules/caddytls/connpolicy.go | 149 | ||||
| -rw-r--r-- | modules/caddytls/fileloader.go | 61 | ||||
| -rw-r--r-- | modules/caddytls/folderloader.go | 122 | ||||
| -rw-r--r-- | modules/caddytls/matchers.go | 79 | ||||
| -rw-r--r-- | modules/caddytls/tls.go | 359 | ||||
| -rw-r--r-- | storage.go | 74 | 
16 files changed, 1278 insertions, 118 deletions
| @@ -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 @@ -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 41fff6e..4be632c 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 7102e76..db2ebaf 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. @@ -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 5f1587d..437e48f 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 dc940b3..dfc9da5 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 95b6ee8..cc26436 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 d1a7a7e..2a6fe37 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 0000000..a7a460a --- /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 0000000..9400034 --- /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 0000000..fae2275 --- /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 0000000..9d46502 --- /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 0000000..c376f87 --- /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 0000000..43ad957 --- /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 0000000..cb93f59 --- /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{} | 
