validation.go RawHistoryFolder
1package domain
2
3import (
4 "net"
5 "regexp"
6 "strings"
7)
8
9// hostnamePattern matches valid hostname characters (alphanumeric, hyphen, dot).
10// Used as a basic sanity check before detailed validation.
11// The actual validation uses net.ParseIP and explicit checks.
12var hostnamePattern = regexp.MustCompile(`^[a-zA-Z0-9.-]+$`)
13
14// IsValidRouteDomain validates that a domain is a public hostname suitable
15// for use as an application route. It rejects IP literals, localhost,
16// internal-only names, and authority strings containing ports.
17//
18// Valid: app.example.com, api.site.org, my-app.co.uk
19// Invalid: 192.168.1.1, localhost, myapp.local, example.com:8080
20func IsValidRouteDomain(domain string) bool {
21 // Reject empty
22 if domain == "" {
23 return false
24 }
25
26 // Basic character sanity check - only allow alphanumeric, hyphen, dot
27 // This also rejects paths, query strings, schemes (which contain :/ etc.)
28 if !hostnamePattern.MatchString(domain) {
29 return false
30 }
31
32 // Reject leading/trailing dots and consecutive dots
33 if strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") ||
34 strings.Contains(domain, "..") {
35 return false
36 }
37
38 // Reject total hostname length > 253 (RFC 1035)
39 if len(domain) > 253 {
40 return false
41 }
42
43 // Must have at least one dot (domain + TLD minimum)
44 // This also rejects single-label names like "localhost", "myapp"
45 if !strings.Contains(domain, ".") {
46 return false
47 }
48
49 // Check if it looks like an IP address (IPv4 or IPv6)
50 // net.ParseIP accepts various formats including IPv6 brackets
51 if net.ParseIP(domain) != nil {
52 return false
53 }
54
55 // Check for internal-only TLDs
56 lowerDomain := strings.ToLower(domain)
57 if strings.HasSuffix(lowerDomain, ".local") ||
58 strings.HasSuffix(lowerDomain, ".internal") {
59 return false
60 }
61
62 // Check for localhost variants
63 if strings.HasPrefix(lowerDomain, "localhost.") {
64 return false
65 }
66 if strings.HasSuffix(lowerDomain, ".localhost") {
67 return false
68 }
69
70 // Validate each label conforms to RFC 1123
71 labels := strings.Split(domain, ".")
72 for _, label := range labels {
73 if !isValidLabel(label) {
74 return false
75 }
76 }
77
78 return true
79}
80
81// isValidLabel checks the per-label RFC 1123 length and hyphen rules.
82// Character allowlisting is enforced earlier by hostnamePattern.
83func isValidLabel(label string) bool {
84 if label == "" {
85 return false
86 }
87 if len(label) > 63 {
88 return false
89 }
90 if strings.HasPrefix(label, "-") || strings.HasSuffix(label, "-") {
91 return false
92 }
93 return true
94}