From b73d37908cbfedaa558c75b713b6b47a9927360b Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Tue, 25 Jan 2022 09:57:20 -0500 Subject: [PATCH] BUGFIX: CAA records may include quoted spaces #1374 (#1377) --- integrationTest/integration_test.go | 5 +++++ models/quotes.go | 16 +++++++++++++++- models/quotes_test.go | 26 +++++++++++++++++++++++++- models/t_caa.go | 6 ++++-- 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 104b969e9..793b332e2 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -1085,6 +1085,11 @@ func makeTests(t *testing.T) []*TestGroup { // Test support of ";" as a value tc("CAA many records", caa("@", "issuewild", 0, ";")), ), + testgroup("Issue 1374", + requires(providers.CanUseCAA), not("DIGITALOCEAN"), + // Test support of ";" as a value + tc("CAA many records", caa("@", "issue", 0, "letsencrypt.org; validationmethods=dns-01; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234")), + ), testgroup("NAPTR", requires(providers.CanUseNAPTR), diff --git a/models/quotes.go b/models/quotes.go index 2fcce74a3..9528dce2b 100644 --- a/models/quotes.go +++ b/models/quotes.go @@ -1,6 +1,9 @@ package models -import "strings" +import ( + "encoding/csv" + "strings" +) // IsQuoted returns true if the string starts and ends with a double quote. func IsQuoted(s string) bool { @@ -35,3 +38,14 @@ func ParseQuotedTxt(s string) []string { } return strings.Split(StripQuotes(s), `" "`) } + +// ParseQuotedFields is like strings.Fields except individual fields +// might be quoted using `"`. +func ParseQuotedFields(s string) ([]string, error) { + // Fields are space-separated but a field might be quoted. This is, + // essentially, a CSV where spaces are the field separator (not + // commas). Therefore, we use the CSV parser. See https://stackoverflow.com/a/47489846/71978 + r := csv.NewReader(strings.NewReader(s)) + r.Comma = ' ' // space + return r.Read() +} diff --git a/models/quotes_test.go b/models/quotes_test.go index 0dd31ce61..c6ff560fd 100644 --- a/models/quotes_test.go +++ b/models/quotes_test.go @@ -71,9 +71,33 @@ func TestSetTxtParse(t *testing.T) { t.Errorf("%v: expected TxtStrings=(%v) got (%v)", i, test.e2, ls) } for i := range ls { - if len(ls[i]) != len(test.e2[i]) { + if ls[i] != test.e2[i] { t.Errorf("%v: expected TxtStrings=(%v) got (%v)", i, test.e2, ls) } } } } + +func TestParseQuotedFields(t *testing.T) { + tests := []struct { + d1 string + e1 []string + }{ + {`1 2 3`, []string{`1`, `2`, `3`}}, + {`1 "2" 3`, []string{`1`, `2`, `3`}}, + {`1 2 "three 3"`, []string{`1`, `2`, `three 3`}}, + {`0 issue "letsencrypt.org; validationmethods=dns-01; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234"`, []string{`0`, `issue`, `letsencrypt.org; validationmethods=dns-01; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234`}}, + } + for i, test := range tests { + ls, _ := ParseQuotedFields(test.d1) + //fmt.Printf("%v: expected TxtStrings:\nWANT: %v\n GOT: %v\n", i, test.e1, ls) + if len(ls) != len(test.e1) { + t.Errorf("%v: expected TxtStrings=(%v) got (%v)", i, test.e1, ls) + } + for i := range ls { + if ls[i] != test.e1[i] { + t.Errorf("%v: expected TxtStrings=(%v) got (%v)", i, test.e1, ls) + } + } + } +} diff --git a/models/t_caa.go b/models/t_caa.go index 8c098a6e9..b389e8976 100644 --- a/models/t_caa.go +++ b/models/t_caa.go @@ -3,7 +3,6 @@ package models import ( "fmt" "strconv" - "strings" ) // SetTargetCAA sets the CAA fields. @@ -37,7 +36,10 @@ func (rc *RecordConfig) SetTargetCAAStrings(flag, tag, target string) error { // SetTargetCAAString is like SetTargetCAA but accepts one big string. // Ex: `0 issue "letsencrypt.org"` func (rc *RecordConfig) SetTargetCAAString(s string) error { - part := strings.Fields(s) + part, err := ParseQuotedFields(s) + if err != nil { + return err + } if len(part) != 3 { return fmt.Errorf("CAA value does not contain 3 fields: (%#v)", s) }