Skip to content

Commit b4d945e

Browse files
committed
httpcaddyfile: Validates TLS DNS challenge options
Adds validation to the TLS Caddyfile adapter to ensure that when DNS challenge options (such as propagation_delay or dns_ttl) are specified, a DNS provider is also configured. Adds new integration tests to verify this validation logic, and implements a new mechanism for adapt tests to assert a config adapt error.
1 parent 33c88bd commit b4d945e

7 files changed

+91
-21
lines changed

caddyconfig/httpcaddyfile/builtins.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
130130
var reusePrivateKeys bool
131131
var forceAutomate bool
132132

133+
// Track which DNS challenge options are set
134+
var dnsOptionsSet []string
135+
133136
firstLine := h.RemainingArgs()
134137
switch len(firstLine) {
135138
case 0:
@@ -350,6 +353,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
350353
if acmeIssuer.Challenges.DNS == nil {
351354
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
352355
}
356+
dnsOptionsSet = append(dnsOptionsSet, "resolvers")
353357
acmeIssuer.Challenges.DNS.Resolvers = args
354358

355359
case "propagation_delay":
@@ -371,6 +375,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
371375
if acmeIssuer.Challenges.DNS == nil {
372376
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
373377
}
378+
dnsOptionsSet = append(dnsOptionsSet, "propagation_delay")
374379
acmeIssuer.Challenges.DNS.PropagationDelay = caddy.Duration(delay)
375380

376381
case "propagation_timeout":
@@ -398,6 +403,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
398403
if acmeIssuer.Challenges.DNS == nil {
399404
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
400405
}
406+
dnsOptionsSet = append(dnsOptionsSet, "propagation_timeout")
401407
acmeIssuer.Challenges.DNS.PropagationTimeout = caddy.Duration(timeout)
402408

403409
case "dns_ttl":
@@ -419,6 +425,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
419425
if acmeIssuer.Challenges.DNS == nil {
420426
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
421427
}
428+
dnsOptionsSet = append(dnsOptionsSet, "dns_ttl")
422429
acmeIssuer.Challenges.DNS.TTL = caddy.Duration(ttl)
423430

424431
case "dns_challenge_override_domain":
@@ -435,6 +442,7 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
435442
if acmeIssuer.Challenges.DNS == nil {
436443
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
437444
}
445+
dnsOptionsSet = append(dnsOptionsSet, "dns_challenge_override_domain")
438446
acmeIssuer.Challenges.DNS.OverrideDomain = arg[0]
439447

440448
case "ca_root":
@@ -470,6 +478,18 @@ func parseTLS(h Helper) ([]ConfigValue, error) {
470478
}
471479
}
472480

