Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 12, 00:45 UTC
go.mod: Upgrade to libdns 1.0 beta APIs (requires upgraded DNS providers)
This is the only way we can properly, reliably support ECH.
Matthew Holt committed
Apr 7, 2025 at 12:43 UTC
9becf61a9f5bafb88a15823ce80c1325d3a30a4f
4 files changed
+25
-329
go.mod
+2
-2
@@ -8,7 +8,7 @@ require (
8
github.com/Masterminds/sprig/v3 v3.3.0
9
github.com/alecthomas/chroma/v2 v2.15.0
10
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
11
- github.com/caddyserver/certmagic v0.22.2
11
+ github.com/caddyserver/certmagic v0.22.3-0.20250407182622-b9399eadfbe7
12
github.com/caddyserver/zerossl v0.1.3
13
github.com/cloudflare/circl v1.6.0
14
github.com/dustin/go-humanize v1.0.1
@@ -116,7 +116,7 @@ require (
116
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
117
github.com/jackc/pgtype v1.14.0 // indirect
118
github.com/jackc/pgx/v4 v4.18.3 // indirect
119
- github.com/libdns/libdns v0.2.3
119
+ github.com/libdns/libdns v1.0.0-beta.1
120
github.com/manifoldco/promptui v0.9.0 // indirect
121
github.com/mattn/go-colorable v0.1.13 // indirect
122
github.com/mattn/go-isatty v0.0.20 // indirect
go.sum
+4
-4
@@ -93,8 +93,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
93
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
94
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
95
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
96
-github.com/caddyserver/certmagic v0.22.2 h1:qzZURXlrxwR5m25/jpvVeEyJHeJJMvAwe5zlMufOTQk=
97
-github.com/caddyserver/certmagic v0.22.2/go.mod h1:hbqE7BnkjhX5IJiFslPmrSeobSeZvI6ux8tyxhsd6qs=
96
+github.com/caddyserver/certmagic v0.22.3-0.20250407182622-b9399eadfbe7 h1:dZCbkHTYh2ulXpmUK4ZrYWMCTa7x4/F3w52RGQfYoIY=
97
+github.com/caddyserver/certmagic v0.22.3-0.20250407182622-b9399eadfbe7/go.mod h1:r8ZK7Y8zaUU3j5g+oTiluLtQX8fwWI6MamcRINlSsJY=
98
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
99
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
100
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
@@ -327,8 +327,8 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
327
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
328
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
329
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
330
-github.com/libdns/libdns v0.2.3 h1:ba30K4ObwMGB/QTmqUxf3H4/GmUrCAIkMWejeGl12v8=
331
-github.com/libdns/libdns v0.2.3/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
330
+github.com/libdns/libdns v1.0.0-beta.1 h1:KIf4wLfsrEpXpZ3vmc/poM8zCATXT2klbdPe6hyOBjQ=
331
+github.com/libdns/libdns v1.0.0-beta.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
332
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
333
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
334
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
modules/caddytls/ech.go
+19
-194
@@ -641,10 +641,6 @@ nextName:
641
}
642
643
relName := libdns.RelativeName(domain+".", zone)
644
- // TODO: libdns.RelativeName should probably return "@" instead of "". (The latest commits of libdns do this, so remove this logic once upgraded.)
645
- if relName == "" {
646
- relName = "@"
647
- }
644
645
// get existing records for this domain; we need to make sure another
646
// record exists for it so we don't accidentally trample a wildcard; we
@@ -657,23 +653,25 @@ nextName:
653
zap.Error(err))
654
continue
655
}
660
- var httpsRec libdns.Record
656
+ var httpsRec libdns.ServiceBinding
657
var nameHasExistingRecord bool
658
for _, rec := range recs {
663
- // TODO: providers SHOULD normalize root-level records to be named "@"; remove the extra conditions when the transition to the new semantics is done
664
- if rec.Name == relName || (rec.Name == "" && relName == "@") {
659
+ rr := rec.RR()
660
+ if rr.Name == relName {
661
// CNAME records are exclusive of all other records, so we cannot publish an HTTPS
662
// record for a domain that is CNAME'd. See #6922.
667
- if rec.Type == "CNAME" {
663
+ if rr.Type == "CNAME" {
664
dnsPub.logger.Warn("domain has CNAME record, so unable to publish ECH data to HTTPS record",
665
zap.String("domain", domain),
670
- zap.String("cname_value", rec.Value))
666
+ zap.String("cname_value", rr.Data))
667
continue nextName
668
}
669
nameHasExistingRecord = true
674
- if rec.Type == "HTTPS" && (rec.Target == "" || rec.Target == ".") {
675
- httpsRec = rec
676
- break
670
+ if svcb, ok := rec.(libdns.ServiceBinding); ok && svcb.Scheme == "https" {
671
+ if svcb.Target == "" || svcb.Target == "." {
672
+ httpsRec = svcb
673
+ break
674
+ }
675
}
676
}
677
}
@@ -689,31 +687,24 @@ nextName:
687
zap.String("zone", zone))
688
continue
689
}
692
- params := make(svcParams)
693
- if httpsRec.Value != "" {
694
- params, err = parseSvcParams(httpsRec.Value)
695
- if err != nil {
696
- dnsPub.logger.Error("unable to parse existing DNS record to publish ECH data to HTTPS DNS record",
697
- zap.String("domain", domain),
698
- zap.String("https_rec_value", httpsRec.Value),
699
- zap.Error(err))
700
- continue
701
- }
690
+ params := httpsRec.Params
691
+ if params == nil {
692
+ params = make(libdns.SvcParams)
693
}
694
704
- // overwrite only the ech SvcParamKey
695
+ // overwrite only the "ech" SvcParamKey
696
params["ech"] = []string{base64.StdEncoding.EncodeToString(configListBin)}
697
698
// publish record
699
_, err = dnsPub.provider.SetRecords(ctx, zone, []libdns.Record{
709
- {
700
+ libdns.ServiceBinding{
701
// HTTPS and SVCB RRs: RFC 9460 (https://www.rfc-editor.org/rfc/rfc9460)
711
- Type: "HTTPS",
702
+ Scheme: "https",
703
Name: relName,
713
- Priority: 2, // allows a manual override with priority 1
714
- Target: ".",
715
- Value: params.String(),
704
TTL: 1 * time.Minute, // TODO: for testing only
705
+ Priority: 2, // allows a manual override with priority 1
706
+ Target: ".",
707
+ Params: params,
708
},
709
})
710
if err != nil {
@@ -952,172 +943,6 @@ func newECHConfigID(ctx caddy.Context) (uint8, error) {
943
return 0, fmt.Errorf("depleted attempts to find an available config_id")
944
}
945
955
-// svcParams represents SvcParamKey and SvcParamValue pairs as
956
-// described in https://www.rfc-editor.org/rfc/rfc9460 (section 2.1).
957
-type svcParams map[string][]string
958
-
959
-// parseSvcParams parses service parameters into a structured type
960
-// for safer manipulation.
961
-func parseSvcParams(input string) (svcParams, error) {
962
- if len(input) > 4096 {
963
- return nil, fmt.Errorf("input too long: %d", len(input))
964
- }
965
-
966
- params := make(svcParams)
967
- input = strings.TrimSpace(input) + " "
968
-
969
- for cursor := 0; cursor < len(input); cursor++ {
970
- var key, rawVal string
971
-
972
- keyValPair:
973
- for i := cursor; i < len(input); i++ {
974
- switch input[i] {
975
- case '=':
976
- key = strings.ToLower(strings.TrimSpace(input[cursor:i]))
977
- i++
978
- cursor = i
979
-
980
- var quoted bool
981
- if input[cursor] == '"' {
982
- quoted = true
983
- i++
984
- cursor = i
985
- }
986
-
987
- var escaped bool
988
-
989
- for j := cursor; j < len(input); j++ {
990
- switch input[j] {
991
- case '"':
992
- if !quoted {
993
- return nil, fmt.Errorf("illegal DQUOTE at position %d", j)
994
- }
995
- if !escaped {
996
- // end of quoted value
997
- rawVal = input[cursor:j]
998
- j++
999
- cursor = j
1000
- break keyValPair
1001
- }
1002
- case '\\':
1003
- escaped = true
1004
- case ' ', '\t', '\n', '\r':
1005
- if !quoted {
1006
- // end of unquoted value
1007
- rawVal = input[cursor:j]
1008
- cursor = j
1009
- break keyValPair
1010
- }
1011
- default:
1012
- escaped = false
1013
- }
1014
- }
1015
-
1016
- case ' ', '\t', '\n', '\r':
1017
- // key with no value (flag)
1018
- key = input[cursor:i]
1019
- params[key] = []string{}
1020
- cursor = i
1021
- break keyValPair
1022
- }
1023
- }
1024
-
1025
- if rawVal == "" {
1026
- continue
1027
- }
1028
-
1029
- var sb strings.Builder
1030
-
1031
- var escape int // start of escape sequence (after \, so 0 is never a valid start)
1032
- for i := 0; i < len(rawVal); i++ {
1033
- ch := rawVal[i]
1034
- if escape > 0 {
1035
- // validate escape sequence
1036
- // (RFC 9460 Appendix A)
1037
- // escaped: "\" ( non-digit / dec-octet )
1038
- // non-digit: "%x21-2F / %x3A-7E"
1039
- // dec-octet: "0-255 as a 3-digit decimal number"
1040
- if ch >= '0' && ch <= '9' {
1041
- // advance to end of decimal octet, which must be 3 digits
1042
- i += 2
1043
- if i > len(rawVal) {
1044
- return nil, fmt.Errorf("value ends with incomplete escape sequence: %s", rawVal[escape:])
1045
- }
1046
- decOctet, err := strconv.Atoi(rawVal[escape : i+1])
1047
- if err != nil {
1048
- return nil, err
1049
- }
1050
- if decOctet < 0 || decOctet > 255 {
1051
- return nil, fmt.Errorf("invalid decimal octet in escape sequence: %s (%d)", rawVal[escape:i], decOctet)
1052
- }
1053
- sb.WriteRune(rune(decOctet))
1054
- escape = 0
1055
- continue
1056
- } else if (ch < 0x21 || ch > 0x2F) && (ch < 0x3A && ch > 0x7E) {
1057
- return nil, fmt.Errorf("illegal escape sequence %s", rawVal[escape:i])
1058
- }
1059
- }
1060
- switch ch {
1061
- case ';', '(', ')':
1062
- // RFC 9460 Appendix A:
1063
- // > contiguous = 1*( non-special / escaped )
1064
- // > non-special is VCHAR minus DQUOTE, ";", "(", ")", and "\".
1065
- return nil, fmt.Errorf("illegal character in value %q at position %d: %s", rawVal, i, string(ch))
1066
- case '\\':
1067
- escape = i + 1
1068
- default:
1069
- sb.WriteByte(ch)
1070
- escape = 0
1071
- }
1072
- }
1073
-
1074
- params[key] = strings.Split(sb.String(), ",")
1075
- }
1076
-
1077
- return params, nil
1078
-}
1079
-
1080
-// String serializes svcParams into zone presentation format.
1081
-func (params svcParams) String() string {
1082
- var sb strings.Builder
1083
- for key, vals := range params {
1084
- if sb.Len() > 0 {
1085
- sb.WriteRune(' ')
1086
- }
1087
- sb.WriteString(key)
1088
- var hasVal, needsQuotes bool
1089
- for _, val := range vals {
1090
- if len(val) > 0 {
1091
- hasVal = true
1092
- }
1093
- if strings.ContainsAny(val, `" `) {
1094
- needsQuotes = true
1095
- }
1096
- if hasVal && needsQuotes {
1097
- break
1098
- }
1099
- }
1100
- if hasVal {
1101
- sb.WriteRune('=')
1102
- }
1103
- if needsQuotes {
1104
- sb.WriteRune('"')
1105
- }
1106
- for i, val := range vals {
1107
- if i > 0 {
1108
- sb.WriteRune(',')
1109
- }
1110
- val = strings.ReplaceAll(val, `"`, `\"`)
1111
- val = strings.ReplaceAll(val, `,`, `\,`)
1112
- sb.WriteString(val)
1113
- }
1114
- if needsQuotes {
1115
- sb.WriteRune('"')
1116
- }
1117
- }
1118
- return sb.String()
1119
-}
1120
-
946
// ECHPublisher is an interface for publishing ECHConfigList values
947
// so that they can be used by clients.
948
type ECHPublisher interface {
modules/caddytls/ech_test.go
deleted
-129
@@ -1,129 +0,0 @@
1
-package caddytls
2
-
3
-import (
4
- "reflect"
5
- "testing"
6
-)
7
-
8
-func TestParseSvcParams(t *testing.T) {
9
- for i, test := range []struct {
10
- input string
11
- expect svcParams
12
- shouldErr bool
13
- }{
14
- {
15
- input: `alpn="h2,h3" no-default-alpn ipv6hint=2001:db8::1 port=443`,
16
- expect: svcParams{
17
- "alpn": {"h2", "h3"},
18
- "no-default-alpn": {},
19
- "ipv6hint": {"2001:db8::1"},
20
- "port": {"443"},
21
- },
22
- },
23
- {
24
- input: `key=value quoted="some string" flag`,
25
- expect: svcParams{
26
- "key": {"value"},
27
- "quoted": {"some string"},
28
- "flag": {},
29
- },
30
- },
31
- {
32
- input: `key="nested \"quoted\" value,foobar"`,
33
- expect: svcParams{
34
- "key": {`nested "quoted" value`, "foobar"},
35
- },
36
- },
37
- {
38
- input: `alpn=h3,h2 tls-supported-groups=29,23 no-default-alpn ech="foobar"`,
39
- expect: svcParams{
40
- "alpn": {"h3", "h2"},
41
- "tls-supported-groups": {"29", "23"},
42
- "no-default-alpn": {},
43
- "ech": {"foobar"},
44
- },
45
- },
46
- {
47
- input: `escape=\097`,
48
- expect: svcParams{
49
- "escape": {"a"},
50
- },
51
- },
52
- {
53
- input: `escapes=\097\098c`,
54
- expect: svcParams{
55
- "escapes": {"abc"},
56
- },
57
- },
58
- } {
59
- actual, err := parseSvcParams(test.input)
60
- if err != nil && !test.shouldErr {
61
- t.Errorf("Test %d: Expected no error, but got: %v (input=%q)", i, err, test.input)
62
- continue
63
- } else if err == nil && test.shouldErr {
64
- t.Errorf("Test %d: Expected an error, but got no error (input=%q)", i, test.input)
65
- continue
66
- }
67
- if !reflect.DeepEqual(test.expect, actual) {
68
- t.Errorf("Test %d: Expected %v, got %v (input=%q)", i, test.expect, actual, test.input)
69
- continue
70
- }
71
- }
72
-}
73
-
74
-func TestSvcParamsString(t *testing.T) {
75
- // this test relies on the parser also working
76
- // because we can't just compare string outputs
77
- // since map iteration is unordered
78
- for i, test := range []svcParams{
79
-
80
- {
81
- "alpn": {"h2", "h3"},
82
- "no-default-alpn": {},
83
- "ipv6hint": {"2001:db8::1"},
84
- "port": {"443"},
85
- },
86
-
87
- {
88
- "key": {"value"},
89
- "quoted": {"some string"},
90
- "flag": {},
91
- },
92
- {
93
- "key": {`nested "quoted" value`, "foobar"},
94
- },
95
- {
96
- "alpn": {"h3", "h2"},
97
- "tls-supported-groups": {"29", "23"},
98
- "no-default-alpn": {},
99
- "ech": {"foobar"},
100
- },
101
- } {
102
- combined := test.String()
103
- parsed, err := parseSvcParams(combined)
104
- if err != nil {
105
- t.Errorf("Test %d: Expected no error, but got: %v (input=%q)", i, err, test)
106
- continue
107
- }
108
- if len(parsed) != len(test) {
109
- t.Errorf("Test %d: Expected %d keys, but got %d", i, len(test), len(parsed))
110
- continue
111
- }
112
- for key, expectedVals := range test {
113
- if expected, actual := len(expectedVals), len(parsed[key]); expected != actual {
114
- t.Errorf("Test %d: Expected key %s to have %d values, but had %d", i, key, expected, actual)
115
- continue
116
- }
117
- for j, expected := range expectedVals {
118
- if actual := parsed[key][j]; actual != expected {
119
- t.Errorf("Test %d key %q value %d: Expected '%s' but got '%s'", i, key, j, expected, actual)
120
- continue
121
- }
122
- }
123
- }
124
- if !reflect.DeepEqual(parsed, test) {
125
- t.Errorf("Test %d: Expected %#v, got %#v", i, test, combined)
126
- continue
127
- }
128
- }
129
-}