From f0216967dca12831b1aac351fc8c4bfcea148697 Mon Sep 17 00:00:00 2001
From: Matthew Holt <mholt@users.noreply.github.com>
Date: Tue, 5 Jan 2021 14:39:30 -0700
Subject: [PATCH] caddyfile: Refactor unmarshaling of module tokens

Eliminates a fair amount of repeated code
---
 caddyconfig/caddyfile/adapter.go            | 26 ++++++++++
 caddyconfig/httpcaddyfile/builtins.go       | 54 ++++++---------------
 caddyconfig/httpcaddyfile/options.go        | 49 ++++++++++---------
 caddyconfig/httpcaddyfile/serveroptions.go  | 13 ++---
 caddyconfig/httpcaddyfile/tlsapp.go         |  7 +--
 modules/caddyhttp/encode/caddyfile.go       | 13 ++---
 modules/caddyhttp/reverseproxy/caddyfile.go | 26 +++-------
 modules/caddytls/acmeissuer.go              | 13 ++---
 modules/logging/filterencoder.go            | 30 +++---------
 9 files changed, 90 insertions(+), 141 deletions(-)

diff --git a/caddyconfig/caddyfile/adapter.go b/caddyconfig/caddyfile/adapter.go
index 8d624f1d8..185816b8d 100644
--- a/caddyconfig/caddyfile/adapter.go
+++ b/caddyconfig/caddyfile/adapter.go
@@ -94,5 +94,31 @@ type ServerType interface {
 	Setup([]ServerBlock, map[string]interface{}) (*caddy.Config, []caddyconfig.Warning, error)
 }
 
+// UnmarshalModule instantiates a module with the given ID and invokes
+// UnmarshalCaddyfile on the new value using the immediate next segment
+// of d as input. In other words, d's next token should be the first
+// token of the module's Caddyfile input.
+//
+// This function is used when the next segment of Caddyfile tokens
+// belongs to another Caddy module. The returned value is often
+// type-asserted to the module's associated type for practical use
+// when setting up a config.
+func UnmarshalModule(d *Dispenser, moduleID string) (Unmarshaler, error) {
+	mod, err := caddy.GetModule(moduleID)
+	if err != nil {
+		return nil, d.Errf("getting module named '%s': %v", moduleID, err)
+	}
+	inst := mod.New()
+	unm, ok := inst.(Unmarshaler)
+	if !ok {
+		return nil, d.Errf("module %s is not a Caddyfile unmarshaler; is %T", mod.ID, inst)
+	}
+	err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
+	if err != nil {
+		return nil, err
+	}
+	return unm, nil
+}
+
 // Interface guard
 var _ caddyconfig.Adapter = (*Adapter)(nil)
diff --git a/caddyconfig/httpcaddyfile/builtins.go b/caddyconfig/httpcaddyfile/builtins.go
index 4a0b335eb..7d16da157 100644
--- a/caddyconfig/httpcaddyfile/builtins.go
+++ b/caddyconfig/httpcaddyfile/builtins.go
@@ -285,21 +285,14 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
 					return nil, h.ArgErr()
 				}
 				modName := h.Val()
-				mod, err := caddy.GetModule("tls.issuance." + modName)
-				if err != nil {
-					return nil, h.Errf("getting issuer module '%s': %v", modName, err)
-				}
-				unm, ok := mod.New().(caddyfile.Unmarshaler)
-				if !ok {
-					return nil, h.Errf("issuer module '%s' is not a Caddyfile unmarshaler", mod.ID)
-				}
-				err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
+				modID := "tls.issuance." + modName
+				unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
 				if err != nil {
 					return nil, err
 				}
 				issuer, ok := unm.(certmagic.Issuer)
 				if !ok {
-					return nil, h.Errf("module %s is not a certmagic.Issuer", mod.ID)
+					return nil, h.Errf("module %s (%T) is not a certmagic.Issuer", modID, unm)
 				}
 				issuers = append(issuers, issuer)
 
