Skip to content

Commit 8fbbc59

Browse files
feat: unicode domain names and entries
1 parent 869edd8 commit 8fbbc59

File tree

4 files changed

+124
-28
lines changed

4 files changed

+124
-28
lines changed

dnslink.go

Lines changed: 101 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ import (
66
"math/rand"
77
"net"
88
"net/url"
9+
"regexp"
910
"sort"
11+
"strconv"
1012
"strings"
1113

12-
isd "github.com/jbenet/go-is-domain"
1314
dns "github.com/miekg/dns"
1415
)
1516

@@ -112,7 +113,28 @@ func (s ByValue) Less(i, j int) bool { return s.LookupEntries[i].Value < s.Looku
112113

113114
type LookupTXTFunc func(name string) (txt []LookupEntry, err error)
114115

115-
func NewUDPLookup(servers []string) LookupTXTFunc {
116+
var utf8Replace = regexp.MustCompile(`\\\d{3}`)
117+
118+
func utf8ReplaceFunc(input []byte) (result []byte) {
119+
result = make([]byte, 1)
120+
num, _ := strconv.ParseUint(string(input[1:]), 10, 9)
121+
result[0] = byte(num)
122+
return result
123+
}
124+
125+
func utf8Value(input []string) string {
126+
str := strings.Join(input, "")
127+
return string(utf8Replace.ReplaceAllFunc([]byte(str), utf8ReplaceFunc))
128+
}
129+
130+
func NewUDPLookup(servers []string, udpSize uint16) LookupTXTFunc {
131+
client := new(dns.Client)
132+
if udpSize == 0 {
133+
// Running into issues with too small buffer size of dns library in some cases
134+
client.UDPSize = 4096
135+
} else {
136+
client.UDPSize = udpSize
137+
}
116138
return func(domain string) (entries []LookupEntry, err error) {
117139
if !strings.HasSuffix(domain, ".") {
118140
domain += "."
@@ -127,7 +149,7 @@ func NewUDPLookup(servers []string) LookupTXTFunc {
127149
Qclass: dns.ClassINET,
128150
}
129151
server := servers[rand.Intn(len(servers))]
130-
res, err := dns.Exchange(req, server)
152+
res, _, err := client.Exchange(req, server)
131153
if err != nil {
132154
return nil, err
133155
}
@@ -136,7 +158,7 @@ func NewUDPLookup(servers []string) LookupTXTFunc {
136158
if answer.Header().Rrtype == dns.TypeTXT {
137159
txtAnswer := answer.(*dns.TXT)
138160
entries[index] = LookupEntry{
139-
Value: strings.Join(txtAnswer.Txt, ""),
161+
Value: utf8Value(txtAnswer.Txt),
140162
Ttl: txtAnswer.Header().Ttl,
141163
}
142164
}
@@ -185,11 +207,11 @@ func resolve(r *Resolver, domain string, recursive bool) (result Result, err err
185207
if lookupTXT == nil {
186208
lookupTXT = defaultLookupTXT
187209
}
188-
lookup, error := validateDomain(domain)
210+
lookup, error := validateDomain(domain, "")
189211
result.Links = map[string][]LookupEntry{}
190212
result.Path = []PathEntry{}
191213
result.Log = []LogStatement{}[:]
192-
if lookup == nil {
214+
if error != nil {
193215
result.Log = append(result.Log, *error)
194216
return result, nil
195217
}
@@ -225,7 +247,7 @@ func resolve(r *Resolver, domain string, recursive bool) (result Result, err err
225247
}
226248
}
227249

228-
func validateDomain(input string) (*URLParts, *LogStatement) {
250+
func validateDomain(input string, entry string) (*URLParts, *LogStatement) {
229251
urlParts := relevantURLParts(input)
230252
domain := urlParts.Domain
231253
if strings.HasPrefix(domain, dnsPrefix) {
@@ -241,21 +263,85 @@ func validateDomain(input string) (*URLParts, *LogStatement) {
241263
}
242264
}
243265
}
244-
if !isd.IsDomain(domain) {
266+
if !isFqdn(domain) {
245267
return nil, &LogStatement{
246-
Code: "INVALID_REDIRECT",
247-
Domain: urlParts.Domain,
248-
Pathname: urlParts.Pathname,
249-
Search: urlParts.Search,
268+
Code: "INVALID_REDIRECT",
269+
Entry: entry,
250270
}
251271
}
272+
domain = strings.TrimSuffix(domain, ".")
252273
return &URLParts{
253274
Domain: dnsPrefix + domain,
254275
Pathname: urlParts.Pathname,
255276
Search: urlParts.Search,
256277
}, nil
257278
}
258279

280+
var intlDomainCharset = regexp.MustCompile("^([a-z\u00a1-\uffff]{2,}|xn[a-z0-9-]{2,})$")
281+
var spacesAndSpecialChars = regexp.MustCompile("[\\s\u2002-\u200B\u202F\u205F\u3000��\u00A9\uFFFD\uFEFF]")
282+
var domainCharset = regexp.MustCompile("^[a-z\u00a1-\u00ff0-9-]+$")
283+
284+
func isFqdn(str string) bool {
285+
str = strings.TrimSuffix(str, ".")
286+
if str == "" {
287+
return false
288+
}
289+
parts := strings.Split(str, ".")
290+
tld := parts[len(parts)-1]
291+
292+
// disallow fqdns without tld
293+
if len(parts) < 2 {
294+
return false
295+
}
296+
297+
if !intlDomainCharset.MatchString(tld) {
298+
return false
299+
}
300+
301+
// disallow spaces && special characers
302+
if spacesAndSpecialChars.MatchString(tld) {
303+
return false
304+
}
305+
306+
// disallow all numbers
307+
if every(parts, isNumber) {
308+
return false
309+
}
310+
311+
return every(parts, isDomainPart)
312+
}
313+
314+
func isDomainPart(part string) bool {
315+
if len(part) > 63 {
316+
return false
317+
}
318+
319+
if !domainCharset.MatchString(part) {
320+
return false
321+
}
322+
323+
// disallow parts starting or ending with hyphen
324+
if strings.HasPrefix(part, "-") || strings.HasSuffix(part, "-") {
325+
return false
326+
}
327+
328+
return true
329+
}
330+
331+
func isNumber(str string) bool {
332+
_, err := strconv.Atoi(str)
333+
return err == nil
334+
}
335+
336+
func every(strings []string, test func(string) bool) bool {
337+
for _, str := range strings {
338+
if !test(str) {
339+
return false
340+
}
341+
}
342+
return true
343+
}
344+
259345
type processedEntry struct {
260346
value string
261347
entry string
@@ -331,9 +417,8 @@ func resolveTxtEntries(domain string, recursive bool, txtEntries []LookupEntry)
331417
hasRedirect := false
332418
var redirect *URLParts
333419
for _, dns := range dnsLinks {
334-
validated, error := validateDomain(dns.value)
420+
validated, error := validateDomain(dns.value, dns.entry)
335421
if error != nil {
336-
delete(found, "dns")
337422
log = append(log, *error)
338423
} else if !hasRedirect {
339424
hasRedirect = true
@@ -345,11 +430,9 @@ func resolveTxtEntries(domain string, recursive bool, txtEntries []LookupEntry)
345430
})
346431
}
347432
}
433+
delete(found, "dns")
348434
if hasRedirect {
349-
for key, foundEntries := range found {
350-
if key == "dns" {
351-
continue
352-
}
435+
for _, foundEntries := range found {
353436
for _, foundEntry := range foundEntries {
354437
log = append(log, LogStatement{
355438
Code: "UNUSED_ENTRY",

dnslink/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ func main() {
321321
}
322322
resolver := dnslink.Resolver{}
323323
if options.has("dns") {
324-
resolver.LookupTXT = dnslink.NewUDPLookup(getServers(options.get("dns")))
324+
resolver.LookupTXT = dnslink.NewUDPLookup(getServers(options.get("dns")), 0)
325325
}
326326
for _, lookup := range lookups {
327327
var result dnslink.Result

dnslink_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,19 +73,19 @@ func TestRelevantURLParts(t *testing.T) {
7373
func TestValidateDomain(t *testing.T) {
7474
var logNil *LogStatement = nil
7575
var partsNil *URLParts = nil
76-
assertResult(t, arr(validateDomain("hello.com")),
76+
assertResult(t, arr(validateDomain("hello.com", "")),
7777
&URLParts{Domain: "_dnslink.hello.com", Pathname: "", Search: make(map[string][]string)},
7878
logNil,
7979
)
80-
assertResult(t, arr(validateDomain("_dnslink.hello.com/foo?bar=baz")),
80+
assertResult(t, arr(validateDomain("_dnslink.hello.com/foo?bar=baz", "")),
8181
&URLParts{Domain: "_dnslink.hello.com", Pathname: "/foo", Search: map[string][]string{"bar": {"baz"}}},
8282
logNil,
8383
)
84-
assertResult(t, arr(validateDomain("hello .com")),
84+
assertResult(t, arr(validateDomain("hello .com", "dnslink=/dns/hello .com/foo")),
8585
partsNil,
86-
&LogStatement{Code: "INVALID_REDIRECT", Domain: "hello .com", Pathname: "", Search: make(map[string][]string)},
86+
&LogStatement{Code: "INVALID_REDIRECT", Entry: "dnslink=/dns/hello .com/foo"},
8787
)
88-
assertResult(t, arr(validateDomain("_dnslink._dnslink.hello.com")),
88+
assertResult(t, arr(validateDomain("_dnslink._dnslink.hello.com", "")),
8989
partsNil,
9090
&LogStatement{Code: "RECURSIVE_DNSLINK_PREFIX", Domain: "_dnslink._dnslink.hello.com", Pathname: "", Search: make(map[string][]string)},
9191
)
@@ -222,7 +222,7 @@ func TestDnsLinkN(t *testing.T) {
222222
}
223223

224224
func TestUDPLookup(t *testing.T) {
225-
lookup := NewUDPLookup([]string{"1.1.1.1:53"})
225+
lookup := NewUDPLookup([]string{"1.1.1.1:53"}, 0)
226226
txt, error := lookup("_dnslink.t05.dnslink.dev")
227227
assert.NoError(t, error)
228228
assert.Equal(t, len(txt), 2)

integration/main.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,31 @@ func main() {
2020
options := Options{}
2121
json.Unmarshal([]byte(os.Args[2]), &options)
2222
r := &dnslink.Resolver{
23-
LookupTXT: dnslink.NewUDPLookup([]string{"127.0.0.1:" + fmt.Sprint(options.Udp)}),
23+
LookupTXT: dnslink.NewUDPLookup([]string{"127.0.0.1:" + fmt.Sprint(options.Udp)}, 0),
2424
}
2525

2626
resolved, error := r.ResolveN(domain)
2727
if error != nil {
28-
panic(error)
28+
exitWithError(error)
2929
}
3030

31-
result, err := json.Marshal(resolved)
31+
result, err := json.MarshalIndent(resolved, "", " ")
3232
if err != nil {
3333
panic(err)
3434
} else {
3535
fmt.Print(string(result))
3636
}
3737
}
38+
39+
func exitWithError(input error) {
40+
result, err := json.MarshalIndent(map[string]map[string]string{
41+
"error": {
42+
"code": input.Error(),
43+
},
44+
}, "", " ")
45+
if err != nil {
46+
panic(err)
47+
}
48+
fmt.Print(string(result))
49+
os.Exit(1)
50+
}

0 commit comments

Comments
 (0)