aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/os/hostname.zig
blob: af9148fbf76acd3012c4523b947c927b5908b1a2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
const std = @import("std");
const builtin = @import("builtin");
const posix = std.posix;

pub const LocalHostnameValidationError = error{
    PermissionDenied,
    Unexpected,
};

/// Validates a hostname according to [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123)
///
/// std.net.isValidHostname is (currently) too generous. It considers strings like
/// ".example.com", "exa..mple.com", and "-example.com" to be valid hostnames, which
/// is incorrect.
pub fn isValid(hostname: []const u8) bool {
    if (hostname.len == 0) return false;
    if (hostname[0] == '.') return false;

    // Ignore trailing dot (FQDN). It doesn't count toward our length.
    const end = if (hostname[hostname.len - 1] == '.') end: {
        if (hostname.len == 1) return false;
        break :end hostname.len - 1;
    } else hostname.len;

    if (end > 253) return false;

    // Hostnames are divided into dot-separated "labels", which:
    //
    // - Start with a letter or digit
    // - Can contain letters, digits, or hyphens
    // - Must end with a letter or digit
    // - Have a minimum of 1 character and a maximum of 63
    var label_start: usize = 0;
    var label_len: usize = 0;
    for (hostname[0..end], 0..) |c, i| {
        switch (c) {
            '.' => {
                if (label_len == 0 or label_len > 63) return false;
                if (!std.ascii.isAlphanumeric(hostname[label_start])) return false;
                if (!std.ascii.isAlphanumeric(hostname[i - 1])) return false;

                label_start = i + 1;
                label_len = 0;
            },
            '-' => {
                label_len += 1;
            },
            else => {
                if (!std.ascii.isAlphanumeric(c)) return false;
                label_len += 1;
            },
        }
    }

    // Validate the final label
    if (label_len == 0 or label_len > 63) return false;
    if (!std.ascii.isAlphanumeric(hostname[label_start])) return false;
    if (!std.ascii.isAlphanumeric(hostname[end - 1])) return false;

    return true;
}

test isValid {
    const testing = std.testing;

    // Valid hostnames
    try testing.expect(isValid("example"));
    try testing.expect(isValid("example.com"));
    try testing.expect(isValid("www.example.com"));
    try testing.expect(isValid("sub.domain.example.com"));
    try testing.expect(isValid("example.com."));
    try testing.expect(isValid("host-name.example.com."));
    try testing.expect(isValid("123.example.com."));
    try testing.expect(isValid("a-b.com"));
    try testing.expect(isValid("a.b.c.d.e.f.g"));
    try testing.expect(isValid("127.0.0.1")); // Also a valid hostname
    try testing.expect(isValid("a" ** 63 ++ ".com")); // Label exactly 63 chars (valid)
    try testing.expect(isValid("a." ** 126 ++ "a")); // Total length 253 (valid)

    // Invalid hostnames
    try testing.expect(!isValid(""));
    try testing.expect(!isValid(".example.com"));
    try testing.expect(!isValid("example.com.."));
    try testing.expect(!isValid("host..domain"));
    try testing.expect(!isValid("-hostname"));
    try testing.expect(!isValid("hostname-"));
    try testing.expect(!isValid("a.-.b"));
    try testing.expect(!isValid("host_name.com"));
    try testing.expect(!isValid("."));
    try testing.expect(!isValid(".."));
    try testing.expect(!isValid("a" ** 64 ++ ".com")); // Label length 64 (too long)
    try testing.expect(!isValid("a." ** 126 ++ "ab")); // Total length 254 (too long)
}

/// Checks if a hostname is local to the current machine. This matches
/// both "localhost" and the current hostname of the machine (as returned
/// by `gethostname`).
pub fn isLocal(hostname: []const u8) LocalHostnameValidationError!bool {
    // A 'localhost' hostname is always considered local.
    if (std.mem.eql(u8, "localhost", hostname)) return true;

    // If hostname is not "localhost" it must match our hostname.
    switch (builtin.os.tag) {
        .windows => {
            const windows = @import("windows.zig");
            var buf: [256:0]u8 = undefined;
            var nSize: windows.DWORD = buf.len;
            if (windows.exp.kernel32.GetComputerNameA(&buf, &nSize) == 0) return false;
            const ourHostname = buf[0..nSize];
            return std.mem.eql(u8, hostname, ourHostname);
        },
        else => {
            var buf: [posix.HOST_NAME_MAX]u8 = undefined;
            const ourHostname = try posix.gethostname(&buf);
            return std.mem.eql(u8, hostname, ourHostname);
        },
    }
}

test "isLocal returns true when provided hostname is localhost" {
    try std.testing.expect(try isLocal("localhost"));
}

test "isLocal returns true when hostname is local" {
    switch (builtin.os.tag) {
        .windows => {
            const windows = @import("windows.zig");
            var buf: [256:0]u8 = undefined;
            var nSize: windows.DWORD = buf.len;
            if (windows.exp.kernel32.GetComputerNameA(&buf, &nSize) == 0) return error.GetComputerNameFailed;
            const localHostname = buf[0..nSize];
            try std.testing.expect(try isLocal(localHostname));
        },
        else => {
            var buf: [posix.HOST_NAME_MAX]u8 = undefined;
            const localHostname = try posix.gethostname(&buf);
            try std.testing.expect(try isLocal(localHostname));
        },
    }
}

test "isLocal returns false when hostname is not local" {
    try std.testing.expectEqual(
        false,
        try isLocal("not-the-local-hostname"),
    );
}