summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-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
-rw-r--r--modules/caddytls/acmeissuer.go207
-rw-r--r--modules/caddytls/acmemanager.go252
-rw-r--r--modules/caddytls/certselection.go2
-rw-r--r--modules/caddytls/connpolicy.go60
-rw-r--r--modules/caddytls/distributedstek/distributedstek.go2
-rw-r--r--modules/caddytls/tls.go248
-rw-r--r--modules/caddytls/values.go41
-rw-r--r--modules/filestorage/filestorage.go2
-rw-r--r--modules/logging/encoders.go155
-rw-r--r--modules/logging/filewriter.go76
-rw-r--r--modules/logging/netwriter.go23
19 files changed, 1038 insertions, 484 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()),
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 {
+// <common encoder config subdirectives...>
+// }
+//
+// 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 {
+// <common encoder config subdirectives...>
+// }
+//
+// 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 {
+// <common encoder config subdirectives...>
+// }
+//
+// 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 <field_name>
+//
+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 <key>
+// level_key <key>
+// time_key <key>
+// name_key <key>
+// caller_key <key>
+// stacktrace_key <key>
+// line_ending <char>
+// time_format <format>
+// level_format <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 <filename> {
+// roll_disabled
+// roll_size <size>
+// roll_keep <num>
+// roll_keep_for <days>
+// }
+//
+// 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 <address>
+//
+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)
)