diff options
author | Matthew Holt <mholt@users.noreply.github.com> | 2020-03-06 23:15:25 -0700 |
---|---|---|
committer | Matthew Holt <mholt@users.noreply.github.com> | 2020-03-06 23:15:25 -0700 |
commit | b8cba62643abf849411856bd92c42b59b98779f4 (patch) | |
tree | 518ddc4db0ce065353fd6f499c8eaf2975b65d13 /modules/caddyhttp | |
parent | 7cca291d62c910c0544f0c0169a8f0c81627e5d3 (diff) |
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.
Diffstat (limited to 'modules/caddyhttp')
-rw-r--r-- | modules/caddyhttp/autohttps.go | 205 | ||||
-rw-r--r-- | modules/caddyhttp/caddyhttp.go | 47 | ||||
-rw-r--r-- | modules/caddyhttp/fileserver/command.go | 2 | ||||
-rw-r--r-- | modules/caddyhttp/httpcache/httpcache.go | 12 | ||||
-rw-r--r-- | modules/caddyhttp/replacer.go | 104 | ||||
-rw-r--r-- | modules/caddyhttp/replacer_test.go | 80 | ||||
-rw-r--r-- | modules/caddyhttp/reverseproxy/command.go | 2 | ||||
-rw-r--r-- | modules/caddyhttp/server.go | 2 |
8 files changed, 328 insertions, 126 deletions
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, "<empty>") 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()), |