Skip to content

Commit 7fc2455

Browse files
committed
refactor: simplify host normalization and enhance hostname validation functions
1 parent 8ab888e commit 7fc2455

File tree

4 files changed

+129
-110
lines changed

4 files changed

+129
-110
lines changed

hostutil/hostutil.go

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,9 @@
11
package hostutil
22

33
import (
4-
"fmt"
5-
"net"
6-
"strconv"
74
"strings"
85
)
96

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-
547
// IsValidHostname checks if the given hostname is valid based on RFC 1123.
558
func IsValidHostname(hostname string) bool {
569
if len(hostname) == 0 || len(hostname) > 255 {

hostutil/hostutil_test.go

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,50 @@
11
package hostutil
22

33
import (
4+
"strings"
45
"testing"
56
)
67

7-
func TestNormalizeHost(t *testing.T) {
8+
func TestIsValidHostname(t *testing.T) {
89
tests := []struct {
9-
input string
10-
expected string
11-
hasError bool
10+
hostname string
11+
valid bool
1212
}{
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},
13+
{"example.com", true},
14+
{"localhost", true},
15+
{"sub.domain.example.com", true},
16+
{"example", true},
17+
{"example123.com", true},
18+
{"123example.com", true},
19+
{"example-com", true},
20+
{"example.com-", false},
21+
{"-example.com", false},
22+
{"exa_mple.com", false},
23+
{"example..com", false},
24+
{"", false},
25+
{strings.Repeat("a", 256), false},
26+
{"example!.com", false},
27+
{"example .com", false},
28+
{".example.com", false},
29+
{"example.com.", false},
30+
{"ex%ample.com", false},
31+
{"example.com/", false},
32+
{"example..com", false},
33+
{"-example-.com", false},
34+
{"ex--ample.com", true},
35+
{"example.-com", false},
36+
{"example.com-", false},
37+
{"example-.com", false},
38+
{"exa*mple.com", false},
39+
{"example@com", false},
40+
{"example,com", false},
3041
}
3142

3243
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)
44+
t.Run(tt.hostname, func(t *testing.T) {
45+
result := IsValidHostname(tt.hostname)
46+
if result != tt.valid {
47+
t.Errorf("IsValidHostname(%q) = %v; want %v", tt.hostname, result, tt.valid)
4048
}
4149
})
4250
}

urlutil/urlutil.go

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -98,27 +98,20 @@ func HasScheme(rawURL string) bool {
9898
return re.MatchString(rawURL)
9999
}
100100

101-
// EnsureScheme ensures a URL has a scheme. If no scheme is provided, it defaults to "http".
102-
func EnsureScheme(rawURL string, scheme ...string) string {
103-
if rawURL == "" {
104-
return rawURL
105-
}
106-
107-
defaultScheme := "http"
108-
if len(scheme) > 0 && scheme[0] != "" {
109-
defaultScheme = scheme[0]
110-
}
111-
112-
u, err := url.Parse(rawURL)
113-
if err != nil {
114-
return defaultScheme + "://" + rawURL
101+
// EnsureHTTP ensures a URL has an HTTP scheme
102+
func EnsureHTTP(rawURL string) string {
103+
if !HasScheme(rawURL) {
104+
rawURL = "http://" + rawURL
115105
}
106+
return rawURL
107+
}
116108

117-
if u.Scheme == "" {
118-
u.Scheme = defaultScheme
109+
// EnsureHTTPS ensures a URL has an HTTPS scheme
110+
func EnsureHTTPS(rawURL string) string {
111+
if !HasScheme(rawURL) {
112+
rawURL = "https://" + rawURL
119113
}
120-
121-
return u.String()
114+
return rawURL
122115
}
123116

124117
// HasFileExtension checks if the given rawURL string has a file extension in its path

urlutil/urlutil_test.go

Lines changed: 82 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -75,23 +75,6 @@ func TestHasScheme(t *testing.T) {
7575
}
7676
}
7777

78-
func TestEnsureScheme(t *testing.T) {
79-
tests := []struct {
80-
input string
81-
expected string
82-
}{
83-
{"example.com", "http://example.com"},
84-
{"http://example.com", "http://example.com"},
85-
}
86-
87-
for _, test := range tests {
88-
result := EnsureScheme(test.input)
89-
if result != test.expected {
90-
t.Errorf("EnsureScheme(%s) = %s; want %s", test.input, result, test.expected)
91-
}
92-
}
93-
}
94-
9578
func TestHasFileExtension(t *testing.T) {
9679
tests := []struct {
9780
input string
@@ -228,3 +211,85 @@ func TestIsMediaExt(t *testing.T) {
228211
}
229212
}
230213
}
214+
215+
func TestRemoveDefaultPort(t *testing.T) {
216+
tests := []struct {
217+
input string
218+
expected string
219+
hasError bool
220+
}{
221+
{"http://example.com:80", "http://example.com", false},
222+
{"https://example.com:443", "https://example.com", false},
223+
{"http://example.com:8080", "http://example.com:8080", false},
224+
{"https://example.com:8443", "https://example.com:8443", false},
225+
{"ftp://example.com:21", "ftp://example.com", false},
226+
{"ftp://example.com:2121", "ftp://example.com:2121", false},
227+
{"http://example.com", "http://example.com", false},
228+
{"https://example.com", "https://example.com", false},
229+
{"invalid_url", "", true},
230+
{"example.com", "", true},
231+
}
232+
233+
for _, tt := range tests {
234+
t.Run(tt.input, func(t *testing.T) {
235+
result, err := RemoveDefaultPort(tt.input)
236+
if (err != nil) != tt.hasError {
237+
t.Errorf("expected error status %v, got %v (error: %v)", tt.hasError, (err != nil), err)
238+
}
239+
if result != tt.expected {
240+
t.Errorf("expected %v, got %v", tt.expected, result)
241+
}
242+
})
243+
}
244+
}
245+
246+
func TestEnsureHTTP(t *testing.T) {
247+
tests := []struct {
248+
rawURL string
249+
expected string
250+
}{
251+
{"example.com", "http://example.com"},
252+
{"http://example.com", "http://example.com"},
253+
{"https://example.com", "https://example.com"},
254+
{"example.com/path", "http://example.com/path"},
255+
{"localhost", "http://localhost"},
256+
{"http://localhost", "http://localhost"},
257+
{"https://localhost", "https://localhost"},
258+
{"192.168.0.1", "http://192.168.0.1"},
259+
{"http://192.168.0.1", "http://192.168.0.1"},
260+
}
261+
262+
for _, tt := range tests {
263+
t.Run(tt.rawURL, func(t *testing.T) {
264+
result := EnsureHTTP(tt.rawURL)
265+
if result != tt.expected {
266+
t.Errorf("expected %v, got %v", tt.expected, result)
267+
}
268+
})
269+
}
270+
}
271+
272+
func TestEnsureHTTPS(t *testing.T) {
273+
tests := []struct {
274+
rawURL string
275+
expected string
276+
}{
277+
{"example.com", "https://example.com"},
278+
{"http://example.com", "http://example.com"},
279+
{"https://example.com", "https://example.com"},
280+
{"example.com/path", "https://example.com/path"},
281+
{"localhost", "https://localhost"},
282+
{"https://localhost", "https://localhost"},
283+
{"192.168.0.1", "https://192.168.0.1"},
284+
{"https://192.168.0.1", "https://192.168.0.1"},
285+
}
286+
287+
for _, tt := range tests {
288+
t.Run(tt.rawURL, func(t *testing.T) {
289+
result := EnsureHTTPS(tt.rawURL)
290+
if result != tt.expected {
291+
t.Errorf("expected %v, got %v", tt.expected, result)
292+
}
293+
})
294+
}
295+
}

0 commit comments

Comments
 (0)