@@ -315,18 +308,12 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
 					acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
 					acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
 				}
-				dnsProvModule, err := caddy.GetModule("dns.providers." + provName)
+				modID := "dns.providers." + provName
+				unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
 				if err != nil {
-					return nil, h.Errf("getting DNS provider module named '%s': %v", provName, err)
+					return nil, err
 				}
-				dnsProvModuleInstance := dnsProvModule.New()
-				if unm, ok := dnsProvModuleInstance.(caddyfile.Unmarshaler); ok {
-					err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
-					if err != nil {
-						return nil, err
-					}
-				}
-				acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(dnsProvModuleInstance, "name", provName, h.warnings)
+				acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, h.warnings)
 
 			case "ca_root":
 				arg := h.RemainingArgs()
@@ -583,21 +570,15 @@ func parseLog(h Helper) ([]ConfigValue, error) {
 				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())
+					modID := "caddy.logging.writers." + moduleName
+					unm, err := caddyfile.UnmarshalModule(h.Dispenser, modID)
 					if err != nil {
 						return nil, err
 					}
+					var ok bool
 					wo, ok = unm.(caddy.WriterOpener)
 					if !ok {
-						return nil, h.Errf("module %s is not a WriterOpener", mod)
+						return nil, h.Errf("module %s (%T) is not a WriterOpener", modID, unm)
 					}
 				}
 				cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings)
@@ -607,21 +588,14 @@ func parseLog(h Helper) ([]ConfigValue, error) {
 					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())
+				moduleID := "caddy.logging.encoders." + moduleName
+				unm, err := caddyfile.UnmarshalModule(h.Dispenser, moduleID)
 				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)
+					return nil, h.Errf("module %s (%T) is not a zapcore.Encoder", moduleID, unm)
 				}
 				cl.EncoderRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, h.warnings)
 
