//// A collection of functions for resolving DNS names in Gleam programs targeting Erlang. //// //// The functions in this module are implemented using Erlang's built-in //// `inet_res` and `inet` modules. Most of the functions and types are simply //// Gleam representations of their Erlang counterparts. import gleam/list import gleam/function import gleam/result import gleam/dynamic.{type Decoder, type Dynamic} import gleam/erlang/atom.{type Atom} import gleam/erlang/charlist.{type Charlist} /// Nameserver address and port. pub type Nameserver = #(IPAddress, Int) /// A DNS class type. pub type DnsClass { In Chaos Hs Any } /// Options for resolving a DNS name. pub type ResolverOption { Nameservers(opt: List(Nameserver)) INet6(opt: Bool) Recurse(opt: Bool) Retry(opt: Int) TimeoutMillis(opt: Int) NxdomainReply(opt: Bool) } /// A DNS SOA record. pub type SOARecord { SOARecord( mname: String, rname: String, serial: Int, refresh: Int, retry: Int, expire: Int, minimum: Int, ) } /// A DNS error. pub type ErrorReason { Formerr Qfmterr Servfail Nxdomain Notimp Refused Badvers TimeoutErr /// Encapuslates any other error, such as Posix errors. Other(error: String) } /// A DNS MX record. pub type MXRecord { MXRecord(priority: Int, exchange: String) } /// Erlang representation of an IPv4 address. pub type IPV4 = #(Int, Int, Int, Int) /// Erlang representation of an IPv6 address. pub type IPV6 = #(Int, Int, Int, Int, Int, Int, Int, Int) /// A timeout for DNS lookups. pub type Timeout { Timeout(value: Int) Infinity } /// A host entry. /// /// For more information, see the [`inet_res:hostent`](https://www.erlang.org/doc/man/inet#type-hostent) Erlang type. pub type Hostent(addr) { Hostent( name: String, aliases: List(String), addrtype: String, length: Int, addr_list: List(addr), ) } /// A union type for IPv4 and IPv6 addresses. pub type IPAddress { IPV4(IPV4) IPV6(IPV6) } /// DNS record types returning plain string data. pub type StringRecordType { CNAME TXT NS } /// Converts an IP address to a string. /// /// For more detail, see the `inet` module's [ntoa/1 function docs](https://www.erlang.org/doc/man/inet#ntoa-1) pub fn ip_to_string(ip_addr: IPAddress) -> Result(String, String) { ip_addr |> ntoa() |> result.map(charlist.to_string) |> result.map_error(atom.to_string) } /// Converts a string to an IP address. /// /// If the given string is not a valid IP address, this function returns an error. /// /// For more detail, see the `inet` module's [parse_address/1 function docs](https://www.erlang.org/doc/man/inet#parse_address-1). pub fn string_to_ip(ip_addr: String) -> Result(IPAddress, String) { ip_addr |> parse_address() |> result.map_error(atom.to_string) |> result.map(fn(dyn) { dyn |> dynamic.tuple4(dynamic.int, dynamic.int, dynamic.int, dynamic.int) |> result.map(IPV4) |> result.or( dyn |> ip_decoder(), ) |> result.replace_error("decode_error") }) |> result.flatten() } /// Returns a `gleam/dynamic.Decoder` for decoding an IP address tuple. pub fn ip_decoder() -> Decoder(IPAddress) { fn(value) { value |> dynamic.tuple4(dynamic.int, dynamic.int, dynamic.int, dynamic.int) |> result.map(IPV4) |> result.lazy_or(fn() { value |> dynamic.decode8( fn(i1, i2, i3, i4, i5, i6, i7, i8) { #(i1, i2, i3, i4, i5, i6, i7, i8) }, dynamic.element(0, dynamic.int), dynamic.element(1, dynamic.int), dynamic.element(2, dynamic.int), dynamic.element(3, dynamic.int), dynamic.element(4, dynamic.int), dynamic.element(5, dynamic.int), dynamic.element(6, dynamic.int), dynamic.element(7, dynamic.int), ) |> result.map(IPV6) }) } } /// Looks up a record of the specified type for the given name. pub fn lookup( name: String, class: DnsClass, srt: StringRecordType, options: List(ResolverOption), ) -> List(String) { let erl_options = list.map(options, to_erl_resolver_option) name |> do_lookup_string(class, srt, erl_options) |> list.map(charlist.to_string) } /// Looks up an `A` record for the given name. pub fn lookup_ipv4( name: String, class: DnsClass, options: List(ResolverOption), ) -> List(IPV4) { let erl_options = list.map(options, to_erl_resolver_option) do_lookup_ipv4(name, class, erl_options) } /// Looks up an `AAAA` record for the given name. pub fn lookup_ipv6( name: String, class: DnsClass, options: List(ResolverOption), ) -> List(IPV6) { let erl_options = list.map(options, to_erl_resolver_option) do_lookup_ipv6(name, class, erl_options) } /// Looks up a SOA record for the given name. pub fn lookup_soa( name: String, class: DnsClass, options: List(ResolverOption), ) -> List(SOARecord) { let erl_options = list.map(options, to_erl_resolver_option) name |> do_lookup_soa(class, erl_options) |> list.map(to_soa) } pub fn lookup_mx( name: String, class: DnsClass, options: List(ResolverOption), ) -> List(MXRecord) { let erl_options = list.map(options, to_erl_resolver_option) name |> do_lookup_mx(class, erl_options) |> list.map(to_mx) } /// Looks up a DNS record of the given type for the given name. pub fn getbyname( name: String, srt: StringRecordType, timeout: Timeout, ) -> Result(Hostent(String), ErrorReason) { name |> do_getbyname_string(srt, timeout) |> result.map(to_hostent(_, charlist.to_string)) |> result.map_error(to_dns_err) } /// Looks up an `A` record for the given name. pub fn getbyname_ipv4( name: String, timeout: Timeout, ) -> Result(Hostent(IPV4), ErrorReason) { name |> do_getbyname_ipv4(timeout) |> result.map(to_hostent(_, function.identity)) |> result.map_error(to_dns_err) } /// Looks up an `AAAA` record for the given name. pub fn getbyname_ipv6( name: String, timeout: Timeout, ) -> Result(Hostent(IPV6), ErrorReason) { name |> do_getbyname_ipv6(timeout) |> result.map(to_hostent(_, function.identity)) |> result.map_error(to_dns_err) } /// Looks up a SOA record for the given name. pub fn getbyname_soa( name: String, timeout: Timeout, ) -> Result(Hostent(SOARecord), ErrorReason) { name |> do_getbyname_soa(timeout) |> result.map(to_hostent(_, to_soa)) |> result.map_error(to_dns_err) } /// Looks up a DNS MX record for the given name. pub fn getbyname_mx( name: String, timeout: Timeout, ) -> Result(Hostent(MXRecord), ErrorReason) { name |> do_getbyname_mx(timeout) |> result.map(to_hostent(_, to_mx)) |> result.map_error(to_dns_err) } /// Looks up a host by IP address. pub fn gethostbyaddr( address: IPAddress, timeout: Timeout, ) -> Result(Hostent(String), ErrorReason) { address |> do_gethostbyaddr(timeout) |> result.map(to_hostent(_, charlist.to_string)) |> result.map_error(to_dns_err) } fn to_erl_resolver_option(option: ResolverOption) -> ErlOptionTuple { let #(opt_name, opt_value) = case option { INet6(inet6) -> #("inet6", dynamic.from(inet6)) Recurse(recurse) -> #("recurse", dynamic.from(recurse)) Retry(retry) -> #("retry", dynamic.from(retry)) TimeoutMillis(timeout) -> #("timeout", dynamic.from(timeout)) NxdomainReply(nxdomain_reply) -> #( "nxdomain_reply", dynamic.from(nxdomain_reply), ) Nameservers(nameservers) -> { let erl_nameservers = list.map(nameservers, fn(ip_port) { let #(ip, port) = ip_port let ip = case ip { IPV4(ip) -> dynamic.from(ip) IPV6(ip) -> dynamic.from(ip) } #(ip, port) }) #("nameservers", dynamic.from(erl_nameservers)) } } #(atom.create_from_string(opt_name), opt_value) } type ErlOptionTuple = #(Atom, Dynamic) type ErlHostent(r) = #(Atom, Dynamic, List(Dynamic), Atom, Int, List(r)) type ErlSoa = #(Dynamic, Dynamic, Int, Int, Int, Int, Int) type ErlMx = #(Int, Dynamic) fn to_dns_err(e: Atom) -> ErrorReason { let e = atom.to_string(e) case e { "formerr" -> Formerr "qfmterror" -> Qfmterr "servfail" -> Servfail "nxdomain" -> Nxdomain "notimp" -> Notimp "refused" -> Refused "badvers" -> Badvers "timeout" -> TimeoutErr _ -> Other(e) } } fn to_hostent( erl_hostent: ErlHostent(r), addr_list_transform: fn(r) -> s, ) -> Hostent(s) { let #(_, name, aliases, addrtype, length, addr_list) = erl_hostent let gleam_aliases = list.map(aliases, from_dns_name) Hostent( name: from_dns_name(name), aliases: gleam_aliases, addrtype: atom.to_string(addrtype), length: length, addr_list: list.map(addr_list, addr_list_transform), ) } fn from_dns_name(dyn: Dynamic) -> String { let atom_from_dynamic = fn() { dyn |> atom.from_dynamic() |> result.map(atom.to_string) } dyn |> charlist_from_dynamic() |> result.map(charlist.to_string) |> result.lazy_or(atom_from_dynamic) |> result.unwrap("") } fn to_mx(erl_mx: ErlMx) -> MXRecord { let #(priority, exchange) = erl_mx MXRecord(priority: priority, exchange: from_dns_name(exchange)) } fn to_soa(erl_soa: ErlSoa) -> SOARecord { let #(mname, rname, serial, refresh, retry, expire, minimum) = erl_soa SOARecord( mname: from_dns_name(mname), rname: from_dns_name(rname), serial: serial, refresh: refresh, retry: retry, expire: expire, minimum: minimum, ) } @external(erlang, "nessie_inet_res_ffi", "charlist_from_dynamic") fn charlist_from_dynamic(d: Dynamic) -> Result(Charlist, dynamic.DecodeErrors) @external(erlang, "nessie_inet_res_ffi", "getbyname") fn do_getbyname_string( name: String, t: StringRecordType, timeout: Timeout, ) -> Result(ErlHostent(Charlist), Atom) @external(erlang, "nessie_inet_res_ffi", "getbyname_soa") fn do_getbyname_soa( name: String, timeout: Timeout, ) -> Result(ErlHostent(ErlSoa), Atom) @external(erlang, "nessie_inet_res_ffi", "getbyname_ipv4") fn do_getbyname_ipv4( name: String, timeout: Timeout, ) -> Result(ErlHostent(IPV4), Atom) @external(erlang, "nessie_inet_res_ffi", "getbyname_ipv6") fn do_getbyname_ipv6( name: String, timeout: Timeout, ) -> Result(ErlHostent(IPV6), Atom) @external(erlang, "nessie_inet_res_ffi", "getbyname_mx") fn do_getbyname_mx( name: String, timeout: Timeout, ) -> Result(ErlHostent(ErlMx), Atom) @external(erlang, "nessie_inet_res_ffi", "gethostbyaddr") fn do_gethostbyaddr( address: IPAddress, timeout: Timeout, ) -> Result(ErlHostent(Charlist), Atom) @external(erlang, "nessie_inet_res_ffi", "lookup_ipv4") fn do_lookup_ipv4( name: String, class: DnsClass, options: List(ErlOptionTuple), ) -> List(IPV4) @external(erlang, "nessie_inet_res_ffi", "lookup_ipv6") fn do_lookup_ipv6( name: String, class: DnsClass, options: List(ErlOptionTuple), ) -> List(IPV6) @external(erlang, "nessie_inet_res_ffi", "lookup") fn do_lookup_string( name: String, class: DnsClass, t: StringRecordType, options: List(ErlOptionTuple), ) -> List(Charlist) @external(erlang, "nessie_inet_res_ffi", "lookup_soa") fn do_lookup_soa( name: String, class: DnsClass, options: List(ErlOptionTuple), ) -> List(ErlSoa) @external(erlang, "nessie_inet_res_ffi", "lookup_mx") fn do_lookup_mx( name: String, class: DnsClass, options: List(ErlOptionTuple), ) -> List(ErlMx) @external(erlang, "nessie_inet_ffi", "ntoa") fn ntoa(ip: IPAddress) -> Result(Charlist, Atom) @external(erlang, "nessie_inet_ffi", "parse_address") fn parse_address(ip: String) -> Result(Dynamic, Atom)