| ... | @@ -24,15 +24,82 @@ pub const ValidateError = error{ | ... | @@ -24,15 +24,82 @@ pub const ValidateError = error{ |
| 24 | InvalidHostName, | 24 | InvalidHostName, |
| 25 | }; | 25 | }; |
| 26 | | 26 | |
| | 27 | /// Validates a hostname according to [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123) |
| 27 | pub fn validate(bytes: []const u8) ValidateError!void { | 28 | pub fn validate(bytes: []const u8) ValidateError!void { |
| 28 | if (bytes.len > max_len) return error.NameTooLong; | 29 | if (bytes.len == 0) return error.InvalidHostName; |
| 29 | if (!std.unicode.utf8ValidateSlice(bytes)) return error.InvalidHostName; | 30 | if (bytes[0] == '.') return error.InvalidHostName; |
| 30 | for (bytes) |byte| { | 31 | |
| 31 | if (!std.ascii.isAscii(byte) or byte == '.' or byte == '-' or std.ascii.isAlphanumeric(byte)) { | 32 | // Ignore trailing dot (FQDN). It doesn't count toward our length. |
| 32 | continue; | 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 | }, |
| 33 | } | 66 | } |
| 34 | return error.InvalidHostName; | | |
| 35 | } | 67 | } |
| | 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) |
| 36 | } | 103 | } |
| 37 | | 104 | |
| 38 | pub fn init(bytes: []const u8) ValidateError!HostName { | 105 | pub fn init(bytes: []const u8) ValidateError!HostName { |