diff --git a/caddyconfig/httpcaddyfile/options.go b/caddyconfig/httpcaddyfile/options.go
index 5001974b6..3a3cdf68d 100644
--- a/caddyconfig/httpcaddyfile/options.go
+++ b/caddyconfig/httpcaddyfile/options.go
@@ -34,7 +34,7 @@ func init() {
 	RegisterGlobalOption("storage", parseOptStorage)
 	RegisterGlobalOption("acme_ca", parseOptSingleString)
 	RegisterGlobalOption("acme_ca_root", parseOptSingleString)
-	RegisterGlobalOption("acme_dns", parseOptSingleString)
+	RegisterGlobalOption("acme_dns", parseOptACMEDNS)
 	RegisterGlobalOption("acme_eab", parseOptACMEEAB)
 	RegisterGlobalOption("cert_issuer", parseOptCertIssuer)
 	RegisterGlobalOption("email", parseOptSingleString)
@@ -165,26 +165,37 @@ func parseOptStorage(d *caddyfile.Dispenser) (interface{}, error) {
 	if !d.Next() { // get storage module name
 		return nil, d.ArgErr()
 	}
-	modName := d.Val()
-	mod, err := caddy.GetModule("caddy.storage." + modName)
-	if err != nil {
-		return nil, d.Errf("getting storage module '%s': %v", modName, err)
-	}
-	unm, ok := mod.New().(caddyfile.Unmarshaler)
-	if !ok {
-		return nil, d.Errf("storage module '%s' is not a Caddyfile unmarshaler", mod.ID)
-	}
-	err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
+	modID := "caddy.storage." + d.Val()
+	unm, err := caddyfile.UnmarshalModule(d, modID)
 	if err != nil {
 		return nil, err
 	}
 	storage, ok := unm.(caddy.StorageConverter)
 	if !ok {
-		return nil, d.Errf("module %s is not a StorageConverter", mod.ID)
+		return nil, d.Errf("module %s is not a caddy.StorageConverter", modID)
 	}
 	return storage, nil
 }
 
+func parseOptACMEDNS(d *caddyfile.Dispenser) (interface{}, error) {
+	if !d.Next() { // consume option name
+		return nil, d.ArgErr()
+	}
+	if !d.Next() { // get DNS module name
+		return nil, d.ArgErr()
+	}
+	modID := "dns.providers." + d.Val()
+	unm, err := caddyfile.UnmarshalModule(d, modID)
+	if err != nil {
+		return nil, err
+	}
+	prov, ok := unm.(certmagic.ACMEDNSProvider)
+	if !ok {
+		return nil, d.Errf("module %s (%T) is not a certmagic.ACMEDNSProvider", modID, unm)
+	}
+	return prov, nil
+}
+
 func parseOptACMEEAB(d *caddyfile.Dispenser) (interface{}, error) {
 	eab := new(acme.EAB)
 	for d.Next() {
@@ -220,22 +231,14 @@ func parseOptCertIssuer(d *caddyfile.Dispenser) (interface{}, error) {
 	if !d.Next() { // get issuer module name
 		return nil, d.ArgErr()
 	}
-	modName := d.Val()
-	mod, err := caddy.GetModule("tls.issuance." + modName)
-	if err != nil {
-		return nil, d.Errf("getting issuer module '%s': %v", modName, err)
-	}
-	unm, ok := mod.New().(caddyfile.Unmarshaler)
-	if !ok {
-		return nil, d.Errf("issuer module '%s' is not a Caddyfile unmarshaler", mod.ID)
-	}
-	err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
+	modID := "tls.issuance." + d.Val()
+	unm, err := caddyfile.UnmarshalModule(d, modID)
 	if err != nil {
 		return nil, err
 	}
 	iss, ok := unm.(certmagic.Issuer)
 	if !ok {
-		return nil, d.Errf("module %s is not a certmagic.Issuer", mod.ID)
+		return nil, d.Errf("module %s (%T) is not a certmagic.Issuer", modID, unm)
 	}
 	return iss, nil
 }
diff --git a/caddyconfig/httpcaddyfile/serveroptions.go b/caddyconfig/httpcaddyfile/serveroptions.go
index 38fa0f155..9e94b863b 100644
--- a/caddyconfig/httpcaddyfile/serveroptions.go
+++ b/caddyconfig/httpcaddyfile/serveroptions.go
@@ -57,21 +57,14 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (interface{}, error
 			switch d.Val() {
 			case "listener_wrappers":
 				for nesting := d.Nesting(); d.NextBlock(nesting); {
-					mod, err := caddy.GetModule("caddy.listeners." + d.Val())
-					if err != nil {
-						return nil, fmt.Errorf("finding listener module '%s': %v", d.Val(), err)
-					}
-					unm, ok := mod.New().(caddyfile.Unmarshaler)
-					if !ok {
-						return nil, fmt.Errorf("listener module '%s' is not a Caddyfile unmarshaler", mod)
-					}
-					err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
+					modID := "caddy.listeners." + d.Val()
+					unm, err := caddyfile.UnmarshalModule(d, modID)
 					if err != nil {
 						return nil, err
 					}
 					listenerWrapper, ok := unm.(caddy.ListenerWrapper)
 					if !ok {
-						return nil, fmt.Errorf("module %s is not a listener wrapper", mod)
+						return nil, fmt.Errorf("module %s (%T) is not a listener wrapper", modID, unm)
 					}
 					jsonListenerWrapper := caddyconfig.JSONModuleObject(
 						listenerWrapper,
diff --git a/caddyconfig/httpcaddyfile/tlsapp.go b/caddyconfig/httpcaddyfile/tlsapp.go
index 1fabc455b..440c447d0 100644
--- a/caddyconfig/httpcaddyfile/tlsapp.go
+++ b/caddyconfig/httpcaddyfile/tlsapp.go
@@ -392,14 +392,9 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]interf
 		acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string))
 	}
 	if globalACMEDNS != nil && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) {
-		provName := globalACMEDNS.(string)
-		dnsProvModule, err := caddy.GetModule("dns.providers." + provName)
-		if err != nil {
-			return fmt.Errorf("getting DNS provider module named '%s': %v", provName, err)
-		}
 		acmeIssuer.Challenges = &caddytls.ChallengesConfig{
 			DNS: &caddytls.DNSChallengeConfig{
-				ProviderRaw: caddyconfig.JSONModuleObject(dnsProvModule.New(), "name", provName, nil),
+				ProviderRaw: caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil),
 			},
 		}
 	}
diff --git a/modules/caddyhttp/encode/caddyfile.go b/modules/caddyhttp/encode/caddyfile.go
index 9d9646c01..2f11ca06b 100644
--- a/modules/caddyhttp/encode/caddyfile.go
+++ b/modules/caddyhttp/encode/caddyfile.go
@@ -64,21 +64,14 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 
 		for d.NextBlock(0) {
 			name := d.Val()
-			mod, err := caddy.GetModule("http.encoders." + name)
-			if err != nil {
-				return fmt.Errorf("getting encoder module '%s': %v", name, err)
-			}
-			unm, ok := mod.New().(caddyfile.Unmarshaler)
-			if !ok {
-				return fmt.Errorf("encoder module '%s' is not a Caddyfile unmarshaler", mod)
-			}
-			err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
+			modID := "http.encoders." + name
+			unm, err := caddyfile.UnmarshalModule(d, modID)
 			if err != nil {
 				return err
 			}
 			encoding, ok := unm.(Encoding)
 			if !ok {
-				return fmt.Errorf("module %s is not an HTTP encoding", mod)
+				return fmt.Errorf("module %s is not an HTTP encoding; is %T", modID, unm)
 			}
 			if enc.EncodingsRaw == nil {
 				enc.EncodingsRaw = make(caddy.ModuleMap)
diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go
index 57f425a4b..895bcbb9e 100644
--- a/modules/caddyhttp/reverseproxy/caddyfile.go
+++ b/modules/caddyhttp/reverseproxy/caddyfile.go
@@ -241,21 +241,14 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 					return d.Err("load balancing selection policy already specified")
 				}
 				name := d.Val()
-				mod, err := caddy.GetModule("http.reverse_proxy.selection_policies." + name)
-				if err != nil {
-					return d.Errf("getting load balancing policy module '%s': %v", mod, err)
-				}
-				unm, ok := mod.New().(caddyfile.Unmarshaler)
-				if !ok {
-					return d.Errf("load balancing policy module '%s' is not a Caddyfile unmarshaler", mod)
-				}
-				err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
+				modID := "http.reverse_proxy.selection_policies." + name
+				unm, err := caddyfile.UnmarshalModule(d, modID)
 				if err != nil {
 					return err
 				}
 				sel, ok := unm.(Selector)
 				if !ok {
-					return d.Errf("module %s is not a Selector", mod)
+					return d.Errf("module %s (%T) is not a reverseproxy.Selector", modID, unm)
 				}
 				if h.LoadBalancing == nil {
 					h.LoadBalancing = new(LoadBalancing)
@@ -574,21 +567,14 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 					return d.Err("transport already specified")
 				}
 				transportModuleName = d.Val()
-				mod, err := caddy.GetModule("http.reverse_proxy.transport." + transportModuleName)
-				if err != nil {
-					return d.Errf("getting transport module '%s': %v", mod, err)
-				}
-				unm, ok := mod.New().(caddyfile.Unmarshaler)
-				if !ok {
-					return d.Errf("transport module '%s' is not a Caddyfile unmarshaler", mod)
-				}
-				err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
+				modID := "http.reverse_proxy.transport." + transportModuleName
+				unm, err := caddyfile.UnmarshalModule(d, modID)
 				if err != nil {
 					return err
 				}
 				rt, ok := unm.(http.RoundTripper)
 				if !ok {
-					return d.Errf("module %s is not a RoundTripper", mod)
+					return d.Errf("module %s (%T) is not a RoundTripper", modID, unm)
 				}
 				transport = rt
 
diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go
index df071c4b3..43e758fd9 100644
--- a/modules/caddytls/acmeissuer.go
+++ b/modules/caddytls/acmeissuer.go
@@ -354,18 +354,11 @@ func (iss *ACMEIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 				if iss.Challenges.DNS == nil {
 					iss.Challenges.DNS = new(DNSChallengeConfig)
 				}
-				dnsProvModule, err := caddy.GetModule("dns.providers." + provName)
+				unm, err := caddyfile.UnmarshalModule(d, "dns.providers."+provName)
 				if err != nil {
-					return d.Errf("getting DNS provider module named '%s': %v", provName, err)
+					return err
 				}
-				dnsProvModuleInstance := dnsProvModule.New()
-				if unm, ok := dnsProvModuleInstance.(caddyfile.Unmarshaler); ok {
-					err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
-					if err != nil {
-						return err
-					}
-				}
-				iss.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(dnsProvModuleInstance, "name", provName, nil)
+				iss.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(unm, "name", provName, nil)
 
 			case "resolvers":
 				if iss.Challenges == nil {
diff --git a/modules/logging/filterencoder.go b/modules/logging/filterencoder.go
index d1c335f46..cdb552d8e 100644
--- a/modules/logging/filterencoder.go
+++ b/modules/logging/filterencoder.go
@@ -115,21 +115,14 @@ func (fe *FilterEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 					return d.ArgErr()
 				}
 				moduleName := d.Val()
-				mod, err := caddy.GetModule("caddy.logging.encoders." + moduleName)
-				if err != nil {
-					return d.Errf("getting log encoder module named '%s': %v", moduleName, err)
-				}
-				unm, ok := mod.New().(caddyfile.Unmarshaler)
-				if !ok {
-					return d.Errf("log encoder module '%s' is not a Caddyfile unmarshaler", mod)
-				}
-				err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
+				moduleID := "caddy.logging.encoders." + moduleName
+				unm, err := caddyfile.UnmarshalModule(d, moduleID)
 				if err != nil {
 					return err
 				}
 				enc, ok := unm.(zapcore.Encoder)
 				if !ok {
-					return d.Errf("module %s is not a zapcore.Encoder", mod)
+					return d.Errf("module %s (%T) is not a zapcore.Encoder", moduleID, unm)
 				}
 				fe.WrappedRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, nil)
 
@@ -140,26 +133,19 @@ func (fe *FilterEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
 						return d.ArgErr()
 					}
 					filterName := d.Val()
-					mod, err := caddy.GetModule("caddy.logging.encoders.filter." + filterName)
-					if err != nil {
-						return d.Errf("getting log filter module named '%s': %v", filterName, err)
-					}
-					unm, ok := mod.New().(caddyfile.Unmarshaler)
-					if !ok {
-						return d.Errf("log encoder module '%s' is not a Caddyfile unmarshaler", mod)
-					}
-					err = unm.UnmarshalCaddyfile(d.NewFromNextSegment())
+					moduleID := "caddy.logging.encoders.filter." + filterName
+					unm, err := caddyfile.UnmarshalModule(d, moduleID)
 					if err != nil {
 						return err
 					}
-					f, ok := unm.(LogFieldFilter)
+					filter, ok := unm.(LogFieldFilter)
 					if !ok {
-						return d.Errf("module %s is not a LogFieldFilter", mod)
+						return d.Errf("module %s (%T) is not a logging.LogFieldFilter", moduleID, unm)
 					}
 					if fe.FieldsRaw == nil {
 						fe.FieldsRaw = make(map[string]json.RawMessage)
 					}
-					fe.FieldsRaw[field] = caddyconfig.JSONModuleObject(f, "filter", filterName, nil)
+					fe.FieldsRaw[field] = caddyconfig.JSONModuleObject(filter, "filter", filterName, nil)
 				}
 
 			default: