From 6ab812eb60617ba3df584c0939987ad725f63424 Mon Sep 17 00:00:00 2001 From: Max Chernoff Date: Mon, 9 Jun 2025 12:14:47 -0600 Subject: [PATCH 1/2] Support CAA `Value`s with embedded spaces (#179) Fixes #178 --- record.go | 10 ++++++++-- record_test.go | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/record.go b/record.go index e21b3d9..f4ba9c2 100644 --- a/record.go +++ b/record.go @@ -159,7 +159,7 @@ func (r RR) toCAA() (CAA, error) { return CAA{}, fmt.Errorf("record type not %s: %s", expectedType, r.Type) } - fields := strings.Fields(r.Data) + fields := strings.SplitN(r.Data, " ", 3) if expectedLen := 3; len(fields) != expectedLen { return CAA{}, fmt.Errorf(`malformed CAA value; expected %d fields in the form 'flags tag "value"'`, expectedLen) } @@ -169,7 +169,13 @@ func (r RR) toCAA() (CAA, error) { return CAA{}, fmt.Errorf("invalid flags %s: %v", fields[0], err) } tag := fields[1] - value := strings.Trim(fields[2], `"`) + + // If only https://tip.golang.org/src/cmd/internal/quoted/quoted.go were + // public... + value, err := strconv.Unquote(fields[2]) + if err != nil { + value = fields[2] + } return CAA{ Name: r.Name, diff --git a/record_test.go b/record_test.go index b5b77d9..15dd680 100644 --- a/record_test.go +++ b/record_test.go @@ -74,6 +74,21 @@ func TestToCAA(t *testing.T) { Value: "letsencrypt.org", }, }, + { + input: RR{ + Name: "@", + TTL: 1 * time.Hour, + Type: "CAA", + Data: `0 issuewild "letsencrypt.org; validationmethods=dns-01; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234567890"`, + }, + expect: CAA{ + Name: "@", + TTL: 1 * time.Hour, + Flags: 0, + Tag: "issuewild", + Value: "letsencrypt.org; validationmethods=dns-01; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234567890", + }, + }, } { actual, err := test.input.toCAA() if err == nil && test.shouldErr { From 6071c1f4fbaa59dac34d456c5b701dc8ecb73bbe Mon Sep 17 00:00:00 2001 From: Max Chernoff Date: Wed, 13 Aug 2025 09:29:26 -0600 Subject: [PATCH 2/2] Clarify `SetRecords` behaviour (#187) Some implementations implement `SetRecords` and `AppendRecords` identically, but this is incorrect. This commit updates the `SetRecords` documentation to clarify that `SetRecords` can only be implemented by a combination of `DeleteRecords` and `AppendRecords`, not `AppendRecords` alone. Fixes #186. --- libdns.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/libdns.go b/libdns.go index ce627a8..2352697 100644 --- a/libdns.go +++ b/libdns.go @@ -129,6 +129,25 @@ type RecordSetter interface { // zone so that for each RRset in the input, the records provided in the input // are the only members of their RRset in the output zone. // + // SetRecords is distinct from [libdns.RecordAppender.AppendRecords] in that + // AppendRecords *only* adds records to the zone, while SetRecords may also + // delete records if necessary. Therefore, SetRecords behaves similarly to + // the following code: + // + // func SetRecords(ctx context.Context, zone string, recs []Record) ([]Record, error) { + // prevs, _ := p.GetRecords(ctx, zone) + // toDelete := []Record{} + // for _, prev := range prevs { + // for _, new := range recs { + // if prev.RR().Name == new.RR().Name && prev.RR().Type == new.RR().Type { + // toDelete = append(toDelete, prev) + // } + // } + // } + // DeleteRecords(ctx, zone, toDelete) + // return AppendRecords(ctx, zone, recs) + // } + // // Implementations may decide whether or not to support DNSSEC-related records // in calls to SetRecords, but should document their decision. Note that the // decision to support DNSSEC records in SetRecords is independent of the