Skip to content

Commit b1a696c

Browse files
committed
std.Io.net: make HostName.validate RFC 1123-compliant
The implementation of HostName.validate was too generous. It considered strings like ".example.com", "exa..mple.com", and "-example.com" to be valid hostnames, which is incorrect according to RFC 1123 (the currently accepted standard).
1 parent 26db54d commit b1a696c

File tree

1 file changed

+73
-6
lines changed

1 file changed

+73
-6
lines changed

lib/std/Io/net/HostName.zig

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,82 @@ pub const ValidateError = error{
2424
InvalidHostName,
2525
};
2626

27+
/// Validates a hostname according to [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123)
2728
pub fn validate(bytes: []const u8) ValidateError!void {
28-
if (bytes.len > max_len) return error.NameTooLong;
29-
if (!std.unicode.utf8ValidateSlice(bytes)) return error.InvalidHostName;
30-
for (bytes) |byte| {
31-
if (!std.ascii.isAscii(byte) or byte == '.' or byte == '-' or std.ascii.isAlphanumeric(byte)) {
32-
continue;
29+
if (bytes.len == 0) return error.InvalidHostName;
30+
if (bytes[0] == '.') return error.InvalidHostName;
31+
32+
// Ignore trailing dot (FQDN). It doesn't count toward our length.
33+
const end = if (bytes[bytes.len - 1] == '.') end: {
34+
if (bytes.len == 1) return error.InvalidHostName;
35+
break :end bytes.len - 1;
36+
} else bytes.len;
37+
38+
// The accepted maximum length of a hostname, including labels and dots.
39+
if (end > max_len) return error.NameTooLong;
40+
41+
// Hostnames are divided into dot-separated "labels", which:
42+
//
43+
// - Start with a letter or digit
44+
// - Can contain letters, digits, or hyphens
45+
// - Must end with a letter or digit
46+
// - Have a minimum of 1 character and a maximum of 63
47+
var label_start: usize = 0;
48+
var label_len: usize = 0;
49+
for (bytes[0..end], 0..) |c, i| {
50+
switch (c) {
51+
'.' => {
52+
if (label_len == 0 or label_len > 63) return error.InvalidHostName;
53+
if (!std.ascii.isAlphanumeric(bytes[label_start])) return error.InvalidHostName;
54+
if (!std.ascii.isAlphanumeric(bytes[i - 1])) return error.InvalidHostName;
55+
56+
label_start = i + 1;
57+
label_len = 0;
58+
},
59+
'-' => {
60+
label_len += 1;
61+
},
62+
else => {
63+
if (!std.ascii.isAlphanumeric(c)) return error.InvalidHostName;
64+
label_len += 1;
65+
},
3366
}
34-
return error.InvalidHostName;
3567
}
68+
69+
// Validate the final label
70+
if (label_len == 0 or label_len > 63) return error.InvalidHostName;
71+
if (!std.ascii.isAlphanumeric(bytes[label_start])) return error.InvalidHostName;
72+
if (!std.ascii.isAlphanumeric(bytes[end - 1])) return error.InvalidHostName;
73+
}
74+
75+
test validate {
76+
// Valid hostnames
77+
try validate("example");
78+
try validate("example.com");
79+
try validate("www.example.com");
80+
try validate("sub.domain.example.com");
81+
try validate("example.com.");
82+
try validate("host-name.example.com.");
83+
try validate("123.example.com.");
84+
try validate("a-b.com");
85+
try validate("a.b.c.d.e.f.g");
86+
try validate("127.0.0.1"); // Also a valid hostname
87+
try validate("a" ** 63 ++ ".com"); // Label exactly 63 chars (valid)
88+
try validate("a." ** 127 ++ "a"); // Total length 255 (valid)
89+
90+
// Invalid hostnames
91+
try std.testing.expectError(error.InvalidHostName, validate(""));
92+
try std.testing.expectError(error.InvalidHostName, validate(".example.com"));
93+
try std.testing.expectError(error.InvalidHostName, validate("example.com.."));
94+
try std.testing.expectError(error.InvalidHostName, validate("host..domain"));
95+
try std.testing.expectError(error.InvalidHostName, validate("-hostname"));
96+
try std.testing.expectError(error.InvalidHostName, validate("hostname-"));
97+
try std.testing.expectError(error.InvalidHostName, validate("a.-.b"));
98+
try std.testing.expectError(error.InvalidHostName, validate("host_name.com"));
99+
try std.testing.expectError(error.InvalidHostName, validate("."));
100+
try std.testing.expectError(error.InvalidHostName, validate(".."));
101+
try std.testing.expectError(error.InvalidHostName, validate("a" ** 64 ++ ".com")); // Label length 64 (too long)
102+
try std.testing.expectError(error.NameTooLong, validate("a." ** 127 ++ "ab")); // Total length 256 (too long)
36103
}
37104

38105
pub fn init(bytes: []const u8) ValidateError!HostName {

0 commit comments

Comments
 (0)