Files
ctrld/cmd/cli/hostname.go
T

87 lines
3.6 KiB
Go

package cli
import (
"fmt"
"regexp"
"strings"
)
// validHostname reports whether hostname is a valid hostname.
// A valid hostname contains 3 -> 64 characters and conform to RFC1123.
// This function validates hostnames to ensure they meet DNS naming standards
// and prevents invalid hostnames from being used in DNS configurations
func validHostname(hostname string) bool {
hostnameLen := len(hostname)
if hostnameLen < 3 || hostnameLen > 64 {
return false
}
// RFC1123 regex pattern ensures hostnames follow DNS naming conventions
// This prevents issues with DNS resolution and system compatibility
validHostnameRfc1123 := regexp.MustCompile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`)
return validHostnameRfc1123.MatchString(hostname)
}
// isHostnameChar reports whether r is part of validHostname's accepted
// charset (letters, digits, hyphen, dot). It does not check position, so a
// hostname can fail validHostname on structure (length, leading/trailing
// hyphen) while every one of its characters passes here.
func isHostnameChar(r rune) bool {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9':
return true
case r == '-' || r == '.':
return true
}
return false
}
// offendingHostnameChars returns the distinct characters in hostname that
// validHostname's charset does not accept, in first-seen order. Empty when
// every character is accepted - a rejection can still come from structure
// alone (too short, too long, a leading or trailing hyphen).
func offendingHostnameChars(hostname string) string {
seen := make(map[rune]bool)
var bad []rune
for _, r := range hostname {
if isHostnameChar(r) || seen[r] {
continue
}
seen[r] = true
bad = append(bad, r)
}
return string(bad)
}
// serverFoldedHostnameChars are the characters ControlD's
// DevicesTableModel.formatDeviceName folds to '-' (or strips) when it
// registers a device name server-side. Only '.' can actually reach
// hostnameMayBeFoldedByServer through validateCustomHostnameFlag's guarded
// path: validHostname runs first and already rejects any hostname
// containing a space or a '+' as CUSTOM_HOSTNAME_INVALID, so those two never
// get here from an explicit --custom-hostname value. They stay in this set
// for completeness: a mobile caller can set CustomHostname to an
// OS-derived default directly, without going through
// validateCustomHostnameFlag at all, so a space or '+' can still reach the
// API unvalidated by this client.
const serverFoldedHostnameChars = ". +"
// hostnameMayBeFoldedByServer reports whether hostname contains a character
// ControlD may fold or strip when it registers the device, so the name ctrld
// accepted may not be the name the dashboard ends up showing.
func hostnameMayBeFoldedByServer(hostname string) bool {
return strings.ContainsAny(hostname, serverFoldedHostnameChars)
}
// customHostnameFailureMessage names the field, the offending character(s)
// when there are any, and the allowed format for CUSTOM_HOSTNAME_INVALID.
// ctrld's accept/reject rule (validHostname) is unchanged - this only
// explains a rejection that used to be a bare fatal exit.
func customHostnameFailureMessage(hostname string) string {
const allowedFormat = "3-64 characters of letters, digits, hyphens, and dots (RFC1123 hostname format)"
reason := "is not a valid hostname"
if bad := offendingHostnameChars(hostname); bad != "" {
reason = fmt.Sprintf("contains characters a hostname cannot use: %q", bad)
}
return fmt.Sprintf("--custom-hostname (CustomHostname) %q %s; allowed format: %s", hostname, reason, allowedFormat)
}