481+
// Validate DNS challenge config: any DNS challenge option except "dns" requires a DNS provider
482+
if acmeIssuer != nil && acmeIssuer.Challenges != nil && acmeIssuer.Challenges.DNS != nil {
483+
dnsCfg := acmeIssuer.Challenges.DNS
484+
providerSet := dnsCfg.ProviderRaw != nil || h.Option("dns") != nil
485+
if len(dnsOptionsSet) > 0 && !providerSet {
486+
return nil, h.Errf(
487+
"setting DNS challenge options [%s] requires a DNS provider (set with the 'dns' subdirective or 'acme_dns' global option)",
488+
strings.Join(dnsOptionsSet, ", "),
489+
)
490+
}
491+
}
492+
473493
// a naked tls directive is not allowed
474494
if len(firstLine) == 0 && !hasBlock {
475495
return nil, h.ArgErr()
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
localhost
2+
3+
tls {
4+
propagation_delay 10s
5+
dns_ttl 5m
6+
}
7+
8+
----------
9+
parsing caddyfile tokens for 'tls': setting DNS challenge options [propagation_delay, dns_ttl] requires a DNS provider (set with the 'dns' subdirective or 'acme_dns' global option), at Caddyfile:6
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
:443 {
2+
tls {
3+
propagation_timeout 30s
4+
}
5+
}
6+
----------
7+
parsing caddyfile tokens for 'tls': setting DNS challenge options [propagation_timeout] requires a DNS provider (set with the 'dns' subdirective or 'acme_dns' global option), at Caddyfile:4
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
:443 {
2+
tls {
3+
propagation_delay 30s
4+
}
5+
}
6+
----------
7+
parsing caddyfile tokens for 'tls': setting DNS challenge options [propagation_delay] requires a DNS provider (set with the 'dns' subdirective or 'acme_dns' global option), at Caddyfile:4

caddytest/integration/caddyfile_adapt/tls_dns_ttl.caddyfiletest

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ localhost
22

33
respond "hello from localhost"
44
tls {
5+
dns mock
56
dns_ttl 5m10s
67
}
78
----------
@@ -54,6 +55,9 @@ tls {
5455
{
5556
"challenges": {
5657
"dns": {
58+
"provider": {
59+
"name": "mock"
60+
},
5761
"ttl": 310000000000
5862
}
5963
},

caddytest/integration/caddyfile_adapt/tls_propagation_options.caddyfiletest

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ localhost
22

33
respond "hello from localhost"
44
tls {
5+
dns mock
56
propagation_delay 5m10s
67
propagation_timeout 10m20s
78
}
@@ -56,7 +57,10 @@ tls {
5657
"challenges": {
5758
"dns": {
5859
"propagation_delay": 310000000000,
59-
"propagation_timeout": 620000000000
60+
"propagation_timeout": 620000000000,
61+
"provider": {
62+
"name": "mock"
63+
}
6064
}
6165
},
6266
"module": "acme"

caddytest/integration/caddyfile_adapt_test.go

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010
"testing"
1111

12+
"github.com/caddyserver/caddy/v2/caddyconfig"
1213
"github.com/caddyserver/caddy/v2/caddytest"
1314
_ "github.com/caddyserver/caddy/v2/internal/testmocks"
1415
)
@@ -27,30 +28,48 @@ func TestCaddyfileAdaptToJSON(t *testing.T) {
2728
if f.IsDir() {
2829
continue
2930
}
30-
31-
// read the test file
3231
filename := f.Name()
33-
data, err := os.ReadFile("./caddyfile_adapt/" + filename)
34-
if err != nil {
35-
t.Errorf("failed to read %s dir: %s", filename, err)
36-
}
3732

38-
// split the Caddyfile (first) and JSON (second) parts
39-
// (append newline to Caddyfile to match formatter expectations)
40-
parts := strings.Split(string(data), "----------")
41-
caddyfile, json := strings.TrimSpace(parts[0])+"\n", strings.TrimSpace(parts[1])
33+
// run each file as a subtest, so that we can see which
34+
// one fails and so that we can run them in parallel.
35+
t.Run(filename, func(t *testing.T) {
36+
t.Parallel()
4237

43-
// replace windows newlines in the json with unix newlines
44-
json = winNewlines.ReplaceAllString(json, "\n")
38+
// read the test file
39+
data, err := os.ReadFile("./caddyfile_adapt/" + filename)
40+
if err != nil {
41+
t.Errorf("failed to read %s dir: %s", filename, err)
42+
}
4543

46-
// replace os-specific default path for file_server's hide field
47-
replacePath, _ := jsonMod.Marshal(fmt.Sprint(".", string(filepath.Separator), "Caddyfile"))
48-
json = strings.ReplaceAll(json, `"./Caddyfile"`, string(replacePath))
44+
// split the Caddyfile (first) and JSON (second) parts
45+
// (append newline to Caddyfile to match formatter expectations)
46+
parts := strings.Split(string(data), "----------")
47+
caddyfile, expected := strings.TrimSpace(parts[0])+"\n", strings.TrimSpace(parts[1])
4948

50-
// run the test
51-
ok := caddytest.CompareAdapt(t, filename, caddyfile, "caddyfile", json)
52-
if !ok {
53-
t.Errorf("failed to adapt %s", filename)
54-
}
49+
// replace windows newlines in the json with unix newlines
50+
expected = winNewlines.ReplaceAllString(expected, "\n")
51+
52+
// replace os-specific default path for file_server's hide field
53+
replacePath, _ := jsonMod.Marshal(fmt.Sprint(".", string(filepath.Separator), "Caddyfile"))
54+
expected = strings.ReplaceAll(expected, `"./Caddyfile"`, string(replacePath))
55+
56+
// if the expected output is JSON, compare it
57+
if len(expected) > 0 && expected[0] == '{' {
58+
ok := caddytest.CompareAdapt(t, filename, caddyfile, "caddyfile", expected)
59+
if !ok {
60+
t.Errorf("failed to adapt %s", filename)
61+
}
62+
return
63+
}
64+
65+
// otherwise, adapt the Caddyfile and check for errors
66+
cfgAdapter := caddyconfig.GetAdapter("caddyfile")
67+
_, _, err = cfgAdapter.Adapt([]byte(caddyfile), nil)
68+
if err == nil {
69+
t.Errorf("expected error for %s but got none", filename)
70+
} else if !strings.Contains(err.Error(), expected) {
71+
t.Errorf("expected error for %s to contain:\n%s\nbut got:\n%s", filename, expected, err.Error())
72+
}
73+
})
5574
}
5675
}

0 commit comments

Comments
 (0)