forked from tailscale/tailscale
-
Notifications
You must be signed in to change notification settings - Fork 0
/
dnsname.go
225 lines (194 loc) · 5.56 KB
/
dnsname.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package dnsname contains string functions for working with DNS names.
package dnsname
import (
"fmt"
"strings"
)
const (
// maxLabelLength is the maximum length of a label permitted by RFC 1035.
maxLabelLength = 63
// maxNameLength is the maximum length of a DNS name.
maxNameLength = 253
)
// A FQDN is a fully-qualified DNS name or name suffix.
type FQDN string
func ToFQDN(s string) (FQDN, error) {
if len(s) == 0 || s == "." {
return FQDN("."), nil
}
if s[0] == '.' {
s = s[1:]
}
raw := s
totalLen := len(s)
if s[len(s)-1] == '.' {
s = s[:len(s)-1]
} else {
totalLen += 1 // account for missing dot
}
if totalLen > maxNameLength {
return "", fmt.Errorf("%q is too long to be a DNS name", s)
}
st := 0
for i := 0; i < len(s); i++ {
if s[i] != '.' {
continue
}
label := s[st:i]
// You might be tempted to do further validation of the
// contents of labels here, based on the hostname rules in RFC
// 1123. However, DNS labels are not always subject to
// hostname rules. In general, they can contain any non-zero
// byte sequence, even though in practice a more restricted
// set is used.
//
// See https://github.com/tailscale/tailscale/issues/2024 for more.
if len(label) == 0 || len(label) > maxLabelLength {
return "", fmt.Errorf("%q is not a valid DNS label", label)
}
st = i + 1
}
if raw[len(raw)-1] != '.' {
raw = raw + "."
}
return FQDN(raw), nil
}
// WithTrailingDot returns f as a string, with a trailing dot.
func (f FQDN) WithTrailingDot() string {
return string(f)
}
// WithoutTrailingDot returns f as a string, with the trailing dot
// removed.
func (f FQDN) WithoutTrailingDot() string {
return string(f[:len(f)-1])
}
func (f FQDN) NumLabels() int {
if f == "." {
return 0
}
return strings.Count(f.WithTrailingDot(), ".")
}
func (f FQDN) Contains(other FQDN) bool {
if f == other {
return true
}
cmp := f.WithTrailingDot()
if cmp != "." {
cmp = "." + cmp
}
return strings.HasSuffix(other.WithTrailingDot(), cmp)
}
// SanitizeLabel takes a string intended to be a DNS name label
// and turns it into a valid name label according to RFC 1035.
func SanitizeLabel(label string) string {
var sb strings.Builder // TODO: don't allocate in common case where label is already fine
start, end := 0, len(label)
// This is technically stricter than necessary as some characters may be dropped,
// but labels have no business being anywhere near this long in any case.
if end > maxLabelLength {
end = maxLabelLength
}
// A label must start with a letter or number...
for ; start < end; start++ {
if isalphanum(label[start]) {
break
}
}
// ...and end with a letter or number.
for ; start < end; end-- {
// This is safe because (start < end) implies (end >= 1).
if isalphanum(label[end-1]) {
break
}
}
for i := start; i < end; i++ {
// Consume a separator only if we are not at a boundary:
// then we can turn it into a hyphen without breaking the rules.
boundary := (i == start) || (i == end-1)
if !boundary && separators[label[i]] {
sb.WriteByte('-')
} else if isdnschar(label[i]) {
sb.WriteByte(tolower(label[i]))
}
}
return sb.String()
}
// HasSuffix reports whether the provided name ends with the
// component(s) in suffix, ignoring any trailing or leading dots.
//
// If suffix is the empty string, HasSuffix always reports false.
func HasSuffix(name, suffix string) bool {
name = strings.TrimSuffix(name, ".")
suffix = strings.TrimSuffix(suffix, ".")
suffix = strings.TrimPrefix(suffix, ".")
nameBase := strings.TrimSuffix(name, suffix)
return len(nameBase) < len(name) && strings.HasSuffix(nameBase, ".")
}
// TrimSuffix trims any trailing dots from a name and removes the
// suffix ending if present. The name will never be returned with
// a trailing dot, even after trimming.
func TrimSuffix(name, suffix string) string {
if HasSuffix(name, suffix) {
name = strings.TrimSuffix(name, ".")
suffix = strings.Trim(suffix, ".")
name = strings.TrimSuffix(name, suffix)
}
return strings.TrimSuffix(name, ".")
}
// TrimCommonSuffixes returns hostname with some common suffixes removed.
func TrimCommonSuffixes(hostname string) string {
hostname = strings.TrimSuffix(hostname, ".local")
hostname = strings.TrimSuffix(hostname, ".localdomain")
hostname = strings.TrimSuffix(hostname, ".lan")
return hostname
}
// SanitizeHostname turns hostname into a valid name label according
// to RFC 1035.
func SanitizeHostname(hostname string) string {
hostname = TrimCommonSuffixes(hostname)
return SanitizeLabel(hostname)
}
// NumLabels returns the number of DNS labels in hostname.
// If hostname is empty or the top-level name ".", returns 0.
func NumLabels(hostname string) int {
if hostname == "" || hostname == "." {
return 0
}
return strings.Count(hostname, ".")
}
// FirstLabel returns the first DNS label of hostname.
func FirstLabel(hostname string) string {
first, _, _ := strings.Cut(hostname, ".")
return first
}
var separators = map[byte]bool{
' ': true,
'.': true,
'@': true,
'_': true,
}
func islower(c byte) bool {
return 'a' <= c && c <= 'z'
}
func isupper(c byte) bool {
return 'A' <= c && c <= 'Z'
}
func isalpha(c byte) bool {
return islower(c) || isupper(c)
}
func isalphanum(c byte) bool {
return isalpha(c) || ('0' <= c && c <= '9')
}
func isdnschar(c byte) bool {
return isalphanum(c) || c == '-'
}
func tolower(c byte) byte {
if isupper(c) {
return c + 'a' - 'A'
} else {
return c
}
}