diff --git a/middleware/canonical.go b/middleware/canonical.go new file mode 100644 index 000000000..5d51231c2 --- /dev/null +++ b/middleware/canonical.go @@ -0,0 +1,57 @@ +package middleware + +import ( + "bytes" + + "github.com/miekg/dns" +) + +// Less returns <0 when a is less than b, 0 when they are equal and +// >0 when a is larger than b. +// The function order names in DNSSEC canonical order. +// +// See http://bert-hubert.blogspot.co.uk/2015/10/how-to-do-fast-canonical-ordering-of.html +// for a blog article on how we do this. And https://tools.ietf.org/html/rfc4034#section-6.1 . +func Less(a, b string) int { + i := 1 + aj := len(a) + bj := len(b) + for { + ai, _ := dns.PrevLabel(a, i) + bi, _ := dns.PrevLabel(b, i) + // sadly this []byte will allocate... + ab := []byte(a[ai:aj]) + toLowerAndDDD(ab) + bb := []byte(b[bi:bj]) + toLowerAndDDD(bb) + + res := bytes.Compare(ab, bb) + if res != 0 { + return res + } + + i++ + aj, bj = ai, bi + } + return 0 +} + +func toLowerAndDDD(b []byte) { + lb := len(b) + for i := 0; i < lb; i++ { + if b[i] >= 'A' && b[i] <= 'Z' { + b[i] += 32 + continue + } + if i+3 < lb && b[i] == '\\' && isDigit(b[i+1]) && isDigit(b[i+2]) && isDigit(b[i+3]) { + b[i] = dddToByte(b[i:]) + for j := i + 1; j < lb-3; j++ { + b[j] = b[j+3] + } + lb -= 3 + } + } +} + +func isDigit(b byte) bool { return b >= '0' && b <= '9' } +func dddToByte(s []byte) byte { return byte((s[1]-'0')*100 + (s[2]-'0')*10 + (s[3] - '0')) } diff --git a/middleware/canonical_test.go b/middleware/canonical_test.go new file mode 100644 index 000000000..390878a67 --- /dev/null +++ b/middleware/canonical_test.go @@ -0,0 +1,72 @@ +package middleware + +import ( + "sort" + "testing" +) + +type set []string + +func (p set) Len() int { return len(p) } +func (p set) Swap(i, j int) { p[i], p[j] = p[j], p[i] } +func (p set) Less(i, j int) bool { d := Less(p[i], p[j]); return d <= 0 } + +func TestLess(t *testing.T) { + tests := []struct { + in []string + out []string + }{ + { + []string{"aaa.powerdns.de", "bbb.powerdns.net.", "xxx.powerdns.com."}, + []string{"xxx.powerdns.com.", "aaa.powerdns.de", "bbb.powerdns.net."}, + }, + { + []string{"aaa.POWERDNS.de", "bbb.PoweRdnS.net.", "xxx.powerdns.com."}, + []string{"xxx.powerdns.com.", "aaa.POWERDNS.de", "bbb.PoweRdnS.net."}, + }, + { + []string{"aaa.aaaa.aa.", "aa.aaa.a.", "bbb.bbbb.bb."}, + []string{"aa.aaa.a.", "aaa.aaaa.aa.", "bbb.bbbb.bb."}, + }, + { + []string{"aaaaa.", "aaa.", "bbb."}, + []string{"aaa.", "aaaaa.", "bbb."}, + }, + { + []string{"a.a.a.a.", "a.a.", "a.a.a."}, + []string{"a.a.", "a.a.a.", "a.a.a.a."}, + }, + { + []string{"example.", "z.example.", "a.example."}, + []string{"example.", "a.example.", "z.example."}, + }, + { + []string{"a.example.", "Z.a.example.", "z.example.", "yljkjljk.a.example.", "\\001.z.example.", "example.", "*.z.example.", "\\200.z.example.", "zABC.a.EXAMPLE."}, + []string{"example.", "a.example.", "yljkjljk.a.example.", "Z.a.example.", "zABC.a.EXAMPLE.", "z.example.", "\\001.z.example.", "*.z.example.", "\\200.z.example."}, + }, + { + // RFC3034 example. + []string{"a.example.", "Z.a.example.", "z.example.", "yljkjljk.a.example.", "example.", "*.z.example.", "zABC.a.EXAMPLE."}, + []string{"example.", "a.example.", "yljkjljk.a.example.", "Z.a.example.", "zABC.a.EXAMPLE.", "z.example.", "*.z.example."}, + }, + } + +Tests: + for j, test := range tests { + sort.Sort(set(test.in)) + for i := 0; i < len(test.in); i++ { + if test.in[i] != test.out[i] { + t.Errorf("test %d: expected %s, got %s\n", j, test.out[i], test.in[i]) + n := "" + for k, in := range test.in { + if k+1 == len(test.in) { + n = "\n" + } + t.Logf("%s <-> %s\n%s", in, test.out[k], n) + } + continue Tests + } + + } + } +}