1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00

REFACTOR: Opinion: TXT records are one long string (#2631)

Co-authored-by: Costas Drogos <costas.drogos@gmail.com>
Co-authored-by: imlonghao <git@imlonghao.com>
Co-authored-by: Jeffrey Cafferata <jeffrey@jcid.nl>
Co-authored-by: Vincent Hagen <blackshadev@users.noreply.github.com>
This commit is contained in:
Tom Limoncelli
2023-12-04 17:45:25 -05:00
committed by GitHub
parent 88d26c3ea2
commit cbccbbeb8d
71 changed files with 882 additions and 747 deletions

155
pkg/txtutil/txtcode.go Normal file
View File

@@ -0,0 +1,155 @@
//go:generate stringer -type=State
package txtutil
import (
"bytes"
"fmt"
"strings"
)
// ParseQuoted parses a string of RFC1035-style quoted items. The resulting
// items are then joined into one string. This is useful for parsing TXT
// records.
// Examples:
// `foo` => foo
// `"foo"` => foo
// `"f\"oo"` => f"oo
// `"f\\oo"` => f\oo
// `"foo" "bar"` => foobar
// `"foo" bar` => foobar
func ParseQuoted(s string) (string, error) {
return txtDecode(s)
}
// EncodeQuoted encodes a string into a series of quoted 255-octet chunks. That
// is, when decoded each chunk would be 255-octets with the remainder in the
// last chunk.
//
// The output looks like:
//
// `""` empty
// `"255\"octets"` quotes are escaped
// `"255\\octets"` backslashes are escaped
// `"255octets" "255octets" "remainder"` long strings are chunked
func EncodeQuoted(t string) string {
return txtEncode(ToChunks(t))
}
type State int
const (
StateStart State = iota // Looking for a non-space
StateUnquoted // A run of unquoted text
StateQuoted // Quoted text
StateBackslash // last char was backlash in a quoted string
StateWantSpace // expect space after closing quote
)
func isRemaining(s string, i, r int) bool {
return (len(s) - 1 - i) > r
}
// txtDecode decodes TXT strings quoted/escaped as Tom interprets RFC10225.
func txtDecode(s string) (string, error) {
// Parse according to RFC1035 zonefile specifications.
// "foo" -> one string: `foo``
// "foo" "bar" -> two strings: `foo` and `bar`
// quotes and backslashes are escaped using \
/*
BNF:
txttarget := `""`` | item | item ` ` item*
item := quoteditem | unquoteditem
quoteditem := quote innertxt quote
quote := `"`
innertxt := (escaped | printable )*
escaped := `\\` | `\"`
printable := (printable ASCII chars)
unquoteditem := (printable ASCII chars but not `"` nor ' ')
*/
//printer.Printf("DEBUG: txtDecode txt inboundv=%v\n", s)
b := &bytes.Buffer{}
state := StateStart
for i, c := range s {
//printer.Printf("DEBUG: state=%v rune=%v\n", state, string(c))
switch state {
case StateStart:
if c == ' ' {
// skip whitespace
} else if c == '"' {
state = StateQuoted
} else {
state = StateUnquoted
b.WriteRune(c)
}
case StateUnquoted:
if c == ' ' {
state = StateStart
} else {
b.WriteRune(c)
}
case StateQuoted:
if c == '\\' {
if isRemaining(s, i, 1) {
state = StateBackslash
} else {
return "", fmt.Errorf("txtDecode quoted string ends with backslash q(%q)", s)
}
} else if c == '"' {
state = StateWantSpace
} else {
b.WriteRune(c)
}
case StateBackslash:
b.WriteRune(c)
state = StateQuoted
case StateWantSpace:
if c == ' ' {
state = StateStart
} else {
return "", fmt.Errorf("txtDecode expected whitespace after close quote q(%q)", s)
}
}
}
r := b.String()
//printer.Printf("DEBUG: txtDecode txt decodedv=%v\n", r)
return r, nil
}
// txtEncode encodes TXT strings in RFC1035 format as interpreted by Tom.
func txtEncode(ts []string) string {
//printer.Printf("DEBUG: txtEncode txt outboundv=%v\n", ts)
if (len(ts) == 0) || (strings.Join(ts, "") == "") {
return `""`
}
var r []string
for i := range ts {
tx := ts[i]
tx = strings.ReplaceAll(tx, `\`, `\\`)
tx = strings.ReplaceAll(tx, `"`, `\"`)
tx = `"` + tx + `"`
r = append(r, tx)
}
t := strings.Join(r, ` `)
//printer.Printf("DEBUG: txtEncode txt encodedv=%v\n", t)
return t
}

View File

@@ -0,0 +1,97 @@
package txtutil
import (
"strings"
"testing"
)
func r(s string, c int) string { return strings.Repeat(s, c) }
func TestTxtDecode(t *testing.T) {
tests := []struct {
data string
expected []string
}{
{``, []string{``}},
{`""`, []string{``}},
{`foo`, []string{`foo`}},
{`"foo"`, []string{`foo`}},
{`"foo bar"`, []string{`foo bar`}},
{`foo bar`, []string{`foo`, `bar`}},
{`"aaa" "bbb"`, []string{`aaa`, `bbb`}},
{`"a\"a" "bbb"`, []string{`a"a`, `bbb`}},
// Seen in live traffic:
{"\"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\"",
[]string{r("B", 254)}},
{"\"CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\"",
[]string{r("C", 255)}},
{"\"DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD\" \"D\"",
[]string{r("D", 255), "D"}},
{"\"EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE\" \"EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE\"",
[]string{r("E", 255), r("E", 255)}},
{"\"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\" \"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\" \"F\"",
[]string{r("F", 255), r("F", 255), "F"}},
{"\"GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG\" \"GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG\" \"GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG\"",
[]string{r("G", 255), r("G", 255), r("G", 255)}},
{"\"HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH\" \"HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH\" \"HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH\" \"H\"",
[]string{r("H", 255), r("H", 255), r("H", 255), "H"}},
{"\"quo'te\"", []string{`quo'te`}},
{"\"blah`blah\"", []string{"blah`blah"}},
{"\"quo\\\"te\"", []string{`quo"te`}},
{"\"q\\\"uo\\\"te\"", []string{`q"uo"te`}},
/// Backslashes are meaningless in unquoted strings. Unquoted strings run until they hit a space.
{`1backs\lash`, []string{`1backs\lash`}},
{`2backs\\lash`, []string{`2backs\\lash`}},
{`3backs\\\lash`, []string{`3backs\\\lash`}},
{`4backs\\\\lash`, []string{`4backs\\\\lash`}},
/// Inside quotes, a backlash means take the next byte literally.
{`"q1backs\lash"`, []string{`q1backslash`}},
{`"q2backs\\lash"`, []string{`q2backs\lash`}},
{`"q3backs\\\lash"`, []string{`q3backs\lash`}},
{`"q4backs\\\\lash"`, []string{`q4backs\\lash`}},
// HETZNER includes a space after the last quote. Make sure we handle that.
{`"one" "more" `, []string{`one`, `more`}},
}
for i, test := range tests {
got, err := txtDecode(test.data)
if err != nil {
t.Error(err)
}
want := strings.Join(test.expected, "")
if got != want {
t.Errorf("%v: expected TxtStrings=(%q) got (%q)", i, want, got)
}
}
}
func TestTxtEncode(t *testing.T) {
tests := []struct {
data []string
expected string
}{
{[]string{}, `""`},
{[]string{``}, `""`},
{[]string{`foo`}, `"foo"`},
{[]string{`aaa`, `bbb`}, `"aaa" "bbb"`},
{[]string{`ccc`, `ddd`, `eee`}, `"ccc" "ddd" "eee"`},
{[]string{`a"a`, `bbb`}, `"a\"a" "bbb"`},
{[]string{`quo'te`}, "\"quo'te\""},
{[]string{"blah`blah"}, "\"blah`blah\""},
{[]string{`quo"te`}, "\"quo\\\"te\""},
{[]string{`quo"te`}, `"quo\"te"`},
{[]string{`q"uo"te`}, "\"q\\\"uo\\\"te\""},
{[]string{`1backs\lash`}, `"1backs\\lash"`},
{[]string{`2backs\\lash`}, `"2backs\\\\lash"`},
{[]string{`3backs\\\lash`}, `"3backs\\\\\\lash"`},
{[]string{strings.Repeat("M", 26), `quo"te`}, `"MMMMMMMMMMMMMMMMMMMMMMMMMM" "quo\"te"`},
}
for i, test := range tests {
got := txtEncode(test.data)
want := test.expected
if got != want {
t.Errorf("%v: expected TxtStrings=v(%v) got (%v)", i, want, got)
}
}
}

View File

@@ -0,0 +1,53 @@
//go:generate stringer -type=State
package txtutil
// func ParseCombined(s string) (string, error) {
// return txtDecodeCombined(s)
// }
// // // txtDecode decodes TXT strings received from ROUTE53 and GCLOUD.
// func txtDecodeCombined(s string) (string, error) {
// // The dns package doesn't expose the quote parser. Therefore we create a TXT record and extract the strings.
// rr, err := dns.NewRR("example.com. IN TXT " + s)
// if err != nil {
// return "", fmt.Errorf("could not parse %q TXT: %w", s, err)
// }
// return strings.Join(rr.(*dns.TXT).Txt, ""), nil
// }
// func EncodeCombined(t string) string {
// return txtEncodeCombined(ToChunks(t))
// }
// // txtEncode encodes TXT strings as the old GetTargetCombined() function did.
// func txtEncodeCombined(ts []string) string {
// //printer.Printf("DEBUG: route53 txt outboundv=%v\n", ts)
// // Don't call this on fake types.
// rdtype := dns.StringToType["TXT"]
// // Magically create an RR of the correct type.
// rr := dns.TypeToRR[rdtype]()
// // Fill in the header.
// rr.Header().Name = "example.com."
// rr.Header().Rrtype = rdtype
// rr.Header().Class = dns.ClassINET
// rr.Header().Ttl = 300
// // Fill in the TXT data.
// rr.(*dns.TXT).Txt = ts
// // Generate the quoted string:
// header := rr.Header().String()
// full := rr.String()
// if !strings.HasPrefix(full, header) {
// panic("assertion failed. dns.Hdr.String() behavior has changed in an incompatible way")
// }
// //printer.Printf("DEBUG: route53 txt encodedv=%v\n", t)
// return full[len(header):]
// }

View File

@@ -1,20 +1,13 @@
package txtutil
import "github.com/StackExchange/dnscontrol/v4/models"
// SplitSingleLongTxt does nothing.
// Deprecated: This is a no-op for backwards compatibility.
func SplitSingleLongTxt(records any) {
}
// SplitSingleLongTxt finds TXT records with a single long string and splits it
// into 255-octet chunks. This is used by providers that, when a user specifies
// one long TXT string, split it into smaller strings behind the scenes.
// This should be called from GetZoneRecordsCorrections().
func SplitSingleLongTxt(records []*models.RecordConfig) {
for _, rc := range records {
if rc.HasFormatIdenticalToTXT() {
s := rc.TxtStrings[0]
if len(rc.TxtStrings) == 1 && len(s) > 255 {
rc.SetTargetTXTs(splitChunks(s, 255))
}
}
}
// ToChunks returns the string as chunks of 255-octet strings (the last string being the remainder).
func ToChunks(s string) []string {
return splitChunks(s, 255)
}
// Segment returns the string as 255-octet segments, the last being the remainder.