[management] Streamline domain validation (#5211)

This commit is contained in:
Viktor Liu
2026-01-29 20:51:44 +08:00
committed by GitHub
parent f74bc48d16
commit 81c11df103
13 changed files with 339 additions and 105 deletions

View File

@@ -10,7 +10,30 @@ const maxDomains = 32
var domainRegex = 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])?$`)
// ValidateDomains checks if each domain in the list is valid and returns a punycode-encoded DomainList.
// IsValidDomain checks if a single domain string is valid.
// Does not convert unicode to punycode - domain must already be ASCII/punycode.
// Allows wildcard prefix (*.example.com).
func IsValidDomain(domain string) bool {
if domain == "" {
return false
}
return domainRegex.MatchString(strings.ToLower(domain))
}
// IsValidDomainNoWildcard checks if a single domain string is valid without wildcard prefix.
// Use for zone domains and CNAME targets where wildcards are not allowed.
func IsValidDomainNoWildcard(domain string) bool {
if domain == "" {
return false
}
if strings.HasPrefix(domain, "*.") {
return false
}
return domainRegex.MatchString(strings.ToLower(domain))
}
// ValidateDomains validates domains and converts unicode to punycode.
// Allows wildcard prefix (*.example.com). Maximum 32 domains.
func ValidateDomains(domains []string) (List, error) {
if len(domains) == 0 {
return nil, fmt.Errorf("domains list is empty")
@@ -37,7 +60,10 @@ func ValidateDomains(domains []string) (List, error) {
return domainList, nil
}
// ValidateDomainsList checks if each domain in the list is valid
// ValidateDomainsList validates domains without punycode conversion.
// Use this for domains that must already be in ASCII/punycode format (e.g., extra DNS labels).
// Unlike ValidateDomains, this does not convert unicode to punycode - unicode domains will fail.
// Allows wildcard prefix (*.example.com). Maximum 32 domains.
func ValidateDomainsList(domains []string) error {
if len(domains) == 0 {
return nil

View File

@@ -2,12 +2,16 @@ package domain
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestValidateDomains(t *testing.T) {
label63 := strings.Repeat("a", 63)
label64 := strings.Repeat("a", 64)
tests := []struct {
name string
domains []string
@@ -26,6 +30,48 @@ func TestValidateDomains(t *testing.T) {
expected: List{"sub.ex-ample.com"},
wantErr: false,
},
{
name: "Valid uppercase domain normalized to lowercase",
domains: []string{"EXAMPLE.COM"},
expected: List{"example.com"},
wantErr: false,
},
{
name: "Valid mixed case domain",
domains: []string{"ExAmPlE.CoM"},
expected: List{"example.com"},
wantErr: false,
},
{
name: "Single letter TLD",
domains: []string{"example.x"},
expected: List{"example.x"},
wantErr: false,
},
{
name: "Two letter domain labels",
domains: []string{"a.b"},
expected: List{"a.b"},
wantErr: false,
},
{
name: "Single character domain",
domains: []string{"x"},
expected: List{"x"},
wantErr: false,
},
{
name: "Wildcard with single letter TLD",
domains: []string{"*.x"},
expected: List{"*.x"},
wantErr: false,
},
{
name: "Multi-level with single letter labels",
domains: []string{"a.b.c"},
expected: List{"a.b.c"},
wantErr: false,
},
{
name: "Valid Unicode domain",
domains: []string{"münchen.de"},
@@ -45,17 +91,92 @@ func TestValidateDomains(t *testing.T) {
wantErr: false,
},
{
name: "Invalid domain format",
name: "Valid domain starting with digit",
domains: []string{"123.example.com"},
expected: List{"123.example.com"},
wantErr: false,
},
// Numeric TLDs are allowed for internal/private DNS use cases.
// While ICANN doesn't issue all-numeric gTLDs, the DNS protocol permits them
// and resolvers like systemd-resolved handle them correctly.
{
name: "Numeric TLD allowed",
domains: []string{"example.123"},
expected: List{"example.123"},
wantErr: false,
},
{
name: "Single digit TLD allowed",
domains: []string{"example.1"},
expected: List{"example.1"},
wantErr: false,
},
{
name: "All numeric labels allowed",
domains: []string{"123.456"},
expected: List{"123.456"},
wantErr: false,
},
{
name: "Single numeric label allowed",
domains: []string{"123"},
expected: List{"123"},
wantErr: false,
},
{
name: "Valid domain with double hyphen",
domains: []string{"test--example.com"},
expected: List{"test--example.com"},
wantErr: false,
},
{
name: "Invalid leading hyphen",
domains: []string{"-example.com"},
expected: nil,
wantErr: true,
},
{
name: "Invalid domain format 2",
name: "Invalid trailing hyphen",
domains: []string{"example.com-"},
expected: nil,
wantErr: true,
},
{
name: "Invalid leading dot",
domains: []string{".com"},
expected: nil,
wantErr: true,
},
{
name: "Invalid dot only",
domains: []string{"."},
expected: nil,
wantErr: true,
},
{
name: "Invalid double dot",
domains: []string{"example..com"},
expected: nil,
wantErr: true,
},
{
name: "Invalid special characters",
domains: []string{"example?,.com"},
expected: nil,
wantErr: true,
},
{
name: "Invalid space in domain",
domains: []string{"space .example.com"},
expected: nil,
wantErr: true,
},
{
name: "Invalid trailing space",
domains: []string{"example.com "},
expected: nil,
wantErr: true,
},
{
name: "Multiple domains valid and invalid",
domains: []string{"google.com", "invalid,nbdomain.com", "münchen.de"},
@@ -86,6 +207,30 @@ func TestValidateDomains(t *testing.T) {
expected: nil,
wantErr: true,
},
{
name: "Valid 63 char label (max)",
domains: []string{label63 + ".com"},
expected: List{Domain(label63 + ".com")},
wantErr: false,
},
{
name: "Invalid 64 char label (exceeds max)",
domains: []string{label64 + ".com"},
expected: nil,
wantErr: true,
},
{
name: "Valid 253 char domain (max)",
domains: []string{strings.Repeat("a.", 126) + "a"},
expected: List{Domain(strings.Repeat("a.", 126) + "a")},
wantErr: false,
},
{
name: "Invalid 254+ char domain (exceeds max)",
domains: []string{strings.Repeat("ab.", 85)},
expected: nil,
wantErr: true,
},
}
for _, tt := range tests {
@@ -118,6 +263,57 @@ func TestValidateDomainsList(t *testing.T) {
domains: []string{"sub.ex-ample.com"},
wantErr: false,
},
{
name: "Uppercase domain accepted",
domains: []string{"EXAMPLE.COM"},
wantErr: false,
},
{
name: "Single letter TLD",
domains: []string{"example.x"},
wantErr: false,
},
{
name: "Two letter domain labels",
domains: []string{"a.b"},
wantErr: false,
},
{
name: "Single character domain",
domains: []string{"x"},
wantErr: false,
},
{
name: "Wildcard with single letter TLD",
domains: []string{"*.x"},
wantErr: false,
},
{
name: "Multi-level with single letter labels",
domains: []string{"a.b.c"},
wantErr: false,
},
// Numeric TLDs are allowed for internal/private DNS use cases.
{
name: "Numeric TLD allowed",
domains: []string{"example.123"},
wantErr: false,
},
{
name: "Single digit TLD allowed",
domains: []string{"example.1"},
wantErr: false,
},
{
name: "All numeric labels allowed",
domains: []string{"123.456"},
wantErr: false,
},
{
name: "Single numeric label allowed",
domains: []string{"123"},
wantErr: false,
},
{
name: "Underscores in labels",
domains: []string{"_jabber._tcp.gmail.com"},