1//! An already-validated host name. A valid host name:
2//! * Has length less than or equal to `max_len`.
3//! * Is valid UTF-8.
4//! * Lacks ASCII characters other than alphanumeric, '-', and '.'.
5const HostName = @This();
6
7const builtin = @import("builtin");
8const native_os = builtin.os.tag;
9
10const std = @import("../../std.zig");
11const Io = std.Io;
12const IpAddress = Io.net.IpAddress;
13const Ip6Address = Io.net.Ip6Address;
14const assert = std.debug.assert;
15const Stream = Io.net.Stream;
16
17/// Externally managed memory. Already checked to be valid.
18bytes: []const u8,
19
20/// The maximum number of bytes needed to store the text representation of
21/// a max length host name, where labels are separated by dots, including
22/// the trailing dot and the zero-length root label.
23///
24/// The max length of a host name is determined by its packet representation,
25/// where each label has a length prefix of 1 octet, the root label has a
26/// length of 0, and the maximum total number of octets is 255.
27///
28/// See [RFC 1035, Section 3.1](https://datatracker.ietf.org/doc/html/rfc1035#section-3.1)
29pub const max_len = 254;
30const max_len_without_root = max_len - 1;
31
32pub const FromUriError = error{UriMissingHost} || ValidateError;
33
34/// Returned `HostName.bytes` may point into `buffer` or `uri.host`.
35pub fn fromUri(uri: std.Uri, buffer: *[HostName.max_len]u8) FromUriError!HostName {
36 const component = uri.host orelse return error.UriMissingHost;
37 const bytes = component.toRaw(buffer) catch |err| switch (err) {
38 error.NoSpaceLeft => return error.NameTooLong,
39 };
40 try validate(bytes);
41 return .{ .bytes = bytes };
42}
43
44pub const ValidateError = error{
45 NameTooLong,
46 InvalidHostName,
47};
48
49/// Validates a hostname according to [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123)
50pub fn validate(bytes: []const u8) ValidateError!void {
51 if (bytes.len == 0) return error.InvalidHostName;
52
53 // Ignore trailing dot (FQDN).
54 const end = if (bytes[bytes.len - 1] == '.') bytes.len - 1 else bytes.len;
55
56 // The accepted maximum length of a hostname, including labels and dots.
57 if (end > max_len_without_root) return error.NameTooLong;
58
59 // Hostnames are divided into dot-separated "labels", which:
60 //
61 // - Start with a letter or digit
62 // - Can contain letters, digits, or hyphens
63 // - Must end with a letter or digit
64 // - Have a minimum of 1 character and a maximum of 63
65 var label_len: usize = 0;
66 for (bytes[0..end], 0..) |c, i| {
67 switch (c) {
68 '.' => {
69 if (label_len == 0 or label_len > 63) return error.InvalidHostName;
70 if (!std.ascii.isAlphanumeric(bytes[i - 1])) return error.InvalidHostName;
71 label_len = 0;
72 },
73 '-' => {
74 if (label_len == 0) return error.InvalidHostName;
75 label_len += 1;
76 },
77 else => {
78 if (!std.ascii.isAlphanumeric(c)) return error.InvalidHostName;
79 label_len += 1;
80 },
81 }
82 }
83
84 // Validate the final label
85 if (label_len == 0 or label_len > 63) return error.InvalidHostName;
86 if (!std.ascii.isAlphanumeric(bytes[end - 1])) return error.InvalidHostName;
87}
88
89test validate {
90 // Valid hostnames
91 try validate("example");
92 try validate("example.com");
93 try validate("www.example.com");
94 try validate("sub.domain.example.com");
95 try validate("example.com.");
96 try validate("host-name.example.com.");
97 try validate("123.example.com.");
98 try validate("a-b.com");
99 try validate("a.b.c.d.e.f.g");
100 try validate("127.0.0.1"); // Also a valid hostname
101
102 const many_a: [63]u8 = @splat('a');
103 try validate(&many_a ++ ".com"); // Label exactly 63 chars (valid)
104
105 const many_a_dot_buf: [126][2]u8 = @splat(.{ 'a', '.' });
106 const many_a_dot: []const u8 = @ptrCast(&many_a_dot_buf);
107 try validate(many_a_dot ++ "a"); // Total length 253 (without the trailing dot)
108 try validate(many_a_dot ++ "a."); // Total length 254 (with the trailing dot)
109
110 // Invalid hostnames
111 try std.testing.expectError(error.InvalidHostName, validate(""));
112 try std.testing.expectError(error.InvalidHostName, validate(".example.com"));
113 try std.testing.expectError(error.InvalidHostName, validate("example.com.."));
114 try std.testing.expectError(error.InvalidHostName, validate("host..domain"));
115 try std.testing.expectError(error.InvalidHostName, validate("-hostname"));
116 try std.testing.expectError(error.InvalidHostName, validate("hostname-"));
117 try std.testing.expectError(error.InvalidHostName, validate("hostname-.com"));
118 try std.testing.expectError(error.InvalidHostName, validate("a.-.b"));
119 try std.testing.expectError(error.InvalidHostName, validate("host_name.com"));
120 try std.testing.expectError(error.InvalidHostName, validate("."));
121 try std.testing.expectError(error.InvalidHostName, validate(".."));
122 try std.testing.expectError(error.InvalidHostName, validate(&many_a ++ "a.com")); // Label length 64 (too long)
123 try std.testing.expectError(error.NameTooLong, validate(many_a_dot ++ "ab")); // Total length 254 (without the trailing dot)
124 try std.testing.expectError(error.NameTooLong, validate(many_a_dot ++ "ab.")); // Total length 255 (with the trailing dot)
125}
126
127pub fn init(bytes: []const u8) ValidateError!HostName {
128 try validate(bytes);
129 return .{ .bytes = bytes };
130}
131
132pub fn sameParentDomain(parent_host: HostName, child_host: HostName) bool {
133 const parent_bytes = parent_host.bytes;
134 const child_bytes = child_host.bytes;
135 if (!std.ascii.endsWithIgnoreCase(child_bytes, parent_bytes)) return false;
136 if (child_bytes.len == parent_bytes.len) return true;
137 if (parent_bytes.len > child_bytes.len) return false;
138 return child_bytes[child_bytes.len - parent_bytes.len - 1] == '.';
139}
140
141test sameParentDomain {
142 try std.testing.expect(!sameParentDomain(try .init("foo.com"), try .init("bar.com")));
143 try std.testing.expect(sameParentDomain(try .init("foo.com"), try .init("foo.com")));
144 try std.testing.expect(sameParentDomain(try .init("foo.com"), try .init("bar.foo.com")));
145 try std.testing.expect(!sameParentDomain(try .init("bar.foo.com"), try .init("foo.com")));
146}
147
148/// Domain names are case-insensitive (RFC 5890, Section 2.3.2.4)
149pub fn eql(a: HostName, b: HostName) bool {
150 return std.ascii.eqlIgnoreCase(a.bytes, b.bytes);
151}
152
153pub const LookupOptions = struct {
154 port: u16,
155 /// `null` means either.
156 canonical_name_buffer: ?*[max_len]u8 = null,
157 family: ?IpAddress.Family = null,
158};
159
160pub const LookupError = error{
161 UnknownHostName,
162 ResolvConfParseFailed,
163 InvalidDnsARecord,
164 InvalidDnsAAAARecord,
165 InvalidDnsCnameRecord,
166 NameServerFailure,
167 NoAddressReturned,
168 /// Failed to open or read "/etc/hosts" or "/etc/resolv.conf".
169 DetectingNetworkConfigurationFailed,
170} || IpAddress.BindError || Io.Cancelable;
171
172pub const LookupResult = union(enum) {
173 address: IpAddress,
174 canonical_name: HostName,
175};
176
177/// Adds any number of `LookupResult.address` into `resolved`, and exactly one
178/// `LookupResult.canonical_name`.
179///
180/// Guaranteed not to block if provided queue has capacity at least 16.
181///
182/// Closes `resolved` before return, even on error.
183///
184/// Asserts `resolved` is not closed until this call returns.
185pub fn lookup(
186 host_name: HostName,
187 io: Io,
188 resolved: *Io.Queue(LookupResult),
189 options: LookupOptions,
190) LookupError!void {
191 return io.vtable.netLookup(io.userdata, host_name, resolved, options);
192}
193
194pub const ExpandError = error{InvalidDnsPacket} || ValidateError;
195
196/// Decompresses a DNS name.
197///
198/// Returns number of bytes consumed from `packet` starting at `i`,
199/// along with the expanded `HostName`.
200///
201/// Asserts `buffer` is has length at least `max_len`.
202pub fn expand(noalias packet: []const u8, start_i: usize, noalias dest_buffer: []u8) ExpandError!struct { usize, HostName } {
203 const dest = dest_buffer[0..max_len];
204
205 var i = start_i;
206 var dest_i: usize = 0;
207 var len: ?usize = null;
208
209 // Detect reference loop using an iteration counter.
210 for (0..packet.len / 2) |_| {
211 if (i >= packet.len) return error.InvalidDnsPacket;
212
213 const c = packet[i];
214 if ((c & 0xc0) != 0) {
215 if (i + 1 >= packet.len) return error.InvalidDnsPacket;
216 const j: usize = (@as(usize, c & 0x3F) << 8) | packet[i + 1];
217 if (j >= packet.len) return error.InvalidDnsPacket;
218 if (len == null) len = (i + 2) - start_i;
219 i = j;
220 } else if (c != 0) {
221 if (dest_i != 0) {
222 dest[dest_i] = '.';
223 dest_i += 1;
224 }
225 const label_len: usize = c;
226 if (i + 1 + label_len > packet.len) return error.InvalidDnsPacket;
227 if (dest_i + label_len + 1 > dest.len) return error.InvalidDnsPacket;
228 @memcpy(dest[dest_i..][0..label_len], packet[i + 1 ..][0..label_len]);
229 dest_i += label_len;
230 i += 1 + label_len;
231 } else {
232 return .{
233 len orelse i - start_i + 1,
234 try .init(dest[0..dest_i]),
235 };
236 }
237 }
238 return error.InvalidDnsPacket;
239}
240
241pub const DnsRecord = enum(u8) {
242 A = 1,
243 CNAME = 5,
244 AAAA = 28,
245 _,
246};
247
248pub const DnsResponse = struct {
249 bytes: []const u8,
250 bytes_index: u32,
251 answers_remaining: u16,
252
253 pub const Answer = struct {
254 rr: DnsRecord,
255 packet: []const u8,
256 data_off: u32,
257 data_len: u16,
258 };
259
260 pub const Error = error{InvalidDnsPacket};
261
262 pub fn init(r: []const u8) Error!DnsResponse {
263 if (r.len < 12) return error.InvalidDnsPacket;
264 if ((r[3] & 15) != 0) return .{ .bytes = r, .bytes_index = 3, .answers_remaining = 0 };
265 var i: u32 = 12;
266 var query_count = std.mem.readInt(u16, r[4..6], .big);
267 while (query_count != 0) : (query_count -= 1) {
268 while (i < r.len and r[i] -% 1 < 127) i += 1;
269 if (r.len - i < 6) return error.InvalidDnsPacket;
270 i = i + 5 + @intFromBool(r[i] != 0);
271 }
272 return .{
273 .bytes = r,
274 .bytes_index = i,
275 .answers_remaining = std.mem.readInt(u16, r[6..8], .big),
276 };
277 }
278
279 pub fn next(dr: *DnsResponse) Error!?Answer {
280 if (dr.answers_remaining == 0) return null;
281 dr.answers_remaining -= 1;
282 const r = dr.bytes;
283 var i = dr.bytes_index;
284 while (i < r.len and r[i] -% 1 < 127) i += 1;
285 if (r.len - i < 12) return error.InvalidDnsPacket;
286 i = i + 1 + @intFromBool(r[i] != 0);
287 const len = std.mem.readInt(u16, r[i + 8 ..][0..2], .big);
288 if (i + 10 + len > r.len) return error.InvalidDnsPacket;
289 defer dr.bytes_index = i + 10 + len;
290 return .{
291 .rr = @fromBackingInt(@intCast(r[i + 1])),
292 .packet = r,
293 .data_off = i + 10,
294 .data_len = len,
295 };
296 }
297};
298
299pub const ConnectError = LookupError || IpAddress.ConnectError;
300
301pub fn connect(
302 host_name: HostName,
303 io: Io,
304 port: u16,
305 options: IpAddress.ConnectOptions,
306) ConnectError!Stream {
307 var connect_many_buffer: [32]IpAddress.ConnectError!Stream = undefined;
308 var connect_many_queue: Io.Queue(IpAddress.ConnectError!Stream) = .init(&connect_many_buffer);
309
310 var connect_many = io.async(connectMany, .{ host_name, io, port, &connect_many_queue, options });
311 defer {
312 connect_many.cancel(io) catch {};
313 while (connect_many_queue.getOneUncancelable(io)) |loser| {
314 if (loser) |s| s.close(io) else |_| {}
315 } else |err| switch (err) {
316 error.Closed => {},
317 }
318 }
319
320 var ip_connect_error: ?IpAddress.ConnectError = null;
321
322 while (connect_many_queue.getOne(io)) |result| {
323 if (result) |stream| {
324 return stream;
325 } else |err| switch (err) {
326 error.Canceled => unreachable,
327
328 error.SystemResources,
329 error.OptionUnsupported,
330 error.ProcessFdQuotaExceeded,
331 error.SystemFdQuotaExceeded,
332 => |e| return e,
333
334 error.WouldBlock => return error.Unexpected,
335
336 else => |e| ip_connect_error = e,
337 }
338 } else |err| switch (err) {
339 error.Canceled => |e| return e,
340 error.Closed => {
341 // There was no successful connection attempt. If there was a lookup error, return that.
342 try connect_many.await(io);
343 // Otherwise, return the error from a failed IP connection attempt.
344 return ip_connect_error orelse
345 return error.UnknownHostName;
346 },
347 }
348}
349
350/// Asynchronously establishes a connection to all IP addresses associated with
351/// a host name, adding them to a results queue upon completion.
352///
353/// `error.Canceled` will never be added to the queue, but other errors may be.
354///
355/// Closes `results` before return, even on error.
356///
357/// Asserts `results` is not closed until this call returns.
358pub fn connectMany(
359 host_name: HostName,
360 io: Io,
361 port: u16,
362 results: *Io.Queue(IpAddress.ConnectError!Stream),
363 options: IpAddress.ConnectOptions,
364) LookupError!void {
365 defer results.close(io);
366
367 var canonical_name_buffer: [max_len]u8 = undefined;
368 var lookup_buffer: [32]HostName.LookupResult = undefined;
369 var lookup_queue: Io.Queue(LookupResult) = .init(&lookup_buffer);
370 var lookup_future = io.async(lookup, .{ host_name, io, &lookup_queue, .{
371 .port = port,
372 .canonical_name_buffer = &canonical_name_buffer,
373 } });
374 defer lookup_future.cancel(io) catch {};
375
376 var group: Io.Group = .init;
377 defer group.cancel(io);
378
379 while (lookup_queue.getOne(io)) |dns_result| switch (dns_result) {
380 .address => |address| group.async(io, enqueueConnection, .{ address, io, results, options }),
381 .canonical_name => continue,
382 } else |err| switch (err) {
383 error.Canceled => |e| return e,
384 error.Closed => {
385 try group.await(io);
386 return lookup_future.await(io);
387 },
388 }
389}
390fn enqueueConnection(
391 address: IpAddress,
392 io: Io,
393 queue: *Io.Queue(IpAddress.ConnectError!Stream),
394 options: IpAddress.ConnectOptions,
395) Io.Cancelable!void {
396 const result = address.connect(io, options) catch |err| switch (err) {
397 error.Canceled => |e| return e,
398 else => |e| e, // other errors go in the result queue
399 };
400 errdefer if (result) |s| s.close(io) else |_| {};
401 queue.putOne(io, result) catch |err| switch (err) {
402 error.Canceled => |e| return e,
403 error.Closed => unreachable, // `queue` must not be closed
404 };
405}
406
407pub const ResolvConf = struct {
408 attempts: u32,
409 ndots: u32,
410 timeout_seconds: u32,
411 nameservers_buffer: [max_nameservers]IpAddress,
412 nameservers_len: usize,
413 search_buffer: [max_len]u8,
414 search_len: usize,
415
416 /// According to resolv.conf(5) there is a maximum of 3 nameservers in this
417 /// file.
418 pub const max_nameservers = 3;
419
420 /// Returns `error.StreamTooLong` if a line is longer than 512 bytes.
421 pub fn init(io: Io) !ResolvConf {
422 var rc: ResolvConf = .{
423 .nameservers_buffer = undefined,
424 .nameservers_len = 0,
425 .search_buffer = undefined,
426 .search_len = 0,
427 .ndots = 1,
428 .timeout_seconds = 5,
429 .attempts = 2,
430 };
431
432 const file = Io.Dir.openFileAbsolute(io, "/etc/resolv.conf", .{}) catch |err| switch (err) {
433 error.FileNotFound,
434 error.NotDir,
435 error.AccessDenied,
436 => {
437 try addNumeric(&rc, io, "127.0.0.1", 53);
438 return rc;
439 },
440
441 else => |e| return e,
442 };
443 defer file.close(io);
444
445 var line_buf: [512]u8 = undefined;
446 var file_reader = file.reader(io, &line_buf);
447 parse(&rc, io, &file_reader.interface) catch |err| switch (err) {
448 error.ReadFailed => return file_reader.err.?,
449 else => |e| return e,
450 };
451 return rc;
452 }
453
454 const Directive = enum { options, nameserver, domain, search };
455 const Option = enum { ndots, attempts, timeout };
456
457 pub fn parse(rc: *ResolvConf, io: Io, reader: *Io.Reader) !void {
458 while (reader.takeSentinel('\n')) |line_with_comment| {
459 const line = line: {
460 var split = std.mem.splitScalar(u8, line_with_comment, '#');
461 break :line split.first();
462 };
463 var line_it = std.mem.tokenizeAny(u8, line, " \t");
464
465 const token = line_it.next() orelse continue;
466 switch (std.meta.stringToEnum(Directive, token) orelse continue) {
467 .options => while (line_it.next()) |sub_tok| {
468 var colon_it = std.mem.splitScalar(u8, sub_tok, ':');
469 const name = colon_it.first();
470 const value_txt = colon_it.next() orelse continue;
471 const value = std.fmt.parseInt(u8, value_txt, 10) catch |err| switch (err) {
472 error.Overflow => 255,
473 error.InvalidCharacter => continue,
474 };
475 switch (std.meta.stringToEnum(Option, name) orelse continue) {
476 .ndots => rc.ndots = @min(value, 15),
477 .attempts => rc.attempts = @min(value, 10),
478 .timeout => rc.timeout_seconds = @min(value, 60),
479 }
480 },
481 .nameserver => {
482 const ip_txt = line_it.next() orelse continue;
483 try addNumeric(rc, io, ip_txt, 53);
484 },
485 .domain, .search => {
486 const rest = line_it.rest();
487 @memcpy(rc.search_buffer[0..rest.len], rest);
488 rc.search_len = rest.len;
489 },
490 }
491 } else |err| switch (err) {
492 error.EndOfStream => if (reader.bufferedLen() != 0) return error.EndOfStream,
493 else => |e| return e,
494 }
495
496 if (rc.nameservers_len == 0) {
497 try addNumeric(rc, io, "127.0.0.1", 53);
498 }
499 }
500
501 fn addNumeric(rc: *ResolvConf, io: Io, name: []const u8, port: u16) !void {
502 if (rc.nameservers_len < rc.nameservers_buffer.len) {
503 rc.nameservers_buffer[rc.nameservers_len] = try .resolve(io, name, port);
504 rc.nameservers_len += 1;
505 }
506 }
507
508 pub fn nameservers(rc: *const ResolvConf) []const IpAddress {
509 return rc.nameservers_buffer[0..rc.nameservers_len];
510 }
511};
512
513test ResolvConf {
514 const input =
515 \\# Generated by resolvconf
516 \\nameserver 1.0.0.1
517 \\nameserver 1.1.1.1
518 \\nameserver fe80::e0e:76ff:fed4:cf22
519 \\options edns0
520 \\
521 ;
522 var reader: Io.Reader = .fixed(input);
523
524 var rc: ResolvConf = .{
525 .nameservers_buffer = undefined,
526 .nameservers_len = 0,
527 .search_buffer = undefined,
528 .search_len = 0,
529 .ndots = 1,
530 .timeout_seconds = 5,
531 .attempts = 2,
532 };
533
534 try rc.parse(std.testing.io, &reader);
535 try std.testing.expectEqual(3, rc.nameservers().len);
536}