| ... | ... | @@ -24,15 +24,82 @@ pub const ValidateError = error{ |
| 24 | 24 | InvalidHostName, |
| 25 | 25 | }; |
| 26 | 26 | |
| 27 | /// Validates a hostname according to [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123) |
| 27 | 28 | 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 | }, |
| 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 | 105 | pub fn init(bytes: []const u8) ValidateError!HostName { |