@marcel91200 / caddy Mirror · read-only / commits / fc7340e1
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 14, 05:15 UTC

httpcaddyfile: Many tls-related improvements including on-demand support

Holy heck this was complicated

Matthew Holt committed Mar 17, 2020 at 21:00 UTC fc7340e11aa9ca6326909aedfd36bb2c5b53d2a8
10 files changed +599 -241
caddyconfig/httpcaddyfile/addresses.go
+1 -3
@@ -274,8 +274,6 @@ func ParseAddress(str string) (Address, error) {
274 return a, nil
275 }
276
277 -// TODO: which of the methods on Address are even used?
278 -
277 // String returns a human-readable form of a. It will
278 // be a cleaned-up and filled-out URL string.
279 func (a Address) String() string {
@@ -312,7 +310,7 @@ func (a Address) Normalize() Address {
310 path := a.Path
311
312 // ensure host is normalized if it's an IP address
315 - host := a.Host
313 + host := strings.TrimSpace(a.Host)
314 if ip := net.ParseIP(host); ip != nil {
315 host = ip.String()
316 }
caddyconfig/httpcaddyfile/builtins.go
+31 -41
@@ -103,14 +103,16 @@ func parseRoot(h Helper) ([]ConfigValue, error) {
103 // load <paths...>
104 // ca <acme_ca_endpoint>
105 // dns <provider_name>
106 +// on_demand
107 // }
108 //
109 func parseTLS(h Helper) ([]ConfigValue, error) {
109 - var cp *caddytls.ConnectionPolicy
110 + cp := new(caddytls.ConnectionPolicy)
111 var fileLoader caddytls.FileLoader
112 var folderLoader caddytls.FolderLoader
113 var acmeIssuer *caddytls.ACMEIssuer
114 var internalIssuer *caddytls.InternalIssuer
115 + var onDemand bool
116
117 for h.Next() {
118 // file certificate loader
@@ -173,10 +175,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
175 tlsCertTags[certFilename] = tag
176 }
177 certSelector := caddytls.CustomCertSelectionPolicy{Tag: tag}
176 - if cp == nil {
177 - cp = new(caddytls.ConnectionPolicy)
178 - }
179 -
178 cp.CertSelection = caddyconfig.JSONModuleObject(certSelector, "policy", "custom", h.warnings)
179 default:
180 return nil, h.ArgErr()
@@ -187,7 +185,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
185 hasBlock = true
186
187 switch h.Val() {
190 - // connection policy
188 case "protocols":
189 args := h.RemainingArgs()
190 if len(args) == 0 {
@@ -197,55 +194,41 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
194 if _, ok := caddytls.SupportedProtocols[args[0]]; !ok {
195 return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[0])
196 }
200 - if cp == nil {
201 - cp = new(caddytls.ConnectionPolicy)
202 - }
197 cp.ProtocolMin = args[0]
198 }
199 if len(args) > 1 {
200 if _, ok := caddytls.SupportedProtocols[args[1]]; !ok {
201 return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[1])
202 }
209 - if cp == nil {
210 - cp = new(caddytls.ConnectionPolicy)
211 - }
203 cp.ProtocolMax = args[1]
204 }
205 +
206 case "ciphers":
207 for h.NextArg() {
208 if _, ok := caddytls.SupportedCipherSuites[h.Val()]; !ok {
209 return nil, h.Errf("Wrong cipher suite name or cipher suite not supported: '%s'", h.Val())
210 }
219 - if cp == nil {
220 - cp = new(caddytls.ConnectionPolicy)
221 - }
211 cp.CipherSuites = append(cp.CipherSuites, h.Val())
212 }
213 +
214 case "curves":
215 for h.NextArg() {
216 if _, ok := caddytls.SupportedCurves[h.Val()]; !ok {
217 return nil, h.Errf("Wrong curve name or curve not supported: '%s'", h.Val())
218 }
229 - if cp == nil {
230 - cp = new(caddytls.ConnectionPolicy)
231 - }
219 cp.Curves = append(cp.Curves, h.Val())
220 }
221 +
222 case "alpn":
223 args := h.RemainingArgs()
224 if len(args) == 0 {
225 return nil, h.ArgErr()
226 }
239 - if cp == nil {
240 - cp = new(caddytls.ConnectionPolicy)
241 - }
227 cp.ALPN = args
228
244 - // certificate folder loader
229 case "load":
230 folderLoader = append(folderLoader, h.RemainingArgs()...)
231
248 - // automation policy
232 case "ca":
233 arg := h.RemainingArgs()
234 if len(arg) != 1 {
@@ -256,7 +239,6 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
239 }
240 acmeIssuer.CA = arg[0]
241
259 - // DNS provider for ACME DNS challenge
242 case "dns":
243 if !h.Next() {
244 return nil, h.ArgErr()
@@ -284,6 +266,12 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
266 }
267 acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, arg[0])
268
269 + case "on_demand":
270 + if h.NextArg() {
271 + return nil, h.ArgErr()
272 + }
273 + onDemand = true
274 +
275 default:
276 return nil, h.Errf("unknown subdirective: %s", h.Val())
277 }
@@ -304,31 +292,15 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
292 Class: "tls.certificate_loader",
293 Value: fileLoader,
294 })
307 - // ensure server uses HTTPS by setting non-nil conn policy
308 - if cp == nil {
309 - cp = new(caddytls.ConnectionPolicy)
310 - }
295 }
296 if len(folderLoader) > 0 {
297 configVals = append(configVals, ConfigValue{
298 Class: "tls.certificate_loader",
299 Value: folderLoader,
300 })
317 - // ensure server uses HTTPS by setting non-nil conn policy
318 - if cp == nil {
319 - cp = new(caddytls.ConnectionPolicy)
320 - }
321 - }
322 -
323 - // connection policy
324 - if cp != nil {
325 - configVals = append(configVals, ConfigValue{
326 - Class: "tls.connection_policy",
327 - Value: cp,
328 - })
301 }
302
331 - // automation policy
303 + // issuer
304 if acmeIssuer != nil && internalIssuer != nil {
305 // the logic to support this would be complex
306 return nil, h.Err("cannot use both ACME and internal issuers in same server block")
@@ -356,6 +328,24 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
328 })
329 }
330
331 + // on-demand TLS
332 + if onDemand {
333 + configVals = append(configVals, ConfigValue{
334 + Class: "tls.on_demand",
335 + Value: true,
336 + })
337 + }
338 +
339 + // connection policy -- always add one, to ensure that TLS
340 + // is enabled, because this directive was used (this is
341 + // needed, for instance, when a site block has a key of
342 + // just ":5000" - i.e. no hostname, and only on-demand TLS
343 + // is enabled)
344 + configVals = append(configVals, ConfigValue{
345 + Class: "tls.connection_policy",
346 + Value: cp,
347 + })
348 +
349 return configVals, nil
350 }
351
caddyconfig/httpcaddyfile/httptype.go
+62 -169
@@ -26,7 +26,6 @@ import (
26 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
27 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
28 "github.com/caddyserver/caddy/v2/modules/caddytls"
29 - "github.com/caddyserver/certmagic"
29 )
30
31 func init() {
@@ -177,105 +176,10 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
176 Servers: servers,
177 }
178
180 - // now for the TLS app! (TODO: refactor into own func)
181 - tlsApp := caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}
182 - var certLoaders []caddytls.CertificateLoader
183 - for _, p := range pairings {
184 - for i, sblock := range p.serverBlocks {
185 - // tls automation policies
186 - if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok {
187 - for _, issuerVal := range issuerVals {
188 - issuer := issuerVal.Value.(certmagic.Issuer)
189 - sblockHosts, err := st.hostsFromServerBlockKeys(sblock.block)
190 - if err != nil {
191 - return nil, warnings, err
192 - }
193 - if len(sblockHosts) > 0 {
194 - if tlsApp.Automation == nil {
195 - tlsApp.Automation = new(caddytls.AutomationConfig)
196 - }
197 - tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, &caddytls.AutomationPolicy{
198 - Subjects: sblockHosts,
199 - IssuerRaw: caddyconfig.JSONModuleObject(issuer, "module", issuer.(caddy.Module).CaddyModule().ID.Name(), &warnings),
200 - })
201 - } else {
202 - warnings = append(warnings, caddyconfig.Warning{
203 - Message: fmt.Sprintf("Server block %d %v has no names that qualify for automatic HTTPS, so no TLS automation policy will be added.", i, sblock.block.Keys),
204 - })
205 - }
206 - }
207 - }
208 - // tls certificate loaders
209 - if clVals, ok := sblock.pile["tls.certificate_loader"]; ok {
210 - for _, clVal := range clVals {
211 - certLoaders = append(certLoaders, clVal.Value.(caddytls.CertificateLoader))
212 - }
213 - }
214 - }
215 - }
216 - // group certificate loaders by module name, then add to config
217 - if len(certLoaders) > 0 {
218 - loadersByName := make(map[string]caddytls.CertificateLoader)
219 - for _, cl := range certLoaders {
220 - name := caddy.GetModuleName(cl)
221 - // ugh... technically, we may have multiple FileLoader and FolderLoader
222 - // modules (because the tls directive returns one per occurrence), but
223 - // the config structure expects only one instance of each kind of loader
224 - // module, so we have to combine them... instead of enumerating each
225 - // possible cert loader module in a type switch, we can use reflection,
226 - // which works on any cert loaders that are slice types
227 - if reflect.TypeOf(cl).Kind() == reflect.Slice {
228 - combined := reflect.ValueOf(loadersByName[name])
229 - if !combined.IsValid() {
230 - combined = reflect.New(reflect.TypeOf(cl)).Elem()
231 - }
232 - clVal := reflect.ValueOf(cl)
233 - for i := 0; i < clVal.Len(); i++ {
234 - combined = reflect.Append(reflect.Value(combined), clVal.Index(i))
235 - }
236 - loadersByName[name] = combined.Interface().(caddytls.CertificateLoader)
237 - }
238 - }
239 - for certLoaderName, loaders := range loadersByName {
240 - tlsApp.CertificatesRaw[certLoaderName] = caddyconfig.JSON(loaders, &warnings)
241 - }
242 - }
243 - // if global ACME CA, DNS, or email were set, append a catch-all automation
244 - // policy that ensures they will be used if no tls directive was used
245 - acmeCA, hasACMECA := options["acme_ca"]
246 - acmeDNS, hasACMEDNS := options["acme_dns"]
247 - email, hasEmail := options["email"]
248 - if hasACMECA || hasACMEDNS || hasEmail {
249 - if tlsApp.Automation == nil {
250 - tlsApp.Automation = new(caddytls.AutomationConfig)
251 - }
252 - if !hasACMECA {
253 - acmeCA = ""
254 - }
255 - if !hasEmail {
256 - email = ""
257 - }
258 - mgr := caddytls.ACMEIssuer{
259 - CA: acmeCA.(string),
260 - Email: email.(string),
261 - }
262 - if hasACMEDNS {
263 - provName := acmeDNS.(string)
264 - dnsProvModule, err := caddy.GetModule("tls.dns." + provName)
265 - if err != nil {
266 - return nil, warnings, fmt.Errorf("getting DNS provider module named '%s': %v", provName, err)
267 - }
268 - mgr.Challenges = &caddytls.ChallengesConfig{
269 - DNSRaw: caddyconfig.JSONModuleObject(dnsProvModule.New(), "provider", provName, &warnings),
270 - }
271 - }
272 - tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, &caddytls.AutomationPolicy{
273 - IssuerRaw: caddyconfig.JSONModuleObject(mgr, "module", "acme", &warnings),
274 - })
275 - }
276 - if tlsApp.Automation != nil {
277 - // consolidate automation policies that are the exact same
278 - tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
179 + // then make the TLS app
180 + tlsApp, warnings, err := st.buildTLSApp(pairings, options, warnings)
181 + if err != nil {
182 + return nil, warnings, err
183 }
184
185 // if experimental HTTP/3 is enabled, enable it on each server
@@ -316,10 +220,10 @@ func (st ServerType) Setup(originalServerBlocks []caddyfile.ServerBlock,
220
221 // annnd the top-level config, then we're done!
222 cfg := &caddy.Config{AppsRaw: make(caddy.ModuleMap)}
319 - if !reflect.DeepEqual(httpApp, caddyhttp.App{}) {
223 + if len(httpApp.Servers) > 0 {
224 cfg.AppsRaw["http"] = caddyconfig.JSON(httpApp, &warnings)
225 }
322 - if !reflect.DeepEqual(tlsApp, caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}) {
226 + if !reflect.DeepEqual(tlsApp, &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}) {
227 cfg.AppsRaw["tls"] = caddyconfig.JSON(tlsApp, &warnings)
228 }
229 if storageCvtr, ok := options["storage"].(caddy.StorageConverter); ok {
@@ -377,7 +281,6 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
281 var val interface{}
282 var err error
283 disp := caddyfile.NewDispenser(segment)
380 - // TODO: make this switch into a map
284 switch dir {
285 case "http_port":
286 val, err = parseOptHTTPPort(disp)
@@ -399,6 +302,10 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
302 val, err = parseOptAdmin(disp)
303 case "debug":
304 options["debug"] = true
305 + case "on_demand_tls":
306 + val, err = parseOptOnDemand(disp)
307 + case "local_certs":
308 + val = true
309 default:
310 return nil, fmt.Errorf("unrecognized parameter name: %s", dir)
311 }
@@ -411,8 +318,10 @@ func (ServerType) evaluateGlobalOptionsBlock(serverBlocks []serverBlock, options
318 return serverBlocks[1:], nil
319 }
320
414 -// hostsFromServerBlockKeys returns a list of all the
415 -// hostnames found in the keys of the server block sb.
321 +// hostsFromServerBlockKeys returns a list of all the non-empty hostnames
322 +// found in the keys of the server block sb. If sb has a key that omits
323 +// the hostname (i.e. is a catch-all/empty host), then the returned list
324 +// is empty, because the server block effectively matches ALL hosts.
325 // The list may not be in a consistent order.
326 func (st *ServerType) hostsFromServerBlockKeys(sb caddyfile.ServerBlock) ([]string, error) {
327 // first get each unique hostname
@@ -424,7 +333,9 @@ func (st *ServerType) hostsFromServerBlockKeys(sb caddyfile.ServerBlock) ([]stri
333 }
334 addr = addr.Normalize()
335 if addr.Host == "" {
427 - continue
336 + // server block contains a key like ":443", i.e. the host portion
337 + // is empty / catch-all, which means to match all hosts
338 + return []string{}, nil
339 }
340 hostMap[addr.Host] = struct{}{}
341 }
@@ -497,25 +408,18 @@ func (st *ServerType) serversFromPairings(
408 return nil, fmt.Errorf("server block %v: compiling matcher sets: %v", sblock.block.Keys, err)
409 }
410
500 - // tls: connection policies and toggle auto HTTPS
501 - if _, ok := sblock.pile["tls.off"]; ok {
502 - // TODO: right now, no directives yield any tls.off value...
503 - // tls off: disable TLS (and automatic HTTPS) for server block's names
504 - if srv.AutoHTTPS == nil {
505 - srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
506 - }
507 - srv.AutoHTTPS.Disabled = true
508 - } else if cpVals, ok := sblock.pile["tls.connection_policy"]; ok {
509 - // tls connection policies
411 + hosts, err := st.hostsFromServerBlockKeys(sblock.block)
412 + if err != nil {
413 + return nil, err
414 + }
415
416 + // tls: connection policies
417 + if cpVals, ok := sblock.pile["tls.connection_policy"]; ok {
418 + // tls connection policies
419 for _, cpVal := range cpVals {
420 cp := cpVal.Value.(*caddytls.ConnectionPolicy)
421
422 // make sure the policy covers all hostnames from the block
515 - hosts, err := st.hostsFromServerBlockKeys(sblock.block)
516 - if err != nil {
517 - return nil, err
518 - }
423 for _, h := range hosts {
424 if h == defaultSNI {
425 hosts = append(hosts, "")
@@ -524,7 +428,6 @@ func (st *ServerType) serversFromPairings(
428 }
429 }
430
527 - // TODO: are matchers needed if every hostname of the resulting config is matched?
431 if len(hosts) > 0 {
432 cp.MatchersRaw = caddy.ModuleMap{
433 "sni": caddyconfig.JSON(hosts, warnings), // make sure to match all hosts, not just auto-HTTPS-qualified ones
@@ -536,7 +439,6 @@ func (st *ServerType) serversFromPairings(
439
440 srv.TLSConnPolicies = append(srv.TLSConnPolicies, cp)
441 }
539 - // TODO: consolidate equal conn policies?
442 }
443
444 // exclude any hosts that were defined explicitly with
@@ -547,7 +449,7 @@ func (st *ServerType) serversFromPairings(
449 return nil, err
450 }
451 addr = addr.Normalize()
550 - if addr.Scheme == "http" {
452 + if addr.Scheme == "http" && addr.Host != "" {
453 if srv.AutoHTTPS == nil {
454 srv.AutoHTTPS = new(caddyhttp.AutoHTTPSConfig)
455 }
@@ -607,10 +509,15 @@ func (st *ServerType) serversFromPairings(
509 // catch-all/default policy if there isn't one already (it's
510 // important that it goes at the end) - see issue #3004:
511 // https://github.com/caddyserver/caddy/issues/3004
512 + // TODO: maybe a smarter way to handle this might be to just make the
513 + // auto-HTTPS logic at provision-time detect if there is any connection
514 + // policy missing for any HTTPS-enabled hosts, if so, add it... maybe?
515 if !hasCatchAllTLSConnPolicy && (len(srv.TLSConnPolicies) > 0 || defaultSNI != "") {
516 srv.TLSConnPolicies = append(srv.TLSConnPolicies, &caddytls.ConnectionPolicy{DefaultSNI: defaultSNI})
517 }
518
519 + // tidy things up a bit
520 + srv.TLSConnPolicies = consolidateConnPolicies(srv.TLSConnPolicies)
521 srv.Routes = consolidateRoutes(srv.Routes)
522
523 servers[fmt.Sprintf("srv%d", i)] = srv
@@ -619,6 +526,26 @@ func (st *ServerType) serversFromPairings(
526 return servers, nil
527 }
528
529 +// consolidateConnPolicies combines TLS connection policies that are the same,
530 +// for a cleaner overall output.
531 +func consolidateConnPolicies(cps caddytls.ConnectionPolicies) caddytls.ConnectionPolicies {
532 + for i := 0; i < len(cps); i++ {
533 + for j := 0; j < len(cps); j++ {
534 + if j == i {
535 + continue
536 + }
537 +
538 + // if they're exactly equal in every way, just keep one of them
539 + if reflect.DeepEqual(cps[i], cps[j]) {
540 + cps = append(cps[:j], cps[j+1:]...)
541 + i--
542 + break
543 + }
544 + }
545 + }
546 + return cps
547 +}
548 +
549 // appendSubrouteToRouteList appends the routes in subroute
550 // to the routeList, optionally qualified by matchers.
551 func appendSubrouteToRouteList(routeList caddyhttp.RouteList,
@@ -750,52 +677,6 @@ func consolidateRoutes(routes caddyhttp.RouteList) caddyhttp.RouteList {
677 return routes
678 }
679
753 -// consolidateAutomationPolicies combines automation policies that are the same,
754 -// for a cleaner overall output.
755 -func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls.AutomationPolicy {
756 - for i := 0; i < len(aps); i++ {
757 - for j := 0; j < len(aps); j++ {
758 - if j == i {
759 - continue
760 - }
761 -
762 - // if they're exactly equal in every way, just keep one of them
763 - if reflect.DeepEqual(aps[i], aps[j]) {
764 - aps = append(aps[:j], aps[j+1:]...)
765 - i--
766 - break
767 - }
768 -
769 - // if the policy is the same, we can keep just one, but we have
770 - // to be careful which one we keep; if only one has any hostnames
771 - // defined, then we need to keep the one without any hostnames,
772 - // otherwise the one without any subjects (a catch-all) would be
773 - // eaten up by the one with subjects; and if both have subjects, we
774 - // need to combine their lists
775 - if reflect.DeepEqual(aps[i].IssuerRaw, aps[j].IssuerRaw) &&
776 - aps[i].ManageSync == aps[j].ManageSync {
777 - if len(aps[i].Subjects) == 0 && len(aps[j].Subjects) > 0 {
778 - aps = append(aps[:j], aps[j+1:]...)
779 - } else if len(aps[i].Subjects) > 0 && len(aps[j].Subjects) == 0 {
780 - aps = append(aps[:i], aps[i+1:]...)
781 - } else {
782 - aps[i].Subjects = append(aps[i].Subjects, aps[j].Subjects...)
783 - aps = append(aps[:j], aps[j+1:]...)
784 - }
785 - i--
786 - break
787 - }
788 - }
789 - }
790 -
791 - // ensure any catch-all policies go last
792 - sort.SliceStable(aps, func(i, j int) bool {
793 - return len(aps[i].Subjects) > len(aps[j].Subjects)
794 - })
795 -
796 - return aps
797 -}
798 -
680 func matcherSetFromMatcherToken(
681 tkn caddyfile.Token,
682 matcherDefs map[string]caddy.ModuleMap,
@@ -831,6 +712,7 @@ func (st *ServerType) compileEncodedMatcherSets(sblock caddyfile.ServerBlock) ([
712 // keep routes with common host and path matchers together
713 var matcherPairs []*hostPathPair
714
715 + var catchAllHosts bool
716 for _, key := range sblock.Keys {
717 addr, err := ParseAddress(key)
718 if err != nil {
@@ -856,6 +738,17 @@ func (st *ServerType) compileEncodedMatcherSets(sblock caddyfile.ServerBlock) ([
738 matcherPairs = append(matcherPairs, chosenMatcherPair)
739 }
740
741 + // if one of the keys has no host (i.e. is a catch-all for
742 + // any hostname), then we need to null out the host matcher
743 + // entirely so that it matches all hosts
744 + if addr.Host == "" && !catchAllHosts {
745 + chosenMatcherPair.hostm = nil
746 + catchAllHosts = true
747 + }
748 + if catchAllHosts {
749 + continue
750 + }
751 +
752 // add this server block's keys to the matcher
753 // pair if it doesn't already exist
754 if addr.Host != "" {
caddyconfig/httpcaddyfile/options.go
+67 -6
@@ -15,11 +15,12 @@
15 package httpcaddyfile
16
17 import (
18 - "fmt"
18 "strconv"
19 + "time"
20
21 "github.com/caddyserver/caddy/v2"
22 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
23 + "github.com/caddyserver/caddy/v2/modules/caddytls"
24 )
25
26 func parseOptHTTPPort(d *caddyfile.Dispenser) (int, error) {
@@ -68,7 +69,7 @@ func parseOptOrder(d *caddyfile.Dispenser) ([]string, error) {
69 }
70 dirName := d.Val()
71 if _, ok := registeredDirectives[dirName]; !ok {
71 - return nil, fmt.Errorf("%s is not a registered directive", dirName)
72 + return nil, d.Errf("%s is not a registered directive", dirName)
73 }
74
75 // get positional token
@@ -104,7 +105,7 @@ func parseOptOrder(d *caddyfile.Dispenser) ([]string, error) {
105 case "before":
106 case "after":
107 default:
107 - return nil, fmt.Errorf("unknown positional '%s'", pos)
108 + return nil, d.Errf("unknown positional '%s'", pos)
109 }
110
111 // get name of other directive
@@ -145,11 +146,11 @@ func parseOptStorage(d *caddyfile.Dispenser) (caddy.StorageConverter, error) {
146 modName := args[0]
147 mod, err := caddy.GetModule("caddy.storage." + modName)
148 if err != nil {
148 - return nil, fmt.Errorf("getting storage module '%s': %v", modName, err)
149 + return nil, d.Errf("getting storage module '%s': %v", modName, err)
150 }
151 unm, ok := mod.New().(caddyfile.Unmarshaler)
152 if !ok {
152 - return nil, fmt.Errorf("storage module '%s' is not a Caddyfile unmarshaler", mod.ID)
153 + return nil, d.Errf("storage module '%s' is not a Caddyfile unmarshaler", mod.ID)
154 }
155 err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
156 if err != nil {
@@ -157,7 +158,7 @@ func parseOptStorage(d *caddyfile.Dispenser) (caddy.StorageConverter, error) {
158 }
159 storage, ok := unm.(caddy.StorageConverter)
160 if !ok {
160 - return nil, fmt.Errorf("module %s is not a StorageConverter", mod.ID)
161 + return nil, d.Errf("module %s is not a StorageConverter", mod.ID)
162 }
163 return storage, nil
164 }
@@ -187,3 +188,63 @@ func parseOptAdmin(d *caddyfile.Dispenser) (string, error) {
188 }
189 return "", nil
190 }
191 +
192 +func parseOptOnDemand(d *caddyfile.Dispenser) (*caddytls.OnDemandConfig, error) {
193 + var ond *caddytls.OnDemandConfig
194 + for d.Next() {
195 + if d.NextArg() {
196 + return nil, d.ArgErr()
197 + }
198 + for nesting := d.Nesting(); d.NextBlock(nesting); {
199 + switch d.Val() {
200 + case "ask":
201 + if !d.NextArg() {
202 + return nil, d.ArgErr()
203 + }
204 + if ond == nil {
205 + ond = new(caddytls.OnDemandConfig)
206 + }
207 + ond.Ask = d.Val()
208 +
209 + case "interval":
210 + if !d.NextArg() {
211 + return nil, d.ArgErr()
212 + }
213 + dur, err := time.ParseDuration(d.Val())
214 + if err != nil {
215 + return nil, err
216 + }
217 + if ond == nil {
218 + ond = new(caddytls.OnDemandConfig)
219 + }
220 + if ond.RateLimit == nil {
221 + ond.RateLimit = new(caddytls.RateLimit)
222 + }
223 + ond.RateLimit.Interval = caddy.Duration(dur)
224 +
225 + case "burst":
226 + if !d.NextArg() {
227 + return nil, d.ArgErr()
228 + }
229 + burst, err := strconv.Atoi(d.Val())
230 + if err != nil {
231 + return nil, err
232 + }
233 + if ond == nil {
234 + ond = new(caddytls.OnDemandConfig)
235 + }
236 + if ond.RateLimit == nil {
237 + ond.RateLimit = new(caddytls.RateLimit)
238 + }
239 + ond.RateLimit.Burst = burst
240 +
241 + default:
242 + return nil, d.Errf("unrecognized parameter '%s'", d.Val())
243 + }
244 + }
245 + }
246 + if ond == nil {
247 + return nil, d.Err("expected at least one config parameter for on_demand_tls")
248 + }
249 + return ond, nil
250 +}
caddyconfig/httpcaddyfile/tlsapp.go new
+386
@@ -0,0 +1,386 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package httpcaddyfile
16 +
17 +import (
18 + "bytes"
19 + "fmt"
20 + "reflect"
21 + "sort"
22 +
23 + "github.com/caddyserver/caddy/v2"
24 + "github.com/caddyserver/caddy/v2/caddyconfig"
25 + "github.com/caddyserver/caddy/v2/modules/caddytls"
26 + "github.com/caddyserver/certmagic"
27 +)
28 +
29 +func (st ServerType) buildTLSApp(
30 + pairings []sbAddrAssociation,
31 + options map[string]interface{},
32 + warnings []caddyconfig.Warning,
33 +) (*caddytls.TLS, []caddyconfig.Warning, error) {
34 +
35 + tlsApp := &caddytls.TLS{CertificatesRaw: make(caddy.ModuleMap)}
36 + var certLoaders []caddytls.CertificateLoader
37 +
38 + // count how many server blocks have a key with no host,
39 + // and find all hosts that share a server block with a
40 + // hostless key, so that they don't get forgotten/omitted
41 + // by auto-HTTPS (since they won't appear in route matchers)
42 + var serverBlocksWithHostlessKey int
43 + hostsSharedWithHostlessKey := make(map[string]struct{})
44 + for _, pair := range pairings {
45 + for _, sb := range pair.serverBlocks {
46 + for _, key := range sb.block.Keys {
47 + addr, err := ParseAddress(key)
48 + if err != nil {
49 + return nil, warnings, err
50 + }
51 + addr = addr.Normalize()
52 + if addr.Host == "" {
53 + serverBlocksWithHostlessKey++
54 + // this server block has a hostless key, now
55 + // go through and add all the hosts to the set
56 + for _, otherKey := range sb.block.Keys {
57 + if otherKey == key {
58 + continue
59 + }
60 + addr, err := ParseAddress(otherKey)
61 + if err != nil {
62 + return nil, warnings, err
63 + }
64 + addr = addr.Normalize()
65 + if addr.Host != "" {
66 + hostsSharedWithHostlessKey[addr.Host] = struct{}{}
67 + }
68 + }
69 + break
70 + }
71 + }
72 + }
73 + }
74 +
75 + catchAllAP, err := newBaseAutomationPolicy(options, warnings, false)
76 + if err != nil {
77 + return nil, warnings, err
78 + }
79 +
80 + for _, p := range pairings {
81 + for _, sblock := range p.serverBlocks {
82 + // get values that populate an automation policy for this block
83 + var ap *caddytls.AutomationPolicy
84 +
85 + sblockHosts, err := st.hostsFromServerBlockKeys(sblock.block)
86 + if err != nil {
87 + return nil, warnings, err
88 + }
89 + if len(sblockHosts) == 0 {
90 + ap = catchAllAP
91 + }
92 +
93 + // on-demand tls
94 + if _, ok := sblock.pile["tls.on_demand"]; ok {
95 + if ap == nil {
96 + var err error
97 + ap, err = newBaseAutomationPolicy(options, warnings, true)
98 + if err != nil {
99 + return nil, warnings, err
100 + }
101 + }
102 + ap.OnDemand = true
103 + }
104 +
105 + // certificate issuers
106 + if issuerVals, ok := sblock.pile["tls.cert_issuer"]; ok {
107 + for _, issuerVal := range issuerVals {
108 + issuer := issuerVal.Value.(certmagic.Issuer)
109 + if ap == nil {
110 + var err error
111 + ap, err = newBaseAutomationPolicy(options, warnings, true)
112 + if err != nil {
113 + return nil, warnings, err
114 + }
115 + }
116 + encoded := caddyconfig.JSONModuleObject(issuer, "module", issuer.(caddy.Module).CaddyModule().ID.Name(), &warnings)
117 + if ap == catchAllAP && ap.IssuerRaw != nil && !bytes.Equal(ap.IssuerRaw, encoded) {
118 + return nil, warnings, fmt.Errorf("conflicting issuer configuration: %s != %s", ap.IssuerRaw, encoded)
119 + }
120 + ap.IssuerRaw = encoded
121 + }
122 + }
123 +
124 + if ap != nil {
125 + // first make sure this block is allowed to create an automation policy;
126 + // doing so is forbidden if it has a key with no host (i.e. ":443")
127 + // and if there is a different server block that also has a key with no
128 + // host -- since a key with no host matches any host, we need its
129 + // associated automation policy to have an empty Subjects list, i.e. no
130 + // host filter, which is indistinguishable between the two server blocks
131 + // because automation is not done in the context of a particular server...
132 + // this is an example of a poor mapping from Caddyfile to JSON but that's
133 + // the least-leaky abstraction I could figure out
134 + if len(sblockHosts) == 0 {
135 + if serverBlocksWithHostlessKey > 1 {
136 + // this server block and at least one other has a key with no host,
137 + // making the two indistinguishable; it is misleading to define such
138 + // a policy within one server block since it actually will apply to
139 + // others as well
140 + return nil, warnings, fmt.Errorf("cannot make a TLS automation policy from a server block that has a host-less address when there are other server block addresses lacking a host")
141 + }
142 + if catchAllAP == nil {
143 + // this server block has a key with no hosts, but there is not yet
144 + // a catch-all automation policy (probably because no global options
145 + // were set), so this one becomes it
146 + catchAllAP = ap
147 + }
148 + }
149 +
150 + // associate our new automation policy with this server block's hosts,
151 + // unless, of course, the server block has a key with no hosts, in which
152 + // case its automation policy becomes or blends with the default/global
153 + // automation policy because, of necessity, it applies to all hostnames
154 + // (i.e. it has no Subjects filter) -- in that case, we'll append it last
155 + if ap != catchAllAP {
156 + ap.Subjects = sblockHosts
157 +
158 + // if a combination of public and internal names were given
159 + // for this same server block and no issuer was specified, we
160 + // need to separate them out in the automation policies so
161 + // that the internal names can use the internal issuer and
162 + // the other names can use the default/public/ACME issuer
163 + var ap2 *caddytls.AutomationPolicy
164 + if ap.Issuer == nil {
165 + var internal, external []string
166 + for _, s := range ap.Subjects {
167 + if certmagic.SubjectQualifiesForPublicCert(s) {
168 + external = append(external, s)
169 + } else {
170 + internal = append(internal, s)
171 + }
172 + }
173 + if len(external) > 0 && len(internal) > 0 {
174 + ap.Subjects = external
175 + apCopy := *ap
176 + ap2 = &apCopy
177 + ap2.Subjects = internal
178 + ap2.IssuerRaw = caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)
179 + }
180 + }
181 + if tlsApp.Automation == nil {
182 + tlsApp.Automation = new(caddytls.AutomationConfig)
183 + }
184 + tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap)
185 + if ap2 != nil {
186 + tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, ap2)
187 + }
188 + }
189 + }
190 +
191 + // certificate loaders
192 + if clVals, ok := sblock.pile["tls.certificate_loader"]; ok {
193 + for _, clVal := range clVals {
194 + certLoaders = append(certLoaders, clVal.Value.(caddytls.CertificateLoader))
195 + }
196 + }
197 + }
198 + }
199 +
200 + // group certificate loaders by module name, then add to config
201 + if len(certLoaders) > 0 {
202 + loadersByName := make(map[string]caddytls.CertificateLoader)
203 + for _, cl := range certLoaders {
204 + name := caddy.GetModuleName(cl)
205 + // ugh... technically, we may have multiple FileLoader and FolderLoader
206 + // modules (because the tls directive returns one per occurrence), but
207 + // the config structure expects only one instance of each kind of loader
208 + // module, so we have to combine them... instead of enumerating each
209 + // possible cert loader module in a type switch, we can use reflection,
210 + // which works on any cert loaders that are slice types
211 + if reflect.TypeOf(cl).Kind() == reflect.Slice {
212 + combined := reflect.ValueOf(loadersByName[name])
213 + if !combined.IsValid() {
214 + combined = reflect.New(reflect.TypeOf(cl)).Elem()
215 + }
216 + clVal := reflect.ValueOf(cl)
217 + for i := 0; i < clVal.Len(); i++ {
218 + combined = reflect.Append(reflect.Value(combined), clVal.Index(i))
219 + }
220 + loadersByName[name] = combined.Interface().(caddytls.CertificateLoader)
221 + }
222 + }
223 + for certLoaderName, loaders := range loadersByName {
224 + tlsApp.CertificatesRaw[certLoaderName] = caddyconfig.JSON(loaders, &warnings)
225 + }
226 + }
227 +
228 + // set any of the on-demand options, for if/when on-demand TLS is enabled
229 + if onDemand, ok := options["on_demand_tls"].(*caddytls.OnDemandConfig); ok {
230 + if tlsApp.Automation == nil {
231 + tlsApp.Automation = new(caddytls.AutomationConfig)
232 + }
233 + tlsApp.Automation.OnDemand = onDemand
234 + }
235 +
236 + // if there is a global/catch-all automation policy, ensure it goes last
237 + if catchAllAP != nil {
238 + if tlsApp.Automation == nil {
239 + tlsApp.Automation = new(caddytls.AutomationConfig)
240 + }
241 + tlsApp.Automation.Policies = append(tlsApp.Automation.Policies, catchAllAP)
242 + }
243 +
244 + // if any hostnames appear on the same server block as a key with
245 + // no host, they will not be used with route matchers because the
246 + // hostless key matches all hosts, therefore, it wouldn't be
247 + // considered for auto-HTTPS, so we need to make sure those hosts
248 + // are manually considered for managed certificates
249 + var al caddytls.AutomateLoader
250 + for h := range hostsSharedWithHostlessKey {
251 + al = append(al, h)
252 + }
253 + if len(al) > 0 {
254 + tlsApp.CertificatesRaw["automate"] = caddyconfig.JSON(al, &warnings)
255 + }
256 +
257 + // do a little verification & cleanup
258 + if tlsApp.Automation != nil {
259 + // ensure automation policies don't overlap subjects (this should be
260 + // an error at provision-time as well, but catch it in the adapt phase
261 + // for convenience)
262 + automationHostSet := make(map[string]struct{})
263 + for _, ap := range tlsApp.Automation.Policies {
264 + for _, s := range ap.Subjects {
265 + if _, ok := automationHostSet[s]; ok {
266 + return nil, warnings, fmt.Errorf("hostname appears in more than one automation policy, making certificate management ambiguous: %s", s)
267 + }
268 + automationHostSet[s] = struct{}{}
269 + }
270 + }
271 +
272 + // consolidate automation policies that are the exact same
273 + tlsApp.Automation.Policies = consolidateAutomationPolicies(tlsApp.Automation.Policies)
274 + }
275 +
276 + return tlsApp, warnings, nil
277 +}
278 +
279 +// newBaseAutomationPolicy returns a new TLS automation policy that gets
280 +// its values from the global options map. It should be used as the base
281 +// for any other automation policies. A nil policy (and no error) will be
282 +// returned if there are no default/global options. However, if always is
283 +// true, a non-nil value will always be returned (unless there is an error).
284 +func newBaseAutomationPolicy(options map[string]interface{}, warnings []caddyconfig.Warning, always bool) (*caddytls.AutomationPolicy, error) {
285 + acmeCA, hasACMECA := options["acme_ca"]
286 + acmeDNS, hasACMEDNS := options["acme_dns"]
287 + acmeCARoot, hasACMECARoot := options["acme_ca_root"]
288 + email, hasEmail := options["email"]
289 + localCerts, hasLocalCerts := options["local_certs"]
290 +
291 + hasGlobalAutomationOpts := hasACMECA || hasACMEDNS || hasACMECARoot || hasEmail || hasLocalCerts
292 +
293 + // if there are no global options related to automation policies
294 + // set, then we can just return right away
295 + if !hasGlobalAutomationOpts {
296 + if always {
297 + return new(caddytls.AutomationPolicy), nil
298 + }
299 + return nil, nil
300 + }
301 +
302 + ap := new(caddytls.AutomationPolicy)
303 +
304 + if localCerts != nil {
305 + // internal issuer enabled trumps any ACME configurations; useful in testing
306 + ap.IssuerRaw = caddyconfig.JSONModuleObject(caddytls.InternalIssuer{}, "module", "internal", &warnings)
307 + } else {
308 + if acmeCA == nil {
309 + acmeCA = ""
310 + }
311 + if email == nil {
312 + email = ""
313 + }
314 + mgr := caddytls.ACMEIssuer{
315 + CA: acmeCA.(string),
316 + Email: email.(string),
317 + }
318 + if acmeDNS != nil {
319 + provName := acmeDNS.(string)
320 + dnsProvModule, err := caddy.GetModule("tls.dns." + provName)
321 + if err != nil {
322 + return nil, fmt.Errorf("getting DNS provider module named '%s': %v", provName, err)
323 + }
324 + mgr.Challenges = &caddytls.ChallengesConfig{
325 + DNSRaw: caddyconfig.JSONModuleObject(dnsProvModule.New(), "provider", provName, &warnings),
326 + }
327 + }
328 + if acmeCARoot != nil {
329 + mgr.TrustedRootsPEMFiles = []string{acmeCARoot.(string)}
330 + }
331 + ap.IssuerRaw = caddyconfig.JSONModuleObject(mgr, "module", "acme", &warnings)
332 + }
333 +
334 + return ap, nil
335 +}
336 +
337 +// consolidateAutomationPolicies combines automation policies that are the same,
338 +// for a cleaner overall output.
339 +func consolidateAutomationPolicies(aps []*caddytls.AutomationPolicy) []*caddytls.AutomationPolicy {
340 + for i := 0; i < len(aps); i++ {
341 + for j := 0; j < len(aps); j++ {
342 + if j == i {
343 + continue
344 + }
345 +
346 + // if they're exactly equal in every way, just keep one of them
347 + if reflect.DeepEqual(aps[i], aps[j]) {
348 + aps = append(aps[:j], aps[j+1:]...)
349 + i--
350 + break
351 + }
352 +
353 + // if the policy is the same, we can keep just one, but we have
354 + // to be careful which one we keep; if only one has any hostnames
355 + // defined, then we need to keep the one without any hostnames,
356 + // otherwise the one without any subjects (a catch-all) would be
357 + // eaten up by the one with subjects; and if both have subjects, we
358 + // need to combine their lists
359 + if bytes.Equal(aps[i].IssuerRaw, aps[j].IssuerRaw) &&
360 + bytes.Equal(aps[i].StorageRaw, aps[j].StorageRaw) &&
361 + aps[i].MustStaple == aps[j].MustStaple &&
362 + aps[i].KeyType == aps[j].KeyType &&
363 + aps[i].OnDemand == aps[j].OnDemand &&
364 + aps[i].RenewalWindowRatio == aps[j].RenewalWindowRatio &&
365 + aps[i].ManageSync == aps[j].ManageSync {
366 + if len(aps[i].Subjects) == 0 && len(aps[j].Subjects) > 0 {
367 + aps = append(aps[:j], aps[j+1:]...)
368 + } else if len(aps[i].Subjects) > 0 && len(aps[j].Subjects) == 0 {
369 + aps = append(aps[:i], aps[i+1:]...)
370 + } else {
371 + aps[i].Subjects = append(aps[i].Subjects, aps[j].Subjects...)
372 + aps = append(aps[:j], aps[j+1:]...)
373 + }
374 + i--
375 + break
376 + }
377 + }
378 + }
379 +
380 + // ensure any catch-all policies go last
381 + sort.SliceStable(aps, func(i, j int) bool {
382 + return len(aps[i].Subjects) > len(aps[j].Subjects)
383 + })
384 +
385 + return aps
386 +}
modules/caddyhttp/autohttps.go
+1 -1
@@ -232,7 +232,7 @@ uniqueDomainsLoop:
232 // some names we've found might already have automation policies
233 // explicitly specified for them; we should exclude those from
234 // our hidden/implicit policy, since applying a name to more than
235 - // one automation policy would be confusing and an error
235 + // one automation policy would be confusing and an error
236 if app.tlsApp.Automation != nil {
237 for _, ap := range app.tlsApp.Automation.Policies {
238 for _, apHost := range ap.Subjects {
modules/caddytls/automation.go
+35 -16
@@ -23,6 +23,7 @@ import (
23 "github.com/caddyserver/caddy/v2"
24 "github.com/caddyserver/certmagic"
25 "github.com/go-acme/lego/v3/challenge"
26 + "go.uber.org/zap"
27 )
28
29 // AutomationConfig designates configuration for the
@@ -131,31 +132,49 @@ func (ap *AutomationPolicy) provision(tlsApp *TLS) error {
132
133 var ond *certmagic.OnDemandConfig
134 if ap.OnDemand {
134 - var onDemand *OnDemandConfig
135 - if tlsApp.Automation != nil {
136 - onDemand = tlsApp.Automation.OnDemand
137 - }
138 -
135 ond = &certmagic.OnDemandConfig{
136 DecisionFunc: func(name string) error {
141 - if onDemand != nil {
142 - if onDemand.Ask != "" {
143 - err := onDemandAskRequest(onDemand.Ask, name)
144 - if err != nil {
145 - return err
146 - }
147 - }
148 - // check the rate limiter last because
149 - // doing so makes a reservation
150 - if !onDemandRateLimiter.Allow() {
151 - return fmt.Errorf("on-demand rate limit exceeded")
137 + // if an "ask" endpoint was defined, consult it first
138 + if tlsApp.Automation != nil &&
139 + tlsApp.Automation.OnDemand != nil &&
140 + tlsApp.Automation.OnDemand.Ask != "" {
141 + err := onDemandAskRequest(tlsApp.Automation.OnDemand.Ask, name)
142 + if err != nil {
143 + return err
144 }
145 }
146 + // check the rate limiter last because
147 + // doing so makes a reservation
148 + if !onDemandRateLimiter.Allow() {
149 + return fmt.Errorf("on-demand rate limit exceeded")
150 + }
151 return nil
152 },
153 }
154 }
155
156 + // if this automation policy has no Issuer defined, and
157 + // none the subjects do not qualify for a public certificate,
158 + // set the issuer to internal so that these names can all
159 + // get certificates; critically, we can only do this if an
160 + // issuer is not explictly configured AND if the list of
161 + // subjects is non-empty
162 + if ap.IssuerRaw == nil && len(ap.Subjects) > 0 {
163 + var anyPublic bool
164 + for _, s := range ap.Subjects {
165 + if certmagic.SubjectQualifiesForPublicCert(s) {
166 + anyPublic = true
167 + break
168 + }
169 + }
170 + if !anyPublic {
171 + tlsApp.logger.Info("setting internal issuer for automation policy that has only internal subjects but no issuer configured",
172 + zap.Strings("subjects", ap.Subjects))
173 + ap.IssuerRaw = json.RawMessage(`{"module":"internal"}`)
174 + }
175 + }
176 +
177 + // load and provision the issuer module
178 if ap.IssuerRaw != nil {
179 val, err := tlsApp.ctx.LoadModule(ap, "IssuerRaw")
180 if err != nil {
modules/caddytls/connpolicy.go
+1 -1
@@ -173,7 +173,7 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error {
173 // TODO: I don't love how this works: we pre-build certmagic configs
174 // so that handshakes are faster. Unfortunately, certmagic configs are
175 // comprised of settings from both a TLS connection policy and a TLS
176 - // automation policy. The only two fields (as of March 2020; v2 beta 16)
176 + // automation policy. The only two fields (as of March 2020; v2 beta 17)
177 // of a certmagic config that come from the TLS connection policy are
178 // CertSelection and DefaultServerName, so an automation policy is what
179 // builds the base certmagic config. Since the pre-built config is
modules/caddytls/tls.go
+10 -2
@@ -179,9 +179,17 @@ func (t *TLS) Validate() error {
179 if t.Automation != nil {
180 // ensure that host aren't repeated; since only the first
181 // automation policy is used, repeating a host in the lists
182 - // isn't useful and is probably a mistake
182 + // isn't useful and is probably a mistake; same for two
183 + // catch-all/default policies
184 + var hasDefault bool
185 hostSet := make(map[string]int)
186 for i, ap := range t.Automation.Policies {
187 + if len(ap.Subjects) == 0 {
188 + if hasDefault {
189 + return fmt.Errorf("automation policy %d is the second policy that acts as default/catch-all, but will never be used", i)
190 + }
191 + hasDefault = true
192 + }
193 for _, h := range ap.Subjects {
194 if first, ok := hostSet[h]; ok {
195 return fmt.Errorf("automation policy %d: cannot apply more than one automation policy to host: %s (first match in policy %d)", i, h, first)
@@ -301,7 +309,7 @@ func (t *TLS) AddAutomationPolicy(ap *AutomationPolicy) error {
309 // fewer names) exists, prioritize this new policy
310 if len(other.Subjects) < len(ap.Subjects) {
311 t.Automation.Policies = append(t.Automation.Policies[:i],
304 - append([]*AutomationPolicy{ap}, t.Automation.Policies[i+1:]...)...)
312 + append([]*AutomationPolicy{ap}, t.Automation.Policies[i:]...)...)
313 return nil
314 }
315 }
modules/filestorage/filestorage.go
+5 -2
@@ -59,5 +59,8 @@ func (s *FileStorage) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
59 return nil
60 }
61
62 -// Interface guard
63 -var _ caddy.StorageConverter = (*FileStorage)(nil)
62 +// Interface guards
63 +var (
64 + _ caddy.StorageConverter = (*FileStorage)(nil)
65 + _ caddyfile.Unmarshaler = (*FileStorage)(nil)
66 +)