| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- // Package dnsname contains string functions for working with DNS names.
- package dnsname
- import (
- "errors"
- "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)
- }
- // ValidLabel reports whether label is a valid DNS label.
- func ValidLabel(label string) error {
- if len(label) == 0 {
- return errors.New("empty DNS label")
- }
- if len(label) > maxLabelLength {
- return fmt.Errorf("%q is too long, max length is %d bytes", label, maxLabelLength)
- }
- if !isalphanum(label[0]) {
- return fmt.Errorf("%q is not a valid DNS label: must start with a letter or number", label)
- }
- if !isalphanum(label[len(label)-1]) {
- return fmt.Errorf("%q is not a valid DNS label: must end with a letter or number", label)
- }
- if len(label) < 2 {
- return nil
- }
- for i := 1; i < len(label)-1; i++ {
- if !isdnschar(label[i]) {
- return fmt.Errorf("%q is not a valid DNS label: contains invalid character %q", label, label[i])
- }
- }
- return nil
- }
- // 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
- }
- // ValidHostname checks if a string is a valid hostname.
- func ValidHostname(hostname string) error {
- fqdn, err := ToFQDN(hostname)
- if err != nil {
- return err
- }
- for _, label := range strings.Split(fqdn.WithoutTrailingDot(), ".") {
- if err := ValidLabel(label); err != nil {
- return err
- }
- }
- return nil
- }
- 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
- }
- }
|