hare

[hare] The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

commit 0deaf9b34e69a5b774bc390103d5e31ce8c207b8
parent ba1f76e8a971de8879a59dac59f40488eba16abc
Author: Drew DeVault <sir@cmpwn.com>
Date:   Mon,  3 Jun 2024 12:04:49 +0200

unix::resolvconf: rewrite and expand parser

Signed-off-by: Drew DeVault <sir@cmpwn.com>

Diffstat:
Mnet/dial/ip.ha | 2++
Mnet/dns/query.ha | 2+-
Munix/resolvconf/README | 12++++++++----
Aunix/resolvconf/errors.ha | 26++++++++++++++++++++++++++
Munix/resolvconf/load.ha | 94++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Aunix/resolvconf/reader.ha | 180+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aunix/resolvconf/types.ha | 47+++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 316 insertions(+), 47 deletions(-)

diff --git a/net/dial/ip.ha b/net/dial/ip.ha @@ -9,6 +9,7 @@ use net::udp; fn dial_tcp(addr: str, service: str) (net::socket | error) = { const result = resolve("tcp", addr, service)?; const addrs = result.0, port = result.1; + defer free(addrs); for (let i = 0z; i < len(addrs); i += 1) { const addr = addrs[i]; match (tcp::connect(addr, port)) { @@ -26,6 +27,7 @@ fn dial_tcp(addr: str, service: str) (net::socket | error) = { fn dial_udp(addr: str, service: str) (net::socket | error) = { const result = resolve("udp", addr, service)?; const addrs = result.0, port = result.1; + defer free(addrs); for (let i = 0z; i < len(addrs); i += 1) { const addr = addrs[i]; match (udp::connect(addr, port)) { diff --git a/net/dns/query.ha b/net/dns/query.ha @@ -22,7 +22,7 @@ def timeout: time::duration = 3 * time::SECOND; // If no DNS servers are provided, the system default servers (if any) are used. export fn query(query: *message, servers: ip::addr...) (*message | error) = { if (len(servers) == 0) { - servers = resolvconf::load(); + servers = resolvconf::load().nameservers; }; if (len(servers) == 0) { // Fall back to localhost diff --git a/unix/resolvconf/README b/unix/resolvconf/README @@ -1,4 +1,8 @@ -unix::resolvconf provides a (basic) reader for the resolv.conf file found on -many Unix-like operating systems. This implementation only supports the -"nameserver" directive, and it will return a list of IP addresses to use as -nameservers. +[[unix::resolvconf]] implements a parser for /etc/resolv.conf files which has +feature parity with the resolv.conf format supported by glibc 2.36. However, +most options are not supported by Hare internally, i.e. via [[net::dns]]. + +The user may parse a resolv.conf file manually via the [[read]] and [[next]] +functions. Additionally, this module maintains a global copy of the local +resolv.conf file, parsed once at runtime and cached for the lifetime of the +process, which is available via [[load]]. diff --git a/unix/resolvconf/errors.ha b/unix/resolvconf/errors.ha @@ -0,0 +1,26 @@ +use errors; +use encoding::utf8; +use io; +use net::ip; + +// The resolv.conf file is not well-formatted. +export type invalid = !void; + +// Any error which can be raised by the resolv.conf parser. +export type error = !(errors::error | io::error | utf8::invalid | ip::invalid | invalid); + +// Converts an [[error]] into a human-friendly representation. +export fn strerror(err: error) const str = { + match (err) { + case invalid => + return "resolv.conf is not well-formatted"; + case let err: errors::error => + return errors::strerror(err); + case let err: io::error => + return io::strerror(err); + case let err: ip::invalid => + return "Invalid IP address in /etc/resolv.conf"; + case utf8::invalid => + return "resolv.conf contains invalid UTF-8 data"; + }; +}; diff --git a/unix/resolvconf/load.ha b/unix/resolvconf/load.ha @@ -2,63 +2,73 @@ // (c) Hare authors <https://harelang.org> use bufio; +use fmt; use io; use memio; use net::ip; use os; use strings; -let cache: []ip::addr = []; +let cache_valid = false; +let cache: config = config { + options = DEFAULT_OPTIONS, + ... +}; @fini fn fini() void = { - free(cache); + if (!cache_valid) { + return; + }; + + strings::freeall(cache.search); + free(cache.nameservers); + free(cache.sortlist); }; -// Reads a list of nameservers from resolv.conf. Aborts the program if the file -// does not exist, is written in an invalid format, or if any other error -// occurs. -export fn load() []ip::addr = { - // XXX: Would be cool if we could do this without allocating anything - if (len(cache) != 0) { - return cache; +// Reads /etc/resolv.conf (or the platform-specific equivalent path) and returns +// the configuration therein. If the file does not exist, or is poorly +// formatted, returns the default resolver configuration. +export fn load() *config = { + if (cache_valid) { + return &cache; }; - const file = os::open(PATH)!; + const file = match (os::open("/etc/resolv.conf")) { + case let file: io::file => + yield file; + case => + cache_valid = true; + return &cache; + }; defer io::close(file)!; - for (true) { - const line = match (bufio::read_line(file)) { - case io::EOF => - break; - case let line: []u8 => - yield line; - }; - defer free(line); - if (len(line) == 0 || line[0] == '#') { - continue; - }; - - const scanner = memio::fixed(line); - const tok = match (bufio::read_tok(&scanner, ' ', '\t')!) { - case io::EOF => - break; - case let tok: []u8 => - yield tok; - }; - defer free(tok); - if (strings::fromutf8(tok)! != "nameserver") { - continue; - }; + match (parse(&cache, file)) { + case let err: error => + fmt::errorfln("Error parsing /etc/resolv.conf: {}", + strerror(err)): void; + return &cache; + case void => + cache_valid = true; + return &cache; + }; +}; - const tok = match (bufio::read_tok(&scanner, ' ')!) { - case io::EOF => - break; - case let tok: []u8 => - yield tok; +// Parses a resolv.conf-formatted file and populates the given config object. +fn parse(conf: *config, in: io::handle) (void | error) = { + const rd = read(in); + for (const param => next(&rd)!) { + switch (param.name) { + case "nameserver" => + append(conf.nameservers, param.value as ip::addr); + case "search" => + strings::freeall(conf.search); + conf.search = strings::dupall(param.value as []str); + case "sortlist" => + free(conf.sortlist); + conf.sortlist = alloc((param.value as []ip::subnet)...); + case "options" => + conf.options = *(param.value as *options); + case => void; }; - defer free(tok); - append(cache, ip::parse(strings::fromutf8(tok)!)!); }; - - return cache; }; diff --git a/unix/resolvconf/reader.ha b/unix/resolvconf/reader.ha @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: MPL-2.0 +// (c) Hare authors <https://harelang.org> + +use bufio; +use io; +use net::ip; +use strconv; +use strings; + +export type reader = struct { + scan: bufio::scanner, + + // Only one of these is valid at a time (return values from [[next]] are + // borrowed from this). + union { + addr_list: []ip::addr, + subnet_list: []ip::subnet, + str_list: []str, + }, + + options: options, +}; + +// Reads an /etc/resolv.conf-formatted file from the provided I/O handle. Use +// [[next]] to enumerate directives from the file and pass the return value to +// [[finish]] to free resources associated with the reader. +export fn read(in: io::handle) reader = { + return reader { + scan = bufio::newscanner(in), + ... + }; +}; + +// Frees resources associated with a [[reader]]. +export fn finish(rd: *reader) void = { + bufio::finish(&rd.scan); + free(rd.addr_list); +}; + +// Reads the next [[parameter]] from a resolv.conf [[reader]]. The return value +// is borrowed from the [[reader]]. +export fn next(rd: *reader) (parameter | io::EOF | error) = { + for (const line => bufio::scan_line(&rd.scan)?) { + if (strings::hasprefix(line, '#') || strings::hasprefix(line, ';')) { + continue; + }; + if (len(line) == 0) { + continue; + }; + + const tok = strings::tokenize(line, " \t"); + + const name = match (strings::next_token(&tok)) { + case let name: str => + yield name; + case done => + continue; + }; + + const val = switch (name) { + case "nameserver" => + yield parse_addr(rd, &tok)?; + case "search" => + yield parse_str_list(rd, &tok)?; + case "sortlist" => + yield parse_subnet_list(rd, &tok)?; + case "options" => + yield parse_options(rd, &tok)?; + case => + continue; + }; + + return parameter { + name = name, + value = val, + }; + }; + + return io::EOF; +}; + +fn parse_addr(rd: *reader, tok: *strings::tokenizer) (value | error) = { + const addr = match (strings::next_token(tok)) { + case let addr: str => + yield addr; + case done => + return invalid; + }; + + return ip::parse(addr)?; +}; + +fn parse_subnet_list(rd: *reader, tok: *strings::tokenizer) (value | error) = { + rd.subnet_list = rd.subnet_list[..0]; + + for (const tok => strings::next_token(tok)) { + if (len(tok) == 0) { + continue; + }; + + const subnet = ip::parsecidr(tok)?; + append(rd.subnet_list, subnet); + }; + + return rd.subnet_list; +}; + +fn parse_str_list(rd: *reader, tok: *strings::tokenizer) (value | error) = { + rd.str_list = rd.str_list[..0]; + + for (const tok => strings::next_token(tok)) { + if (len(tok) == 0) { + continue; + }; + append(rd.str_list, tok); + }; + + return rd.str_list; +}; + +fn parse_options(rd: *reader, tok: *strings::tokenizer) (value | error) = { + rd.options = DEFAULT_OPTIONS; + let opts = &rd.options; + + for (const tok => strings::next_token(tok)) { + if (len(tok) == 0) { + continue; + }; + + const (name, val) = strings::cut(tok, ":"); + switch (name) { + case "debug" => + opts.debug = true; + case "ndots" => + match (strconv::stou(val)) { + case let u: uint => + opts.ndots = u; + case => + return invalid; + }; + case "timeout" => + match (strconv::stou(val)) { + case let u: uint => + opts.timeout = u; + case => + return invalid; + }; + case "attempts" => + match (strconv::stou(val)) { + case let u: uint => + opts.attempts = u; + case => + return invalid; + }; + case "rotate" => + opts.rotate = true; + case "no-aaaa" => + opts.no_aaaa = true; + case "no-check-names" => + opts.no_check_names = true; + case "inet6" => + opts.inet6 = true; + case "edns0" => + opts.edns0 = true; + case "single-request" => + opts.single_request = true; + case "no-tld-query" => + opts.no_tld_query = true; + case "use-vc" => + opts.use_vc = true; + case "no-reload" => + opts.no_reload = true; + case "trust-ad" => + opts.trust_ad = true; + case => void; + }; + }; + + return opts; +}; diff --git a/unix/resolvconf/types.ha b/unix/resolvconf/types.ha @@ -0,0 +1,47 @@ +use net::ip; + +// A list of [[net::ip::subnet]]s. +export type subnet_list = []ip::subnet; + +// Values set in an "options" directive. +export type options = struct { + debug: bool, + ndots: uint, + timeout: uint, + attempts: uint, + rotate: bool, + no_aaaa: bool, + no_check_names: bool, + inet6: bool, + edns0: bool, + single_request: bool, + single_request_reopen: bool, + no_tld_query: bool, + use_vc: bool, + no_reload: bool, + trust_ad: bool, +}; + +def DEFAULT_OPTIONS = options { + ndots = 1, + timeout = 5, + attempts = 2, + ... +}; + +// The value associated with a configuration parameter. +export type value = (ip::addr | subnet_list | *options | []str); + +// A configuration parameter from resolv.conf. +export type parameter = struct { + name: const str, + value: value, +}; + +// A complete configuration parsed from resolv.conf. +export type config = struct { + nameservers: []ip::addr, + search: []str, + sortlist: []ip::subnet, + options: options, +};