@marcel91200 / caddy Mirror · read-only / commits / 9becf61a
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 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 -}