Skip to content

Commit f92dd00

Browse files
committed
Add hostutil.go
1 parent 364e62f commit f92dd00

File tree

2 files changed

+124
-0
lines changed

2 files changed

+124
-0
lines changed

hostutil/hostutil.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package hostutil
2+
3+
import (
4+
"fmt"
5+
"net"
6+
"strconv"
7+
"strings"
8+
)
9+
10+
// NormalizeHost takes a host input (either a domain name or hostname) and returns it in a standardized format.
11+
// It converts the input to lowercase, removes redundant ports (e.g., 443 for HTTPS and 80 for HTTP),
12+
// validates that the input is a valid fully qualified domain name (FQDN), and ensures that the hostname is properly formatted.
13+
func NormalizeHost(host string) (string, error) {
14+
host = strings.TrimSpace(strings.ToLower(host))
15+
16+
if strings.Contains(host, "://") || strings.Contains(host, "/") {
17+
return "", fmt.Errorf("input contains URL scheme or path: %s", host)
18+
}
19+
20+
hostname, port, err := net.SplitHostPort(host)
21+
if err != nil {
22+
if strings.Contains(host, ":") {
23+
return "", fmt.Errorf("failed to parse host input: %s", host)
24+
}
25+
hostname = host
26+
}
27+
28+
if port != "" {
29+
if _, err := strconv.Atoi(port); err != nil {
30+
return "", fmt.Errorf("invalid port number: %s", port)
31+
}
32+
}
33+
34+
if net.ParseIP(hostname) == nil && !IsValidHostname(hostname) {
35+
return "", fmt.Errorf("invalid hostname or IP address: %s", hostname)
36+
}
37+
38+
if !strings.Contains(hostname, ".") {
39+
return "", fmt.Errorf("invalid FQDN: %s", hostname)
40+
}
41+
42+
switch port {
43+
case "443", "80":
44+
port = ""
45+
}
46+
47+
if port != "" {
48+
hostname = net.JoinHostPort(hostname, port)
49+
}
50+
51+
return hostname, nil
52+
}
53+
54+
// IsValidHostname checks if the given hostname is valid based on RFC 1123.
55+
func IsValidHostname(hostname string) bool {
56+
if len(hostname) == 0 || len(hostname) > 255 {
57+
return false
58+
}
59+
labels := strings.Split(hostname, ".")
60+
for _, label := range labels {
61+
if len(label) == 0 || len(label) > 63 {
62+
return false
63+
}
64+
startChar := label[0]
65+
endChar := label[len(label)-1]
66+
if !((startChar >= 'a' && startChar <= 'z') || (startChar >= '0' && startChar <= '9')) {
67+
return false
68+
}
69+
if !((endChar >= 'a' && endChar <= 'z') || (endChar >= '0' && endChar <= '9')) {
70+
return false
71+
}
72+
for i := 0; i < len(label); i++ {
73+
c := label[i]
74+
if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' {
75+
continue
76+
}
77+
return false
78+
}
79+
}
80+
return true
81+
}

hostutil/hostutil_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package hostutil
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestNormalizeHost(t *testing.T) {
8+
tests := []struct {
9+
input string
10+
expected string
11+
hasError bool
12+
}{
13+
{"example.com", "example.com", false},
14+
{"Example.COM", "example.com", false},
15+
{"example.com:8080", "example.com:8080", false},
16+
{"example.com:80", "example.com", false},
17+
{"example.com:443", "example.com", false},
18+
{"subdomain.example.com:443", "subdomain.example.com", false},
19+
{"subdomain.example.com:80", "subdomain.example.com", false},
20+
{" example.com ", "example.com", false},
21+
{"invalid_host:port", "", true},
22+
{"subdomain:invalidport", "", true},
23+
{"http://example.com", "", true},
24+
{"https://example.com", "", true},
25+
{"ftp://example.com", "", true},
26+
{"example.com/path", "", true},
27+
{"http://example.com:443", "", true},
28+
{"example", "", true},
29+
{"localhost", "", true},
30+
}
31+
32+
for _, tt := range tests {
33+
t.Run(tt.input, func(t *testing.T) {
34+
result, err := NormalizeHost(tt.input)
35+
if (err != nil) != tt.hasError {
36+
t.Errorf("expected error status %v, got %v (error: %v)", tt.hasError, (err != nil), err)
37+
}
38+
if result != tt.expected {
39+
t.Errorf("expected %v, got %v", tt.expected, result)
40+
}
41+
})
42+
}
43+
}

0 commit comments

Comments
 (0)