From b8cba62643abf849411856bd92c42b59b98779f4 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 6 Mar 2020 23:15:25 -0700 Subject: Refactor for CertMagic v0.10; prepare for PKI app This is a breaking change primarily in two areas: - Storage paths for certificates have changed - Slight changes to JSON config parameters Huge improvements in this commit, to be detailed more in the release notes. The upcoming PKI app will be powered by Smallstep libraries. --- modules/caddyhttp/autohttps.go | 205 +++++++++-------- modules/caddyhttp/caddyhttp.go | 47 ++-- modules/caddyhttp/fileserver/command.go | 2 +- modules/caddyhttp/httpcache/httpcache.go | 12 +- modules/caddyhttp/replacer.go | 104 +++++++-- modules/caddyhttp/replacer_test.go | 80 ++++++- modules/caddyhttp/reverseproxy/command.go | 2 +- modules/caddyhttp/server.go | 2 +- modules/caddytls/acmeissuer.go | 207 +++++++++++++++++ modules/caddytls/acmemanager.go | 252 --------------------- modules/caddytls/certselection.go | 2 +- modules/caddytls/connpolicy.go | 60 +++-- .../caddytls/distributedstek/distributedstek.go | 2 +- modules/caddytls/tls.go | 248 +++++++++++++++----- modules/caddytls/values.go | 41 +++- modules/filestorage/filestorage.go | 2 +- modules/logging/encoders.go | 155 ++++++++++++- modules/logging/filewriter.go | 76 ++++++- modules/logging/netwriter.go | 23 +- 19 files changed, 1038 insertions(+), 484 deletions(-) create mode 100644 modules/caddytls/acmeissuer.go delete mode 100644 modules/caddytls/acmemanager.go (limited to 'modules') diff --git a/modules/caddyhttp/autohttps.go b/modules/caddyhttp/autohttps.go index 8b6fa4d..6b53d39 100644 --- a/modules/caddyhttp/autohttps.go +++ b/modules/caddyhttp/autohttps.go @@ -8,7 +8,7 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddytls" - "github.com/mholt/certmagic" + "github.com/caddyserver/certmagic" "go.uber.org/zap" ) @@ -42,12 +42,10 @@ type AutoHTTPSConfig struct { // enabled. To force automated certificate management // regardless of loaded certificates, set this to true. IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"` - - domainSet map[string]struct{} } // Skipped returns true if name is in skipSlice, which -// should be one of the Skip* fields on ahc. +// should be either the Skip or SkipCerts field on ahc. func (ahc AutoHTTPSConfig) Skipped(name string, skipSlice []string) bool { for _, n := range skipSlice { if name == n { @@ -68,6 +66,8 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er // addresses to the routes that do HTTP->HTTPS redirects lnAddrRedirRoutes := make(map[string]Route) + uniqueDomainsForCerts := make(map[string]struct{}) + for srvName, srv := range app.Servers { // as a prerequisite, provision route matchers; this is // required for all routes on all servers, and must be @@ -116,8 +116,8 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er srv.TLSConnPolicies = defaultConnPolicies } - // find all qualifying domain names in this server - srv.AutoHTTPS.domainSet = make(map[string]struct{}) + // find all qualifying domain names (deduplicated) in this server + serverDomainSet := make(map[string]struct{}) for routeIdx, route := range srv.Routes { for matcherSetIdx, matcherSet := range route.MatcherSets { for matcherIdx, m := range matcherSet { @@ -131,7 +131,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er } if certmagic.HostQualifies(d) && !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.Skip) { - srv.AutoHTTPS.domainSet[d] = struct{}{} + serverDomainSet[d] = struct{}{} } } } @@ -141,10 +141,29 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er // nothing more to do here if there are no // domains that qualify for automatic HTTPS - if len(srv.AutoHTTPS.domainSet) == 0 { + if len(serverDomainSet) == 0 { continue } + // for all the hostnames we found, filter them so we have + // a deduplicated list of names for which to obtain certs + for d := range serverDomainSet { + if !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) { + // if a certificate for this name is already loaded, + // don't obtain another one for it, unless we are + // supposed to ignore loaded certificates + if !srv.AutoHTTPS.IgnoreLoadedCerts && + len(app.tlsApp.AllMatchingCertificates(d)) > 0 { + app.logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded", + zap.String("domain", d), + zap.String("server_name", srvName), + ) + continue + } + uniqueDomainsForCerts[d] = struct{}{} + } + } + // tell the server to use TLS if it is not already doing so if srv.TLSConnPolicies == nil { srv.TLSConnPolicies = defaultConnPolicies @@ -209,6 +228,19 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er } } + // we now have a list of all the unique names for which we need certs; + // turn the set into a slice so that phase 2 can use it + app.allCertDomains = make([]string, 0, len(uniqueDomainsForCerts)) + for d := range uniqueDomainsForCerts { + app.allCertDomains = append(app.allCertDomains, d) + } + + // ensure there is an automation policy to handle these certs + err := app.createAutomationPolicy(ctx) + if err != nil { + return err + } + // if there are HTTP->HTTPS redirects to add, do so now if len(lnAddrRedirRoutes) == 0 { return nil @@ -258,28 +290,78 @@ redirRoutesLoop: return nil } -// automaticHTTPSPhase2 attaches a TLS app pointer to each -// server. This phase must occur after provisioning, and -// at the beginning of the app start, before starting each -// of the servers. -func (app *App) automaticHTTPSPhase2() error { - tlsAppIface, err := app.ctx.App("tls") - if err != nil { - return fmt.Errorf("getting tls app: %v", err) +// createAutomationPolicy ensures that certificates for this app are +// managed properly; for example, it's implied that the HTTPPort +// should also be the port the HTTP challenge is solved on; the same +// for HTTPS port and TLS-ALPN challenge also. We need to tell the +// TLS app to manage these certs by honoring those port configurations, +// so we either find an existing matching automation policy with an +// ACME issuer, or make a new one and append it. +func (app *App) createAutomationPolicy(ctx caddy.Context) error { + var matchingPolicy *caddytls.AutomationPolicy + var acmeIssuer *caddytls.ACMEIssuer + if app.tlsApp.Automation != nil { + // maybe we can find an exisitng one that matches; this is + // useful if the user made a single automation policy to + // set the CA endpoint to a test/staging endpoint (very + // common), but forgot to customize the ports here, while + // setting them in the HTTP app instead (I did this too + // many times) + for _, ap := range app.tlsApp.Automation.Policies { + if len(ap.Hosts) == 0 { + matchingPolicy = ap + break + } + } + } + if matchingPolicy != nil { + // if it has an ACME issuer, maybe we can just use that + acmeIssuer, _ = matchingPolicy.Issuer.(*caddytls.ACMEIssuer) + } + if acmeIssuer.Challenges == nil { + acmeIssuer.Challenges = new(caddytls.ChallengesConfig) + } + if acmeIssuer.Challenges.HTTP == nil { + acmeIssuer.Challenges.HTTP = new(caddytls.HTTPChallengeConfig) + } + if acmeIssuer.Challenges.HTTP.AlternatePort == 0 { + // don't overwrite existing explicit config + acmeIssuer.Challenges.HTTP.AlternatePort = app.HTTPPort + } + if acmeIssuer.Challenges.TLSALPN == nil { + acmeIssuer.Challenges.TLSALPN = new(caddytls.TLSALPNChallengeConfig) + } + if acmeIssuer.Challenges.TLSALPN.AlternatePort == 0 { + // don't overwrite existing explicit config + acmeIssuer.Challenges.TLSALPN.AlternatePort = app.HTTPSPort } - tlsApp := tlsAppIface.(*caddytls.TLS) - // set the tlsApp pointer before starting any - // challenges, since it is required to solve - // the ACME HTTP challenge - for _, srv := range app.Servers { - srv.tlsApp = tlsApp + if matchingPolicy == nil { + // if there was no matching policy, we'll have to append our own + err := app.tlsApp.AddAutomationPolicy(&caddytls.AutomationPolicy{ + Hosts: app.allCertDomains, + Issuer: acmeIssuer, + }) + if err != nil { + return err + } + } else { + // if there was an existing matching policy, we need to reprovision + // its issuer (because we just changed its port settings and it has + // to re-build its stored certmagic config template with the new + // values), then re-assign the Issuer pointer on the policy struct + // because our type assertion changed the address + err := acmeIssuer.Provision(ctx) + if err != nil { + return err + } + matchingPolicy.Issuer = acmeIssuer } return nil } -// automaticHTTPSPhase3 begins certificate management for +// automaticHTTPSPhase2 begins certificate management for // all names in the qualifying domain set for each server. // This phase must occur after provisioning and at the end // of app start, after all the servers have been started. @@ -289,72 +371,17 @@ func (app *App) automaticHTTPSPhase2() error { // first, then our servers would fail to bind to them, // which would be bad, since CertMagic's bindings are // temporary and don't serve the user's sites!). -func (app *App) automaticHTTPSPhase3() error { - // begin managing certificates for enabled servers - for srvName, srv := range app.Servers { - if srv.AutoHTTPS == nil || - srv.AutoHTTPS.Disabled || - len(srv.AutoHTTPS.domainSet) == 0 { - continue - } - - // marshal the domains into a slice - var domains, domainsForCerts []string - for d := range srv.AutoHTTPS.domainSet { - domains = append(domains, d) - if !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) { - // if a certificate for this name is already loaded, - // don't obtain another one for it, unless we are - // supposed to ignore loaded certificates - if !srv.AutoHTTPS.IgnoreLoadedCerts && - len(srv.tlsApp.AllMatchingCertificates(d)) > 0 { - app.logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded", - zap.String("domain", d), - zap.String("server_name", srvName), - ) - continue - } - domainsForCerts = append(domainsForCerts, d) - } - } - - // ensure that these certificates are managed properly; - // for example, it's implied that the HTTPPort should also - // be the port the HTTP challenge is solved on, and so - // for HTTPS port and TLS-ALPN challenge also - we need - // to tell the TLS app to manage these certs by honoring - // those port configurations - acmeManager := &caddytls.ACMEManagerMaker{ - Challenges: &caddytls.ChallengesConfig{ - HTTP: &caddytls.HTTPChallengeConfig{ - AlternatePort: app.HTTPPort, // we specifically want the user-configured port, if any - }, - TLSALPN: &caddytls.TLSALPNChallengeConfig{ - AlternatePort: app.HTTPSPort, // we specifically want the user-configured port, if any - }, - }, - } - if srv.tlsApp.Automation == nil { - srv.tlsApp.Automation = new(caddytls.AutomationConfig) - } - srv.tlsApp.Automation.Policies = append(srv.tlsApp.Automation.Policies, - &caddytls.AutomationPolicy{ - Hosts: domainsForCerts, - Management: acmeManager, - }) - - // manage their certificates - app.logger.Info("enabling automatic TLS certificate management", - zap.Strings("domains", domainsForCerts), - ) - err := srv.tlsApp.Manage(domainsForCerts) - if err != nil { - return fmt.Errorf("%s: managing certificate for %s: %s", srvName, domains, err) - } - - // no longer needed; allow GC to deallocate - srv.AutoHTTPS.domainSet = nil +func (app *App) automaticHTTPSPhase2() error { + if len(app.allCertDomains) == 0 { + return nil } - + app.logger.Info("enabling automatic TLS certificate management", + zap.Strings("domains", app.allCertDomains), + ) + err := app.tlsApp.Manage(app.allCertDomains) + if err != nil { + return fmt.Errorf("managing certificates for %v: %s", app.allCertDomains, err) + } + app.allCertDomains = nil // no longer needed; allow GC to deallocate return nil } diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go index 30c2f79..94b2eee 100644 --- a/modules/caddyhttp/caddyhttp.go +++ b/modules/caddyhttp/caddyhttp.go @@ -28,6 +28,7 @@ import ( "time" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/lucas-clemente/quic-go/http3" "go.uber.org/zap" ) @@ -71,6 +72,16 @@ func init() { // `{http.request.remote.port}` | The port part of the remote client's address // `{http.request.remote}` | The address of the remote client // `{http.request.scheme}` | The request scheme +// `{http.request.tls.version}` | The TLS version name +// `{http.request.tls.cipher_suite}` | The TLS cipher suite +// `{http.request.tls.resumed}` | The TLS connection resumed a previous connection +// `{http.request.tls.proto}` | The negotiated next protocol +// `{http.request.tls.proto_mutual}` | The negotiated next protocol was advertised by the server +// `{http.request.tls.server_name}` | The server name requested by the client, if any +// `{http.request.tls.client.fingerprint}` | The SHA256 checksum of the client certificate +// `{http.request.tls.client.issuer}` | The issuer DN of the client certificate +// `{http.request.tls.client.serial}` | The serial number of the client certificate +// `{http.request.tls.client.subject}` | The subject DN of the client certificate // `{http.request.uri.path.*}` | Parts of the path, split by `/` (0-based from left) // `{http.request.uri.path.dir}` | The directory, excluding leaf filename // `{http.request.uri.path.file}` | The filename of the path, excluding directory @@ -107,6 +118,10 @@ type App struct { ctx caddy.Context logger *zap.Logger + tlsApp *caddytls.TLS + + // used temporarily between phases 1 and 2 of auto HTTPS + allCertDomains []string } // CaddyModule returns the Caddy module information. @@ -119,6 +134,12 @@ func (App) CaddyModule() caddy.ModuleInfo { // Provision sets up the app. func (app *App) Provision(ctx caddy.Context) error { + // store some references + tlsAppIface, err := ctx.App("tls") + if err != nil { + return fmt.Errorf("getting tls app: %v", err) + } + app.tlsApp = tlsAppIface.(*caddytls.TLS) app.ctx = ctx app.logger = ctx.Logger(app) @@ -127,12 +148,14 @@ func (app *App) Provision(ctx caddy.Context) error { // this provisions the matchers for each route, // and prepares auto HTTP->HTTP redirects, and // is required before we provision each server - err := app.automaticHTTPSPhase1(ctx, repl) + err = app.automaticHTTPSPhase1(ctx, repl) if err != nil { return err } + // prepare each server for srvName, srv := range app.Servers { + srv.tlsApp = app.tlsApp srv.logger = app.logger.Named("log") srv.errorLogger = app.logger.Named("log.error") @@ -185,9 +208,14 @@ func (app *App) Provision(ctx caddy.Context) error { if err != nil { return fmt.Errorf("server %s: setting up server error handling routes: %v", srvName, err) } - srv.errorHandlerChain = srv.Errors.Routes.Compile(errorEmptyHandler) } + + // prepare the TLS connection policies + err = srv.TLSConnPolicies.Provision(ctx) + if err != nil { + return fmt.Errorf("server %s: setting up TLS connection policies: %v", srvName, err) + } } return nil @@ -221,14 +249,6 @@ func (app *App) Validate() error { // Start runs the app. It finishes automatic HTTPS if enabled, // including management of certificates. func (app *App) Start() error { - // give each server a pointer to the TLS app; - // this is required before they are started so - // they can solve ACME challenges - err := app.automaticHTTPSPhase2() - if err != nil { - return fmt.Errorf("enabling automatic HTTPS, phase 2: %v", err) - } - for srvName, srv := range app.Servers { s := &http.Server{ ReadTimeout: time.Duration(srv.ReadTimeout), @@ -262,10 +282,7 @@ func (app *App) Start() error { if len(srv.TLSConnPolicies) > 0 && int(listenAddr.StartPort+portOffset) != app.httpPort() { // create TLS listener - tlsCfg, err := srv.TLSConnPolicies.TLSConfig(app.ctx) - if err != nil { - return fmt.Errorf("%s/%s: making TLS configuration: %v", listenAddr.Network, hostport, err) - } + tlsCfg := srv.TLSConnPolicies.TLSConfig(app.ctx) ln = tls.NewListener(ln, tlsCfg) ///////// @@ -301,7 +318,7 @@ func (app *App) Start() error { // finish automatic HTTPS by finally beginning // certificate management - err = app.automaticHTTPSPhase3() + err := app.automaticHTTPSPhase2() if err != nil { return fmt.Errorf("finalizing automatic HTTPS: %v", err) } diff --git a/modules/caddyhttp/fileserver/command.go b/modules/caddyhttp/fileserver/command.go index e553182..fa6560b 100644 --- a/modules/caddyhttp/fileserver/command.go +++ b/modules/caddyhttp/fileserver/command.go @@ -26,7 +26,7 @@ import ( "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" caddycmd "github.com/caddyserver/caddy/v2/cmd" "github.com/caddyserver/caddy/v2/modules/caddyhttp" - "github.com/mholt/certmagic" + "github.com/caddyserver/certmagic" ) func init() { diff --git a/modules/caddyhttp/httpcache/httpcache.go b/modules/caddyhttp/httpcache/httpcache.go index f8bdde8..605a183 100644 --- a/modules/caddyhttp/httpcache/httpcache.go +++ b/modules/caddyhttp/httpcache/httpcache.go @@ -16,6 +16,7 @@ package httpcache import ( "bytes" + "context" "encoding/gob" "fmt" "io" @@ -108,7 +109,8 @@ func (c *Cache) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp return next.ServeHTTP(w, r) } - ctx := getterContext{w, r, next} + getterCtx := getterContext{w, r, next} + ctx := context.WithValue(r.Context(), getterContextCtxKey, getterCtx) // TODO: rigorous performance testing @@ -152,8 +154,8 @@ func (c *Cache) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp return nil } -func (c *Cache) getter(ctx groupcache.Context, key string, dest groupcache.Sink) error { - combo := ctx.(getterContext) +func (c *Cache) getter(ctx context.Context, key string, dest groupcache.Sink) error { + combo := ctx.Value(getterContextCtxKey).(getterContext) // the buffer will store the gob-encoded header, then the body buf := bufPool.Get().(*bytes.Buffer) @@ -228,6 +230,10 @@ var errUncacheable = fmt.Errorf("uncacheable") const groupName = "http_requests" +type ctxKey string + +const getterContextCtxKey ctxKey = "getter_context" + // Interface guards var ( _ caddy.Provisioner = (*Cache)(nil) diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go index cea820d..c9c7522 100644 --- a/modules/caddyhttp/replacer.go +++ b/modules/caddyhttp/replacer.go @@ -15,6 +15,9 @@ package caddyhttp import ( + "crypto/sha256" + "crypto/tls" + "crypto/x509" "fmt" "net" "net/http" @@ -24,14 +27,15 @@ import ( "strings" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddytls" ) func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.ResponseWriter) { httpVars := func(key string) (string, bool) { if req != nil { // query string parameters - if strings.HasPrefix(key, queryReplPrefix) { - vals := req.URL.Query()[key[len(queryReplPrefix):]] + if strings.HasPrefix(key, reqURIQueryReplPrefix) { + vals := req.URL.Query()[key[len(reqURIQueryReplPrefix):]] // always return true, since the query param might // be present only in some requests return strings.Join(vals, ","), true @@ -47,8 +51,8 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo } // cookies - if strings.HasPrefix(key, cookieReplPrefix) { - name := key[len(cookieReplPrefix):] + if strings.HasPrefix(key, reqCookieReplPrefix) { + name := key[len(reqCookieReplPrefix):] for _, cookie := range req.Cookies() { if strings.EqualFold(name, cookie.Name) { // always return true, since the cookie might @@ -58,6 +62,11 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo } } + // http.request.tls. + if strings.HasPrefix(key, reqTLSReplPrefix) { + return getReqTLSReplacement(req, key) + } + switch key { case "http.request.method": return req.Method, true @@ -129,8 +138,8 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo } // hostname labels - if strings.HasPrefix(key, hostLabelReplPrefix) { - idxStr := key[len(hostLabelReplPrefix):] + if strings.HasPrefix(key, reqHostLabelsReplPrefix) { + idxStr := key[len(reqHostLabelsReplPrefix):] idx, err := strconv.Atoi(idxStr) if err != nil { return "", false @@ -150,8 +159,8 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo } // path parts - if strings.HasPrefix(key, pathPartsReplPrefix) { - idxStr := key[len(pathPartsReplPrefix):] + if strings.HasPrefix(key, reqURIPathReplPrefix) { + idxStr := key[len(reqURIPathReplPrefix):] idx, err := strconv.Atoi(idxStr) if err != nil { return "", false @@ -208,12 +217,77 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo repl.Map(httpVars) } +func getReqTLSReplacement(req *http.Request, key string) (string, bool) { + if req == nil || req.TLS == nil { + return "", false + } + + if len(key) < len(reqTLSReplPrefix) { + return "", false + } + + field := strings.ToLower(key[len(reqTLSReplPrefix):]) + + if strings.HasPrefix(field, "client.") { + cert := getTLSPeerCert(req.TLS) + if cert == nil { + return "", false + } + + switch field { + case "client.fingerprint": + return fmt.Sprintf("%x", sha256.Sum256(cert.Raw)), true + case "client.issuer": + return cert.Issuer.String(), true + case "client.serial": + return fmt.Sprintf("%x", cert.SerialNumber), true + case "client.subject": + return cert.Subject.String(), true + default: + return "", false + } + } + + switch field { + case "version": + return caddytls.ProtocolName(req.TLS.Version), true + case "cipher_suite": + return tls.CipherSuiteName(req.TLS.CipherSuite), true + case "resumed": + if req.TLS.DidResume { + return "true", true + } + return "false", true + case "proto": + return req.TLS.NegotiatedProtocol, true + case "proto_mutual": + if req.TLS.NegotiatedProtocolIsMutual { + return "true", true + } + return "false", true + case "server_name": + return req.TLS.ServerName, true + default: + return "", false + } +} + +// getTLSPeerCert retrieves the first peer certificate from a TLS session. +// Returns nil if no peer cert is in use. +func getTLSPeerCert(cs *tls.ConnectionState) *x509.Certificate { + if len(cs.PeerCertificates) == 0 { + return nil + } + return cs.PeerCertificates[0] +} + const ( - queryReplPrefix = "http.request.uri.query." - reqHeaderReplPrefix = "http.request.header." - cookieReplPrefix = "http.request.cookie." - hostLabelReplPrefix = "http.request.host.labels." - pathPartsReplPrefix = "http.request.uri.path." - varsReplPrefix = "http.vars." - respHeaderReplPrefix = "http.response.header." + reqCookieReplPrefix = "http.request.cookie." + reqHeaderReplPrefix = "http.request.header." + reqHostLabelsReplPrefix = "http.request.host.labels." + reqTLSReplPrefix = "http.request.tls." + reqURIPathReplPrefix = "http.request.uri.path." + reqURIQueryReplPrefix = "http.request.uri.query." + respHeaderReplPrefix = "http.response.header." + varsReplPrefix = "http.vars." ) diff --git a/modules/caddyhttp/replacer_test.go b/modules/caddyhttp/replacer_test.go index b355c7f..ea9fa65 100644 --- a/modules/caddyhttp/replacer_test.go +++ b/modules/caddyhttp/replacer_test.go @@ -16,6 +16,9 @@ package caddyhttp import ( "context" + "crypto/tls" + "crypto/x509" + "encoding/pem" "net/http" "net/http/httptest" "testing" @@ -30,6 +33,41 @@ func TestHTTPVarReplacement(t *testing.T) { req = req.WithContext(ctx) req.Host = "example.com:80" req.RemoteAddr = "localhost:1234" + + clientCert := []byte(`-----BEGIN CERTIFICATE----- +MIIB9jCCAV+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1DYWRk +eSBUZXN0IENBMB4XDTE4MDcyNDIxMzUwNVoXDTI4MDcyMTIxMzUwNVowHTEbMBkG +A1UEAwwSY2xpZW50LmxvY2FsZG9tYWluMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB +iQKBgQDFDEpzF0ew68teT3xDzcUxVFaTII+jXH1ftHXxxP4BEYBU4q90qzeKFneF +z83I0nC0WAQ45ZwHfhLMYHFzHPdxr6+jkvKPASf0J2v2HDJuTM1bHBbik5Ls5eq+ +fVZDP8o/VHKSBKxNs8Goc2NTsr5b07QTIpkRStQK+RJALk4x9QIDAQABo0swSTAJ +BgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8A +AAEwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADgYEANSjz2Sk+ +eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV +3Q9fgDkiUod+uIK0IynzIKvw+Cjg+3nx6NQ0IM0zo8c7v398RzB4apbXKZyeeqUH +9fNwfEi+OoXR6s+upSKobCmLGLGi9Na5s5g= +-----END CERTIFICATE-----`) + + block, _ := pem.Decode(clientCert) + if block == nil { + t.Fatalf("failed to decode PEM certificate") + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Fatalf("failed to decode PEM certificate: %v", err) + } + + req.TLS = &tls.ConnectionState{ + Version: tls.VersionTLS13, + HandshakeComplete: true, + ServerName: "foo.com", + CipherSuite: tls.TLS_AES_256_GCM_SHA384, + PeerCertificates: []*x509.Certificate{cert}, + NegotiatedProtocol: "h2", + NegotiatedProtocolIsMutual: true, + } + res := httptest.NewRecorder() addHTTPVarsToReplacer(repl, req, res) @@ -39,7 +77,7 @@ func TestHTTPVarReplacement(t *testing.T) { }{ { input: "{http.request.scheme}", - expect: "http", + expect: "https", }, { input: "{http.request.host}", @@ -69,6 +107,46 @@ func TestHTTPVarReplacement(t *testing.T) { input: "{http.request.host.labels.1}", expect: "example", }, + { + input: "{http.request.tls.cipher_suite}", + expect: "TLS_AES_256_GCM_SHA384", + }, + { + input: "{http.request.tls.proto}", + expect: "h2", + }, + { + input: "{http.request.tls.proto_mutual}", + expect: "true", + }, + { + input: "{http.request.tls.resumed}", + expect: "false", + }, + { + input: "{http.request.tls.server_name}", + expect: "foo.com", + }, + { + input: "{http.request.tls.version}", + expect: "tls1.3", + }, + { + input: "{http.request.tls.client.fingerprint}", + expect: "9f57b7b497cceacc5459b76ac1c3afedbc12b300e728071f55f84168ff0f7702", + }, + { + input: "{http.request.tls.client.issuer}", + expect: "CN=Caddy Test CA", + }, + { + input: "{http.request.tls.client.serial}", + expect: "2", + }, + { + input: "{http.request.tls.client.subject}", + expect: "CN=client.localdomain", + }, } { actual := repl.ReplaceAll(tc.input, "") if actual != tc.expect { diff --git a/modules/caddyhttp/reverseproxy/command.go b/modules/caddyhttp/reverseproxy/command.go index 1638d82..462be1b 100644 --- a/modules/caddyhttp/reverseproxy/command.go +++ b/modules/caddyhttp/reverseproxy/command.go @@ -29,7 +29,7 @@ import ( caddycmd "github.com/caddyserver/caddy/v2/cmd" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" - "github.com/mholt/certmagic" + "github.com/caddyserver/certmagic" ) func init() { diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index 124331d..580449b 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -173,7 +173,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } log("handled request", - zap.String("common_log", repl.ReplaceAll(commonLogFormat, "-")), + zap.String("common_log", repl.ReplaceAll(commonLogFormat, commonLogEmptyValue)), zap.Duration("latency", latency), zap.Int("size", wrec.Size()), zap.Int("status", wrec.Status()), diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go new file mode 100644 index 0000000..36fd76c --- /dev/null +++ b/modules/caddytls/acmeissuer.go @@ -0,0 +1,207 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddytls + +import ( + "context" + "crypto/x509" + "fmt" + "io/ioutil" + "net/url" + "time" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/certmagic" + "github.com/go-acme/lego/v3/challenge" +) + +func init() { + caddy.RegisterModule(ACMEIssuer{}) +} + +// ACMEIssuer makes an ACME manager +// for managing certificates using ACME. +// +// TODO: support multiple ACME endpoints (probably +// requires an array of these structs) - caddy would +// also have to load certs from the backup CAs if the +// first one is expired... +type ACMEIssuer struct { + // The URL to the CA's ACME directory endpoint. + CA string `json:"ca,omitempty"` + + // The URL to the test CA's ACME directory endpoint. + // This endpoint is only used during retries if there + // is a failure using the primary CA. + TestCA string `json:"test_ca,omitempty"` + + // Your email address, so the CA can contact you if necessary. + // Not required, but strongly recommended to provide one so + // you can be reached if there is a problem. Your email is + // not sent to any Caddy mothership or used for any purpose + // other than ACME transactions. + Email string `json:"email,omitempty"` + + // Time to wait before timing out an ACME operation. + ACMETimeout caddy.Duration `json:"acme_timeout,omitempty"` + + // Configures the various ACME challenge types. + Challenges *ChallengesConfig `json:"challenges,omitempty"` + + // An array of files of CA certificates to accept when connecting to the + // ACME CA. Generally, you should only use this if the ACME CA endpoint + // is internal or for development/testing purposes. + TrustedRootsPEMFiles []string `json:"trusted_roots_pem_files,omitempty"` + + rootPool *x509.CertPool + template certmagic.ACMEManager + magic *certmagic.Config +} + +// CaddyModule returns the Caddy module information. +func (ACMEIssuer) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "tls.issuance.acme", + New: func() caddy.Module { return new(ACMEIssuer) }, + } +} + +// Provision sets up m. +func (m *ACMEIssuer) Provision(ctx caddy.Context) error { + // DNS providers + if m.Challenges != nil && m.Challenges.DNSRaw != nil { + val, err := ctx.LoadModule(m.Challenges, "DNSRaw") + if err != nil { + return fmt.Errorf("loading DNS provider module: %v", err) + } + prov, err := val.(DNSProviderMaker).NewDNSProvider() + if err != nil { + return fmt.Errorf("making DNS provider: %v", err) + } + m.Challenges.DNS = prov + } + + // add any custom CAs to trust store + if len(m.TrustedRootsPEMFiles) > 0 { + m.rootPool = x509.NewCertPool() + for _, pemFile := range m.TrustedRootsPEMFiles { + pemData, err := ioutil.ReadFile(pemFile) + if err != nil { + return fmt.Errorf("loading trusted root CA's PEM file: %s: %v", pemFile, err) + } + if !m.rootPool.AppendCertsFromPEM(pemData) { + return fmt.Errorf("unable to add %s to trust pool: %v", pemFile, err) + } + } + } + + m.template = m.makeIssuerTemplate() + + return nil +} + +func (m *ACMEIssuer) makeIssuerTemplate() certmagic.ACMEManager { + template := certmagic.ACMEManager{ + CA: m.CA, + Email: m.Email, + Agreed: true, + CertObtainTimeout: time.Duration(m.ACMETimeout), + TrustedRoots: m.rootPool, + } + + if m.Challenges != nil { + if m.Challenges.HTTP != nil { + template.DisableHTTPChallenge = m.Challenges.HTTP.Disabled + template.AltHTTPPort = m.Challenges.HTTP.AlternatePort + } + if m.Challenges.TLSALPN != nil { + template.DisableTLSALPNChallenge = m.Challenges.TLSALPN.Disabled + template.AltTLSALPNPort = m.Challenges.TLSALPN.AlternatePort + } + template.DNSProvider = m.Challenges.DNS + } + + return template +} + +// SetConfig sets the associated certmagic config for this issuer. +// This is required because ACME needs values from the config in +// order to solve the challenges during issuance. This implements +// the ConfigSetter interface. +func (m *ACMEIssuer) SetConfig(cfg *certmagic.Config) { + m.magic = cfg +} + +// PreCheck implements the certmagic.PreChecker interface. +func (m *ACMEIssuer) PreCheck(names []string, interactive bool) (skip bool, err error) { + return certmagic.NewACMEManager(m.magic, m.template).PreCheck(names, interactive) +} + +// Issue obtains a certificate for the given csr. +func (m *ACMEIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) { + return certmagic.NewACMEManager(m.magic, m.template).Issue(ctx, csr) +} + +// IssuerKey returns the unique issuer key for the configured CA endpoint. +func (m *ACMEIssuer) IssuerKey() string { + return m.template.IssuerKey() // does not need storage and cache +} + +// Revoke revokes the given certificate. +func (m *ACMEIssuer) Revoke(ctx context.Context, cert certmagic.CertificateResource) error { + return certmagic.NewACMEManager(m.magic, m.template).Revoke(ctx, cert) +} + +// onDemandAskRequest makes a request to the ask URL +// to see if a certificate can be obtained for name. +// The certificate request should be denied if this +// returns an error. +func onDemandAskRequest(ask string, name string) error { + askURL, err := url.Parse(ask) + if err != nil { + return fmt.Errorf("parsing ask URL: %v", err) + } + qs := askURL.Query() + qs.Set("domain", name) + askURL.RawQuery = qs.Encode() + + resp, err := onDemandAskClient.Get(askURL.String()) + if err != nil { + return fmt.Errorf("error checking %v to deterine if certificate for hostname '%s' should be allowed: %v", + ask, name, err) + } + resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return fmt.Errorf("certificate for hostname '%s' not allowed; non-2xx status code %d returned from %v", + name, resp.StatusCode, ask) + } + + return nil +} + +// DNSProviderMaker is a type that can create a new DNS provider. +// Modules in the tls.dns namespace should implement this interface. +type DNSProviderMaker interface { + NewDNSProvider() (challenge.Provider, error) +} + +// Interface guards +var ( + _ certmagic.Issuer = (*ACMEIssuer)(nil) + _ certmagic.Revoker = (*ACMEIssuer)(nil) + _ certmagic.PreChecker = (*ACMEIssuer)(nil) + _ ConfigSetter = (*ACMEIssuer)(nil) +) diff --git a/modules/caddytls/acmemanager.go b/modules/caddytls/acmemanager.go deleted file mode 100644 index df73545..0000000 --- a/modules/caddytls/acmemanager.go +++ /dev/null @@ -1,252 +0,0 @@ -// Copyright 2015 Matthew Holt and The Caddy Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddytls - -import ( - "crypto/x509" - "encoding/json" - "fmt" - "io/ioutil" - "net/url" - "time" - - "github.com/caddyserver/caddy/v2" - "github.com/go-acme/lego/v3/challenge" - "github.com/mholt/certmagic" -) - -func init() { - caddy.RegisterModule(ACMEManagerMaker{}) -} - -// ACMEManagerMaker makes an ACME manager -// for managing certificates using ACME. -// If crafting one manually rather than -// through the config-unmarshal process -// (provisioning), be sure to call -// SetDefaults to ensure sane defaults -// after you have configured this struct -// to your liking. -type ACMEManagerMaker struct { - // The URL to the CA's ACME directory endpoint. - CA string `json:"ca,omitempty"` - - // Your email address, so the CA can contact you if necessary. - // Not required, but strongly recommended to provide one so - // you can be reached if there is a problem. Your email is - // not sent to any Caddy mothership or used for any purpose - // other than ACME transactions. - Email string `json:"email,omitempty"` - - // How long before a certificate's expiration to try renewing it. - // Should usually be about 1/3 of certificate lifetime, but long - // enough to give yourself time to troubleshoot problems before - // expiration. Default: 30d - RenewAhead caddy.Duration `json:"renew_ahead,omitempty"` - - // The type of key to generate for the certificate. - // Supported values: `rsa2048`, `rsa4096`, `p256`, `p384`. - KeyType string `json:"key_type,omitempty"` - - // Time to wait before timing out an ACME operation. - ACMETimeout caddy.Duration `json:"acme_timeout,omitempty"` - - // If true, certificates will be requested with MustStaple. Not all - // CAs support this, and there are potentially serious consequences - // of enabling this feature without proper threat modeling. - MustStaple bool `json:"must_staple,omitempty"` - - // Configures the various ACME challenge types. - Challenges *ChallengesConfig `json:"challenges,omitempty"` - - // If true, certificates will be managed "on demand", that is, during - // TLS handshakes or when needed, as opposed to at startup or config - // load. - OnDemand bool `json:"on_demand,omitempty"` - - // Optionally configure a separate storage module associated with this - // manager, instead of using Caddy's global/default-configured storage. - Storage json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"` - - // An array of files of CA certificates to accept when connecting to the - // ACME CA. Generally, you should only use this if the ACME CA endpoint - // is internal or for development/testing purposes. - TrustedRootsPEMFiles []string `json:"trusted_roots_pem_files,omitempty"` - - storage certmagic.Storage - rootPool *x509.CertPool -} - -// CaddyModule returns the Caddy module information. -func (ACMEManagerMaker) CaddyModule() caddy.ModuleInfo { - return caddy.ModuleInfo{ - ID: "tls.management.acme", - New: func() caddy.Module { return new(ACMEManagerMaker) }, - } -} - -// NewManager is a no-op to satisfy the ManagerMaker interface, -// because this manager type is a special case. -func (m ACMEManagerMaker) NewManager(interactive bool) (certmagic.Manager, error) { - return nil, nil -} - -// Provision sets up m. -func (m *ACMEManagerMaker) Provision(ctx caddy.Context) error { - // DNS providers - if m.Challenges != nil && m.Challenges.DNSRaw != nil { - val, err := ctx.LoadModule(m.Challenges, "DNSRaw") - if err != nil { - return fmt.Errorf("loading DNS provider module: %v", err) - } - prov, err := val.(DNSProviderMaker).NewDNSProvider() - if err != nil { - return fmt.Errorf("making DNS provider: %v", err) - } - m.Challenges.DNS = prov - } - - // policy-specific storage implementation - if m.Storage != nil { - val, err := ctx.LoadModule(m, "Storage") - if err != nil { - return fmt.Errorf("loading TLS storage module: %v", err) - } - cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage() - if err != nil { - return fmt.Errorf("creating TLS storage configuration: %v", err) - } - m.storage = cmStorage - } - - // add any custom CAs to trust store - if len(m.TrustedRootsPEMFiles) > 0 { - m.rootPool = x509.NewCertPool() - for _, pemFile := range m.TrustedRootsPEMFiles { - pemData, err := ioutil.ReadFile(pemFile) - if err != nil { - return fmt.Errorf("loading trusted root CA's PEM file: %s: %v", pemFile, err) - } - if !m.rootPool.AppendCertsFromPEM(pemData) { - return fmt.Errorf("unable to add %s to trust pool: %v", pemFile, err) - } - } - } - - return nil -} - -// makeCertMagicConfig converts m into a certmagic.Config, because -// this is a special case where the default manager is the certmagic -// Config and not a separate manager. -func (m *ACMEManagerMaker) makeCertMagicConfig(ctx caddy.Context) certmagic.Config { - storage := m.storage - if storage == nil { - storage = ctx.Storage() - } - - var ond *certmagic.OnDemandConfig - if m.OnDemand { - var onDemand *OnDemandConfig - appVal, err := ctx.App("tls") - if err == nil && appVal.(*TLS).Automation != nil { - onDemand = appVal.(*TLS).Automation.OnDemand - } - - ond = &certmagic.OnDemandConfig{ - DecisionFunc: func(name string) error { - if onDemand != nil { - if onDemand.Ask != "" { - err := onDemandAskRequest(onDemand.Ask, name) - if err != nil { - return err - } - } - // check the rate limiter last because - // doing so makes a reservation - if !onDemandRateLimiter.Allow() { - return fmt.Errorf("on-demand rate limit exceeded") - } - } - return nil - }, - } - } - - cfg := certmagic.Config{ - CA: m.CA, - Email: m.Email, - Agreed: true, - RenewDurationBefore: time.Duration(m.RenewAhead), - KeyType: supportedCertKeyTypes[m.KeyType], - CertObtainTimeout: time.Duration(m.ACMETimeout), - OnDemand: ond, - MustStaple: m.MustStaple, - Storage: storage, - TrustedRoots: m.rootPool, - // TODO: listenHost - } - - if m.Challenges != nil { - if m.Challenges.HTTP != nil { - cfg.DisableHTTPChallenge = m.Challenges.HTTP.Disabled - cfg.AltHTTPPort = m.Challenges.HTTP.AlternatePort - } - if m.Challenges.TLSALPN != nil { - cfg.DisableTLSALPNChallenge = m.Challenges.TLSALPN.Disabled - cfg.AltTLSALPNPort = m.Challenges.TLSALPN.AlternatePort - } - cfg.DNSProvider = m.Challenges.DNS - } - - return cfg -} - -// onDemandAskRequest makes a request to the ask URL -// to see if a certificate can be obtained for name. -// The certificate request should be denied if this -// returns an error. -func onDemandAskRequest(ask string, name string) error { - askURL, err := url.Parse(ask) - if err != nil { - return fmt.Errorf("parsing ask URL: %v", err) - } - qs := askURL.Query() - qs.Set("domain", name) - askURL.RawQuery = qs.Encode() - - resp, err := onDemandAskClient.Get(askURL.String()) - if err != nil { - return fmt.Errorf("error checking %v to deterine if certificate for hostname '%s' should be allowed: %v", - ask, name, err) - } - resp.Body.Close() - - if resp.StatusCode < 200 || resp.StatusCode > 299 { - return fmt.Errorf("certificate for hostname '%s' not allowed; non-2xx status code %d returned from %v", - name, resp.StatusCode, ask) - } - - return nil -} - -// DNSProviderMaker is a type that can create a new DNS provider. -// Modules in the tls.dns namespace should implement this interface. -type DNSProviderMaker interface { - NewDNSProvider() (challenge.Provider, error) -} - -// Interface guard -var _ ManagerMaker = (*ACMEManagerMaker)(nil) diff --git a/modules/caddytls/certselection.go b/modules/caddytls/certselection.go index 0d49eb7..343c740 100644 --- a/modules/caddytls/certselection.go +++ b/modules/caddytls/certselection.go @@ -7,7 +7,7 @@ import ( "math/big" "github.com/caddyserver/caddy/v2" - "github.com/mholt/certmagic" + "github.com/caddyserver/certmagic" ) func init() { diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index cdc9b9d..9c61c72 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -23,8 +23,8 @@ import ( "strings" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/certmagic" "github.com/go-acme/lego/v3/challenge/tlsalpn01" - "github.com/mholt/certmagic" ) // ConnectionPolicies is an ordered group of connection policies; @@ -32,16 +32,15 @@ import ( // 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(ctx caddy.Context) (*tls.Config, error) { - // set up each of the connection policies +// Provision sets up each connection policy. It should be called +// during the Validate() phase, after the TLS app (if any) is +// already set up. +func (cp ConnectionPolicies) Provision(ctx caddy.Context) error { for i, pol := range cp { // matchers mods, err := ctx.LoadModule(pol, "MatchersRaw") if err != nil { - return nil, fmt.Errorf("loading handshake matchers: %v", err) + return fmt.Errorf("loading handshake matchers: %v", err) } for _, modIface := range mods.(map[string]interface{}) { cp[i].matchers = append(cp[i].matchers, modIface.(ConnectionMatcher)) @@ -51,20 +50,24 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) (*tls.Config, error) { if pol.CertSelection != nil { val, err := ctx.LoadModule(pol, "CertSelection") if err != nil { - return nil, fmt.Errorf("loading certificate selection module: %s", err) + return fmt.Errorf("loading certificate selection module: %s", err) } cp[i].certSelector = val.(certmagic.CertificateSelector) } - } - // pre-build standard TLS configs so we don't have to at handshake-time - for i := range cp { - err := cp[i].buildStandardTLSConfig(ctx) + // pre-build standard TLS config so we don't have to at handshake-time + err = pol.buildStandardTLSConfig(ctx) if err != nil { - return nil, fmt.Errorf("connection policy %d: building standard TLS config: %s", i, err) + return fmt.Errorf("connection policy %d: building standard TLS config: %s", i, err) } } + return nil +} + +// TLSConfig returns a standard-lib-compatible TLS configuration which +// selects the first matching policy based on the ClientHello. +func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config { // using ServerName to match policies is extremely common, especially in configs // with lots and lots of different policies; we can fast-track those by indexing // them by SNI, so we don't have to iterate potentially thousands of policies @@ -102,7 +105,7 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) (*tls.Config, error) { return nil, fmt.Errorf("no server TLS configuration available for ClientHello: %+v", hello) }, - }, nil + } } // ConnectionPolicy specifies the logic for handling a TLS handshake. @@ -137,6 +140,10 @@ type ConnectionPolicy struct { // Enables and configures TLS client authentication. ClientAuthentication *ClientAuthentication `json:"client_authentication,omitempty"` + // DefaultSNI becomes the ServerName in a ClientHello if there + // is no policy configured for the empty SNI value. + DefaultSNI string `json:"default_sni,omitempty"` + matchers []ConnectionMatcher certSelector certmagic.CertificateSelector @@ -158,15 +165,24 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error { NextProtos: p.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) + // TODO: I don't love how this works: we pre-build certmagic configs + // so that handshakes are faster. Unfortunately, certmagic configs are + // comprised of settings from both a TLS connection policy and a TLS + // automation policy. The only two fields (as of March 2020; v2 beta 16) + // of a certmagic config that come from the TLS connection policy are + // CertSelection and DefaultServerName, so an automation policy is what + // builds the base certmagic config. Since the pre-built config is + // shared, I don't think we can change any of its fields per-handshake, + // hence the awkward shallow copy (dereference) here and the subsequent + // changing of some of its fields. I'm worried this dereference allocates + // more at handshake-time, but I don't know how to practically pre-build + // a certmagic config for each combination of conn policy + automation policy... + cfg := *tlsApp.getConfigForName(hello.ServerName) if p.certSelector != nil { - newCfg.CertSelection = p.certSelector + cfg.CertSelection = p.certSelector } - return newCfg.GetCertificate(hello) + cfg.DefaultServerName = p.DefaultSNI + return cfg.GetCertificate(hello) }, MinVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS13, @@ -240,8 +256,6 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error { } } - // TODO: other fields - setDefaultTLSParams(cfg) p.stdTLSConfig = cfg diff --git a/modules/caddytls/distributedstek/distributedstek.go b/modules/caddytls/distributedstek/distributedstek.go index cef3733..6fc48a2 100644 --- a/modules/caddytls/distributedstek/distributedstek.go +++ b/modules/caddytls/distributedstek/distributedstek.go @@ -32,7 +32,7 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddytls" - "github.com/mholt/certmagic" + "github.com/caddyserver/certmagic" ) func init() { diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 6be480a..a490ffe 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -23,8 +23,8 @@ import ( "time" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/certmagic" "github.com/go-acme/lego/v3/challenge" - "github.com/mholt/certmagic" "go.uber.org/zap" ) @@ -71,13 +71,15 @@ func (TLS) CaddyModule() caddy.ModuleInfo { // Provision sets up the configuration for the TLS app. func (t *TLS) Provision(ctx caddy.Context) error { + // TODO: Move assets to the new folder structure!! + t.ctx = ctx t.logger = ctx.Logger(t) // set up a new certificate cache; this (re)loads all certificates cacheOpts := certmagic.CacheOptions{ - GetConfigForCert: func(cert certmagic.Certificate) (certmagic.Config, error) { - return t.getConfigForName(cert.Names[0]) + GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) { + return t.getConfigForName(cert.Names[0]), nil }, } if t.Automation != nil { @@ -87,20 +89,25 @@ func (t *TLS) Provision(ctx caddy.Context) error { t.certCache = certmagic.NewCache(cacheOpts) // automation/management policies - if t.Automation != nil { - for i, ap := range t.Automation.Policies { - val, err := ctx.LoadModule(ap, "ManagementRaw") - if err != nil { - return fmt.Errorf("loading TLS automation management module: %s", err) - } - t.Automation.Policies[i].Management = val.(ManagerMaker) + if t.Automation == nil { + t.Automation = new(AutomationConfig) + } + t.Automation.defaultAutomationPolicy = new(AutomationPolicy) + err := t.Automation.defaultAutomationPolicy.provision(t) + if err != nil { + return fmt.Errorf("provisioning default automation policy: %v", err) + } + for i, ap := range t.Automation.Policies { + err := ap.provision(t) + if err != nil { + return fmt.Errorf("provisioning automation policy %d: %v", i, err) } } // certificate loaders val, err := ctx.LoadModule(t, "CertificatesRaw") if err != nil { - return fmt.Errorf("loading TLS automation management module: %s", err) + return fmt.Errorf("loading certificate loader modules: %s", err) } for modName, modIface := range val.(map[string]interface{}) { if modName == "automate" { @@ -216,12 +223,11 @@ func (t *TLS) Manage(names []string) error { // certmagic.Config for each (potentially large) group of names // and call ManageSync/ManageAsync just once for the whole batch for ap, names := range policyToNames { - magic := certmagic.New(t.certCache, ap.makeCertMagicConfig(t.ctx)) var err error if ap.ManageSync { - err = magic.ManageSync(names) + err = ap.magic.ManageSync(names) } else { - err = magic.ManageAsync(t.ctx.Context, names) + err = ap.magic.ManageAsync(t.ctx.Context, names) } if err != nil { return fmt.Errorf("automate: manage %v: %v", names, err) @@ -232,36 +238,54 @@ func (t *TLS) Manage(names []string) error { } // HandleHTTPChallenge ensures that the HTTP challenge is handled for the -// certificate named by r.Host, if it is an HTTP challenge request. +// certificate named by r.Host, if it is an HTTP challenge request. It +// requires that the automation policy for r.Host has an issue of type +// *certmagic.ACMEManager. 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(t.ctx)) - return magic.HandleHTTPChallenge(w, r) + if ap.magic.Issuer == nil { + return false + } + if am, ok := ap.magic.Issuer.(*certmagic.ACMEManager); ok { + return am.HandleHTTPChallenge(w, r) + } + return false +} + +// AddAutomationPolicy provisions and adds ap to the list of the app's +// automation policies. +func (t *TLS) AddAutomationPolicy(ap *AutomationPolicy) error { + if t.Automation == nil { + t.Automation = new(AutomationConfig) + } + err := ap.provision(t) + if err != nil { + return err + } + t.Automation.Policies = append(t.Automation.Policies, ap) + return nil } -func (t *TLS) getConfigForName(name string) (certmagic.Config, error) { +func (t *TLS) getConfigForName(name string) *certmagic.Config { ap := t.getAutomationPolicyForName(name) - return ap.makeCertMagicConfig(t.ctx), nil + return ap.magic } func (t *TLS) getAutomationPolicyForName(name string) *AutomationPolicy { - if t.Automation != nil { - for _, ap := range t.Automation.Policies { - if len(ap.Hosts) == 0 { - // no host filter is an automatic match + for _, ap := range t.Automation.Policies { + if len(ap.Hosts) == 0 { + return ap // no host filter is an automatic match + } + for _, h := range ap.Hosts { + if h == name { return ap } - for _, h := range ap.Hosts { - if h == name { - return ap - } - } } } - return defaultAutomationPolicy + return t.Automation.defaultAutomationPolicy } // AllMatchingCertificates returns the list of all certificates in @@ -309,10 +333,8 @@ func (t *TLS) cleanStorageUnits() { // then clean each storage defined in ACME automation policies if t.Automation != nil { for _, ap := range t.Automation.Policies { - if acmeMgmt, ok := ap.Management.(ACMEManagerMaker); ok { - if acmeMgmt.storage != nil { - certmagic.CleanStorage(acmeMgmt.storage, options) - } + if ap.storage != nil { + certmagic.CleanStorage(ap.storage, options) } } } @@ -355,23 +377,56 @@ type AutomationConfig struct { OCSPCheckInterval caddy.Duration `json:"ocsp_interval,omitempty"` // Every so often, Caddy will scan all loaded, managed - // certificates for expiration. Certificates which are - // about 2/3 into their valid lifetime are due for - // renewal. This setting changes how frequently the scan - // is performed. If your certificate lifetimes are very - // short (less than ~1 week), you should customize this. + // certificates for expiration. This setting changes how + // frequently the scan for expiring certificates is + // performed. If your certificate lifetimes are very + // short (less than ~24 hours), you should set this to + // a low value. RenewCheckInterval caddy.Duration `json:"renew_interval,omitempty"` + + defaultAutomationPolicy *AutomationPolicy } // AutomationPolicy designates the policy for automating the // management (obtaining, renewal, and revocation) of managed // TLS certificates. +// +// An AutomationPolicy value is not valid until it has been +// provisioned; use the `AddAutomationPolicy()` method on the +// TLS app to properly provision a new policy. type AutomationPolicy struct { // Which hostnames this policy applies to. Hosts []string `json:"hosts,omitempty"` - // How to manage certificates. - ManagementRaw json.RawMessage `json:"management,omitempty" caddy:"namespace=tls.management inline_key=module"` + // The module that will issue certificates. Default: acme + IssuerRaw json.RawMessage `json:"issuer,omitempty" caddy:"namespace=tls.issuance inline_key=module"` + + // If true, certificates will be requested with MustStaple. Not all + // CAs support this, and there are potentially serious consequences + // of enabling this feature without proper threat modeling. + MustStaple bool `json:"must_staple,omitempty"` + + // How long before a certificate's expiration to try renewing it, + // as a function of its total lifetime. As a general and conservative + // rule, it is a good idea to renew a certificate when it has about + // 1/3 of its total lifetime remaining. This utilizes the majority + // of the certificate's lifetime while still saving time to + // troubleshoot problems. However, for extremely short-lived certs, + // you may wish to increase the ratio to ~1/2. + RenewalWindowRatio float64 `json:"renewal_window_ratio,omitempty"` + + // The type of key to generate for certificates. + // Supported values: `ed25519`, `p256`, `p384`, `rsa2048`, `rsa4096`. + KeyType string `json:"key_type,omitempty"` + + // Optionally configure a separate storage module associated with this + // manager, instead of using Caddy's global/default-configured storage. + StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"` + + // If true, certificates will be managed "on demand", that is, during + // TLS handshakes or when needed, as opposed to at startup or config + // load. + OnDemand bool `json:"on_demand,omitempty"` // If true, certificate management will be conducted // in the foreground; this will block config reloads @@ -381,23 +436,96 @@ type AutomationPolicy struct { // of your control. Default: false ManageSync bool `json:"manage_sync,omitempty"` - Management ManagerMaker `json:"-"` + Issuer certmagic.Issuer `json:"-"` + + magic *certmagic.Config + storage certmagic.Storage } -// makeCertMagicConfig converts ap into a CertMagic config. Passing onDemand -// is necessary because the automation policy does not have convenient access -// to the TLS app's global on-demand policies; -func (ap AutomationPolicy) makeCertMagicConfig(ctx caddy.Context) certmagic.Config { - // default manager (ACME) is a special case because of how CertMagic is designed - // TODO: refactor certmagic so that ACME manager is not a special case by extracting - // its config fields out of the certmagic.Config struct, or something... - if acmeMgmt, ok := ap.Management.(*ACMEManagerMaker); ok { - return acmeMgmt.makeCertMagicConfig(ctx) +// provision converts ap into a CertMagic config. +func (ap *AutomationPolicy) provision(tlsApp *TLS) error { + // policy-specific storage implementation + if ap.StorageRaw != nil { + val, err := tlsApp.ctx.LoadModule(ap, "StorageRaw") + if err != nil { + return fmt.Errorf("loading TLS storage module: %v", err) + } + cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage() + if err != nil { + return fmt.Errorf("creating TLS storage configuration: %v", err) + } + ap.storage = cmStorage + } + + var ond *certmagic.OnDemandConfig + if ap.OnDemand { + var onDemand *OnDemandConfig + if tlsApp.Automation != nil { + onDemand = tlsApp.Automation.OnDemand + } + + ond = &certmagic.OnDemandConfig{ + DecisionFunc: func(name string) error { + if onDemand != nil { + if onDemand.Ask != "" { + err := onDemandAskRequest(onDemand.Ask, name) + if err != nil { + return err + } + } + // check the rate limiter last because + // doing so makes a reservation + if !onDemandRateLimiter.Allow() { + return fmt.Errorf("on-demand rate limit exceeded") + } + } + return nil + }, + } + } + + keySource := certmagic.StandardKeyGenerator{ + KeyType: supportedCertKeyTypes[ap.KeyType], + } + + storage := ap.storage + if storage == nil { + storage = tlsApp.ctx.Storage() } - return certmagic.Config{ - NewManager: ap.Management.NewManager, + template := certmagic.Config{ + MustStaple: ap.MustStaple, + RenewalWindowRatio: ap.RenewalWindowRatio, + KeySource: keySource, + OnDemand: ond, + Storage: storage, } + cfg := certmagic.New(tlsApp.certCache, template) + ap.magic = cfg + + if ap.IssuerRaw != nil { + val, err := tlsApp.ctx.LoadModule(ap, "IssuerRaw") + if err != nil { + return fmt.Errorf("loading TLS automation management module: %s", err) + } + ap.Issuer = val.(certmagic.Issuer) + } + + // sometimes issuers may need the parent certmagic.Config in + // order to function properly (for example, ACMEIssuer needs + // access to the correct storage and cache so it can solve + // ACME challenges -- it's an annoying, inelegant circular + // dependency that I don't know how to resolve nicely!) + if configger, ok := ap.Issuer.(ConfigSetter); ok { + configger.SetConfig(cfg) + } + + cfg.Issuer = ap.Issuer + if rev, ok := ap.Issuer.(certmagic.Revoker); ok { + cfg.Revoker = rev + } + + return nil } // ChallengesConfig configures the ACME challenges. @@ -482,11 +610,6 @@ type RateLimit struct { Burst int `json:"burst,omitempty"` } -// ManagerMaker makes a certificate manager. -type ManagerMaker interface { - NewManager(interactive bool) (certmagic.Manager, error) -} - // AutomateLoader is a no-op certificate loader module // that is treated as a special case: it uses this app's // automation features to load certificates for the @@ -502,6 +625,15 @@ func (AutomateLoader) CaddyModule() caddy.ModuleInfo { } } +// ConfigSetter is implemented by certmagic.Issuers that +// need access to a parent certmagic.Config as part of +// their provisioning phase. For example, the ACMEIssuer +// requires a config so it can access storage and the +// cache to solve ACME challenges. +type ConfigSetter interface { + SetConfig(cfg *certmagic.Config) +} + // These perpetual values are used for on-demand TLS. var ( onDemandRateLimiter = certmagic.NewRateLimiter(0, 0) @@ -521,8 +653,6 @@ var ( storageCleanMu sync.Mutex ) -var defaultAutomationPolicy = &AutomationPolicy{Management: new(ACMEManagerMaker)} - // Interface guards var ( _ caddy.App = (*TLS)(nil) diff --git a/modules/caddytls/values.go b/modules/caddytls/values.go index 21a6b33..40b0de0 100644 --- a/modules/caddytls/values.go +++ b/modules/caddytls/values.go @@ -17,8 +17,9 @@ package caddytls import ( "crypto/tls" "crypto/x509" + "fmt" - "github.com/go-acme/lego/v3/certcrypto" + "github.com/caddyserver/certmagic" "github.com/klauspost/cpuid" ) @@ -101,11 +102,12 @@ var SupportedCurves = map[string]tls.CurveID{ // supportedCertKeyTypes is all the key types that are supported // for certificates that are obtained through ACME. -var supportedCertKeyTypes = map[string]certcrypto.KeyType{ - "rsa_2048": certcrypto.RSA2048, - "rsa_4096": certcrypto.RSA4096, - "ec_p256": certcrypto.EC256, - "ec_p384": certcrypto.EC384, +var supportedCertKeyTypes = map[string]certmagic.KeyType{ + "rsa2048": certmagic.RSA2048, + "rsa4096": certmagic.RSA4096, + "p256": certmagic.P256, + "p384": certmagic.P384, + "ed25519": certmagic.ED25519, } // defaultCurves is the list of only the curves we want to use @@ -127,9 +129,36 @@ var SupportedProtocols = map[string]uint16{ "tls1.3": tls.VersionTLS13, } +// unsupportedProtocols is a map of unsupported protocols. +// Used for logging only, not enforcement. +var unsupportedProtocols = map[string]uint16{ + "ssl3.0": tls.VersionSSL30, + "tls1.0": tls.VersionTLS10, + "tls1.1": tls.VersionTLS11, +} + // publicKeyAlgorithms is the map of supported public key algorithms. var publicKeyAlgorithms = map[string]x509.PublicKeyAlgorithm{ "rsa": x509.RSA, "dsa": x509.DSA, "ecdsa": x509.ECDSA, } + +// ProtocolName returns the standard name for the passed protocol version ID +// (e.g. "TLS1.3") or a fallback representation of the ID value if the version +// is not supported. +func ProtocolName(id uint16) string { + for k, v := range SupportedProtocols { + if v == id { + return k + } + } + + for k, v := range unsupportedProtocols { + if v == id { + return k + } + } + + return fmt.Sprintf("0x%04x", id) +} diff --git a/modules/filestorage/filestorage.go b/modules/filestorage/filestorage.go index 55607ba..0b2d79a 100644 --- a/modules/filestorage/filestorage.go +++ b/modules/filestorage/filestorage.go @@ -17,7 +17,7 @@ package filestorage import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" - "github.com/mholt/certmagic" + "github.com/caddyserver/certmagic" ) func init() { diff --git a/modules/logging/encoders.go b/modules/logging/encoders.go index 49ad11a..bd120d5 100644 --- a/modules/logging/encoders.go +++ b/modules/logging/encoders.go @@ -21,6 +21,7 @@ import ( "time" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" zaplogfmt "github.com/jsternberg/zap-logfmt" "go.uber.org/zap" "go.uber.org/zap/buffer" @@ -31,7 +32,7 @@ func init() { caddy.RegisterModule(ConsoleEncoder{}) caddy.RegisterModule(JSONEncoder{}) caddy.RegisterModule(LogfmtEncoder{}) - caddy.RegisterModule(StringEncoder{}) + caddy.RegisterModule(SingleFieldEncoder{}) } // ConsoleEncoder encodes log entries that are mostly human-readable. @@ -54,6 +55,27 @@ func (ce *ConsoleEncoder) Provision(_ caddy.Context) error { return nil } +// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: +// +// console { +// +// } +// +// See the godoc on the LogEncoderConfig type for the syntax of +// subdirectives that are common to most/all encoders. +func (ce *ConsoleEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + if d.NextArg() { + return d.ArgErr() + } + err := ce.LogEncoderConfig.UnmarshalCaddyfile(d) + if err != nil { + return err + } + } + return nil +} + // JSONEncoder encodes entries as JSON. type JSONEncoder struct { zapcore.Encoder `json:"-"` @@ -74,6 +96,27 @@ func (je *JSONEncoder) Provision(_ caddy.Context) error { return nil } +// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: +// +// json { +// +// } +// +// See the godoc on the LogEncoderConfig type for the syntax of +// subdirectives that are common to most/all encoders. +func (je *JSONEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + if d.NextArg() { + return d.ArgErr() + } + err := je.LogEncoderConfig.UnmarshalCaddyfile(d) + if err != nil { + return err + } + } + return nil +} + // LogfmtEncoder encodes log entries as logfmt: // https://www.brandur.org/logfmt type LogfmtEncoder struct { @@ -95,26 +138,47 @@ func (lfe *LogfmtEncoder) Provision(_ caddy.Context) error { return nil } -// StringEncoder writes a log entry that consists entirely +// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: +// +// logfmt { +// +// } +// +// See the godoc on the LogEncoderConfig type for the syntax of +// subdirectives that are common to most/all encoders. +func (lfe *LogfmtEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + if d.NextArg() { + return d.ArgErr() + } + err := lfe.LogEncoderConfig.UnmarshalCaddyfile(d) + if err != nil { + return err + } + } + return nil +} + +// SingleFieldEncoder writes a log entry that consists entirely // of a single string field in the log entry. This is useful // for custom, self-encoded log entries that consist of a // single field in the structured log entry. -type StringEncoder struct { +type SingleFieldEncoder struct { zapcore.Encoder `json:"-"` FieldName string `json:"field,omitempty"` FallbackRaw json.RawMessage `json:"fallback,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"` } // CaddyModule returns the Caddy module information. -func (StringEncoder) CaddyModule() caddy.ModuleInfo { +func (SingleFieldEncoder) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ - ID: "caddy.logging.encoders.string", - New: func() caddy.Module { return new(StringEncoder) }, + ID: "caddy.logging.encoders.single_field", + New: func() caddy.Module { return new(SingleFieldEncoder) }, } } // Provision sets up the encoder. -func (se *StringEncoder) Provision(ctx caddy.Context) error { +func (se *SingleFieldEncoder) Provision(ctx caddy.Context) error { if se.FallbackRaw != nil { val, err := ctx.LoadModule(se, "FallbackRaw") if err != nil { @@ -132,16 +196,16 @@ func (se *StringEncoder) Provision(ctx caddy.Context) error { // necessary because we implement our own EncodeEntry, // and if we simply let the embedded encoder's Clone // be promoted, it would return a clone of that, and -// we'd lose our StringEncoder's EncodeEntry. -func (se StringEncoder) Clone() zapcore.Encoder { - return StringEncoder{ +// we'd lose our SingleFieldEncoder's EncodeEntry. +func (se SingleFieldEncoder) Clone() zapcore.Encoder { + return SingleFieldEncoder{ Encoder: se.Encoder.Clone(), FieldName: se.FieldName, } } // EncodeEntry partially implements the zapcore.Encoder interface. -func (se StringEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) { +func (se SingleFieldEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) { for _, f := range fields { if f.Key == se.FieldName { buf := bufferpool.Get() @@ -158,6 +222,21 @@ func (se StringEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) ( return se.Encoder.EncodeEntry(ent, fields) } +// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: +// +// single_field +// +func (se *SingleFieldEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + var fieldName string + if !d.AllArgs(&fieldName) { + return d.ArgErr() + } + se.FieldName = d.Val() + } + return nil +} + // LogEncoderConfig holds configuration common to most encoders. type LogEncoderConfig struct { MessageKey *string `json:"message_key,omitempty"` @@ -172,6 +251,53 @@ type LogEncoderConfig struct { LevelFormat string `json:"level_format,omitempty"` } +// UnmarshalCaddyfile populates the struct from Caddyfile tokens. Syntax: +// +// { +// message_key +// level_key +// time_key +// name_key +// caller_key +// stacktrace_key +// line_ending +// time_format +// level_format +// } +// +func (lec *LogEncoderConfig) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for nesting := d.Nesting(); d.NextBlock(nesting); { + subdir := d.Val() + var arg string + if !d.AllArgs(&arg) { + return d.ArgErr() + } + switch subdir { + case "message_key": + lec.MessageKey = &arg + case "level_key": + lec.LevelKey = &arg + case "time_key": + lec.TimeKey = &arg + case "name_key": + lec.NameKey = &arg + case "caller_key": + lec.CallerKey = &arg + case "stacktrace_key": + lec.StacktraceKey = &arg + case "line_ending": + lec.LineEnding = &arg + case "time_format": + lec.TimeFormat = arg + case "level_format": + lec.LevelFormat = arg + default: + return d.Errf("unrecognized subdirective %s", subdir) + } + } + return nil +} + // ZapcoreEncoderConfig returns the equivalent zapcore.EncoderConfig. // If lec is nil, zap.NewProductionEncoderConfig() is returned. func (lec *LogEncoderConfig) ZapcoreEncoderConfig() zapcore.EncoderConfig { @@ -263,5 +389,10 @@ var ( _ zapcore.Encoder = (*ConsoleEncoder)(nil) _ zapcore.Encoder = (*JSONEncoder)(nil) _ zapcore.Encoder = (*LogfmtEncoder)(nil) - _ zapcore.Encoder = (*StringEncoder)(nil) + _ zapcore.Encoder = (*SingleFieldEncoder)(nil) + + _ caddyfile.Unmarshaler = (*ConsoleEncoder)(nil) + _ caddyfile.Unmarshaler = (*JSONEncoder)(nil) + _ caddyfile.Unmarshaler = (*LogfmtEncoder)(nil) + _ caddyfile.Unmarshaler = (*SingleFieldEncoder)(nil) ) diff --git a/modules/logging/filewriter.go b/modules/logging/filewriter.go index f17f975..e9c2dd8 100644 --- a/modules/logging/filewriter.go +++ b/modules/logging/filewriter.go @@ -19,8 +19,12 @@ import ( "io" "os" "path/filepath" + "strconv" + "time" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/dustin/go-humanize" "gopkg.in/natefinch/lumberjack.v2" ) @@ -125,7 +129,77 @@ func (fw FileWriter) OpenWriter() (io.WriteCloser, error) { return os.OpenFile(fw.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666) } +// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: +// +// file { +// roll_disabled +// roll_size +// roll_keep +// roll_keep_for +// } +// +// The roll_size value will be rounded down to number of megabytes (MiB). +// The roll_keep_for duration will be rounded down to number of days. +func (fw *FileWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + if !d.NextArg() { + return d.ArgErr() + } + fw.Filename = d.Val() + if d.NextArg() { + return d.ArgErr() + } + + for d.NextBlock(0) { + switch d.Val() { + case "roll_disabled": + var f bool + fw.Roll = &f + if d.NextArg() { + return d.ArgErr() + } + + case "roll_size": + var sizeStr string + if !d.AllArgs(&sizeStr) { + return d.ArgErr() + } + size, err := humanize.ParseBytes(sizeStr) + if err != nil { + return d.Errf("parsing size: %v", err) + } + fw.RollSizeMB = int(size) / 1024 / 1024 + + case "roll_keep": + var keepStr string + if !d.AllArgs(&keepStr) { + return d.ArgErr() + } + keep, err := strconv.Atoi(keepStr) + if err != nil { + return d.Errf("parsing roll_keep number: %v", err) + } + fw.RollKeep = keep + + case "roll_keep_for": + var keepForStr string + if !d.AllArgs(&keepForStr) { + return d.ArgErr() + } + keepFor, err := time.ParseDuration(keepForStr) + if err != nil { + return d.Errf("parsing roll_keep_for duration: %v", err) + } + fw.RollKeepDays = int(keepFor.Hours()) / 24 + } + } + } + return nil +} + // Interface guards var ( - _ caddy.Provisioner = (*FileWriter)(nil) + _ caddy.Provisioner = (*FileWriter)(nil) + _ caddy.WriterOpener = (*FileWriter)(nil) + _ caddyfile.Unmarshaler = (*FileWriter)(nil) ) diff --git a/modules/logging/netwriter.go b/modules/logging/netwriter.go index 1df80b6..7d2dafa 100644 --- a/modules/logging/netwriter.go +++ b/modules/logging/netwriter.go @@ -20,6 +20,7 @@ import ( "net" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) func init() { @@ -75,8 +76,26 @@ func (nw NetWriter) OpenWriter() (io.WriteCloser, error) { return net.Dial(nw.addr.Network, nw.addr.JoinHostPort(0)) } +// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: +// +// net
+// +func (nw *NetWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + if !d.NextArg() { + return d.ArgErr() + } + nw.Address = d.Val() + if d.NextArg() { + return d.ArgErr() + } + } + return nil +} + // Interface guards var ( - _ caddy.Provisioner = (*NetWriter)(nil) - _ caddy.WriterOpener = (*NetWriter)(nil) + _ caddy.Provisioner = (*NetWriter)(nil) + _ caddy.WriterOpener = (*NetWriter)(nil) + _ caddyfile.Unmarshaler = (*NetWriter)(nil) ) -- cgit v1.2.3