Added hosts file name resolution
This commit is contained in:
parent
f097cc195b
commit
c66fb357a8
|
@ -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<InetSocketAddress> 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<String, List<InetSocketAddress>> by lazy {
|
||||
getUnsortedNameServers()
|
||||
}
|
||||
|
||||
/** Returns all default name servers. */
|
||||
val defaultNameServers: List<InetSocketAddress> 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<InetSocketAddress> {
|
||||
val defaultNameServers = mutableListOf<InetSocketAddress>()
|
||||
private fun getUnsortedNameServers() : Map<String, List<InetSocketAddress>> {
|
||||
val nameServerDomains = mutableMapOf<String, List<InetSocketAddress>>()
|
||||
|
||||
// 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<Boolean, Map<String, List<InetSocketAddress>>> {
|
||||
|
@ -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) })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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<String, Inet4Address> = emptyMap(), val ipv6Entries: Map<String, Inet6Address> = emptyMap())
|
|
@ -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<String, Inet4Address>()
|
||||
val ipv6Entries = mutableMapOf<String, Inet6Address>()
|
||||
|
||||
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<String> = 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue