//// 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/dynamic.{type Dynamic} import gleam/dynamic/decode.{type Decoder} import gleam/erlang/atom.{type Atom} import gleam/erlang/charlist.{type Charlist} import gleam/function import gleam/list import gleam/result /// 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") decode.run(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) { let ipv4_decoder = { use i1 <- decode.field(0, decode.int) use i2 <- decode.field(1, decode.int) use i3 <- decode.field(2, decode.int) use i4 <- decode.field(3, decode.int) decode.success(IPV4(#(i1, i2, i3, i4))) } let ipv6_decoder = { use i1 <- decode.field(0, decode.int) use i2 <- decode.field(1, decode.int) use i3 <- decode.field(2, decode.int) use i4 <- decode.field(3, decode.int) use i5 <- decode.field(4, decode.int) use i6 <- decode.field(5, decode.int) use i7 <- decode.field(6, decode.int) use i8 <- decode.field(7, decode.int) decode.success(IPV6(#(i1, i2, i3, i4, i5, i6, i7, i8))) } decode.one_of(ipv6_decoder, [ipv4_decoder]) } /// 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) { do_getbyname_string(name, 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.bool(inet6)) Recurse(recurse) -> #("recurse", dynamic.bool(recurse)) Retry(retry) -> #("retry", dynamic.int(retry)) TimeoutMillis(timeout) -> #("timeout", dynamic.int(timeout)) NxdomainReply(nxdomain_reply) -> #( "nxdomain_reply", dynamic.bool(nxdomain_reply), ) Nameservers(nameservers) -> { let erl_nameservers = list.map(nameservers, fn(ip_port) { let #(ip, port) = ip_port let ip = { let ints = case ip { IPV4(#(n1, n2, n3, n4)) -> [n1, n2, n3, n4] IPV6(#(n1, n2, n3, n4, n5, n6, n7, n8)) -> [ n1, n2, n3, n4, n5, n6, n7, n8, ] } list.map(ints, dynamic.int) |> dynamic.list } // #(ip, port) dynamic.array([ip, dynamic.int(port)]) }) #("nameservers", dynamic.list(erl_nameservers)) } } #(atom.create(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.cast_from_dynamic() |> atom.to_string |> Ok } 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, List(decode.DecodeError)) @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)