mirror of
				https://github.com/coredns/coredns.git
				synced 2025-10-31 10:13:14 -04:00 
			
		
		
		
	[plugin/route53] Support wildcards and other escaped chars. (#2352)
* [plugin/route53] Support wildcards and other escaped chars. * Fix multiple issues. Add tests. * Cleanup some comments.
This commit is contained in:
		| @@ -4,7 +4,10 @@ package route53 | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| @@ -140,10 +143,79 @@ func (h *Route53) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg | ||||
| 	return dns.RcodeSuccess, nil | ||||
| } | ||||
|  | ||||
| const escapeSeq = `\\` | ||||
|  | ||||
| // maybeUnescape parses s and converts escaped ASCII codepoints (in octal) back | ||||
| // to its ASCII representation. | ||||
| // | ||||
| // From AWS docs: | ||||
| // | ||||
| // "If the domain name includes any characters other than a to z, 0 to 9, - | ||||
| // (hyphen), or _ (underscore), Route 53 API actions return the characters as | ||||
| // escape codes." | ||||
| // | ||||
| // For our purposes (and with respect to RFC 1035), we'll fish for a-z, 0-9, | ||||
| // '-', '.' and '*' as the leftmost character (for wildcards) and throw error | ||||
| // for everything else. | ||||
| // | ||||
| // Example: | ||||
| //   `\\052.example.com.` -> `*.example.com` | ||||
| //   `\\137.example.com.` -> error ('_' is not valid) | ||||
| func maybeUnescape(s string) (string, error) { | ||||
| 	var out string | ||||
| 	for { | ||||
| 		i := strings.Index(s, escapeSeq) | ||||
| 		if i < 0 { | ||||
| 			return out + s, nil | ||||
| 		} | ||||
|  | ||||
| 		out += s[:i] | ||||
|  | ||||
| 		li, ri := i+len(escapeSeq), i+len(escapeSeq)+3 | ||||
| 		if ri > len(s) { | ||||
| 			return "", fmt.Errorf("invalid escape sequence: '%s%s'", escapeSeq, s[li:]) | ||||
| 		} | ||||
| 		// Parse `\\xxx` in base 8 (2nd arg) and attempt to fit into | ||||
| 		// 8-bit result (3rd arg). | ||||
| 		n, err := strconv.ParseInt(s[li:ri], 8, 8) | ||||
| 		if err != nil { | ||||
| 			return "", fmt.Errorf("invalid escape sequence: '%s%s'", escapeSeq, s[li:ri]) | ||||
| 		} | ||||
|  | ||||
| 		r := rune(n) | ||||
| 		switch { | ||||
| 		case r >= rune('a') && r <= rune('z'): // Route53 converts everything to lowercase. | ||||
| 		case r >= rune('0') && r <= rune('9'): | ||||
| 		case r == rune('*'): | ||||
| 			if out != "" { | ||||
| 				return "", errors.New("`*' ony supported as wildcard (leftmost label)") | ||||
| 			} | ||||
| 		case r == rune('-'): | ||||
| 		case r == rune('.'): | ||||
| 		default: | ||||
| 			return "", fmt.Errorf("invalid character: %s%#03o", escapeSeq, r) | ||||
| 		} | ||||
|  | ||||
| 		out += string(r) | ||||
|  | ||||
| 		s = s[i+len(escapeSeq)+3:] | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func updateZoneFromRRS(rrs *route53.ResourceRecordSet, z *file.Zone) error { | ||||
| 	for _, rr := range rrs.ResourceRecords { | ||||
|  | ||||
| 		n, err := maybeUnescape(aws.StringValue(rrs.Name)) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("failed to unescape `%s' name: %v", aws.StringValue(rrs.Name), err) | ||||
| 		} | ||||
| 		v, err := maybeUnescape(aws.StringValue(rr.Value)) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("failed to unescape `%s' value: %v", aws.StringValue(rr.Value), err) | ||||
| 		} | ||||
|  | ||||
| 		// Assemble RFC 1035 conforming record to pass into dns scanner. | ||||
| 		rfc1035 := fmt.Sprintf("%s %d IN %s %s", aws.StringValue(rrs.Name), aws.Int64Value(rrs.TTL), aws.StringValue(rrs.Type), aws.StringValue(rr.Value)) | ||||
| 		rfc1035 := fmt.Sprintf("%s %d IN %s %s", n, aws.Int64Value(rrs.TTL), aws.StringValue(rrs.Type), v) | ||||
| 		r, err := dns.NewRR(rfc1035) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("failed to parse resource record: %v", err) | ||||
|   | ||||
| @@ -36,6 +36,8 @@ func (fakeRoute53) ListResourceRecordSetsPagesWithContext(_ aws.Context, in *rou | ||||
| 		rType, name, value, hostedZoneID string | ||||
| 	}{ | ||||
| 		{"A", "example.org.", "1.2.3.4", "1234567890"}, | ||||
| 		{"A", "www.example.org", "1.2.3.4", "1234567890"}, | ||||
| 		{"CNAME", `\\052.www.example.org`, "www.example.org", "1234567890"}, | ||||
| 		{"AAAA", "example.org.", "2001:db8:85a3::8a2e:370:7334", "1234567890"}, | ||||
| 		{"CNAME", "sample.example.org.", "example.org", "1234567890"}, | ||||
| 		{"PTR", "example.org.", "ptr.example.org.", "1234567890"}, | ||||
| @@ -203,6 +205,16 @@ func TestRoute53(t *testing.T) { | ||||
| 			expectedCode: dns.RcodeSuccess, | ||||
| 			wantAnswer: []string{"other-example.org.	300	IN	A	3.5.7.9"}, | ||||
| 		}, | ||||
| 		// 11. *.www.example.org is a wildcard CNAME to www.example.org. | ||||
| 		{ | ||||
| 			qname:        "a.www.example.org", | ||||
| 			qtype:        dns.TypeA, | ||||
| 			expectedCode: dns.RcodeSuccess, | ||||
| 			wantAnswer: []string{ | ||||
| 				"a.www.example.org.	300	IN	CNAME	www.example.org.", | ||||
| 				"www.example.org.	300	IN	A	1.2.3.4", | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for ti, tc := range tests { | ||||
| @@ -244,3 +256,35 @@ func TestRoute53(t *testing.T) { | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestMaybeUnescape(t *testing.T) { | ||||
| 	for ti, tc := range []struct { | ||||
| 		escaped, want string | ||||
| 		wantErr       error | ||||
| 	}{ | ||||
| 		// 0. empty string is fine. | ||||
| 		{escaped: "", want: ""}, | ||||
| 		// 1. non-escaped sequence. | ||||
| 		{escaped: "example.com.", want: "example.com."}, | ||||
| 		// 2. escaped `*` as first label - OK. | ||||
| 		{escaped: `\\052.example.com`, want: "*.example.com"}, | ||||
| 		// 3. Escaped dot, 'a' and a hyphen. No idea why but we'll allow it. | ||||
| 		{escaped: `weird\\055ex\\141mple\\056com\\056\\056`, want: "weird-example.com.."}, | ||||
| 		// 4. escaped `*` in the middle - NOT OK. | ||||
| 		{escaped: `e\\052ample.com`, wantErr: errors.New("`*' ony supported as wildcard (leftmost label)")}, | ||||
| 		// 5. Invalid character. | ||||
| 		{escaped: `\\000.example.com`, wantErr: errors.New(`invalid character: \\000`)}, | ||||
| 		// 6. Invalid escape sequence in the middle. | ||||
| 		{escaped: `example\\0com`, wantErr: errors.New(`invalid escape sequence: '\\0co'`)}, | ||||
| 		// 7. Invalid escape sequence at the end. | ||||
| 		{escaped: `example.com\\0`, wantErr: errors.New(`invalid escape sequence: '\\0'`)}, | ||||
| 	} { | ||||
| 		got, gotErr := maybeUnescape(tc.escaped) | ||||
| 		if tc.wantErr != gotErr && !reflect.DeepEqual(tc.wantErr, gotErr) { | ||||
| 			t.Fatalf("Test %d: Expected error: `%v', but got: `%v'", ti, tc.wantErr, gotErr) | ||||
| 		} | ||||
| 		if tc.want != got { | ||||
| 			t.Errorf("Test %d: Expected unescaped: `%s', but got: `%s'", ti, tc.want, got) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user