summaryrefslogtreecommitdiff
path: root/modules/caddyhttp
diff options
context:
space:
mode:
Diffstat (limited to 'modules/caddyhttp')
-rw-r--r--modules/caddyhttp/autohttps.go205
-rw-r--r--modules/caddyhttp/caddyhttp.go47
-rw-r--r--modules/caddyhttp/fileserver/command.go2
-rw-r--r--modules/caddyhttp/httpcache/httpcache.go12
-rw-r--r--modules/caddyhttp/replacer.go104
-rw-r--r--modules/caddyhttp/replacer_test.go80
-rw-r--r--modules/caddyhttp/reverseproxy/command.go2
-rw-r--r--modules/caddyhttp/server.go2
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()),