From 2d056fbe66849f041a233a0d961639fae3835cbb Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 25 Apr 2019 13:54:48 -0600 Subject: [PATCH] Initial commit of Storage, TLS, and automatic HTTPS implementations --- admin.go | 6 +- caddy.go | 265 ++++++++++---- cmd/caddy2/main.go | 1 + listeners.go | 4 +- modules.go | 66 +++- modules/caddyhttp/caddyhttp.go | 109 +++++- modules/caddyhttp/caddylog/log.go | 2 +- modules/caddyhttp/routes.go | 19 +- modules/caddyhttp/staticfiles/staticfiles.go | 4 +- modules/caddytls/acmemanager.go | 84 +++++ modules/caddytls/connpolicy.go | 149 ++++++++ modules/caddytls/fileloader.go | 61 ++++ modules/caddytls/folderloader.go | 122 +++++++ modules/caddytls/matchers.go | 79 ++++ modules/caddytls/tls.go | 359 +++++++++++++++++++ storage.go | 74 ++++ 16 files changed, 1282 insertions(+), 122 deletions(-) create mode 100644 modules/caddytls/acmemanager.go create mode 100644 modules/caddytls/connpolicy.go create mode 100644 modules/caddytls/fileloader.go create mode 100644 modules/caddytls/folderloader.go create mode 100644 modules/caddytls/matchers.go create mode 100644 modules/caddytls/tls.go create mode 100644 storage.go diff --git a/admin.go b/admin.go index 487569b1..a123a64f 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 36c9239f..62b12b63 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) + if cfg.storage == nil { + cfg.storage = &certmagic.FileStorage{Path: dataDir()} + } + certmagic.Default.Storage = cfg.storage + + return nil + }() + if err != nil { + return err } - // start the new runners - 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) + // Load, Provision, Validate + err = func() error { + for modName, rawMsg := range cfg.AppsRaw { + val, err := LoadModule(modName, rawMsg) + if err != nil { + return fmt.Errorf("loading app module '%s': %v", modName, err) + } + cfg.apps[modName] = val.(App) } + return nil + }() + if err != nil { + return err } - // shut down down the old runners + // swap old config with the new one, and + // roll back this change if anything fails currentCfgMu.Lock() - if currentCfg != nil { - for name, r := range currentCfg.runners { - err := r.Cancel() - if err != nil { - log.Printf("[ERROR] cancel %s: %v", name, err) - } - } - } oldCfg := currentCfg - 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 + } + + // Start + err = func() error { + h := Handle{cfg} + for name, a := range cfg.apps { + err := a.Start(h) + if err != nil { + for otherAppName, otherApp := range cfg.apps { + err := otherApp.Stop() + if err != nil { + log.Printf("aborting app %s: %v", otherAppName, err) + } + } + return fmt.Errorf("%s app module: start: %v", name, err) + } + } + return nil + }() + if err != nil { + return err + } + + // Stop + if oldCfg != nil { + for name, a := range oldCfg.apps { + err := a.Stop() + if err != nil { + log.Printf("[ERROR] stop %s: %v", name, err) } } } - // invoke load callbacks on new configuration - for modName, instances := range moduleInstances { - mod, err := GetModule(modName) - if err != nil { - return err - } - if mod.OnLoad != nil { - var priorState interface{} - if oldCfg != nil { - priorState = oldCfg.moduleStates[modName] - } - 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 { - TestVal string `json:"testval"` - Modules map[string]json.RawMessage `json:"modules"` + StorageRaw json.RawMessage `json:"storage"` + storage certmagic.Storage - // runners stores the decoded Modules values, + TestVal string `json:"testval"` + AppsRaw map[string]json.RawMessage `json:"apps"` + + // apps stores the decoded Apps values, // keyed by module name. - 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 41fff6e8..4be632cc 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 7102e766..db2ebaf5 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 ac41f16a..0fd898e9 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 5f1587df..437e48f7 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 dc940b3c..dfc9da58 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 95b6ee82..cc26436e 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 d1a7a7e9..2a6fe37f 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 00000000..a7a460a2 --- /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 00000000..94000345 --- /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 00000000..fae2275a --- /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 00000000..9d46502c --- /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 00000000..c376f874 --- /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 00000000..43ad957f --- /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 00000000..cb93f598 --- /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{}