Files
netbird/shared/management/domain/validate.go
2025-08-08 15:32:33 +02:00

151 lines
3.8 KiB
Go

package domain
import (
"fmt"
"regexp"
"strings"
"sync"
)
const maxFQDN = 32
var regexCache = map[string]*regexp.Regexp{}
var regexCacheMu sync.Mutex
var fqdnRegex = regexp.MustCompile(`^(?:(?:xn--)?[a-zA-Z0-9_](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?\.)*(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-_]{0,61}[a-zA-Z0-9])?$`)
func buildDomainRegex(allowWildcard, allowSingleToplevel bool) *regexp.Regexp {
key := fmt.Sprintf("%t:%t", allowWildcard, allowSingleToplevel)
regexCacheMu.Lock()
defer regexCacheMu.Unlock()
if re, ok := regexCache[key]; ok {
return re
}
var pattern strings.Builder
pattern.WriteString("^")
if allowWildcard {
pattern.WriteString(`(?:\*\.)?`)
}
label := `(?:xn--)?[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?`
if allowSingleToplevel {
pattern.WriteString(label + `(?:\.` + label + `)*`)
} else {
pattern.WriteString(label + `(?:\.` + label + `)+`)
}
pattern.WriteString("$")
re := regexp.MustCompile(pattern.String())
regexCache[key] = re
return re
}
// ValidateFQDNs checks if each domain in the list is valid and returns a punycode-encoded DomainList.
func ValidateFQDNs(fqdns []string) (List, error) {
if len(fqdns) == 0 {
return nil, fmt.Errorf("fqdns list is empty")
}
if len(fqdns) > maxFQDN {
return nil, fmt.Errorf("fqdns list exceeds maximum allowed fqdns: %d", maxFQDN)
}
var domainList List
for _, d := range fqdns {
validDomain, err := ToValidFQDN(d)
if err != nil {
return nil, fmt.Errorf("invalid domain %s: %w", d, err)
}
domainList = append(domainList, validDomain)
}
return domainList, nil
}
// ValidateDomains checks if each domain in the list is valid and returns a punycode-encoded DomainList.
func ValidateDomains(domains []string) (List, error) {
if len(domains) == 0 {
return nil, fmt.Errorf("domains list is empty")
}
if len(domains) > maxFQDN {
return nil, fmt.Errorf("domains list exceeds maximum allowed domains: %d", maxFQDN)
}
var domainList List
for _, d := range domains {
validDomain, err := ToValidDomain(d, true, true)
if err != nil {
return nil, fmt.Errorf("invalid domain %s: %w", d, err)
}
domainList = append(domainList, validDomain)
}
return domainList, nil
}
// ValidateFQDNsList checks if each domain in the list is valid
func ValidateFQDNsList(fqdns []string) error {
if len(fqdns) == 0 {
return nil
}
if len(fqdns) > maxFQDN {
return fmt.Errorf("fqdns list exceeds maximum allowed fqdns: %d", maxFQDN)
}
for _, d := range fqdns {
d := strings.ToLower(d)
if !fqdnRegex.MatchString(d) {
return fmt.Errorf("invalid fqdns format: %s", d)
}
}
return nil
}
// IsValidDomain checks if the given domain is valid.
func IsValidDomain(domain string, allowWildcard, allowSingleToplevel bool) bool {
// handles length and idna conversion
punycode, err := FromString(domain)
if err != nil {
return false
}
domainRegex := buildDomainRegex(allowWildcard, allowSingleToplevel)
return domainRegex.MatchString(string(punycode))
}
// ToValidDomain converts a domain to a valid domain format.
func ToValidDomain(domain string, allowWildcard, allowSingleToplevel bool) (Domain, error) {
// handles length and idna conversion
punycode, err := FromString(domain)
if err != nil {
return "", fmt.Errorf("convert domain to punycode: %s: %w", domain, err)
}
domainRegex := buildDomainRegex(allowWildcard, allowSingleToplevel)
if !domainRegex.MatchString(string(punycode)) {
return "", fmt.Errorf("invalid domain format: %s", domain)
}
return punycode, nil
}
// ToValidFQDN converts a domain to a valid fqdn format.
func ToValidFQDN(domain string) (Domain, error) {
// handles length and idna conversion
punycode, err := FromString(domain)
if err != nil {
return "", fmt.Errorf("convert domain to punycode: %s: %w", domain, err)
}
if !fqdnRegex.MatchString(string(punycode)) {
return "", fmt.Errorf("invalid domain format: %s", domain)
}
return punycode, nil
}