mirror of
https://github.com/tailscale/tailscale.git
synced 2026-02-11 02:31:20 +01:00
This file was never truly necessary and has never actually been used in the history of Tailscale's open source releases. A Brief History of AUTHORS files --- The AUTHORS file was a pattern developed at Google, originally for Chromium, then adopted by Go and a bunch of other projects. The problem was that Chromium originally had a copyright line only recognizing Google as the copyright holder. Because Google (and most open source projects) do not require copyright assignemnt for contributions, each contributor maintains their copyright. Some large corporate contributors then tried to add their own name to the copyright line in the LICENSE file or in file headers. This quickly becomes unwieldy, and puts a tremendous burden on anyone building on top of Chromium, since the license requires that they keep all copyright lines intact. The compromise was to create an AUTHORS file that would list all of the copyright holders. The LICENSE file and source file headers would then include that list by reference, listing the copyright holder as "The Chromium Authors". This also become cumbersome to simply keep the file up to date with a high rate of new contributors. Plus it's not always obvious who the copyright holder is. Sometimes it is the individual making the contribution, but many times it may be their employer. There is no way for the proejct maintainer to know. Eventually, Google changed their policy to no longer recommend trying to keep the AUTHORS file up to date proactively, and instead to only add to it when requested: https://opensource.google/docs/releasing/authors. They are also clear that: > Adding contributors to the AUTHORS file is entirely within the > project's discretion and has no implications for copyright ownership. It was primarily added to appease a small number of large contributors that insisted that they be recognized as copyright holders (which was entirely their right to do). But it's not truly necessary, and not even the most accurate way of identifying contributors and/or copyright holders. In practice, we've never added anyone to our AUTHORS file. It only lists Tailscale, so it's not really serving any purpose. It also causes confusion because Tailscalars put the "Tailscale Inc & AUTHORS" header in other open source repos which don't actually have an AUTHORS file, so it's ambiguous what that means. Instead, we just acknowledge that the contributors to Tailscale (whoever they are) are copyright holders for their individual contributions. We also have the benefit of using the DCO (developercertificate.org) which provides some additional certification of their right to make the contribution. The source file changes were purely mechanical with: git ls-files | xargs sed -i -e 's/\(Tailscale Inc &\) AUTHORS/\1 contributors/g' Updates #cleanup Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d Signed-off-by: Will Norris <will@tailscale.com>
267 lines
6.6 KiB
Go
267 lines
6.6 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Package dnsname contains string functions for working with DNS names.
|
|
package dnsname
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"tailscale.com/util/vizerror"
|
|
)
|
|
|
|
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 = 254
|
|
)
|
|
|
|
// 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 "", vizerror.Errorf("%q is too long to be a DNS name", s)
|
|
}
|
|
|
|
st := 0
|
|
for i := range len(s) {
|
|
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 "", vizerror.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. All errors are
|
|
// [vizerror.Error].
|
|
func ValidLabel(label string) error {
|
|
if len(label) == 0 {
|
|
return vizerror.New("empty DNS label")
|
|
}
|
|
if len(label) > maxLabelLength {
|
|
return vizerror.Errorf("%q is too long, max length is %d bytes", label, maxLabelLength)
|
|
}
|
|
if !isalphanum(label[0]) {
|
|
return vizerror.Errorf("%q is not a valid DNS label: must start with a letter or number", label)
|
|
}
|
|
if !isalphanum(label[len(label)-1]) {
|
|
return vizerror.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 vizerror.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
|
|
}
|
|
}
|