From 9897a4dc2dee3333c645d562f2b2e9931a92d679 Mon Sep 17 00:00:00 2001 From: Robinson Date: Thu, 8 Apr 2021 00:13:18 +0200 Subject: [PATCH] Moved ResolveConf specific logic to it's own file. --- src/dorkbox/netUtil/Dns.kt | 444 +----------------- .../DefaultHostsFileResolver.kt | 4 +- .../{hosts => dnsUtils}/HostsFileEntries.kt | 2 +- .../{hosts => dnsUtils}/HostsFileParser.kt | 2 +- src/dorkbox/netUtil/dnsUtils/ResolveConf.kt | 437 +++++++++++++++++ .../ResolvedAddressTypes.kt | 2 +- 6 files changed, 456 insertions(+), 435 deletions(-) rename src/dorkbox/netUtil/{hosts => dnsUtils}/DefaultHostsFileResolver.kt (96%) rename src/dorkbox/netUtil/{hosts => dnsUtils}/HostsFileEntries.kt (96%) rename src/dorkbox/netUtil/{hosts => dnsUtils}/HostsFileParser.kt (99%) create mode 100644 src/dorkbox/netUtil/dnsUtils/ResolveConf.kt rename src/dorkbox/netUtil/{hosts => dnsUtils}/ResolvedAddressTypes.kt (96%) diff --git a/src/dorkbox/netUtil/Dns.kt b/src/dorkbox/netUtil/Dns.kt index ef62157..2bc8004 100644 --- a/src/dorkbox/netUtil/Dns.kt +++ b/src/dorkbox/netUtil/Dns.kt @@ -1,23 +1,17 @@ package dorkbox.netUtil -import com.sun.jna.Memory -import com.sun.jna.Pointer.* -import com.sun.jna.platform.win32.WinError import dorkbox.executor.Executor -import dorkbox.netUtil.hosts.DefaultHostsFileResolver -import dorkbox.netUtil.hosts.ResolvedAddressTypes -import dorkbox.netUtil.jna.windows.IPHlpAPI -import dorkbox.netUtil.jna.windows.structs.IP_ADAPTER_ADDRESSES_LH -import dorkbox.netUtil.jna.windows.structs.IP_ADAPTER_DNS_SERVER_ADDRESS_XP -import java.io.* -import java.net.* -import java.nio.file.Files -import java.nio.file.Paths -import java.util.* -import javax.naming.Context -import javax.naming.NamingException -import javax.naming.directory.DirContext -import javax.naming.directory.InitialDirContext +import dorkbox.netUtil.dnsUtils.DefaultHostsFileResolver +import dorkbox.netUtil.dnsUtils.ResolveConf +import dorkbox.netUtil.dnsUtils.ResolvedAddressTypes +import java.io.BufferedWriter +import java.io.File +import java.io.FileWriter +import java.io.IOException +import java.net.Inet4Address +import java.net.Inet6Address +import java.net.InetAddress +import java.net.InetSocketAddress object Dns { @@ -28,10 +22,6 @@ object Dns { const val DEFAULT_SEARCH_DOMAIN = "" - private const val NAMESERVER_ROW_LABEL = "nameserver" - private const val DOMAIN_ROW_LABEL = "domain" - private const val PORT_ROW_LABEL = "port" - /** * @throws IOException if the DNS resolve.conf file cannot be read */ @@ -61,7 +51,6 @@ object Dns { } } - /** * Resolve the address of a hostname against the entries in a hosts file, depending on some address types. * @@ -76,7 +65,7 @@ object Dns { /** Returns all name servers, including the default ones. */ val nameServers: Map> by lazy { - getUnsortedNameServers() + ResolveConf.getUnsortedNameServers() } /** Returns all default name servers. */ @@ -94,212 +83,6 @@ object Dns { } } - // largely from: - // https://github.com/dnsjava/dnsjava/blob/fb4889ee7a73f391f43bf6dc78b019d87ae15f15/src/main/java/org/xbill/DNS/config/BaseResolverConfigProvider.java#L22 - private fun getUnsortedNameServers(): Map> { - val nameServerDomains = mutableMapOf>() - - // Using jndi-dns to obtain the default name servers. - // - // See: - // - http://docs.oracle.com/javase/8/docs/technotes/guides/jndi/jndi-dns.html - // - http://mail.openjdk.java.net/pipermail/net-dev/2017-March/010695.html - - // Using jndi-dns to obtain the default name servers. - // - // See: - // - http://docs.oracle.com/javase/8/docs/technotes/guides/jndi/jndi-dns.html - // - http://mail.openjdk.java.net/pipermail/net-dev/2017-March/010695.html - val env = Hashtable() - env[Context.INITIAL_CONTEXT_FACTORY] = "com.sun.jndi.dns.DnsContextFactory" - env["java.naming.provider.url"] = "dns://" - try { - val ctx: DirContext = InitialDirContext(env) - val dnsUrls = ctx.environment["java.naming.provider.url"] as String? - if (dnsUrls != null) { - val servers = dnsUrls.split(" ".toRegex()).toTypedArray() - - for (server in servers) { - try { - putIfAbsent(nameServerDomains, DEFAULT_SEARCH_DOMAIN, Common.socketAddress(URI(server).host, 53)) - } catch (e: URISyntaxException) { - Common.logger.debug("Skipping a malformed nameserver URI: {}", server, e) - } - } - } - } catch (ignore: NamingException) { - // Will also try JNA/etc if this fails. - } - - if (Common.OS_WINDOWS) { - // have to use JNA to access the WINDOWS resolver info - - // modified from: https://github.com/dnsjava/dnsjava/blob/master/src/main/java/org/xbill/DNS/config/WindowsResolverConfigProvider.java - // The recommended method of calling the GetAdaptersAddresses function is to pre-allocate a 15KB working buffer - var buffer: Memory? = Memory(15 * 1024) - val size: com.sun.jna.ptr.IntByReference = com.sun.jna.ptr.IntByReference(0) - - val flags: Int = IPHlpAPI.GAA_FLAG_SKIP_UNICAST or - IPHlpAPI.GAA_FLAG_SKIP_ANYCAST or - IPHlpAPI.GAA_FLAG_SKIP_MULTICAST or - IPHlpAPI.GAA_FLAG_SKIP_FRIENDLY_NAME - - var error: Int = IPHlpAPI.GetAdaptersAddresses( - IPHlpAPI.AF_UNSPEC, - flags, - NULL, - buffer, - size - ) - - if (error == WinError.ERROR_BUFFER_OVERFLOW) { - buffer = Memory(size.value.toLong()) - error = IPHlpAPI.GetAdaptersAddresses( - IPHlpAPI.AF_UNSPEC, - flags, - NULL, - buffer, - size - ) - } - - if (error == WinError.ERROR_SUCCESS) { - var result: IP_ADAPTER_ADDRESSES_LH? = IP_ADAPTER_ADDRESSES_LH(buffer) - while (result != null) { - - // only interfaces with IfOperStatusUp - if (result.OperStatus == 1) { - var dns: IP_ADAPTER_DNS_SERVER_ADDRESS_XP? = result.FirstDnsServerAddress - while (dns != null) { - - var address: InetAddress - try { - address = dns.Address.toAddress() - if (address is Inet4Address || !address.isSiteLocalAddress) { - - putIfAbsent(nameServerDomains, DEFAULT_SEARCH_DOMAIN, Common.socketAddress(address, 53)) - } else { - Common.logger.debug( - "Skipped site-local IPv6 server address {} on adapter index {}", - address, - result.IfIndex - ) - } - } catch (e: UnknownHostException) { - Common.logger.warn("Invalid nameserver address on adapter index {}", result.IfIndex, e) - } - dns = dns.Next - } - } - - result = result.Next - } - } - } else { - // try resolve.conf - - // first try the default unix config path - var tryParse = tryParseResolvConfNameservers("/etc/resolv.conf") - if (!tryParse.first) { - // then fallback to netware - tryParse = tryParseResolvConfNameservers("sys:/etc/resolv.cfg") - } - - if (tryParse.first) { - // we can have DIFFERENT name servers for DIFFERENT domains! - tryParse.second - } - } - - // if we STILL don't have anything, add global nameservers to the default search domain - if (nameServerDomains[DEFAULT_SEARCH_DOMAIN] == null) { - putIfAbsent(nameServerDomains, DEFAULT_SEARCH_DOMAIN, Common.socketAddress("1.1.1.1", 53)) // cloudflare - putIfAbsent(nameServerDomains, DEFAULT_SEARCH_DOMAIN, Common.socketAddress("1.1.1.1", 53)) // google - } - - return nameServerDomains - } - - private fun tryParseResolvConfNameservers(path: String): Pair>> { - val p = Paths.get(path) - if (Files.exists(p)) { - try { - FileReader(path).use { fr -> - BufferedReader(fr).use { br -> - var nameServers = mutableListOf() - val nameServerDomains = mutableMapOf>() - - var domainName = DEFAULT_SEARCH_DOMAIN - var port = 53 - var line0: String? - loop@ while (br.readLine().also { line0 = it?.trim() } != null) { - val line = line0!! - - if (line.isEmpty()) { - continue@loop - } - - val c = line[0] - if (c == '#' || c == ';') { - continue - } - - if (line.startsWith(NAMESERVER_ROW_LABEL)) { - var i = indexOfNonWhiteSpace(line, NAMESERVER_ROW_LABEL.length) - require(i < 0) { - "error parsing label $NAMESERVER_ROW_LABEL in file $path. value: $line" - } - - var maybeIP = line.substring(i) - // There may be a port appended onto the IP address so we attempt to extract it. - - // There may be a port appended onto the IP address so we attempt to extract it. - if (!IPv4.isValid(maybeIP) && !IPv6.isValid(maybeIP)) { - i = maybeIP.lastIndexOf('.') - require(i + 1 >= maybeIP.length) { - "error parsing label $NAMESERVER_ROW_LABEL in file $path. invalid IP value: $line" - } - - port = maybeIP.substring(i + 1).toInt() - maybeIP = maybeIP.substring(0, i) - } - - nameServers.add(Common.socketAddress(maybeIP, port)) - } else if (line.startsWith(DOMAIN_ROW_LABEL)) { - // nameservers can be SPECIFIC to a search domain - val i = indexOfNonWhiteSpace(line, DOMAIN_ROW_LABEL.length) - require(i >= 0) { - "error parsing label $DOMAIN_ROW_LABEL in file $path value: $line" - } - - // we have a NEW domain! add the PREVIOUS nameServers and start again. - putIfAbsent(nameServerDomains, domainName, nameServers) - - nameServers = mutableListOf() - domainName = line.substring(i) - } else if (line.startsWith(PORT_ROW_LABEL)) { - val i = indexOfNonWhiteSpace(line, PORT_ROW_LABEL.length) - require(i < 0) { - "error parsing label $PORT_ROW_LABEL in file $path value: $line" - } - - port = line.substring(i).toInt() - } - } - - // when done parsing the file, ALWAYS add the nameServer domains (since they have not been added yet) - putIfAbsent(nameServerDomains, domainName, nameServers) - return Pair(true, nameServerDomains) - } - } - } catch (e: IOException) { - Common.logger.error("Error parsing $path", e) - } - } - - return Pair(false, mutableMapOf()) - } - /** * Gets the threshold for the number of dots which must appear in a name before it is considered * absolute. The default is 1. @@ -309,211 +92,12 @@ object Dns { 1 } else { // first try the default unix config path - var tryParse = tryParseResolvConfNDots("/etc/resolv.conf") + var tryParse = ResolveConf.tryParseResolvConfNDots("/etc/resolv.conf") if (!tryParse.first) { // then fallback to netware - tryParse = tryParseResolvConfNDots("sys:/etc/resolv.cfg") + tryParse = ResolveConf.tryParseResolvConfNDots("sys:/etc/resolv.cfg") } tryParse.second } } - - private fun tryParseResolvConfNDots(path: String): Pair { - val p = Paths.get(path) - if (Files.exists(p)) { - try { - Files.newInputStream(p).use { `in` -> - return Pair(true, getNdots(`in`)) - } - } catch (e: IOException) { - // ignore - } - } - return Pair(false, 1) - } - - - // @kotlin.jvm.Throws(IOException::class) -// private fun parseResolvConf(`in`: InputStream) { -// InputStreamReader(`in`).use { isr -> -// BufferedReader(isr).use { br -> -// -// var line: String? -// loop@ while (br.readLine().also { line = it } != null) { -// val st = StringTokenizer(line) -// if (!st.hasMoreTokens()) { -// continue -// } -// when (st.nextToken()) { -// "domain" -> { -// // man resolv.conf: -// // The domain and search keywords are mutually exclusive. If more than one instance of -// // these keywords is present, the last instance wins. -// searchlist.clear() -// if (!st.hasMoreTokens()) { -// continue@loop -// } -// addSearchPath(st.nextToken()) -// } -// "search" -> { -// // man resolv.conf: -// // The domain and search keywords are mutually exclusive. If more than one instance of -// // these keywords is present, the last instance wins. -// searchlist.clear() -// while (st.hasMoreTokens()) { -// addSearchPath(st.nextToken()) -// } -// } -// } -// } -// } -// } -// -// // man resolv.conf: -// // The search keyword of a system's resolv.conf file can be overridden on a per-process basis by -// // setting the environment variable LOCALDOMAIN to a space-separated list of search domains. -// val localdomain = System.getenv("LOCALDOMAIN") -// if (localdomain != null && !localdomain.isEmpty()) { -// searchlist.clear() -// parseSearchPathList(localdomain, " ") -// } -// } - - // @kotlin.jvm.Throws(IOException::class) - private fun getNdots(`in`: InputStream): Int { - var ndots = 1 - - InputStreamReader(`in`).use { isr -> - BufferedReader(isr).use { br -> - var line: String? - loop@ while (br.readLine().also { line = it } != null) { - val st = StringTokenizer(line) - if (!st.hasMoreTokens()) { - continue - } - - when (st.nextToken()) { - "domain" -> { - // man resolv.conf: - // The domain and search keywords are mutually exclusive. If more than one instance of - // these keywords is present, the last instance wins. - if (!st.hasMoreTokens()) { - continue@loop - } - } - "search" -> { - // man resolv.conf: - // The domain and search keywords are mutually exclusive. If more than one instance of - // these keywords is present, the last instance wins. - while (st.hasMoreTokens()) { - val token = st.nextToken() - if (token.startsWith("ndots:")) { - ndots = parseNdots(token.substring(6)) - } - } - } - "options" -> while (st.hasMoreTokens()) { - val token = st.nextToken() - if (token.startsWith("ndots:")) { - ndots = parseNdots(token.substring(6)) - } - } - } - } - } - } - - // man resolv.conf: - // The options keyword of a system's resolv.conf file can be amended on a per-process basis by - // setting the environment variable RES_OPTIONS to a space-separated list of resolver options as - // explained above under options. - val resOptions = System.getenv("RES_OPTIONS") - if (resOptions != null && !resOptions.isEmpty()) { - val st = StringTokenizer(resOptions, " ") - while (st.hasMoreTokens()) { - val token = st.nextToken() - if (token.startsWith("ndots:")) { - ndots = parseNdots(token.substring(6)) - } - } - } - - return ndots - } - - private fun parseNdots(token: String): Int { - if (token.isNotEmpty()) { - try { - var ndots = token.toInt() - if (ndots >= 0) { - if (ndots > 15) { - // man resolv.conf: - // The value for this option is silently capped to 15 - ndots = 15 - } - return ndots - } - } catch (e: NumberFormatException) { - // ignore - } - } - - return 1 - } - - /** - * Find the index of the first non-white space character in `s` starting at `offset`. - * - * @param seq The string to search. - * @param offset The offset to start searching at. - * @return the index of the first non-white space character or <`-1` if none was found. - */ - private fun indexOfNonWhiteSpace(seq: CharSequence, offset: Int): Int { - var o = offset - while (o < seq.length) { - if (!Character.isWhitespace(seq[o])) { - return o - } - ++o - } - return -1 - } - - private fun putIfAbsent( - nameServerDomains: MutableMap>, - domainName: String, - nameServer: InetSocketAddress - ) { - var list = nameServerDomains[domainName] - if (list == null) { - list = mutableListOf() - nameServerDomains[domainName] = list - } - - (list as MutableList) - if (!list.contains(nameServer)) { - list.add(nameServer) - } - } - - private fun putIfAbsent( - nameServerDomains: MutableMap>, - domainName: String, - nameServers: List - ) { - var list = nameServerDomains[domainName] - if (list == null) { - list = mutableListOf() - nameServerDomains[domainName] = list - } - - (list as MutableList) - nameServers.forEach { - if (!list.contains(it)) { - list.add(it) - } - } - - nameServers - } } diff --git a/src/dorkbox/netUtil/hosts/DefaultHostsFileResolver.kt b/src/dorkbox/netUtil/dnsUtils/DefaultHostsFileResolver.kt similarity index 96% rename from src/dorkbox/netUtil/hosts/DefaultHostsFileResolver.kt rename to src/dorkbox/netUtil/dnsUtils/DefaultHostsFileResolver.kt index 89d4765..1a952a3 100644 --- a/src/dorkbox/netUtil/hosts/DefaultHostsFileResolver.kt +++ b/src/dorkbox/netUtil/dnsUtils/DefaultHostsFileResolver.kt @@ -14,10 +14,10 @@ * License for the specific language governing permissions and limitations * under the License. */ -package dorkbox.netUtil.hosts +package dorkbox.netUtil.dnsUtils import dorkbox.netUtil.Common.OS_WINDOWS -import dorkbox.netUtil.hosts.HostsFileParser.parse +import dorkbox.netUtil.dnsUtils.HostsFileParser.parse import java.net.InetAddress import java.nio.charset.Charset import java.nio.charset.StandardCharsets diff --git a/src/dorkbox/netUtil/hosts/HostsFileEntries.kt b/src/dorkbox/netUtil/dnsUtils/HostsFileEntries.kt similarity index 96% rename from src/dorkbox/netUtil/hosts/HostsFileEntries.kt rename to src/dorkbox/netUtil/dnsUtils/HostsFileEntries.kt index 53e4a19..c3b1307 100644 --- a/src/dorkbox/netUtil/hosts/HostsFileEntries.kt +++ b/src/dorkbox/netUtil/dnsUtils/HostsFileEntries.kt @@ -14,7 +14,7 @@ * License for the specific language governing permissions and limitations * under the License. */ -package dorkbox.netUtil.hosts +package dorkbox.netUtil.dnsUtils import java.net.Inet4Address import java.net.Inet6Address diff --git a/src/dorkbox/netUtil/hosts/HostsFileParser.kt b/src/dorkbox/netUtil/dnsUtils/HostsFileParser.kt similarity index 99% rename from src/dorkbox/netUtil/hosts/HostsFileParser.kt rename to src/dorkbox/netUtil/dnsUtils/HostsFileParser.kt index 98b2cdc..d102486 100644 --- a/src/dorkbox/netUtil/hosts/HostsFileParser.kt +++ b/src/dorkbox/netUtil/dnsUtils/HostsFileParser.kt @@ -14,7 +14,7 @@ * License for the specific language governing permissions and limitations * under the License. */ -package dorkbox.netUtil.hosts +package dorkbox.netUtil.dnsUtils import dorkbox.netUtil.Common.OS_WINDOWS import dorkbox.netUtil.Common.logger diff --git a/src/dorkbox/netUtil/dnsUtils/ResolveConf.kt b/src/dorkbox/netUtil/dnsUtils/ResolveConf.kt new file mode 100644 index 0000000..3f1ddf8 --- /dev/null +++ b/src/dorkbox/netUtil/dnsUtils/ResolveConf.kt @@ -0,0 +1,437 @@ +package dorkbox.netUtil.dnsUtils + +import com.sun.jna.Memory +import com.sun.jna.Pointer +import com.sun.jna.platform.win32.WinError +import dorkbox.netUtil.Common +import dorkbox.netUtil.Dns +import dorkbox.netUtil.IPv4 +import dorkbox.netUtil.IPv6 +import dorkbox.netUtil.jna.windows.IPHlpAPI +import dorkbox.netUtil.jna.windows.structs.IP_ADAPTER_ADDRESSES_LH +import dorkbox.netUtil.jna.windows.structs.IP_ADAPTER_DNS_SERVER_ADDRESS_XP +import java.io.* +import java.net.* +import java.nio.file.Files +import java.nio.file.Paths +import java.util.* +import javax.naming.Context +import javax.naming.NamingException +import javax.naming.directory.DirContext +import javax.naming.directory.InitialDirContext + +/** + * + */ +object ResolveConf { + private const val NAMESERVER_ROW_LABEL = "nameserver" + private const val DOMAIN_ROW_LABEL = "domain" + private const val PORT_ROW_LABEL = "port" + + + // largely from: + // https://github.com/dnsjava/dnsjava/blob/fb4889ee7a73f391f43bf6dc78b019d87ae15f15/src/main/java/org/xbill/DNS/config/BaseResolverConfigProvider.java#L22 + internal fun getUnsortedNameServers(): Map> { + val nameServerDomains = mutableMapOf>() + + // Using jndi-dns to obtain the default name servers. + // + // See: + // - http://docs.oracle.com/javase/8/docs/technotes/guides/jndi/jndi-dns.html + // - http://mail.openjdk.java.net/pipermail/net-dev/2017-March/010695.html + + // Using jndi-dns to obtain the default name servers. + // + // See: + // - http://docs.oracle.com/javase/8/docs/technotes/guides/jndi/jndi-dns.html + // - http://mail.openjdk.java.net/pipermail/net-dev/2017-March/010695.html + val env = Hashtable() + env[Context.INITIAL_CONTEXT_FACTORY] = "com.sun.jndi.dns.DnsContextFactory" + env["java.naming.provider.url"] = "dns://" + try { + val ctx: DirContext = InitialDirContext(env) + val dnsUrls = ctx.environment["java.naming.provider.url"] as String? + if (dnsUrls != null) { + val servers = dnsUrls.split(" ".toRegex()).toTypedArray() + + for (server in servers) { + try { + putIfAbsent(nameServerDomains, Dns.DEFAULT_SEARCH_DOMAIN, Common.socketAddress(URI(server).host, 53)) + } catch (e: URISyntaxException) { + Common.logger.debug("Skipping a malformed nameserver URI: {}", server, e) + } + } + } + } catch (ignore: NamingException) { + // Will also try JNA/etc if this fails. + } + + if (Common.OS_WINDOWS) { + // have to use JNA to access the WINDOWS resolver info + + // modified from: https://github.com/dnsjava/dnsjava/blob/master/src/main/java/org/xbill/DNS/config/WindowsResolverConfigProvider.java + // The recommended method of calling the GetAdaptersAddresses function is to pre-allocate a 15KB working buffer + var buffer: Memory? = Memory(15 * 1024) + val size: com.sun.jna.ptr.IntByReference = com.sun.jna.ptr.IntByReference(0) + + val flags: Int = IPHlpAPI.GAA_FLAG_SKIP_UNICAST or + IPHlpAPI.GAA_FLAG_SKIP_ANYCAST or + IPHlpAPI.GAA_FLAG_SKIP_MULTICAST or + IPHlpAPI.GAA_FLAG_SKIP_FRIENDLY_NAME + + var error: Int = IPHlpAPI.GetAdaptersAddresses( + IPHlpAPI.AF_UNSPEC, + flags, + Pointer.NULL, + buffer, + size + ) + + if (error == WinError.ERROR_BUFFER_OVERFLOW) { + buffer = Memory(size.value.toLong()) + error = IPHlpAPI.GetAdaptersAddresses( + IPHlpAPI.AF_UNSPEC, + flags, + Pointer.NULL, + buffer, + size + ) + } + + if (error == WinError.ERROR_SUCCESS) { + var result: IP_ADAPTER_ADDRESSES_LH? = IP_ADAPTER_ADDRESSES_LH(buffer) + while (result != null) { + + // only interfaces with IfOperStatusUp + if (result.OperStatus == 1) { + var dns: IP_ADAPTER_DNS_SERVER_ADDRESS_XP? = result.FirstDnsServerAddress + while (dns != null) { + + var address: InetAddress + try { + address = dns.Address.toAddress() + if (address is Inet4Address || !address.isSiteLocalAddress) { + + putIfAbsent(nameServerDomains, Dns.DEFAULT_SEARCH_DOMAIN, Common.socketAddress(address, 53)) + } else { + Common.logger.debug( + "Skipped site-local IPv6 server address {} on adapter index {}", + address, + result.IfIndex + ) + } + } catch (e: UnknownHostException) { + Common.logger.warn("Invalid nameserver address on adapter index {}", result.IfIndex, e) + } + dns = dns.Next + } + } + + result = result.Next + } + } + } else { + // try resolve.conf + + // first try the default unix config path + var tryParse = tryParseResolvConfNameservers("/etc/resolv.conf") + if (!tryParse.first) { + // then fallback to netware + tryParse = tryParseResolvConfNameservers("sys:/etc/resolv.cfg") + } + + if (tryParse.first) { + // we can have DIFFERENT name servers for DIFFERENT domains! + tryParse.second + } + } + + // if we STILL don't have anything, add global nameservers to the default search domain + if (nameServerDomains[Dns.DEFAULT_SEARCH_DOMAIN] == null) { + putIfAbsent(nameServerDomains, Dns.DEFAULT_SEARCH_DOMAIN, Common.socketAddress("1.1.1.1", 53)) // cloudflare + putIfAbsent(nameServerDomains, Dns.DEFAULT_SEARCH_DOMAIN, Common.socketAddress("1.1.1.1", 53)) // google + } + + return nameServerDomains + } + + private fun tryParseResolvConfNameservers(path: String): Pair>> { + val p = Paths.get(path) + if (Files.exists(p)) { + try { + FileReader(path).use { fr -> + BufferedReader(fr).use { br -> + var nameServers = mutableListOf() + val nameServerDomains = mutableMapOf>() + + var domainName = Dns.DEFAULT_SEARCH_DOMAIN + var port = 53 + var line0: String? + loop@ while (br.readLine().also { line0 = it?.trim() } != null) { + val line = line0!! + + if (line.isEmpty()) { + continue@loop + } + + val c = line[0] + if (c == '#' || c == ';') { + continue + } + + if (line.startsWith(NAMESERVER_ROW_LABEL)) { + var i = indexOfNonWhiteSpace(line, NAMESERVER_ROW_LABEL.length) + require(i < 0) { + "error parsing label ${NAMESERVER_ROW_LABEL} in file $path. value: $line" + } + + var maybeIP = line.substring(i) + // There may be a port appended onto the IP address so we attempt to extract it. + + // There may be a port appended onto the IP address so we attempt to extract it. + if (!IPv4.isValid(maybeIP) && !IPv6.isValid(maybeIP)) { + i = maybeIP.lastIndexOf('.') + require(i + 1 >= maybeIP.length) { + "error parsing label ${NAMESERVER_ROW_LABEL} in file $path. invalid IP value: $line" + } + + port = maybeIP.substring(i + 1).toInt() + maybeIP = maybeIP.substring(0, i) + } + + nameServers.add(Common.socketAddress(maybeIP, port)) + } else if (line.startsWith(DOMAIN_ROW_LABEL)) { + // nameservers can be SPECIFIC to a search domain + val i = indexOfNonWhiteSpace(line, DOMAIN_ROW_LABEL.length) + require(i >= 0) { + "error parsing label ${DOMAIN_ROW_LABEL} in file $path value: $line" + } + + // we have a NEW domain! add the PREVIOUS nameServers and start again. + putIfAbsent(nameServerDomains, domainName, nameServers) + + nameServers = mutableListOf() + domainName = line.substring(i) + } else if (line.startsWith(PORT_ROW_LABEL)) { + val i = indexOfNonWhiteSpace(line, PORT_ROW_LABEL.length) + require(i < 0) { + "error parsing label ${PORT_ROW_LABEL} in file $path value: $line" + } + + port = line.substring(i).toInt() + } + } + + // when done parsing the file, ALWAYS add the nameServer domains (since they have not been added yet) + putIfAbsent(nameServerDomains, domainName, nameServers) + return Pair(true, nameServerDomains) + } + } + } catch (e: IOException) { + Common.logger.error("Error parsing $path", e) + } + } + + return Pair(false, mutableMapOf()) + } + + /** + * Find the index of the first non-white space character in `s` starting at `offset`. + * + * @param seq The string to search. + * @param offset The offset to start searching at. + * @return the index of the first non-white space character or <`-1` if none was found. + */ + private fun indexOfNonWhiteSpace(seq: CharSequence, offset: Int): Int { + var o = offset + while (o < seq.length) { + if (!Character.isWhitespace(seq[o])) { + return o + } + ++o + } + return -1 + } + + private fun putIfAbsent( + nameServerDomains: MutableMap>, + domainName: String, + nameServer: InetSocketAddress + ) { + var list = nameServerDomains[domainName] + if (list == null) { + list = mutableListOf() + nameServerDomains[domainName] = list + } + + (list as MutableList) + if (!list.contains(nameServer)) { + list.add(nameServer) + } + } + + private fun putIfAbsent( + nameServerDomains: MutableMap>, + domainName: String, + nameServers: List + ) { + var list = nameServerDomains[domainName] + if (list == null) { + list = mutableListOf() + nameServerDomains[domainName] = list + } + + (list as MutableList) + nameServers.forEach { + if (!list.contains(it)) { + list.add(it) + } + } + + nameServers + } + + + + internal fun tryParseResolvConfNDots(path: String): Pair { + val p = Paths.get(path) + if (Files.exists(p)) { + try { + Files.newInputStream(p).use { `in` -> + return Pair(true, getNdots(`in`)) + } + } catch (e: IOException) { + // ignore + } + } + return Pair(false, 1) + } + + + // @kotlin.jvm.Throws(IOException::class) +// private fun parseResolvConf(`in`: InputStream) { +// InputStreamReader(`in`).use { isr -> +// BufferedReader(isr).use { br -> +// +// var line: String? +// loop@ while (br.readLine().also { line = it } != null) { +// val st = StringTokenizer(line) +// if (!st.hasMoreTokens()) { +// continue +// } +// when (st.nextToken()) { +// "domain" -> { +// // man resolv.conf: +// // The domain and search keywords are mutually exclusive. If more than one instance of +// // these keywords is present, the last instance wins. +// searchlist.clear() +// if (!st.hasMoreTokens()) { +// continue@loop +// } +// addSearchPath(st.nextToken()) +// } +// "search" -> { +// // man resolv.conf: +// // The domain and search keywords are mutually exclusive. If more than one instance of +// // these keywords is present, the last instance wins. +// searchlist.clear() +// while (st.hasMoreTokens()) { +// addSearchPath(st.nextToken()) +// } +// } +// } +// } +// } +// } +// +// // man resolv.conf: +// // The search keyword of a system's resolv.conf file can be overridden on a per-process basis by +// // setting the environment variable LOCALDOMAIN to a space-separated list of search domains. +// val localdomain = System.getenv("LOCALDOMAIN") +// if (localdomain != null && !localdomain.isEmpty()) { +// searchlist.clear() +// parseSearchPathList(localdomain, " ") +// } +// } + + private fun getNdots(`in`: InputStream): Int { + var ndots = 1 + + InputStreamReader(`in`).use { isr -> + BufferedReader(isr).use { br -> + var line: String? + loop@ while (br.readLine().also { line = it } != null) { + val st = StringTokenizer(line) + if (!st.hasMoreTokens()) { + continue + } + + when (st.nextToken()) { + "domain" -> { + // man resolv.conf: + // The domain and search keywords are mutually exclusive. If more than one instance of + // these keywords is present, the last instance wins. + if (!st.hasMoreTokens()) { + continue@loop + } + } + "search" -> { + // man resolv.conf: + // The domain and search keywords are mutually exclusive. If more than one instance of + // these keywords is present, the last instance wins. + while (st.hasMoreTokens()) { + val token = st.nextToken() + if (token.startsWith("ndots:")) { + ndots = parseNdots(token.substring(6)) + } + } + } + "options" -> while (st.hasMoreTokens()) { + val token = st.nextToken() + if (token.startsWith("ndots:")) { + ndots = parseNdots(token.substring(6)) + } + } + } + } + } + } + + // man resolv.conf: + // The options keyword of a system's resolv.conf file can be amended on a per-process basis by + // setting the environment variable RES_OPTIONS to a space-separated list of resolver options as + // explained above under options. + val resOptions = System.getenv("RES_OPTIONS") + if (resOptions != null && !resOptions.isEmpty()) { + val st = StringTokenizer(resOptions, " ") + while (st.hasMoreTokens()) { + val token = st.nextToken() + if (token.startsWith("ndots:")) { + ndots = parseNdots(token.substring(6)) + } + } + } + + return ndots + } + + private fun parseNdots(token: String): Int { + if (token.isNotEmpty()) { + try { + var ndots = token.toInt() + if (ndots >= 0) { + if (ndots > 15) { + // man resolv.conf: + // The value for this option is silently capped to 15 + ndots = 15 + } + return ndots + } + } catch (e: NumberFormatException) { + // ignore + } + } + + return 1 + } +} diff --git a/src/dorkbox/netUtil/hosts/ResolvedAddressTypes.kt b/src/dorkbox/netUtil/dnsUtils/ResolvedAddressTypes.kt similarity index 96% rename from src/dorkbox/netUtil/hosts/ResolvedAddressTypes.kt rename to src/dorkbox/netUtil/dnsUtils/ResolvedAddressTypes.kt index 20f2327..b49d975 100644 --- a/src/dorkbox/netUtil/hosts/ResolvedAddressTypes.kt +++ b/src/dorkbox/netUtil/dnsUtils/ResolvedAddressTypes.kt @@ -14,7 +14,7 @@ * License for the specific language governing permissions and limitations * under the License. */ -package dorkbox.netUtil.hosts +package dorkbox.netUtil.dnsUtils /** * Defined resolved address types.