124 lines
2.9 KiB
Go
124 lines
2.9 KiB
Go
package hostname
|
|
|
|
import (
|
|
"net/netip"
|
|
"net/url"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"golang.org/x/net/idna"
|
|
)
|
|
|
|
const (
|
|
maxDNSHostnameLength = 253
|
|
maxDNSLabelLength = 63
|
|
)
|
|
|
|
// NormalizeHostnameV1 returns the canonical A-label hostname used by domain
|
|
// bindings. It accepts a bare DNS name, IPv4 address, bracketed IPv6 literal,
|
|
// localhost, or a single-label internal hostname. Invalid input returns "".
|
|
func NormalizeHostnameV1(input string) string {
|
|
value := strings.TrimSpace(input)
|
|
if value == "" || strings.IndexFunc(value, unicode.IsSpace) >= 0 || strings.ContainsAny(value, "\\/?#@") {
|
|
return ""
|
|
}
|
|
|
|
if strings.HasPrefix(value, "[") || strings.HasSuffix(value, "]") {
|
|
if !strings.HasPrefix(value, "[") || !strings.HasSuffix(value, "]") {
|
|
return ""
|
|
}
|
|
return normalizeIPv6(value[1 : len(value)-1])
|
|
}
|
|
if strings.Contains(value, ":") {
|
|
return ""
|
|
}
|
|
|
|
value = strings.TrimSuffix(value, ".")
|
|
if value == "" || strings.HasSuffix(value, ".") {
|
|
return ""
|
|
}
|
|
if isNumericHostname(value) {
|
|
return normalizeIPv4(value)
|
|
}
|
|
return normalizeDNS(value)
|
|
}
|
|
|
|
// NormalizeURLHostnameV1 returns the canonical hostname from an HTTP(S) URL.
|
|
// Ports, paths, credentials, and fragments are intentionally not preserved.
|
|
func NormalizeURLHostnameV1(input string) string {
|
|
value := strings.TrimSpace(input)
|
|
if value == "" {
|
|
return ""
|
|
}
|
|
parsed, err := url.ParseRequestURI(value)
|
|
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") {
|
|
return ""
|
|
}
|
|
host := parsed.Hostname()
|
|
if host == "" {
|
|
return ""
|
|
}
|
|
if address, err := netip.ParseAddr(host); err == nil && address.Is6() {
|
|
return address.String()
|
|
}
|
|
return NormalizeHostnameV1(host)
|
|
}
|
|
|
|
func normalizeIPv4(value string) string {
|
|
address, err := netip.ParseAddr(value)
|
|
if err != nil || !address.Is4() || address.String() != value {
|
|
return ""
|
|
}
|
|
return address.String()
|
|
}
|
|
|
|
func normalizeIPv6(value string) string {
|
|
address, err := netip.ParseAddr(value)
|
|
if err != nil || !address.Is6() {
|
|
return ""
|
|
}
|
|
return address.String()
|
|
}
|
|
|
|
func normalizeDNS(value string) string {
|
|
ascii, err := idna.Lookup.ToASCII(value)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
ascii = strings.ToLower(strings.TrimSuffix(ascii, "."))
|
|
if ascii == "" || strings.HasSuffix(ascii, ".") || len(ascii) > maxDNSHostnameLength {
|
|
return ""
|
|
}
|
|
for _, label := range strings.Split(ascii, ".") {
|
|
if len(label) == 0 || len(label) > maxDNSLabelLength || !isDNSLabel(label) {
|
|
return ""
|
|
}
|
|
}
|
|
return ascii
|
|
}
|
|
|
|
func isNumericHostname(value string) bool {
|
|
for _, char := range value {
|
|
if (char < '0' || char > '9') && char != '.' {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isDNSLabel(label string) bool {
|
|
if !isASCIIAlphaNum(label[0]) || !isASCIIAlphaNum(label[len(label)-1]) {
|
|
return false
|
|
}
|
|
for _, char := range label {
|
|
if !isASCIIAlphaNum(byte(char)) && char != '-' {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func isASCIIAlphaNum(char byte) bool {
|
|
return char >= 'a' && char <= 'z' || char >= '0' && char <= '9'
|
|
}
|