From c66fb357a808fa393ad05115b4a45343bbee33aa Mon Sep 17 00:00:00 2001 From: Robinson Date: Wed, 7 Apr 2021 02:31:13 +0200 Subject: [PATCH] Added hosts file name resolution --- src/dorkbox/netUtil/Dns.kt | 90 +++++---- .../netUtil/hosts/DefaultHostsFileResolver.kt | 63 ++++++ src/dorkbox/netUtil/hosts/HostsFileEntries.kt | 25 +++ src/dorkbox/netUtil/hosts/HostsFileParser.kt | 188 ++++++++++++++++++ .../netUtil/hosts/ResolvedAddressTypes.kt | 42 ++++ 5 files changed, 372 insertions(+), 36 deletions(-) create mode 100644 src/dorkbox/netUtil/hosts/DefaultHostsFileResolver.kt create mode 100644 src/dorkbox/netUtil/hosts/HostsFileEntries.kt create mode 100644 src/dorkbox/netUtil/hosts/HostsFileParser.kt create mode 100644 src/dorkbox/netUtil/hosts/ResolvedAddressTypes.kt diff --git a/src/dorkbox/netUtil/Dns.kt b/src/dorkbox/netUtil/Dns.kt index 50f1433..3ce6ac5 100644 --- a/src/dorkbox/netUtil/Dns.kt +++ b/src/dorkbox/netUtil/Dns.kt @@ -4,6 +4,8 @@ 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 @@ -11,8 +13,6 @@ import java.io.* import java.net.* import java.nio.file.Files import java.nio.file.Paths -import java.security.AccessController -import java.security.PrivilegedAction import java.util.* import javax.naming.Context import javax.naming.NamingException @@ -26,7 +26,8 @@ object Dns { */ const val version = "2.1" - private const val DEFAULT_SEARCH_DOMAIN = "" + 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" @@ -60,26 +61,43 @@ object Dns { } } - /** Returns all located name servers, which may be empty. */ - val defaultNameServers: List by lazy { - val nameServers = getUnsortedDefaultNameServers() + /** + * Resolve the address of a hostname against the entries in a hosts file, depending on some address types. + * + * @param inetHost the hostname to resolve + * @param resolvedAddressTypes the address types to resolve + * + * @return the first matching address or null + */ + fun resolveFromHosts(inetHost: String, resolvedAddressTypes: ResolvedAddressTypes = ResolvedAddressTypes.IPV4_PREFERRED): InetAddress? { + return DefaultHostsFileResolver.address(inetHost, resolvedAddressTypes) + } + + /** Returns all name servers, including the default ones. */ + val nameServers: Map> by lazy { + getUnsortedNameServers() + } + + /** Returns all default name servers. */ + val defaultNameServers: List by lazy { + val defaultServers = nameServers[DEFAULT_SEARCH_DOMAIN]!! if (IPv6.isPreferred) { // prefer IPv6: return IPv6 first, then IPv4 (each in the order added) - nameServers.filter { it.address is Inet6Address } + nameServers.filter { it.address is Inet4Address } + defaultServers.filter { it.address is Inet6Address } + defaultServers.filter { it.address is Inet4Address } } else if (IPv4.isPreferred) { // skip IPv6 addresses - nameServers.filter { it.address is Inet4Address } + defaultServers.filter { it.address is Inet4Address } } // neither is specified, return in the order added - nameServers + defaultServers } // largely from: // https://github.com/dnsjava/dnsjava/blob/fb4889ee7a73f391f43bf6dc78b019d87ae15f15/src/main/java/org/xbill/DNS/config/BaseResolverConfigProvider.java#L22 - private fun getUnsortedDefaultNameServers() : List { - val defaultNameServers = mutableListOf() + private fun getUnsortedNameServers() : Map> { + val nameServerDomains = mutableMapOf>() // Using jndi-dns to obtain the default name servers. // @@ -98,23 +116,25 @@ object Dns { try { val ctx: DirContext = InitialDirContext(env) val dnsUrls = ctx.environment["java.naming.provider.url"] as String? - val servers = dnsUrls!!.split(" ".toRegex()).toTypedArray() + if (dnsUrls != null) { + val servers = dnsUrls.split(" ".toRegex()).toTypedArray() - for (server in servers) { - try { - defaultNameServers.add(Common.socketAddress(URI(server).host, 53)) - } catch (e: URISyntaxException) { - Common.logger.debug("Skipping a malformed nameserver URI: {}", server, e) + for (server in servers) { + try { + if (nameServerDomains[DEFAULT_SEARCH_DOMAIN] == null) { + nameServerDomains[DEFAULT_SEARCH_DOMAIN] = mutableListOf() + } + (nameServerDomains[DEFAULT_SEARCH_DOMAIN] as MutableList).add(Common.socketAddress(server, 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 (defaultNameServers.isNotEmpty()) { - return defaultNameServers - } - if (Common.OS_WINDOWS) { // have to use JNA to access the WINDOWS resolver info @@ -160,7 +180,11 @@ object Dns { try { address = dns.Address.toAddress() if (address is Inet4Address || !address.isSiteLocalAddress) { - defaultNameServers.add(InetSocketAddress(address, 53)) + if (nameServerDomains[DEFAULT_SEARCH_DOMAIN] == null) { + nameServerDomains[DEFAULT_SEARCH_DOMAIN] = mutableListOf() + } + + (nameServerDomains[DEFAULT_SEARCH_DOMAIN] as MutableList).add(Common.socketAddress(address, 53)) } else { Common.logger.debug("Skipped site-local IPv6 server address {} on adapter index {}", address, result.IfIndex) } @@ -186,20 +210,18 @@ object Dns { if (tryParse.first) { // we can have DIFFERENT name servers for DIFFERENT domains! - val defaultSearchDomainNameServers = tryParse.second[DEFAULT_SEARCH_DOMAIN] - if (defaultSearchDomainNameServers != null) { - defaultNameServers.addAll(defaultSearchDomainNameServers) - } + tryParse.second } } - // if we STILL don't have anything, add global nameservers - if (defaultNameServers.isEmpty()) { - defaultNameServers.add(InetSocketAddress("1.1.1.1", 53)) // cloudflare - defaultNameServers.add(InetSocketAddress("8.8.8.8", 53)) // google + // if we STILL don't have anything, add global nameservers to the default search domain + if (nameServerDomains[DEFAULT_SEARCH_DOMAIN] == null) { + nameServerDomains[DEFAULT_SEARCH_DOMAIN] = mutableListOf() + (nameServerDomains[DEFAULT_SEARCH_DOMAIN] as MutableList).add(InetSocketAddress("1.1.1.1", 53)) // cloudflare + (nameServerDomains[DEFAULT_SEARCH_DOMAIN] as MutableList).add(InetSocketAddress("8.8.8.8", 53)) // google } - return defaultNameServers + return nameServerDomains } private fun tryParseResolvConfNameservers(path: String): Pair>> { @@ -246,7 +268,7 @@ object Dns { maybeIP = maybeIP.substring(0, i) } - nameServers.add(socketAddress(maybeIP, port)) + 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) @@ -467,8 +489,4 @@ object Dns { } return -1 } - - private fun socketAddress(hostname: String, port: Int): InetSocketAddress { - return AccessController.doPrivileged(PrivilegedAction { InetSocketAddress(hostname, port) }) - } } diff --git a/src/dorkbox/netUtil/hosts/DefaultHostsFileResolver.kt b/src/dorkbox/netUtil/hosts/DefaultHostsFileResolver.kt new file mode 100644 index 0000000..89d4765 --- /dev/null +++ b/src/dorkbox/netUtil/hosts/DefaultHostsFileResolver.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2015 The Netty Project + * Copyright 2021 dorkbox, llc + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package dorkbox.netUtil.hosts + +import dorkbox.netUtil.Common.OS_WINDOWS +import dorkbox.netUtil.hosts.HostsFileParser.parse +import java.net.InetAddress +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import java.util.* + +/** + * Will resolve host file entries only once, from the `hosts` file. + */ +object DefaultHostsFileResolver { + private val parseEntries: HostsFileEntries by lazy { + if (OS_WINDOWS) { + // Only windows there seems to be no standard for the encoding used for the hosts file, so let us + // try multiple until we either were able to parse it or there is none left and so we return an + // empty instance. + parse( + Charset.defaultCharset(), + StandardCharsets.UTF_16, + StandardCharsets.UTF_8 + ) + } else { + parse(Charset.defaultCharset()) + } + } + + fun address(inetHost: String, resolvedAddressTypes: ResolvedAddressTypes): InetAddress? { + val normalized = normalize(inetHost) + + return when (resolvedAddressTypes) { + ResolvedAddressTypes.IPV4_ONLY -> parseEntries.ipv4Entries[normalized] + ResolvedAddressTypes.IPV6_ONLY -> parseEntries.ipv6Entries[normalized] + ResolvedAddressTypes.IPV4_PREFERRED -> { + parseEntries.ipv4Entries[normalized] ?: parseEntries.ipv6Entries[normalized] + } + ResolvedAddressTypes.IPV6_PREFERRED -> { + parseEntries.ipv6Entries[normalized] ?: parseEntries.ipv4Entries[normalized] + } + } + } + + private fun normalize(inetHost: String): String { + return inetHost.toLowerCase(Locale.ENGLISH) + } +} diff --git a/src/dorkbox/netUtil/hosts/HostsFileEntries.kt b/src/dorkbox/netUtil/hosts/HostsFileEntries.kt new file mode 100644 index 0000000..53e4a19 --- /dev/null +++ b/src/dorkbox/netUtil/hosts/HostsFileEntries.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2015 The Netty Project + * Copyright 2021 dorkbox, llc + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package dorkbox.netUtil.hosts + +import java.net.Inet4Address +import java.net.Inet6Address + +/** + * A container of hosts file entries + */ +data class HostsFileEntries(val ipv4Entries: Map = emptyMap(), val ipv6Entries: Map = emptyMap()) diff --git a/src/dorkbox/netUtil/hosts/HostsFileParser.kt b/src/dorkbox/netUtil/hosts/HostsFileParser.kt new file mode 100644 index 0000000..98b2cdc --- /dev/null +++ b/src/dorkbox/netUtil/hosts/HostsFileParser.kt @@ -0,0 +1,188 @@ +/* + * Copyright 2015 The Netty Project + * Copyright 2021 dorkbox, llc + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package dorkbox.netUtil.hosts + +import dorkbox.netUtil.Common.OS_WINDOWS +import dorkbox.netUtil.Common.logger +import dorkbox.netUtil.IP.toBytes +import java.io.* +import java.net.Inet4Address +import java.net.Inet6Address +import java.net.InetAddress +import java.nio.charset.Charset +import java.util.* +import java.util.regex.Pattern + +/** + * A parser for hosts files. + */ +object HostsFileParser { + private const val WINDOWS_DEFAULT_SYSTEM_ROOT = "C:\\Windows" + private const val WINDOWS_HOSTS_FILE_RELATIVE_PATH = "\\system32\\drivers\\etc\\hosts" + private const val X_PLATFORMS_HOSTS_FILE_PATH = "/etc/hosts" + + private val WHITESPACES = Pattern.compile("[ \t]+") + + private fun locateHostsFile(): File { + var hostsFile: File + + if (OS_WINDOWS) { + hostsFile = File(System.getenv("SystemRoot") + WINDOWS_HOSTS_FILE_RELATIVE_PATH) + + if (!hostsFile.exists()) { + hostsFile = File(WINDOWS_DEFAULT_SYSTEM_ROOT + WINDOWS_HOSTS_FILE_RELATIVE_PATH) + } + } else { + hostsFile = File(X_PLATFORMS_HOSTS_FILE_PATH) + } + + return hostsFile + } + + /** + * Parse hosts file at standard OS location using the systems default [Charset] for decoding. + * + * @return a [HostsFileEntries] + */ + fun parse(): HostsFileEntries { + return parse(Charset.defaultCharset()) + } + + /** + * Parse hosts file at standard OS location using the given [Charset]s one after each other until + * we were able to parse something or none is left. + * + * @param charsets the [Charset]s to try as file encodings when parsing. + * + * @return a [HostsFileEntries] + */ + fun parse(vararg charsets: Charset): HostsFileEntries { + val hostsFile = locateHostsFile() + return try { + parse(hostsFile, *charsets) + } catch (e: IOException) { + logger.warn("Failed to load and parse hosts file at " + hostsFile.path, e) + HostsFileEntries() + } + } + + /** + * Parse a hosts file. + * + * @param file the file to be parsed + * @param charsets the [Charset]s to try as file encodings when parsing. + * + * @return a [HostsFileEntries] + */ + fun parse(file: File, vararg charsets: Charset): HostsFileEntries { + try { + if (file.exists() && file.isFile) { + for (charset in charsets) { + BufferedReader(InputStreamReader(FileInputStream(file), charset)).use { reader -> + val entries = parse(reader) + if (entries != HostsFileEntries()) { + return entries + } + } + } + } + } catch (e: IOException) { + logger.warn("Failed to load and parse hosts file at " + file.path, e) + } + + return HostsFileEntries() + } + + /** + * Parse a reader of hosts file format. + * + * @param reader the file to be parsed + * + * @return a [HostsFileEntries] + */ + fun parse(reader: Reader): HostsFileEntries { + val buff = BufferedReader(reader) + + return try { + val ipv4Entries = mutableMapOf() + val ipv6Entries = mutableMapOf() + + var line: String + while (buff.readLine().also { line = it } != null) { + // remove comment + val commentPosition = line.indexOf('#') + if (commentPosition != -1) { + line = line.substring(0, commentPosition) + } + // skip empty lines + line = line.trim { it <= ' ' } + if (line.isEmpty()) { + continue + } + + // split + val lineParts: MutableList = ArrayList() + for (s in WHITESPACES.split(line)) { + if (s.isNotEmpty()) { + lineParts.add(s) + } + } + + // a valid line should be [IP, hostname, alias*] + if (lineParts.size < 2) { + // skip invalid line + continue + } + + val ipBytes = toBytes(lineParts[0]) + if (ipBytes.isEmpty()) { + // skip invalid IP + continue + } + + // loop over hostname and aliases + for (i in 1 until lineParts.size) { + val hostname = lineParts[i] + val hostnameLower = hostname.toLowerCase(Locale.ENGLISH) + val address = InetAddress.getByAddress(hostname, ipBytes) + if (address is Inet4Address) { + val previous = ipv4Entries.put(hostnameLower, address) + if (previous != null) { + // restore, we want to keep the first entry + ipv4Entries[hostnameLower] = previous + } + } else { + val previous = ipv6Entries.put(hostnameLower, address as Inet6Address) + if (previous != null) { + // restore, we want to keep the first entry + ipv6Entries[hostnameLower] = previous + } + } + } + } + + HostsFileEntries(ipv4Entries, ipv6Entries) + } finally { + try { + buff.close() + } catch (e: IOException) { + logger + .warn("Failed to close a reader", e) + } + } + } +} diff --git a/src/dorkbox/netUtil/hosts/ResolvedAddressTypes.kt b/src/dorkbox/netUtil/hosts/ResolvedAddressTypes.kt new file mode 100644 index 0000000..20f2327 --- /dev/null +++ b/src/dorkbox/netUtil/hosts/ResolvedAddressTypes.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2015 The Netty Project + * Copyright 2021 dorkbox, llc + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package dorkbox.netUtil.hosts + +/** + * Defined resolved address types. + */ +enum class ResolvedAddressTypes { + /** + * Only resolve IPv4 addresses + */ + IPV4_ONLY, + + /** + * Only resolve IPv6 addresses + */ + IPV6_ONLY, + + /** + * Prefer IPv4 addresses over IPv6 ones + */ + IPV4_PREFERRED, + + /** + * Prefer IPv6 addresses over IPv4 ones + */ + IPV6_PREFERRED +}