mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-16 21:56:40 -05:00
Refactor for CertMagic v0.10; prepare for PKI app
This is a breaking change primarily in two areas: - Storage paths for certificates have changed - Slight changes to JSON config parameters Huge improvements in this commit, to be detailed more in the release notes. The upcoming PKI app will be powered by Smallstep libraries.
This commit is contained in:
parent
7cca291d62
commit
b8cba62643
36 changed files with 1944 additions and 618 deletions
|
@ -44,7 +44,7 @@ This is the development branch for Caddy 2.
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<b>Powered by</b>
|
<b>Powered by</b>
|
||||||
<br>
|
<br>
|
||||||
<a href="https://github.com/mholt/certmagic"><img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250"></a>
|
<a href="https://github.com/caddyserver/certmagic"><img src="https://user-images.githubusercontent.com/1128849/49704830-49d37200-fbd5-11e8-8385-767e0cd033c3.png" alt="CertMagic" width="250"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Build from source
|
## Build from source
|
||||||
|
|
12
caddy.go
12
caddy.go
|
@ -32,7 +32,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mholt/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -382,14 +382,12 @@ func run(newCfg *Config, start bool) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load, Provision, Validate each app and their submodules
|
// Load and Provision each app and their submodules
|
||||||
err = func() error {
|
err = func() error {
|
||||||
appsIface, err := ctx.LoadModule(newCfg, "AppsRaw")
|
for appName := range newCfg.AppsRaw {
|
||||||
if err != nil {
|
if _, err := ctx.App(appName); err != nil {
|
||||||
return fmt.Errorf("loading app modules: %v", err)
|
return err
|
||||||
}
|
}
|
||||||
for appName, appIface := range appsIface.(map[string]interface{}) {
|
|
||||||
newCfg.apps[appName] = appIface.(App)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -23,7 +23,7 @@ import (
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"github.com/mholt/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mapAddressToServerBlocks returns a map of listener address to list of server
|
// mapAddressToServerBlocks returns a map of listener address to list of server
|
||||||
|
|
|
@ -24,8 +24,10 @@ import (
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -37,6 +39,7 @@ func init() {
|
||||||
RegisterHandlerDirective("route", parseRoute)
|
RegisterHandlerDirective("route", parseRoute)
|
||||||
RegisterHandlerDirective("handle", parseSegmentAsSubroute)
|
RegisterHandlerDirective("handle", parseSegmentAsSubroute)
|
||||||
RegisterDirective("handle_errors", parseHandleErrors)
|
RegisterDirective("handle_errors", parseHandleErrors)
|
||||||
|
RegisterDirective("log", parseLog)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseBind parses the bind directive. Syntax:
|
// parseBind parses the bind directive. Syntax:
|
||||||
|
@ -108,7 +111,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||||
var cp *caddytls.ConnectionPolicy
|
var cp *caddytls.ConnectionPolicy
|
||||||
var fileLoader caddytls.FileLoader
|
var fileLoader caddytls.FileLoader
|
||||||
var folderLoader caddytls.FolderLoader
|
var folderLoader caddytls.FolderLoader
|
||||||
var mgr caddytls.ACMEManagerMaker
|
var mgr caddytls.ACMEIssuer
|
||||||
|
|
||||||
// fill in global defaults, if configured
|
// fill in global defaults, if configured
|
||||||
if email := h.Option("email"); email != nil {
|
if email := h.Option("email"); email != nil {
|
||||||
|
@ -307,9 +310,9 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// automation policy
|
// automation policy
|
||||||
if !reflect.DeepEqual(mgr, caddytls.ACMEManagerMaker{}) {
|
if !reflect.DeepEqual(mgr, caddytls.ACMEIssuer{}) {
|
||||||
configVals = append(configVals, ConfigValue{
|
configVals = append(configVals, ConfigValue{
|
||||||
Class: "tls.automation_manager",
|
Class: "tls.cert_issuer",
|
||||||
Value: mgr,
|
Value: mgr,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -426,9 +429,116 @@ func parseHandleErrors(h Helper) ([]ConfigValue, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseLog parses the log directive. Syntax:
|
||||||
|
//
|
||||||
|
// log {
|
||||||
|
// output <writer_module> ...
|
||||||
|
// format <encoder_module> ...
|
||||||
|
// level <level>
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
func parseLog(h Helper) ([]ConfigValue, error) {
|
||||||
|
var configValues []ConfigValue
|
||||||
|
for h.Next() {
|
||||||
|
cl := new(caddy.CustomLog)
|
||||||
|
|
||||||
|
for h.NextBlock(0) {
|
||||||
|
switch h.Val() {
|
||||||
|
case "output":
|
||||||
|
if !h.NextArg() {
|
||||||
|
return nil, h.ArgErr()
|
||||||
|
}
|
||||||
|
moduleName := h.Val()
|
||||||
|
|
||||||
|
// can't use the usual caddyfile.Unmarshaler flow with the
|
||||||
|
// standard writers because they are in the caddy package
|
||||||
|
// (because they are the default) and implementing that
|
||||||
|
// interface there would unfortunately create circular import
|
||||||
|
var wo caddy.WriterOpener
|
||||||
|
switch moduleName {
|
||||||
|
case "stdout":
|
||||||
|
wo = caddy.StdoutWriter{}
|
||||||
|
case "stderr":
|
||||||
|
wo = caddy.StderrWriter{}
|
||||||
|
case "discard":
|
||||||
|
wo = caddy.DiscardWriter{}
|
||||||
|
default:
|
||||||
|
mod, err := caddy.GetModule("caddy.logging.writers." + moduleName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, h.Errf("getting log writer module named '%s': %v", moduleName, err)
|
||||||
|
}
|
||||||
|
unm, ok := mod.New().(caddyfile.Unmarshaler)
|
||||||
|
if !ok {
|
||||||
|
return nil, h.Errf("log writer module '%s' is not a Caddyfile unmarshaler", mod)
|
||||||
|
}
|
||||||
|
err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
wo, ok = unm.(caddy.WriterOpener)
|
||||||
|
if !ok {
|
||||||
|
return nil, h.Errf("module %s is not a WriterOpener", mod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings)
|
||||||
|
|
||||||
|
case "format":
|
||||||
|
if !h.NextArg() {
|
||||||
|
return nil, h.ArgErr()
|
||||||
|
}
|
||||||
|
moduleName := h.Val()
|
||||||
|
mod, err := caddy.GetModule("caddy.logging.encoders." + moduleName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, h.Errf("getting log encoder module named '%s': %v", moduleName, err)
|
||||||
|
}
|
||||||
|
unm, ok := mod.New().(caddyfile.Unmarshaler)
|
||||||
|
if !ok {
|
||||||
|
return nil, h.Errf("log encoder module '%s' is not a Caddyfile unmarshaler", mod)
|
||||||
|
}
|
||||||
|
err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
enc, ok := unm.(zapcore.Encoder)
|
||||||
|
if !ok {
|
||||||
|
return nil, h.Errf("module %s is not a zapcore.Encoder", mod)
|
||||||
|
}
|
||||||
|
cl.EncoderRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, h.warnings)
|
||||||
|
|
||||||
|
case "level":
|
||||||
|
if !h.NextArg() {
|
||||||
|
return nil, h.ArgErr()
|
||||||
|
}
|
||||||
|
cl.Level = h.Val()
|
||||||
|
if h.NextArg() {
|
||||||
|
return nil, h.ArgErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, h.Errf("unrecognized subdirective: %s", h.Val())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var val namedCustomLog
|
||||||
|
if !reflect.DeepEqual(cl, new(caddy.CustomLog)) {
|
||||||
|
cl.Include = []string{"http.log.access"}
|
||||||
|
val.name = fmt.Sprintf("log%d", logCounter)
|
||||||
|
val.log = cl
|
||||||
|
logCounter++
|
||||||
|
}
|
||||||
|
configValues = append(configValues, ConfigValue{
|
||||||
|
Class: "custom_log",
|
||||||
|
Value: val,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return configValues, nil
|
||||||
|
}
|
||||||
|
|
||||||
// tlsCertTags maps certificate filenames to their tag.
|
// tlsCertTags maps certificate filenames to their tag.
|
||||||
// This is used to remember which tag is used for each
|
// This is used to remember which tag is used for each
|
||||||
// certificate files, since we need to avoid loading
|
// certificate files, since we need to avoid loading
|
||||||
// the same certificate files more than once, overwriting
|
// the same certificate files more than once, overwriting
|
||||||
// previous tags
|
// previous tags
|
||||||
var tlsCertTags = make(map[string]string)
|
var tlsCertTags = make(map[string]string)
|
||||||
|
|
||||||
|
var logCounter int
|
||||||
|
|
|
@ -17,6 +17,7 @@ package httpcaddyfile
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
|
@ -298,17 +299,43 @@ func sortRoutes(routes []ConfigValue) {
|
||||||
// and returned.
|
// and returned.
|
||||||
func parseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
func parseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||||
var allResults []ConfigValue
|
var allResults []ConfigValue
|
||||||
for h.Next() {
|
|
||||||
for nesting := h.Nesting(); h.NextBlock(nesting); {
|
|
||||||
dir := h.Val()
|
|
||||||
|
|
||||||
|
for h.Next() {
|
||||||
|
// slice the linear list of tokens into top-level segments
|
||||||
|
var segments []caddyfile.Segment
|
||||||
|
for nesting := h.Nesting(); h.NextBlock(nesting); {
|
||||||
|
segments = append(segments, h.NextSegment())
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy existing matcher definitions so we can augment
|
||||||
|
// new ones that are defined only in this scope
|
||||||
|
matcherDefs := make(map[string]caddy.ModuleMap, len(h.matcherDefs))
|
||||||
|
for key, val := range h.matcherDefs {
|
||||||
|
matcherDefs[key] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// find and extract any embedded matcher definitions in this scope
|
||||||
|
for i, seg := range segments {
|
||||||
|
if strings.HasPrefix(seg.Directive(), matcherPrefix) {
|
||||||
|
err := parseMatcherDefinitions(caddyfile.NewDispenser(seg), matcherDefs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
segments = append(segments[:i], segments[i+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// with matchers ready to go, evaluate each directive's segment
|
||||||
|
for _, seg := range segments {
|
||||||
|
dir := seg.Directive()
|
||||||
dirFunc, ok := registeredDirectives[dir]
|
dirFunc, ok := registeredDirectives[dir]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, h.Errf("unrecognized directive: %s", dir)
|
return nil, h.Errf("unrecognized directive: %s", dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
subHelper := h
|
subHelper := h
|
||||||
subHelper.Dispenser = h.NewFromNextSegment()
|
subHelper.Dispenser = caddyfile.NewDispenser(seg)
|
||||||
|
subHelper.matcherDefs = matcherDefs
|
||||||
|
|
||||||
results, err := dirFunc(subHelper)
|
results, err := dirFunc(subHelper)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -319,9 +346,9 @@ func parseSegmentAsSubroute(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||||
allResults = append(allResults, result)
|
allResults = append(allResults, result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return buildSubroute(allResults, h.groupCounter) // TODO: should we move this outside the loop?
|
|
||||||
}
|
}
|
||||||
return nil, nil
|
|
||||||
|
return buildSubroute(allResults, h.groupCounter)
|
||||||
}
|
}
|
||||||
|
|
||||||
// serverBlock pairs a Caddyfile server block
|
// serverBlock pairs a Caddyfile server block
|
||||||
|
|
|
@ -26,7 +26,7 @@ import (
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
"github.com/mholt/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -88,6 +88,13 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
|
||||||
"{remote}", "{http.request.remote}",
|
"{remote}", "{http.request.remote}",
|
||||||
"{scheme}", "{http.request.scheme}",
|
"{scheme}", "{http.request.scheme}",
|
||||||
"{uri}", "{http.request.uri}",
|
"{uri}", "{http.request.uri}",
|
||||||
|
|
||||||
|
"{tls_cipher}", "{http.request.tls.cipher_suite}",
|
||||||
|
"{tls_version}", "{http.request.tls.version}",
|
||||||
|
"{tls_client_fingerprint}", "{http.request.tls.client.fingerprint}",
|
||||||
|
"{tls_client_issuer}", "{http.request.tls.client.issuer}",
|
||||||
|
"{tls_client_serial}", "{http.request.tls.client.serial}",
|
||||||
|
"{tls_client_subject}", "{http.request.tls.client.subject}",
|
||||||
)
|
)
|
||||||
for _, segment := range sb.block.Segments {
|
for _, segment := range sb.block.Segments {
|
||||||
for i := 0; i < len(segment); i++ {
|
for i := 0; i < len(segment); i++ {
|
||||||
|
@ -173,9 +180,9 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
|
||||||
for _, p := range pairings {
|
for _, p := range pairings {
|
||||||
for i, sblock := range p.serverBlocks {
|
for i, sblock := range p.serverBlocks {
|
||||||
// tls automation policies
|
// tls automation policies
|
||||||
if mmVals, ok := sblock.pile["tls.automation_manager"]; ok {
|
if mmVals, ok := sblock.pile["tls.cert_issuer"]; ok {
|
||||||
for _, mmVal := range mmVals {
|
for _, mmVal := range mmVals {
|
||||||
mm := mmVal.Value.(caddytls.ManagerMaker)
|
mm := mmVal.Value.(certmagic.Issuer)
|
||||||
sblockHosts, err := st.autoHTTPSHosts(sblock)
|
sblockHosts, err := st.autoHTTPSHosts(sblock)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, warnings, err
|
return nil, warnings, err
|
||||||
|
@ -186,7 +193,7 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
|
||||||
}
|
}
|
||||||
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, &caddytls.AutomationPolicy{
|
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, &caddytls.AutomationPolicy{
|
||||||
Hosts: sblockHosts,
|
Hosts: sblockHosts,
|
||||||
ManagementRaw: caddyconfig.JSONModuleObject(mm, "module", mm.(caddy.Module).CaddyModule().ID.Name(), &warnings),
|
IssuerRaw: caddyconfig.JSONModuleObject(mm, "module", mm.(caddy.Module).CaddyModule().ID.Name(), &warnings),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
warnings = append(warnings, caddyconfig.Warning{
|
warnings = append(warnings, caddyconfig.Warning{
|
||||||
|
@ -245,7 +252,7 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
|
||||||
if !hasEmail {
|
if !hasEmail {
|
||||||
email = ""
|
email = ""
|
||||||
}
|
}
|
||||||
mgr := caddytls.ACMEManagerMaker{
|
mgr := caddytls.ACMEIssuer{
|
||||||
CA: acmeCA.(string),
|
CA: acmeCA.(string),
|
||||||
Email: email.(string),
|
Email: email.(string),
|
||||||
}
|
}
|
||||||
|
@ -260,7 +267,7 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, &caddytls.AutomationPolicy{
|
tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, &caddytls.AutomationPolicy{
|
||||||
ManagementRaw: caddyconfig.JSONModuleObject(mgr, "module", "acme", &warnings),
|
IssuerRaw: caddyconfig.JSONModuleObject(mgr, "module", "acme", &warnings),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if tlsApp.Automation != nil {
|
if tlsApp.Automation != nil {
|
||||||
|
@ -275,6 +282,35 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extract any custom logs, and enforce configured levels
|
||||||
|
var customLogs []namedCustomLog
|
||||||
|
var hasDefaultLog bool
|
||||||
|
for _, sb := range serverBlocks {
|
||||||
|
for _, clVal := range sb.pile["custom_log"] {
|
||||||
|
ncl := clVal.Value.(namedCustomLog)
|
||||||
|
if ncl.name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ncl.name == "default" {
|
||||||
|
hasDefaultLog = true
|
||||||
|
}
|
||||||
|
if _, ok := options["debug"]; ok && ncl.log.Level == "" {
|
||||||
|
ncl.log.Level = "DEBUG"
|
||||||
|
}
|
||||||
|
customLogs = append(customLogs, ncl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasDefaultLog {
|
||||||
|
// if the default log was not customized, ensure we
|
||||||
|
// configure it with any applicable options
|
||||||
|
if _, ok := options["debug"]; ok {
|
||||||
|
customLogs = append(customLogs, namedCustomLog{
|
||||||
|
name: "default",
|
||||||
|
log: &caddy.CustomLog{Level: "DEBUG"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// annnd the top-level config, then we're done!
|
// annnd the top-level config, then we're done!
|
||||||
cfg := &caddy.Config{AppsRaw: make(caddy.ModuleMap)}
|
cfg := &caddy.Config{AppsRaw: make(caddy.ModuleMap)}
|
||||||
if !reflect.DeepEqual(httpApp, caddyhttp.App{}) {
|
if !reflect.DeepEqual(httpApp, caddyhttp.App{}) {
|
||||||
|
@ -292,6 +328,18 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
|
||||||
if adminConfig, ok := options["admin"].(string); ok && adminConfig != "" {
|
if adminConfig, ok := options["admin"].(string); ok && adminConfig != "" {
|
||||||
cfg.Admin = &caddy.AdminConfig{Listen: adminConfig}
|
cfg.Admin = &caddy.AdminConfig{Listen: adminConfig}
|
||||||
}
|
}
|
||||||
|
if len(customLogs) > 0 {
|
||||||
|
if cfg.Logging == nil {
|
||||||
|
cfg.Logging = &caddy.Logging{
|
||||||
|
Logs: make(map[string]*caddy.CustomLog),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ncl := range customLogs {
|
||||||
|
if ncl.name != "" {
|
||||||
|
cfg.Logging.Logs[ncl.name] = ncl.log
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return cfg, warnings, nil
|
return cfg, warnings, nil
|
||||||
}
|
}
|
||||||
|
@ -316,6 +364,8 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
|
||||||
val, err = parseOptHTTPPort(disp)
|
val, err = parseOptHTTPPort(disp)
|
||||||
case "https_port":
|
case "https_port":
|
||||||
val, err = parseOptHTTPSPort(disp)
|
val, err = parseOptHTTPSPort(disp)
|
||||||
|
case "default_sni":
|
||||||
|
val, err = parseOptSingleString(disp)
|
||||||
case "order":
|
case "order":
|
||||||
val, err = parseOptOrder(disp)
|
val, err = parseOptOrder(disp)
|
||||||
case "experimental_http3":
|
case "experimental_http3":
|
||||||
|
@ -323,11 +373,13 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
|
||||||
case "storage":
|
case "storage":
|
||||||
val, err = parseOptStorage(disp)
|
val, err = parseOptStorage(disp)
|
||||||
case "acme_ca", "acme_dns", "acme_ca_root":
|
case "acme_ca", "acme_dns", "acme_ca_root":
|
||||||
val, err = parseOptACME(disp)
|
val, err = parseOptSingleString(disp)
|
||||||
case "email":
|
case "email":
|
||||||
val, err = parseOptEmail(disp)
|
val, err = parseOptSingleString(disp)
|
||||||
case "admin":
|
case "admin":
|
||||||
val, err = parseOptAdmin(disp)
|
val, err = parseOptAdmin(disp)
|
||||||
|
case "debug":
|
||||||
|
options["debug"] = true
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unrecognized parameter name: %s", dir)
|
return nil, fmt.Errorf("unrecognized parameter name: %s", dir)
|
||||||
}
|
}
|
||||||
|
@ -426,6 +478,7 @@ func (st *ServerType) serversFromPairings(
|
||||||
}
|
}
|
||||||
|
|
||||||
// tls: connection policies and toggle auto HTTPS
|
// tls: connection policies and toggle auto HTTPS
|
||||||
|
defaultSNI := tryString(options["default_sni"], warnings)
|
||||||
autoHTTPSQualifiedHosts, err := st.autoHTTPSHosts(sblock)
|
autoHTTPSQualifiedHosts, err := st.autoHTTPSHosts(sblock)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -438,6 +491,7 @@ func (st *ServerType) serversFromPairings(
|
||||||
srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, autoHTTPSQualifiedHosts...)
|
srv.AutoHTTPS.Skip = append(srv.AutoHTTPS.Skip, autoHTTPSQualifiedHosts...)
|
||||||
} else if cpVals, ok := sblock.pile["tls.connection_policy"]; ok {
|
} else if cpVals, ok := sblock.pile["tls.connection_policy"]; ok {
|
||||||
// tls connection policies
|
// tls connection policies
|
||||||
|
|
||||||
for _, cpVal := range cpVals {
|
for _, cpVal := range cpVals {
|
||||||
cp := cpVal.Value.(*caddytls.ConnectionPolicy)
|
cp := cpVal.Value.(*caddytls.ConnectionPolicy)
|
||||||
|
|
||||||
|
@ -446,6 +500,13 @@ func (st *ServerType) serversFromPairings(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
for _, h := range hosts {
|
||||||
|
if h == defaultSNI {
|
||||||
|
hosts = append(hosts, "")
|
||||||
|
cp.DefaultSNI = defaultSNI
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: are matchers needed if every hostname of the resulting config is matched?
|
// TODO: are matchers needed if every hostname of the resulting config is matched?
|
||||||
if len(hosts) > 0 {
|
if len(hosts) > 0 {
|
||||||
|
@ -459,6 +520,11 @@ func (st *ServerType) serversFromPairings(
|
||||||
srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp)
|
srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp)
|
||||||
}
|
}
|
||||||
// TODO: consolidate equal conn policies
|
// TODO: consolidate equal conn policies
|
||||||
|
} else if defaultSNI != "" {
|
||||||
|
hasCatchAllTLSConnPolicy = true
|
||||||
|
srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{
|
||||||
|
DefaultSNI: defaultSNI,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// exclude any hosts that were defined explicitly with
|
// exclude any hosts that were defined explicitly with
|
||||||
|
@ -499,6 +565,25 @@ func (st *ServerType) serversFromPairings(
|
||||||
srv.Errors.Routes = appendSubrouteToRouteList(srv.Errors.Routes, sr, matcherSetsEnc, p, warnings)
|
srv.Errors.Routes = appendSubrouteToRouteList(srv.Errors.Routes, sr, matcherSetsEnc, p, warnings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add log associations
|
||||||
|
for _, cval := range sblock.pile["custom_log"] {
|
||||||
|
ncl := cval.Value.(namedCustomLog)
|
||||||
|
if srv.Logs == nil {
|
||||||
|
srv.Logs = &caddyhttp.ServerLogConfig{
|
||||||
|
LoggerNames: make(map[string]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hosts, err := st.hostsFromServerBlockKeys(sblock.block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, h := range hosts {
|
||||||
|
if ncl.name != "" {
|
||||||
|
srv.Logs.LoggerNames[h] = ncl.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// a catch-all TLS conn policy is necessary to ensure TLS can
|
// a catch-all TLS conn policy is necessary to ensure TLS can
|
||||||
|
@ -690,7 +775,7 @@ func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls
|
||||||
// otherwise the one without any hosts (a catch-all) would be
|
// otherwise the one without any hosts (a catch-all) would be
|
||||||
// eaten up by the one with hosts; and if both have hosts, we
|
// eaten up by the one with hosts; and if both have hosts, we
|
||||||
// need to combine their lists
|
// need to combine their lists
|
||||||
if reflect.DeepEqual(aps[i].ManagementRaw, aps[j].ManagementRaw) &&
|
if reflect.DeepEqual(aps[i].IssuerRaw, aps[j].IssuerRaw) &&
|
||||||
aps[i].ManageSync == aps[j].ManageSync {
|
aps[i].ManageSync == aps[j].ManageSync {
|
||||||
if len(aps[i].Hosts) == 0 && len(aps[j].Hosts) > 0 {
|
if len(aps[i].Hosts) == 0 && len(aps[j].Hosts) > 0 {
|
||||||
aps = append(aps[:j], aps[j+1:]...)
|
aps = append(aps[:j], aps[j+1:]...)
|
||||||
|
@ -882,6 +967,14 @@ func tryInt(val interface{}, warnings *[]caddyconfig.Warning) int {
|
||||||
return intVal
|
return intVal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tryString(val interface{}, warnings *[]caddyconfig.Warning) string {
|
||||||
|
stringVal, ok := val.(string)
|
||||||
|
if val != nil && !ok && warnings != nil {
|
||||||
|
*warnings = append(*warnings, caddyconfig.Warning{Message: "not a string type"})
|
||||||
|
}
|
||||||
|
return stringVal
|
||||||
|
}
|
||||||
|
|
||||||
// sliceContains returns true if needle is in haystack.
|
// sliceContains returns true if needle is in haystack.
|
||||||
func sliceContains(haystack []string, needle string) bool {
|
func sliceContains(haystack []string, needle string) bool {
|
||||||
for _, s := range haystack {
|
for _, s := range haystack {
|
||||||
|
@ -933,6 +1026,11 @@ type matcherSetAndTokens struct {
|
||||||
tokens []caddyfile.Token
|
tokens []caddyfile.Token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type namedCustomLog struct {
|
||||||
|
name string
|
||||||
|
log *caddy.CustomLog
|
||||||
|
}
|
||||||
|
|
||||||
// sbAddrAssocation is a mapping from a list of
|
// sbAddrAssocation is a mapping from a list of
|
||||||
// addresses to a list of server blocks that are
|
// addresses to a list of server blocks that are
|
||||||
// served on those addresses.
|
// served on those addresses.
|
||||||
|
|
|
@ -162,19 +162,7 @@ func parseOptStorage(d *caddyfile.Dispenser) (caddy.StorageConverter, error) {
|
||||||
return storage, nil
|
return storage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOptACME(d *caddyfile.Dispenser) (string, error) {
|
func parseOptSingleString(d *caddyfile.Dispenser) (string, error) {
|
||||||
d.Next() // consume parameter name
|
|
||||||
if !d.Next() {
|
|
||||||
return "", d.ArgErr()
|
|
||||||
}
|
|
||||||
val := d.Val()
|
|
||||||
if d.Next() {
|
|
||||||
return "", d.ArgErr()
|
|
||||||
}
|
|
||||||
return val, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseOptEmail(d *caddyfile.Dispenser) (string, error) {
|
|
||||||
d.Next() // consume parameter name
|
d.Next() // consume parameter name
|
||||||
if !d.Next() {
|
if !d.Next() {
|
||||||
return "", d.ArgErr()
|
return "", d.ArgErr()
|
||||||
|
@ -190,11 +178,9 @@ func parseOptAdmin(d *caddyfile.Dispenser) (string, error) {
|
||||||
if d.Next() {
|
if d.Next() {
|
||||||
var listenAddress string
|
var listenAddress string
|
||||||
d.AllArgs(&listenAddress)
|
d.AllArgs(&listenAddress)
|
||||||
|
|
||||||
if listenAddress == "" {
|
if listenAddress == "" {
|
||||||
listenAddress = caddy.DefaultAdminListen
|
listenAddress = caddy.DefaultAdminListen
|
||||||
}
|
}
|
||||||
|
|
||||||
return listenAddress, nil
|
return listenAddress, nil
|
||||||
}
|
}
|
||||||
return "", nil
|
return "", nil
|
||||||
|
|
|
@ -34,7 +34,7 @@ import (
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/mholt/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ func cmdStart(fl Flags) (int, error) {
|
||||||
|
|
||||||
// begin writing the confirmation bytes to the child's
|
// begin writing the confirmation bytes to the child's
|
||||||
// stdin; use a goroutine since the child hasn't been
|
// stdin; use a goroutine since the child hasn't been
|
||||||
// started yet, and writing sychronously would result
|
// started yet, and writing synchronously would result
|
||||||
// in a deadlock
|
// in a deadlock
|
||||||
go func() {
|
go func() {
|
||||||
stdinpipe.Write(expect)
|
stdinpipe.Write(expect)
|
||||||
|
|
|
@ -256,6 +256,8 @@ provisioning stages.`,
|
||||||
// This function panics if the name is already registered,
|
// This function panics if the name is already registered,
|
||||||
// if the name does not meet the described format, or if
|
// if the name does not meet the described format, or if
|
||||||
// any of the fields are missing from cmd.
|
// any of the fields are missing from cmd.
|
||||||
|
//
|
||||||
|
// This function should be used in init().
|
||||||
func RegisterCommand(cmd Command) {
|
func RegisterCommand(cmd Command) {
|
||||||
if cmd.Name == "" {
|
if cmd.Name == "" {
|
||||||
panic("command name is required")
|
panic("command name is required")
|
||||||
|
|
10
context.go
10
context.go
|
@ -21,7 +21,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/mholt/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -384,9 +384,13 @@ func (ctx Context) App(name string) (interface{}, error) {
|
||||||
if app, ok := ctx.cfg.apps[name]; ok {
|
if app, ok := ctx.cfg.apps[name]; ok {
|
||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
modVal, err := ctx.LoadModuleByID(name, nil)
|
appRaw := ctx.cfg.AppsRaw[name]
|
||||||
|
modVal, err := ctx.LoadModuleByID(name, appRaw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("instantiating new module %s: %v", name, err)
|
return nil, fmt.Errorf("loading %s app module: %v", name, err)
|
||||||
|
}
|
||||||
|
if appRaw != nil {
|
||||||
|
ctx.cfg.AppsRaw[name] = nil // allow GC to deallocate
|
||||||
}
|
}
|
||||||
ctx.cfg.apps[name] = modVal.(App)
|
ctx.cfg.apps[name] = modVal.(App)
|
||||||
return modVal, nil
|
return modVal, nil
|
||||||
|
|
49
go.mod
49
go.mod
|
@ -1,38 +1,35 @@
|
||||||
module github.com/caddyserver/caddy/v2
|
module github.com/caddyserver/caddy/v2
|
||||||
|
|
||||||
go 1.13
|
go 1.14
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Masterminds/sprig/v3 v3.0.0
|
github.com/Masterminds/sprig/v3 v3.0.2
|
||||||
github.com/alecthomas/chroma v0.7.0
|
github.com/alecthomas/chroma v0.7.1
|
||||||
github.com/andybalholm/brotli v0.0.0-20190821151343-b60f0d972eeb
|
github.com/andybalholm/brotli v1.0.0
|
||||||
github.com/cenkalti/backoff/v3 v3.1.1 // indirect
|
github.com/caddyserver/certmagic v0.10.0
|
||||||
github.com/dustin/go-humanize v1.0.0
|
github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac
|
||||||
github.com/go-acme/lego/v3 v3.3.0
|
github.com/go-acme/lego/v3 v3.4.0
|
||||||
github.com/golang/groupcache v0.0.0-20191002201903-404acd9df4cc
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e
|
||||||
github.com/ilibs/json5 v1.0.1
|
github.com/ilibs/json5 v1.0.1
|
||||||
github.com/imdario/mergo v0.3.8 // indirect
|
|
||||||
github.com/jsternberg/zap-logfmt v1.2.0
|
github.com/jsternberg/zap-logfmt v1.2.0
|
||||||
github.com/klauspost/compress v1.8.6
|
github.com/klauspost/compress v1.10.2
|
||||||
github.com/klauspost/cpuid v1.2.2
|
github.com/klauspost/cpuid v1.2.3
|
||||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
github.com/lucas-clemente/quic-go v0.15.1
|
||||||
github.com/lucas-clemente/quic-go v0.14.4
|
github.com/manifoldco/promptui v0.7.0 // indirect
|
||||||
github.com/mholt/certmagic v0.9.3
|
github.com/miekg/dns v1.1.27 // indirect
|
||||||
github.com/miekg/dns v1.1.25 // indirect
|
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20200303171503-1e787b591db7
|
||||||
github.com/muhammadmuzzammil1998/jsonc v0.0.0-20190906142622-1265e9b150c6
|
|
||||||
github.com/naoina/go-stringutil v0.1.0 // indirect
|
github.com/naoina/go-stringutil v0.1.0 // indirect
|
||||||
github.com/naoina/toml v0.1.1
|
github.com/naoina/toml v0.1.1
|
||||||
github.com/onsi/ginkgo v1.8.0 // indirect
|
github.com/smallstep/certificates v0.14.0-rc.5
|
||||||
github.com/onsi/gomega v1.5.0 // indirect
|
github.com/smallstep/cli v0.14.0-rc.3
|
||||||
|
github.com/smallstep/truststore v0.9.4
|
||||||
github.com/vulcand/oxy v1.0.0
|
github.com/vulcand/oxy v1.0.0
|
||||||
github.com/yuin/goldmark v1.1.17
|
github.com/yuin/goldmark v1.1.24
|
||||||
github.com/yuin/goldmark-highlighting v0.0.0-20191202084645-78f32c8dd6d5
|
github.com/yuin/goldmark-highlighting v0.0.0-20200218065240-d1af22c1126f
|
||||||
go.uber.org/multierr v1.2.0 // indirect
|
go.uber.org/zap v1.14.0
|
||||||
go.uber.org/zap v1.10.0
|
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073
|
||||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413
|
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
|
||||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553
|
|
||||||
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 // indirect
|
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||||
gopkg.in/square/go-jose.v2 v2.4.1 // indirect
|
gopkg.in/square/go-jose.v2 v2.4.1 // indirect
|
||||||
gopkg.in/yaml.v2 v2.2.2
|
gopkg.in/yaml.v2 v2.2.8
|
||||||
)
|
)
|
||||||
|
|
|
@ -101,7 +101,7 @@ func (id ModuleID) Namespace() string {
|
||||||
return string(id)[:lastDot]
|
return string(id)[:lastDot]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name returns the Name (last element) of a module name.
|
// Name returns the Name (last element) of a module ID.
|
||||||
func (id ModuleID) Name() string {
|
func (id ModuleID) Name() string {
|
||||||
if id == "" {
|
if id == "" {
|
||||||
return ""
|
return ""
|
||||||
|
@ -294,8 +294,8 @@ type Provisioner interface {
|
||||||
// Validator is implemented by modules which can verify that their
|
// Validator is implemented by modules which can verify that their
|
||||||
// configurations are valid. This method will be called after
|
// configurations are valid. This method will be called after
|
||||||
// Provision() (if implemented). Validation should always be fast
|
// Provision() (if implemented). Validation should always be fast
|
||||||
// (imperceptible running time) and an error should be returned only
|
// (imperceptible running time) and an error must be returned if
|
||||||
// if the value's configuration is invalid.
|
// the module's configuration is invalid.
|
||||||
type Validator interface {
|
type Validator interface {
|
||||||
Validate() error
|
Validate() error
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
"github.com/mholt/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -42,12 +42,10 @@ type AutoHTTPSConfig struct {
|
||||||
// enabled. To force automated certificate management
|
// enabled. To force automated certificate management
|
||||||
// regardless of loaded certificates, set this to true.
|
// regardless of loaded certificates, set this to true.
|
||||||
IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"`
|
IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"`
|
||||||
|
|
||||||
domainSet map[string]struct{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skipped returns true if name is in skipSlice, which
|
// 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 {
|
func (ahc AutoHTTPSConfig) Skipped(name string, skipSlice []string) bool {
|
||||||
for _, n := range skipSlice {
|
for _, n := range skipSlice {
|
||||||
if name == n {
|
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
|
// addresses to the routes that do HTTP->HTTPS redirects
|
||||||
lnAddrRedirRoutes := make(map[string]Route)
|
lnAddrRedirRoutes := make(map[string]Route)
|
||||||
|
|
||||||
|
uniqueDomainsForCerts := make(map[string]struct{})
|
||||||
|
|
||||||
for srvName, srv := range app.Servers {
|
for srvName, srv := range app.Servers {
|
||||||
// as a prerequisite, provision route matchers; this is
|
// as a prerequisite, provision route matchers; this is
|
||||||
// required for all routes on all servers, and must be
|
// 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
|
srv.TLSConnPolicies = defaultConnPolicies
|
||||||
}
|
}
|
||||||
|
|
||||||
// find all qualifying domain names in this server
|
// find all qualifying domain names (deduplicated) in this server
|
||||||
srv.AutoHTTPS.domainSet = make(map[string]struct{})
|
serverDomainSet := make(map[string]struct{})
|
||||||
for routeIdx, route := range srv.Routes {
|
for routeIdx, route := range srv.Routes {
|
||||||
for matcherSetIdx, matcherSet := range route.MatcherSets {
|
for matcherSetIdx, matcherSet := range route.MatcherSets {
|
||||||
for matcherIdx, m := range matcherSet {
|
for matcherIdx, m := range matcherSet {
|
||||||
|
@ -131,7 +131,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
|
||||||
}
|
}
|
||||||
if certmagic.HostQualifies(d) &&
|
if certmagic.HostQualifies(d) &&
|
||||||
!srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.Skip) {
|
!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
|
// nothing more to do here if there are no
|
||||||
// domains that qualify for automatic HTTPS
|
// domains that qualify for automatic HTTPS
|
||||||
if len(srv.AutoHTTPS.domainSet) == 0 {
|
if len(serverDomainSet) == 0 {
|
||||||
continue
|
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
|
// tell the server to use TLS if it is not already doing so
|
||||||
if srv.TLSConnPolicies == nil {
|
if srv.TLSConnPolicies == nil {
|
||||||
srv.TLSConnPolicies = defaultConnPolicies
|
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 there are HTTP->HTTPS redirects to add, do so now
|
||||||
if len(lnAddrRedirRoutes) == 0 {
|
if len(lnAddrRedirRoutes) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
@ -258,28 +290,78 @@ redirRoutesLoop:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// automaticHTTPSPhase2 attaches a TLS app pointer to each
|
// createAutomationPolicy ensures that certificates for this app are
|
||||||
// server. This phase must occur after provisioning, and
|
// managed properly; for example, it's implied that the HTTPPort
|
||||||
// at the beginning of the app start, before starting each
|
// should also be the port the HTTP challenge is solved on; the same
|
||||||
// of the servers.
|
// for HTTPS port and TLS-ALPN challenge also. We need to tell the
|
||||||
func (app *App) automaticHTTPSPhase2() error {
|
// TLS app to manage these certs by honoring those port configurations,
|
||||||
tlsAppIface, err := app.ctx.App("tls")
|
// so we either find an existing matching automation policy with an
|
||||||
if err != nil {
|
// ACME issuer, or make a new one and append it.
|
||||||
return fmt.Errorf("getting tls app: %v", err)
|
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
|
if matchingPolicy == nil {
|
||||||
// challenges, since it is required to solve
|
// if there was no matching policy, we'll have to append our own
|
||||||
// the ACME HTTP challenge
|
err := app.tlsApp.AddAutomationPolicy(&caddytls.AutomationPolicy{
|
||||||
for _, srv := range app.Servers {
|
Hosts: app.allCertDomains,
|
||||||
srv.tlsApp = tlsApp
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// automaticHTTPSPhase3 begins certificate management for
|
// automaticHTTPSPhase2 begins certificate management for
|
||||||
// all names in the qualifying domain set for each server.
|
// all names in the qualifying domain set for each server.
|
||||||
// This phase must occur after provisioning and at the end
|
// This phase must occur after provisioning and at the end
|
||||||
// of app start, after all the servers have been started.
|
// 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,
|
// first, then our servers would fail to bind to them,
|
||||||
// which would be bad, since CertMagic's bindings are
|
// which would be bad, since CertMagic's bindings are
|
||||||
// temporary and don't serve the user's sites!).
|
// temporary and don't serve the user's sites!).
|
||||||
func (app *App) automaticHTTPSPhase3() error {
|
func (app *App) automaticHTTPSPhase2() error {
|
||||||
// begin managing certificates for enabled servers
|
if len(app.allCertDomains) == 0 {
|
||||||
for srvName, srv := range app.Servers {
|
return nil
|
||||||
if srv.AutoHTTPS == nil ||
|
}
|
||||||
srv.AutoHTTPS.Disabled ||
|
app.logger.Info("enabling automatic TLS certificate management",
|
||||||
len(srv.AutoHTTPS.domainSet) == 0 {
|
zap.Strings("domains", app.allCertDomains),
|
||||||
continue
|
)
|
||||||
}
|
err := app.tlsApp.Manage(app.allCertDomains)
|
||||||
|
if err != nil {
|
||||||
// marshal the domains into a slice
|
return fmt.Errorf("managing certificates for %v: %s", app.allCertDomains, err)
|
||||||
var domains, domainsForCerts []string
|
}
|
||||||
for d := range srv.AutoHTTPS.domainSet {
|
app.allCertDomains = nil // no longer needed; allow GC to deallocate
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
"github.com/lucas-clemente/quic-go/http3"
|
"github.com/lucas-clemente/quic-go/http3"
|
||||||
"go.uber.org/zap"
|
"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.port}` | The port part of the remote client's address
|
||||||
// `{http.request.remote}` | The address of the remote client
|
// `{http.request.remote}` | The address of the remote client
|
||||||
// `{http.request.scheme}` | The request scheme
|
// `{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.*}` | Parts of the path, split by `/` (0-based from left)
|
||||||
// `{http.request.uri.path.dir}` | The directory, excluding leaf filename
|
// `{http.request.uri.path.dir}` | The directory, excluding leaf filename
|
||||||
// `{http.request.uri.path.file}` | The filename of the path, excluding directory
|
// `{http.request.uri.path.file}` | The filename of the path, excluding directory
|
||||||
|
@ -107,6 +118,10 @@ type App struct {
|
||||||
|
|
||||||
ctx caddy.Context
|
ctx caddy.Context
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
|
tlsApp *caddytls.TLS
|
||||||
|
|
||||||
|
// used temporarily between phases 1 and 2 of auto HTTPS
|
||||||
|
allCertDomains []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// CaddyModule returns the Caddy module information.
|
// CaddyModule returns the Caddy module information.
|
||||||
|
@ -119,6 +134,12 @@ func (App) CaddyModule() caddy.ModuleInfo {
|
||||||
|
|
||||||
// Provision sets up the app.
|
// Provision sets up the app.
|
||||||
func (app *App) Provision(ctx caddy.Context) error {
|
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.ctx = ctx
|
||||||
app.logger = ctx.Logger(app)
|
app.logger = ctx.Logger(app)
|
||||||
|
|
||||||
|
@ -127,12 +148,14 @@ func (app *App) Provision(ctx caddy.Context) error {
|
||||||
// this provisions the matchers for each route,
|
// this provisions the matchers for each route,
|
||||||
// and prepares auto HTTP->HTTP redirects, and
|
// and prepares auto HTTP->HTTP redirects, and
|
||||||
// is required before we provision each server
|
// is required before we provision each server
|
||||||
err := app.automaticHTTPSPhase1(ctx, repl)
|
err = app.automaticHTTPSPhase1(ctx, repl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// prepare each server
|
||||||
for srvName, srv := range app.Servers {
|
for srvName, srv := range app.Servers {
|
||||||
|
srv.tlsApp = app.tlsApp
|
||||||
srv.logger = app.logger.Named("log")
|
srv.logger = app.logger.Named("log")
|
||||||
srv.errorLogger = app.logger.Named("log.error")
|
srv.errorLogger = app.logger.Named("log.error")
|
||||||
|
|
||||||
|
@ -185,9 +208,14 @@ func (app *App) Provision(ctx caddy.Context) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("server %s: setting up server error handling routes: %v", srvName, err)
|
return fmt.Errorf("server %s: setting up server error handling routes: %v", srvName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
srv.errorHandlerChain = srv.Errors.Routes.Compile(errorEmptyHandler)
|
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
|
return nil
|
||||||
|
@ -221,14 +249,6 @@ func (app *App) Validate() error {
|
||||||
// Start runs the app. It finishes automatic HTTPS if enabled,
|
// Start runs the app. It finishes automatic HTTPS if enabled,
|
||||||
// including management of certificates.
|
// including management of certificates.
|
||||||
func (app *App) Start() error {
|
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 {
|
for srvName, srv := range app.Servers {
|
||||||
s := &http.Server{
|
s := &http.Server{
|
||||||
ReadTimeout: time.Duration(srv.ReadTimeout),
|
ReadTimeout: time.Duration(srv.ReadTimeout),
|
||||||
|
@ -262,10 +282,7 @@ func (app *App) Start() error {
|
||||||
if len(srv.TLSConnPolicies) > 0 &&
|
if len(srv.TLSConnPolicies) > 0 &&
|
||||||
int(listenAddr.StartPort+portOffset) != app.httpPort() {
|
int(listenAddr.StartPort+portOffset) != app.httpPort() {
|
||||||
// create TLS listener
|
// create TLS listener
|
||||||
tlsCfg, err := srv.TLSConnPolicies.TLSConfig(app.ctx)
|
tlsCfg := srv.TLSConnPolicies.TLSConfig(app.ctx)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s/%s: making TLS configuration: %v", listenAddr.Network, hostport, err)
|
|
||||||
}
|
|
||||||
ln = tls.NewListener(ln, tlsCfg)
|
ln = tls.NewListener(ln, tlsCfg)
|
||||||
|
|
||||||
/////////
|
/////////
|
||||||
|
@ -301,7 +318,7 @@ func (app *App) Start() error {
|
||||||
|
|
||||||
// finish automatic HTTPS by finally beginning
|
// finish automatic HTTPS by finally beginning
|
||||||
// certificate management
|
// certificate management
|
||||||
err = app.automaticHTTPSPhase3()
|
err := app.automaticHTTPSPhase2()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("finalizing automatic HTTPS: %v", err)
|
return fmt.Errorf("finalizing automatic HTTPS: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ import (
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"github.com/mholt/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
@ -16,6 +16,7 @@ package httpcache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -108,7 +109,8 @@ func (c *Cache) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp
|
||||||
return next.ServeHTTP(w, r)
|
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
|
// TODO: rigorous performance testing
|
||||||
|
|
||||||
|
@ -152,8 +154,8 @@ func (c *Cache) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) getter(ctx groupcache.Context, key string, dest groupcache.Sink) error {
|
func (c *Cache) getter(ctx context.Context, key string, dest groupcache.Sink) error {
|
||||||
combo := ctx.(getterContext)
|
combo := ctx.Value(getterContextCtxKey).(getterContext)
|
||||||
|
|
||||||
// the buffer will store the gob-encoded header, then the body
|
// the buffer will store the gob-encoded header, then the body
|
||||||
buf := bufPool.Get().(*bytes.Buffer)
|
buf := bufPool.Get().(*bytes.Buffer)
|
||||||
|
@ -228,6 +230,10 @@ var errUncacheable = fmt.Errorf("uncacheable")
|
||||||
|
|
||||||
const groupName = "http_requests"
|
const groupName = "http_requests"
|
||||||
|
|
||||||
|
type ctxKey string
|
||||||
|
|
||||||
|
const getterContextCtxKey ctxKey = "getter_context"
|
||||||
|
|
||||||
// Interface guards
|
// Interface guards
|
||||||
var (
|
var (
|
||||||
_ caddy.Provisioner = (*Cache)(nil)
|
_ caddy.Provisioner = (*Cache)(nil)
|
||||||
|
|
|
@ -15,6 +15,9 @@
|
||||||
package caddyhttp
|
package caddyhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -24,14 +27,15 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
)
|
)
|
||||||
|
|
||||||
func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.ResponseWriter) {
|
func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.ResponseWriter) {
|
||||||
httpVars := func(key string) (string, bool) {
|
httpVars := func(key string) (string, bool) {
|
||||||
if req != nil {
|
if req != nil {
|
||||||
// query string parameters
|
// query string parameters
|
||||||
if strings.HasPrefix(key, queryReplPrefix) {
|
if strings.HasPrefix(key, reqURIQueryReplPrefix) {
|
||||||
vals := req.URL.Query()[key[len(queryReplPrefix):]]
|
vals := req.URL.Query()[key[len(reqURIQueryReplPrefix):]]
|
||||||
// always return true, since the query param might
|
// always return true, since the query param might
|
||||||
// be present only in some requests
|
// be present only in some requests
|
||||||
return strings.Join(vals, ","), true
|
return strings.Join(vals, ","), true
|
||||||
|
@ -47,8 +51,8 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
||||||
}
|
}
|
||||||
|
|
||||||
// cookies
|
// cookies
|
||||||
if strings.HasPrefix(key, cookieReplPrefix) {
|
if strings.HasPrefix(key, reqCookieReplPrefix) {
|
||||||
name := key[len(cookieReplPrefix):]
|
name := key[len(reqCookieReplPrefix):]
|
||||||
for _, cookie := range req.Cookies() {
|
for _, cookie := range req.Cookies() {
|
||||||
if strings.EqualFold(name, cookie.Name) {
|
if strings.EqualFold(name, cookie.Name) {
|
||||||
// always return true, since the cookie might
|
// 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 {
|
switch key {
|
||||||
case "http.request.method":
|
case "http.request.method":
|
||||||
return req.Method, true
|
return req.Method, true
|
||||||
|
@ -129,8 +138,8 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
||||||
}
|
}
|
||||||
|
|
||||||
// hostname labels
|
// hostname labels
|
||||||
if strings.HasPrefix(key, hostLabelReplPrefix) {
|
if strings.HasPrefix(key, reqHostLabelsReplPrefix) {
|
||||||
idxStr := key[len(hostLabelReplPrefix):]
|
idxStr := key[len(reqHostLabelsReplPrefix):]
|
||||||
idx, err := strconv.Atoi(idxStr)
|
idx, err := strconv.Atoi(idxStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false
|
return "", false
|
||||||
|
@ -150,8 +159,8 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
||||||
}
|
}
|
||||||
|
|
||||||
// path parts
|
// path parts
|
||||||
if strings.HasPrefix(key, pathPartsReplPrefix) {
|
if strings.HasPrefix(key, reqURIPathReplPrefix) {
|
||||||
idxStr := key[len(pathPartsReplPrefix):]
|
idxStr := key[len(reqURIPathReplPrefix):]
|
||||||
idx, err := strconv.Atoi(idxStr)
|
idx, err := strconv.Atoi(idxStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false
|
return "", false
|
||||||
|
@ -208,12 +217,77 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
|
||||||
repl.Map(httpVars)
|
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 (
|
const (
|
||||||
queryReplPrefix = "http.request.uri.query."
|
reqCookieReplPrefix = "http.request.cookie."
|
||||||
reqHeaderReplPrefix = "http.request.header."
|
reqHeaderReplPrefix = "http.request.header."
|
||||||
cookieReplPrefix = "http.request.cookie."
|
reqHostLabelsReplPrefix = "http.request.host.labels."
|
||||||
hostLabelReplPrefix = "http.request.host.labels."
|
reqTLSReplPrefix = "http.request.tls."
|
||||||
pathPartsReplPrefix = "http.request.uri.path."
|
reqURIPathReplPrefix = "http.request.uri.path."
|
||||||
varsReplPrefix = "http.vars."
|
reqURIQueryReplPrefix = "http.request.uri.query."
|
||||||
respHeaderReplPrefix = "http.response.header."
|
respHeaderReplPrefix = "http.response.header."
|
||||||
|
varsReplPrefix = "http.vars."
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,6 +16,9 @@ package caddyhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -30,6 +33,41 @@ func TestHTTPVarReplacement(t *testing.T) {
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
req.Host = "example.com:80"
|
req.Host = "example.com:80"
|
||||||
req.RemoteAddr = "localhost:1234"
|
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()
|
res := httptest.NewRecorder()
|
||||||
addHTTPVarsToReplacer(repl, req, res)
|
addHTTPVarsToReplacer(repl, req, res)
|
||||||
|
|
||||||
|
@ -39,7 +77,7 @@ func TestHTTPVarReplacement(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
input: "{http.request.scheme}",
|
input: "{http.request.scheme}",
|
||||||
expect: "http",
|
expect: "https",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
input: "{http.request.host}",
|
input: "{http.request.host}",
|
||||||
|
@ -69,6 +107,46 @@ func TestHTTPVarReplacement(t *testing.T) {
|
||||||
input: "{http.request.host.labels.1}",
|
input: "{http.request.host.labels.1}",
|
||||||
expect: "example",
|
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>")
|
actual := repl.ReplaceAll(tc.input, "<empty>")
|
||||||
if actual != tc.expect {
|
if actual != tc.expect {
|
||||||
|
|
|
@ -29,7 +29,7 @@ import (
|
||||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
|
||||||
"github.com/mholt/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
@ -173,7 +173,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
log("handled request",
|
log("handled request",
|
||||||
zap.String("common_log", repl.ReplaceAll(commonLogFormat, "-")),
|
zap.String("common_log", repl.ReplaceAll(commonLogFormat, commonLogEmptyValue)),
|
||||||
zap.Duration("latency", latency),
|
zap.Duration("latency", latency),
|
||||||
zap.Int("size", wrec.Size()),
|
zap.Int("size", wrec.Size()),
|
||||||
zap.Int("status", wrec.Status()),
|
zap.Int("status", wrec.Status()),
|
||||||
|
|
207
modules/caddytls/acmeissuer.go
Normal file
207
modules/caddytls/acmeissuer.go
Normal file
|
@ -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)
|
||||||
|
)
|
|
@ -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)
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/mholt/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
@ -23,8 +23,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/go-acme/lego/v3/challenge/tlsalpn01"
|
"github.com/go-acme/lego/v3/challenge/tlsalpn01"
|
||||||
"github.com/mholt/certmagic"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConnectionPolicies is an ordered group of connection policies;
|
// ConnectionPolicies is an ordered group of connection policies;
|
||||||
|
@ -32,16 +32,15 @@ import (
|
||||||
// connections at handshake-time.
|
// connections at handshake-time.
|
||||||
type ConnectionPolicies []*ConnectionPolicy
|
type ConnectionPolicies []*ConnectionPolicy
|
||||||
|
|
||||||
// TLSConfig converts the group of policies to a standard-lib-compatible
|
// Provision sets up each connection policy. It should be called
|
||||||
// TLS configuration which selects the first matching policy based on
|
// during the Validate() phase, after the TLS app (if any) is
|
||||||
// the ClientHello.
|
// already set up.
|
||||||
func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) (*tls.Config, error) {
|
func (cp ConnectionPolicies) Provision(ctx caddy.Context) error {
|
||||||
// set up each of the connection policies
|
|
||||||
for i, pol := range cp {
|
for i, pol := range cp {
|
||||||
// matchers
|
// matchers
|
||||||
mods, err := ctx.LoadModule(pol, "MatchersRaw")
|
mods, err := ctx.LoadModule(pol, "MatchersRaw")
|
||||||
if err != nil {
|
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{}) {
|
for _, modIface := range mods.(map[string]interface{}) {
|
||||||
cp[i].matchers = append(cp[i].matchers, modIface.(ConnectionMatcher))
|
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 {
|
if pol.CertSelection != nil {
|
||||||
val, err := ctx.LoadModule(pol, "CertSelection")
|
val, err := ctx.LoadModule(pol, "CertSelection")
|
||||||
if err != nil {
|
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)
|
cp[i].certSelector = val.(certmagic.CertificateSelector)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// pre-build standard TLS configs so we don't have to at handshake-time
|
// pre-build standard TLS config so we don't have to at handshake-time
|
||||||
for i := range cp {
|
err = pol.buildStandardTLSConfig(ctx)
|
||||||
err := cp[i].buildStandardTLSConfig(ctx)
|
|
||||||
if err != nil {
|
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
|
// 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
|
// 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
|
// 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)
|
return nil, fmt.Errorf("no server TLS configuration available for ClientHello: %+v", hello)
|
||||||
},
|
},
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectionPolicy specifies the logic for handling a TLS handshake.
|
// ConnectionPolicy specifies the logic for handling a TLS handshake.
|
||||||
|
@ -137,6 +140,10 @@ type ConnectionPolicy struct {
|
||||||
// Enables and configures TLS client authentication.
|
// Enables and configures TLS client authentication.
|
||||||
ClientAuthentication *ClientAuthentication `json:"client_authentication,omitempty"`
|
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
|
matchers []ConnectionMatcher
|
||||||
certSelector certmagic.CertificateSelector
|
certSelector certmagic.CertificateSelector
|
||||||
|
|
||||||
|
@ -158,15 +165,24 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
|
||||||
NextProtos: p.ALPN,
|
NextProtos: p.ALPN,
|
||||||
PreferServerCipherSuites: true,
|
PreferServerCipherSuites: true,
|
||||||
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
cfgTpl, err := tlsApp.getConfigForName(hello.ServerName)
|
// TODO: I don't love how this works: we pre-build certmagic configs
|
||||||
if err != nil {
|
// so that handshakes are faster. Unfortunately, certmagic configs are
|
||||||
return nil, fmt.Errorf("getting config for name %s: %v", hello.ServerName, err)
|
// 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)
|
||||||
newCfg := certmagic.New(tlsApp.certCache, cfgTpl)
|
// 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 {
|
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,
|
MinVersion: tls.VersionTLS12,
|
||||||
MaxVersion: tls.VersionTLS13,
|
MaxVersion: tls.VersionTLS13,
|
||||||
|
@ -240,8 +256,6 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: other fields
|
|
||||||
|
|
||||||
setDefaultTLSParams(cfg)
|
setDefaultTLSParams(cfg)
|
||||||
|
|
||||||
p.stdTLSConfig = cfg
|
p.stdTLSConfig = cfg
|
||||||
|
|
|
@ -32,7 +32,7 @@ import (
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
||||||
"github.com/mholt/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
@ -23,8 +23,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/go-acme/lego/v3/challenge"
|
"github.com/go-acme/lego/v3/challenge"
|
||||||
"github.com/mholt/certmagic"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -71,13 +71,15 @@ func (TLS) CaddyModule() caddy.ModuleInfo {
|
||||||
|
|
||||||
// Provision sets up the configuration for the TLS app.
|
// Provision sets up the configuration for the TLS app.
|
||||||
func (t *TLS) Provision(ctx caddy.Context) error {
|
func (t *TLS) Provision(ctx caddy.Context) error {
|
||||||
|
// TODO: Move assets to the new folder structure!!
|
||||||
|
|
||||||
t.ctx = ctx
|
t.ctx = ctx
|
||||||
t.logger = ctx.Logger(t)
|
t.logger = ctx.Logger(t)
|
||||||
|
|
||||||
// set up a new certificate cache; this (re)loads all certificates
|
// set up a new certificate cache; this (re)loads all certificates
|
||||||
cacheOpts := certmagic.CacheOptions{
|
cacheOpts := certmagic.CacheOptions{
|
||||||
GetConfigForCert: func(cert certmagic.Certificate) (certmagic.Config, error) {
|
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
|
||||||
return t.getConfigForName(cert.Names[0])
|
return t.getConfigForName(cert.Names[0]), nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if t.Automation != nil {
|
if t.Automation != nil {
|
||||||
|
@ -87,20 +89,25 @@ func (t *TLS) Provision(ctx caddy.Context) error {
|
||||||
t.certCache = certmagic.NewCache(cacheOpts)
|
t.certCache = certmagic.NewCache(cacheOpts)
|
||||||
|
|
||||||
// automation/management policies
|
// automation/management policies
|
||||||
if t.Automation != nil {
|
if t.Automation == nil {
|
||||||
for i, ap := range t.Automation.Policies {
|
t.Automation = new(AutomationConfig)
|
||||||
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)
|
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
|
// certificate loaders
|
||||||
val, err := ctx.LoadModule(t, "CertificatesRaw")
|
val, err := ctx.LoadModule(t, "CertificatesRaw")
|
||||||
if err != nil {
|
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{}) {
|
for modName, modIface := range val.(map[string]interface{}) {
|
||||||
if modName == "automate" {
|
if modName == "automate" {
|
||||||
|
@ -216,12 +223,11 @@ func (t *TLS) Manage(names []string) error {
|
||||||
// certmagic.Config for each (potentially large) group of names
|
// certmagic.Config for each (potentially large) group of names
|
||||||
// and call ManageSync/ManageAsync just once for the whole batch
|
// and call ManageSync/ManageAsync just once for the whole batch
|
||||||
for ap, names := range policyToNames {
|
for ap, names := range policyToNames {
|
||||||
magic := certmagic.New(t.certCache, ap.makeCertMagicConfig(t.ctx))
|
|
||||||
var err error
|
var err error
|
||||||
if ap.ManageSync {
|
if ap.ManageSync {
|
||||||
err = magic.ManageSync(names)
|
err = ap.magic.ManageSync(names)
|
||||||
} else {
|
} else {
|
||||||
err = magic.ManageAsync(t.ctx.Context, names)
|
err = ap.magic.ManageAsync(t.ctx.Context, names)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("automate: manage %v: %v", names, err)
|
return fmt.Errorf("automate: manage %v: %v", names, err)
|
||||||
|
@ -232,27 +238,46 @@ func (t *TLS) Manage(names []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleHTTPChallenge ensures that the HTTP challenge is handled for the
|
// 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 {
|
func (t *TLS) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool {
|
||||||
if !certmagic.LooksLikeHTTPChallenge(r) {
|
if !certmagic.LooksLikeHTTPChallenge(r) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
ap := t.getAutomationPolicyForName(r.Host)
|
ap := t.getAutomationPolicyForName(r.Host)
|
||||||
magic := certmagic.New(t.certCache, ap.makeCertMagicConfig(t.ctx))
|
if ap.magic.Issuer == nil {
|
||||||
return magic.HandleHTTPChallenge(w, r)
|
return false
|
||||||
|
}
|
||||||
|
if am, ok := ap.magic.Issuer.(*certmagic.ACMEManager); ok {
|
||||||
|
return am.HandleHTTPChallenge(w, r)
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TLS) getConfigForName(name string) (certmagic.Config, error) {
|
// 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 {
|
||||||
ap := t.getAutomationPolicyForName(name)
|
ap := t.getAutomationPolicyForName(name)
|
||||||
return ap.makeCertMagicConfig(t.ctx), nil
|
return ap.magic
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TLS) getAutomationPolicyForName(name string) *AutomationPolicy {
|
func (t *TLS) getAutomationPolicyForName(name string) *AutomationPolicy {
|
||||||
if t.Automation != nil {
|
|
||||||
for _, ap := range t.Automation.Policies {
|
for _, ap := range t.Automation.Policies {
|
||||||
if len(ap.Hosts) == 0 {
|
if len(ap.Hosts) == 0 {
|
||||||
// no host filter is an automatic match
|
return ap // no host filter is an automatic match
|
||||||
return ap
|
|
||||||
}
|
}
|
||||||
for _, h := range ap.Hosts {
|
for _, h := range ap.Hosts {
|
||||||
if h == name {
|
if h == name {
|
||||||
|
@ -260,8 +285,7 @@ func (t *TLS) getAutomationPolicyForName(name string) *AutomationPolicy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return t.Automation.defaultAutomationPolicy
|
||||||
return defaultAutomationPolicy
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AllMatchingCertificates returns the list of all certificates in
|
// 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
|
// then clean each storage defined in ACME automation policies
|
||||||
if t.Automation != nil {
|
if t.Automation != nil {
|
||||||
for _, ap := range t.Automation.Policies {
|
for _, ap := range t.Automation.Policies {
|
||||||
if acmeMgmt, ok := ap.Management.(ACMEManagerMaker); ok {
|
if ap.storage != nil {
|
||||||
if acmeMgmt.storage != nil {
|
certmagic.CleanStorage(ap.storage, options)
|
||||||
certmagic.CleanStorage(acmeMgmt.storage, options)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -355,23 +377,56 @@ type AutomationConfig struct {
|
||||||
OCSPCheckInterval caddy.Duration `json:"ocsp_interval,omitempty"`
|
OCSPCheckInterval caddy.Duration `json:"ocsp_interval,omitempty"`
|
||||||
|
|
||||||
// Every so often, Caddy will scan all loaded, managed
|
// Every so often, Caddy will scan all loaded, managed
|
||||||
// certificates for expiration. Certificates which are
|
// certificates for expiration. This setting changes how
|
||||||
// about 2/3 into their valid lifetime are due for
|
// frequently the scan for expiring certificates is
|
||||||
// renewal. This setting changes how frequently the scan
|
// performed. If your certificate lifetimes are very
|
||||||
// is performed. If your certificate lifetimes are very
|
// short (less than ~24 hours), you should set this to
|
||||||
// short (less than ~1 week), you should customize this.
|
// a low value.
|
||||||
RenewCheckInterval caddy.Duration `json:"renew_interval,omitempty"`
|
RenewCheckInterval caddy.Duration `json:"renew_interval,omitempty"`
|
||||||
|
|
||||||
|
defaultAutomationPolicy *AutomationPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
// AutomationPolicy designates the policy for automating the
|
// AutomationPolicy designates the policy for automating the
|
||||||
// management (obtaining, renewal, and revocation) of managed
|
// management (obtaining, renewal, and revocation) of managed
|
||||||
// TLS certificates.
|
// 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 {
|
type AutomationPolicy struct {
|
||||||
// Which hostnames this policy applies to.
|
// Which hostnames this policy applies to.
|
||||||
Hosts []string `json:"hosts,omitempty"`
|
Hosts []string `json:"hosts,omitempty"`
|
||||||
|
|
||||||
// How to manage certificates.
|
// The module that will issue certificates. Default: acme
|
||||||
ManagementRaw json.RawMessage `json:"management,omitempty" caddy:"namespace=tls.management inline_key=module"`
|
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
|
// If true, certificate management will be conducted
|
||||||
// in the foreground; this will block config reloads
|
// in the foreground; this will block config reloads
|
||||||
|
@ -381,23 +436,96 @@ type AutomationPolicy struct {
|
||||||
// of your control. Default: false
|
// of your control. Default: false
|
||||||
ManageSync bool `json:"manage_sync,omitempty"`
|
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
|
// provision converts ap into a CertMagic config.
|
||||||
// is necessary because the automation policy does not have convenient access
|
func (ap *AutomationPolicy) provision(tlsApp *TLS) error {
|
||||||
// to the TLS app's global on-demand policies;
|
// policy-specific storage implementation
|
||||||
func (ap AutomationPolicy) makeCertMagicConfig(ctx caddy.Context) certmagic.Config {
|
if ap.StorageRaw != nil {
|
||||||
// default manager (ACME) is a special case because of how CertMagic is designed
|
val, err := tlsApp.ctx.LoadModule(ap, "StorageRaw")
|
||||||
// TODO: refactor certmagic so that ACME manager is not a special case by extracting
|
if err != nil {
|
||||||
// its config fields out of the certmagic.Config struct, or something...
|
return fmt.Errorf("loading TLS storage module: %v", err)
|
||||||
if acmeMgmt, ok := ap.Management.(*ACMEManagerMaker); ok {
|
}
|
||||||
return acmeMgmt.makeCertMagicConfig(ctx)
|
cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating TLS storage configuration: %v", err)
|
||||||
|
}
|
||||||
|
ap.storage = cmStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
return certmagic.Config{
|
var ond *certmagic.OnDemandConfig
|
||||||
NewManager: ap.Management.NewManager,
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
// ChallengesConfig configures the ACME challenges.
|
||||||
|
@ -482,11 +610,6 @@ type RateLimit struct {
|
||||||
Burst int `json:"burst,omitempty"`
|
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
|
// AutomateLoader is a no-op certificate loader module
|
||||||
// that is treated as a special case: it uses this app's
|
// that is treated as a special case: it uses this app's
|
||||||
// automation features to load certificates for the
|
// 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.
|
// These perpetual values are used for on-demand TLS.
|
||||||
var (
|
var (
|
||||||
onDemandRateLimiter = certmagic.NewRateLimiter(0, 0)
|
onDemandRateLimiter = certmagic.NewRateLimiter(0, 0)
|
||||||
|
@ -521,8 +653,6 @@ var (
|
||||||
storageCleanMu sync.Mutex
|
storageCleanMu sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
var defaultAutomationPolicy = &AutomationPolicy{Management: new(ACMEManagerMaker)}
|
|
||||||
|
|
||||||
// Interface guards
|
// Interface guards
|
||||||
var (
|
var (
|
||||||
_ caddy.App = (*TLS)(nil)
|
_ caddy.App = (*TLS)(nil)
|
||||||
|
|
|
@ -17,8 +17,9 @@ package caddytls
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/go-acme/lego/v3/certcrypto"
|
"github.com/caddyserver/certmagic"
|
||||||
"github.com/klauspost/cpuid"
|
"github.com/klauspost/cpuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -101,11 +102,12 @@ var SupportedCurves = map[string]tls.CurveID{
|
||||||
|
|
||||||
// supportedCertKeyTypes is all the key types that are supported
|
// supportedCertKeyTypes is all the key types that are supported
|
||||||
// for certificates that are obtained through ACME.
|
// for certificates that are obtained through ACME.
|
||||||
var supportedCertKeyTypes = map[string]certcrypto.KeyType{
|
var supportedCertKeyTypes = map[string]certmagic.KeyType{
|
||||||
"rsa_2048": certcrypto.RSA2048,
|
"rsa2048": certmagic.RSA2048,
|
||||||
"rsa_4096": certcrypto.RSA4096,
|
"rsa4096": certmagic.RSA4096,
|
||||||
"ec_p256": certcrypto.EC256,
|
"p256": certmagic.P256,
|
||||||
"ec_p384": certcrypto.EC384,
|
"p384": certmagic.P384,
|
||||||
|
"ed25519": certmagic.ED25519,
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultCurves is the list of only the curves we want to use
|
// 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,
|
"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.
|
// publicKeyAlgorithms is the map of supported public key algorithms.
|
||||||
var publicKeyAlgorithms = map[string]x509.PublicKeyAlgorithm{
|
var publicKeyAlgorithms = map[string]x509.PublicKeyAlgorithm{
|
||||||
"rsa": x509.RSA,
|
"rsa": x509.RSA,
|
||||||
"dsa": x509.DSA,
|
"dsa": x509.DSA,
|
||||||
"ecdsa": x509.ECDSA,
|
"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)
|
||||||
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ package filestorage
|
||||||
import (
|
import (
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
"github.com/mholt/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
zaplogfmt "github.com/jsternberg/zap-logfmt"
|
zaplogfmt "github.com/jsternberg/zap-logfmt"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"go.uber.org/zap/buffer"
|
"go.uber.org/zap/buffer"
|
||||||
|
@ -31,7 +32,7 @@ func init() {
|
||||||
caddy.RegisterModule(ConsoleEncoder{})
|
caddy.RegisterModule(ConsoleEncoder{})
|
||||||
caddy.RegisterModule(JSONEncoder{})
|
caddy.RegisterModule(JSONEncoder{})
|
||||||
caddy.RegisterModule(LogfmtEncoder{})
|
caddy.RegisterModule(LogfmtEncoder{})
|
||||||
caddy.RegisterModule(StringEncoder{})
|
caddy.RegisterModule(SingleFieldEncoder{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConsoleEncoder encodes log entries that are mostly human-readable.
|
// ConsoleEncoder encodes log entries that are mostly human-readable.
|
||||||
|
@ -54,6 +55,27 @@ func (ce *ConsoleEncoder) Provision(_ caddy.Context) error {
|
||||||
return nil
|
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.
|
// JSONEncoder encodes entries as JSON.
|
||||||
type JSONEncoder struct {
|
type JSONEncoder struct {
|
||||||
zapcore.Encoder `json:"-"`
|
zapcore.Encoder `json:"-"`
|
||||||
|
@ -74,6 +96,27 @@ func (je *JSONEncoder) Provision(_ caddy.Context) error {
|
||||||
return nil
|
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:
|
// LogfmtEncoder encodes log entries as logfmt:
|
||||||
// https://www.brandur.org/logfmt
|
// https://www.brandur.org/logfmt
|
||||||
type LogfmtEncoder struct {
|
type LogfmtEncoder struct {
|
||||||
|
@ -95,26 +138,47 @@ func (lfe *LogfmtEncoder) Provision(_ caddy.Context) error {
|
||||||
return nil
|
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
|
// of a single string field in the log entry. This is useful
|
||||||
// for custom, self-encoded log entries that consist of a
|
// for custom, self-encoded log entries that consist of a
|
||||||
// single field in the structured log entry.
|
// single field in the structured log entry.
|
||||||
type StringEncoder struct {
|
type SingleFieldEncoder struct {
|
||||||
zapcore.Encoder `json:"-"`
|
zapcore.Encoder `json:"-"`
|
||||||
FieldName string `json:"field,omitempty"`
|
FieldName string `json:"field,omitempty"`
|
||||||
FallbackRaw json.RawMessage `json:"fallback,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"`
|
FallbackRaw json.RawMessage `json:"fallback,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CaddyModule returns the Caddy module information.
|
// CaddyModule returns the Caddy module information.
|
||||||
func (StringEncoder) CaddyModule() caddy.ModuleInfo {
|
func (SingleFieldEncoder) CaddyModule() caddy.ModuleInfo {
|
||||||
return caddy.ModuleInfo{
|
return caddy.ModuleInfo{
|
||||||
ID: "caddy.logging.encoders.string",
|
ID: "caddy.logging.encoders.single_field",
|
||||||
New: func() caddy.Module { return new(StringEncoder) },
|
New: func() caddy.Module { return new(SingleFieldEncoder) },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provision sets up the encoder.
|
// 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 {
|
if se.FallbackRaw != nil {
|
||||||
val, err := ctx.LoadModule(se, "FallbackRaw")
|
val, err := ctx.LoadModule(se, "FallbackRaw")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -132,16 +196,16 @@ func (se *StringEncoder) Provision(ctx caddy.Context) error {
|
||||||
// necessary because we implement our own EncodeEntry,
|
// necessary because we implement our own EncodeEntry,
|
||||||
// and if we simply let the embedded encoder's Clone
|
// and if we simply let the embedded encoder's Clone
|
||||||
// be promoted, it would return a clone of that, and
|
// be promoted, it would return a clone of that, and
|
||||||
// we'd lose our StringEncoder's EncodeEntry.
|
// we'd lose our SingleFieldEncoder's EncodeEntry.
|
||||||
func (se StringEncoder) Clone() zapcore.Encoder {
|
func (se SingleFieldEncoder) Clone() zapcore.Encoder {
|
||||||
return StringEncoder{
|
return SingleFieldEncoder{
|
||||||
Encoder: se.Encoder.Clone(),
|
Encoder: se.Encoder.Clone(),
|
||||||
FieldName: se.FieldName,
|
FieldName: se.FieldName,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncodeEntry partially implements the zapcore.Encoder interface.
|
// 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 {
|
for _, f := range fields {
|
||||||
if f.Key == se.FieldName {
|
if f.Key == se.FieldName {
|
||||||
buf := bufferpool.Get()
|
buf := bufferpool.Get()
|
||||||
|
@ -158,6 +222,21 @@ func (se StringEncoder) EncodeEntry(ent zapcore.Entry, fields []zapcore.Field) (
|
||||||
return se.Encoder.EncodeEntry(ent, fields)
|
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.
|
// LogEncoderConfig holds configuration common to most encoders.
|
||||||
type LogEncoderConfig struct {
|
type LogEncoderConfig struct {
|
||||||
MessageKey *string `json:"message_key,omitempty"`
|
MessageKey *string `json:"message_key,omitempty"`
|
||||||
|
@ -172,6 +251,53 @@ type LogEncoderConfig struct {
|
||||||
LevelFormat string `json:"level_format,omitempty"`
|
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.
|
// ZapcoreEncoderConfig returns the equivalent zapcore.EncoderConfig.
|
||||||
// If lec is nil, zap.NewProductionEncoderConfig() is returned.
|
// If lec is nil, zap.NewProductionEncoderConfig() is returned.
|
||||||
func (lec *LogEncoderConfig) ZapcoreEncoderConfig() zapcore.EncoderConfig {
|
func (lec *LogEncoderConfig) ZapcoreEncoderConfig() zapcore.EncoderConfig {
|
||||||
|
@ -263,5 +389,10 @@ var (
|
||||||
_ zapcore.Encoder = (*ConsoleEncoder)(nil)
|
_ zapcore.Encoder = (*ConsoleEncoder)(nil)
|
||||||
_ zapcore.Encoder = (*JSONEncoder)(nil)
|
_ zapcore.Encoder = (*JSONEncoder)(nil)
|
||||||
_ zapcore.Encoder = (*LogfmtEncoder)(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)
|
||||||
)
|
)
|
||||||
|
|
|
@ -19,8 +19,12 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
"gopkg.in/natefinch/lumberjack.v2"
|
"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)
|
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
|
// Interface guards
|
||||||
var (
|
var (
|
||||||
_ caddy.Provisioner = (*FileWriter)(nil)
|
_ caddy.Provisioner = (*FileWriter)(nil)
|
||||||
|
_ caddy.WriterOpener = (*FileWriter)(nil)
|
||||||
|
_ caddyfile.Unmarshaler = (*FileWriter)(nil)
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -75,8 +76,26 @@ func (nw NetWriter) OpenWriter() (io.WriteCloser, error) {
|
||||||
return net.Dial(nw.addr.Network, nw.addr.JoinHostPort(0))
|
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
|
// Interface guards
|
||||||
var (
|
var (
|
||||||
_ caddy.Provisioner = (*NetWriter)(nil)
|
_ caddy.Provisioner = (*NetWriter)(nil)
|
||||||
_ caddy.WriterOpener = (*NetWriter)(nil)
|
_ caddy.WriterOpener = (*NetWriter)(nil)
|
||||||
|
_ caddyfile.Unmarshaler = (*NetWriter)(nil)
|
||||||
)
|
)
|
||||||
|
|
|
@ -148,11 +148,10 @@ func (r *Replacer) replace(input, empty string,
|
||||||
if errOnUnknown {
|
if errOnUnknown {
|
||||||
return "", fmt.Errorf("unrecognized placeholder %s%s%s",
|
return "", fmt.Errorf("unrecognized placeholder %s%s%s",
|
||||||
string(phOpen), key, string(phClose))
|
string(phOpen), key, string(phClose))
|
||||||
} else if treatUnknownAsEmpty {
|
} else if !treatUnknownAsEmpty {
|
||||||
if empty != "" {
|
// if treatUnknownAsEmpty is true, we'll
|
||||||
sb.WriteString(empty)
|
// handle an empty val later; so only
|
||||||
}
|
// continue otherwise
|
||||||
} else {
|
|
||||||
lastWriteCursor = i
|
lastWriteCursor = i
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,11 @@ func TestReplacer(t *testing.T) {
|
||||||
input: `{{}`,
|
input: `{{}`,
|
||||||
expect: "",
|
expect: "",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
input: `{unknown}`,
|
||||||
|
empty: "-",
|
||||||
|
expect: "-",
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
actual := rep.ReplaceAll(tc.input, tc.empty)
|
actual := rep.ReplaceAll(tc.input, tc.empty)
|
||||||
if actual != tc.expect {
|
if actual != tc.expect {
|
||||||
|
|
|
@ -21,7 +21,7 @@ import (
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/mholt/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"github.com/mholt/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue