437 lines
18 KiB
Kotlin
437 lines
18 KiB
Kotlin
package dorkbox.netUtil
|
|
|
|
import dorkbox.netUtil.Common.logger
|
|
import java.io.BufferedReader
|
|
import java.io.InputStreamReader
|
|
import java.net.*
|
|
import java.util.regex.Pattern
|
|
|
|
/**
|
|
* A class that holds a number of network-related constants, also from:
|
|
* (Netty, apache 2.0 license)
|
|
* https://github.com/netty/netty/blob/4.1/common/src/main/java/io/netty/util/NetUtil.java
|
|
*
|
|
* This class borrowed some of its methods from a modified fork of the
|
|
* [Inet6Util class]
|
|
* (http://svn.apache.org/repos/asf/harmony/enhanced/java/branches/java6/classlib/modules/luni/src/main/java/org/apache/harmony/luni/util/Inet6Util.java) which was part of Apache Harmony.
|
|
*/
|
|
object IP {
|
|
|
|
/**
|
|
* Gets the version number.
|
|
*/
|
|
const val version = "2.9.1"
|
|
|
|
/**
|
|
* The [InetAddress] that represents the loopback address. If IPv6 stack is available, it will refer to
|
|
* [.LOCALHOST6]. Otherwise, [.LOCALHOST4].
|
|
*/
|
|
val LOCALHOST: InetAddress
|
|
|
|
/**
|
|
* The loopback [NetworkInterface] of the current machine
|
|
*/
|
|
val LOOPBACK_IF: NetworkInterface
|
|
|
|
init {
|
|
logger.trace("-Djava.net.preferIPv4Stack: ${IPv4.isPreferred}")
|
|
logger.trace("-Djava.net.preferIPv6Addresses: ${IPv6.isPreferred}")
|
|
|
|
// Retrieve the list of available network interfaces.
|
|
val netInterfaces = mutableListOf<NetworkInterface>()
|
|
try {
|
|
val interfaces = NetworkInterface.getNetworkInterfaces()
|
|
if (interfaces != null) {
|
|
while (interfaces.hasMoreElements()) {
|
|
val iface: NetworkInterface = interfaces.nextElement()
|
|
// Use the interface with proper INET addresses only.
|
|
if (SocketUtils.addressesFromNetworkInterface(iface).hasMoreElements()) {
|
|
netInterfaces.add(iface)
|
|
}
|
|
}
|
|
}
|
|
} catch (e: SocketException) {
|
|
logger.warn("Failed to retrieve the list of available network interfaces", e)
|
|
}
|
|
|
|
|
|
// Find the first loopback interface available from its INET address (127.0.0.1 or ::1)
|
|
// Note that we do not use NetworkInterface.isLoopback() in the first place because it takes long time
|
|
// on a certain environment. (e.g. Windows with -Djava.net.preferIPv4Stack=true)
|
|
|
|
var loopbackIface: NetworkInterface? = null
|
|
var loopbackAddr: InetAddress? = null
|
|
|
|
loop@ for (iface in netInterfaces) {
|
|
val i = SocketUtils.addressesFromNetworkInterface(iface)
|
|
while (i.hasMoreElements()) {
|
|
val addr: InetAddress = i.nextElement()
|
|
if (addr.isLoopbackAddress) {
|
|
// Found
|
|
loopbackIface = iface
|
|
loopbackAddr = addr
|
|
break@loop
|
|
}
|
|
}
|
|
}
|
|
|
|
// If failed to find the loopback interface from its INET address, fall back to isLoopback().
|
|
if (loopbackIface == null) {
|
|
try {
|
|
for (iface in netInterfaces) {
|
|
if (iface.isLoopback) {
|
|
val i = SocketUtils.addressesFromNetworkInterface(iface)
|
|
if (i.hasMoreElements()) {
|
|
// Found the one with INET address.
|
|
loopbackIface = iface
|
|
loopbackAddr = i.nextElement()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if (loopbackIface == null) {
|
|
logger.warn("Failed to find the loopback interface")
|
|
}
|
|
} catch (e: SocketException) {
|
|
logger.warn("Failed to find the loopback interface", e)
|
|
}
|
|
}
|
|
|
|
if (loopbackIface != null) {
|
|
// Found the loopback interface with an INET address.
|
|
logger.trace("Loopback interface: ${loopbackIface.name} (${loopbackIface.displayName}, ${loopbackAddr!!.hostAddress})")
|
|
} else {
|
|
// Could not find the loopback interface, but we can't leave LOCALHOST as null.
|
|
// Use LOCALHOST6 or LOCALHOST4, preferably the IPv6 one.
|
|
if (loopbackAddr == null) {
|
|
try {
|
|
if (NetworkInterface.getByInetAddress(IPv6.LOCALHOST) != null) {
|
|
logger.debug("Using hard-coded IPv6 localhost address: ${IPv6.LOCALHOST}")
|
|
|
|
loopbackAddr = IPv6.LOCALHOST
|
|
}
|
|
} catch (ignore: Exception) {
|
|
|
|
}
|
|
if (loopbackAddr == null) {
|
|
logger.debug("Using hard-coded IPv4 localhost address: ${IPv4.LOCALHOST}")
|
|
loopbackAddr = IPv4.LOCALHOST
|
|
}
|
|
}
|
|
}
|
|
|
|
LOOPBACK_IF = loopbackIface!!
|
|
LOCALHOST = loopbackAddr
|
|
}
|
|
|
|
/**
|
|
* The LAN address of the machine, to the best of our ability
|
|
*/
|
|
fun lanAddress(): InetAddress {
|
|
/**
|
|
* Returns an `InetAddress` object encapsulating what is most likely the machine's LAN IP address.
|
|
*
|
|
*
|
|
* This method is intended for use as a replacement of JDK method `InetAddress.getLocalHost`, because
|
|
* that method is ambiguous on Linux systems. Linux systems enumerate the loopback network interface the same
|
|
* way as regular LAN network interfaces, but the JDK `InetAddress.getLocalHost` method does not
|
|
* specify the algorithm used to select the address returned under such circumstances, and will often return the
|
|
* loopback address, which is not valid for network communication. Details
|
|
* [here](http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4665037).
|
|
*
|
|
*
|
|
* This method will scan all IP addresses on all network interfaces on the host machine to determine the IP address
|
|
* most likely to be the machine's LAN address. If the machine has multiple IP addresses, this method will prefer
|
|
* a site-local IP address (e.g. 192.168.x.x or 10.10.x.x, usually IPv4) if the machine has one (and will return the
|
|
* first site-local address if the machine has more than one), but if the machine does not hold a site-local
|
|
* address, this method will return simply the first non-loopback address found (IPv4 or IPv6).
|
|
*
|
|
*
|
|
* If this method cannot find a non-loopback address using this selection algorithm, it will fall back to
|
|
* making an HTTP call and checking the address, and if that fails, by then calling and returning the result
|
|
* of JDK method `InetAddress.getLocalHost`.
|
|
*
|
|
*
|
|
* @throws UnknownHostException If the LAN address of the machine cannot be found.
|
|
*
|
|
* From: https://issues.apache.org/jira/browse/JCS-40
|
|
*/
|
|
val likelyAddress = mutableListOf<InetAddress>()
|
|
val candidates = mutableListOf<InetAddress>()
|
|
|
|
try {
|
|
// Iterate all NICs (network interface cards)...
|
|
val ifaces = NetworkInterface.getNetworkInterfaces()
|
|
while (ifaces.hasMoreElements()) {
|
|
val iface = ifaces.nextElement()
|
|
// Iterate all IP addresses assigned to each card...
|
|
val inetAddrs = iface.inetAddresses
|
|
while (inetAddrs.hasMoreElements()) {
|
|
val inetAddr = inetAddrs.nextElement()
|
|
if (!inetAddr.isLoopbackAddress) {
|
|
if (inetAddr.isSiteLocalAddress) {
|
|
// Found non-loopback site-local address. Return it immediately...
|
|
likelyAddress.add(inetAddr)
|
|
} else {
|
|
// Found non-loopback address, but not necessarily site-local.
|
|
// Store it as a candidate to be returned if site-local address is not subsequently found...
|
|
candidates.add(inetAddr)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (e: Exception) {
|
|
// ignored
|
|
}
|
|
|
|
if (likelyAddress.size == 1) {
|
|
return likelyAddress.first()
|
|
}
|
|
|
|
// we have MORE than 1 likely address, but we DO NOT know which one is used for 0.0.0.0 traffic.
|
|
// we **COULD** parse out the gateway information from the routes for each interface... but that is a huge amount of work.
|
|
// it's MUCH easier to see if open a connection to 1.1.1.1 and get the interface this traffic was on, and use that interface IP address
|
|
runCatching {
|
|
Socket().use {
|
|
it.connect(InetSocketAddress("1.1.1.1", 80))
|
|
return it.localAddress
|
|
}
|
|
}.onFailure {
|
|
Common.logger.error("Unable to determine outbound traffic local address. Using alternate logic instead.", it)
|
|
}
|
|
|
|
// there was an error doing this! (it's possible that outbound traffic is not allowed
|
|
if (IPv6.isPreferred) {
|
|
val ipv6 = likelyAddress.filterIsInstance<Inet6Address>()
|
|
if (ipv6.isNotEmpty()) {
|
|
return ipv6.first()
|
|
}
|
|
} else if (IPv4.isPreferred) {
|
|
val ipv4 = likelyAddress.filterIsInstance<Inet4Address>()
|
|
if (ipv4.isNotEmpty()) {
|
|
return ipv4.first()
|
|
}
|
|
}
|
|
|
|
// we STILL don't have something. Possibly that we have no likely addresses, but we
|
|
// found some other non-loopback address.
|
|
// The machine might have a non-site-local address assigned to its NIC (or it might be running
|
|
// IPv6 which deprecates the "site-local" concept).
|
|
// Return this non-loopback candidate address...
|
|
if (IPv6.isPreferred) {
|
|
val ipv6 = candidates.filterIsInstance<Inet6Address>()
|
|
if (ipv6.isNotEmpty()) {
|
|
return ipv6.first()
|
|
}
|
|
} else if (IPv4.isPreferred) {
|
|
val ipv4 = candidates.filterIsInstance<Inet4Address>()
|
|
if (ipv4.isNotEmpty()) {
|
|
return ipv4.first()
|
|
}
|
|
}
|
|
|
|
// At this point, we did not find a non-loopback address.
|
|
// Fall back to returning whatever InetAddress.getLocalHost() returns...
|
|
return InetAddress.getLocalHost()
|
|
?: throw UnknownHostException("The JDK InetAddress.getLocalHost() method unexpectedly returned null.")
|
|
}
|
|
|
|
/**
|
|
* This will retrieve your IP address via an HTTP server.
|
|
*
|
|
* **NOTE: Can optionally use DnsClient.getPublicIp() instead. It's much faster and more reliable as it uses DNS.**
|
|
*
|
|
* @return the public IP address if found, or null if it didn't find it
|
|
*/
|
|
fun publicIpViaHttp(): String? {
|
|
// method 1: use DNS servers
|
|
// dig +short myip.opendns.com @resolver1.opendns.com
|
|
|
|
// method 2: use public http servers
|
|
// @formatter:off
|
|
val websites = arrayOf(
|
|
"http://ip.dorkbox.com/",
|
|
"http://checkip.dyndns.com/",
|
|
"http://checkip.dyn.com/",
|
|
"http://curlmyip.com/",
|
|
"http://ipecho.net/plain",
|
|
"http://icanhazip.com/")
|
|
// @formatter:on
|
|
|
|
// loop, since they won't always work.
|
|
for (i in websites.indices) {
|
|
try {
|
|
val autoIP = URL(websites[i])
|
|
val `in` = BufferedReader(InputStreamReader(autoIP.openStream()))
|
|
val response = `in`.readLine()
|
|
.trim { it <= ' ' }
|
|
`in`.close()
|
|
val pattern = Pattern.compile("\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b")
|
|
val matcher = pattern.matcher(response)
|
|
if (matcher.find()) {
|
|
return matcher.group()
|
|
.trim { it <= ' ' }
|
|
}
|
|
} catch (ignored: java.lang.Exception) {
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Creates an byte[] based on an ipAddressString. No error handling is performed here.
|
|
*/
|
|
fun toBytes(ipAddressString: String): ByteArray {
|
|
if (IPv4.isValid(ipAddressString)) {
|
|
return IPv4.toBytes(ipAddressString)
|
|
}
|
|
|
|
return IPv6.toBytes(ipAddressString)
|
|
}
|
|
|
|
/**
|
|
* Converts 4-byte or 16-byte data into an IPv4 or IPv6 string respectively.
|
|
*
|
|
* @throws IllegalArgumentException
|
|
* if `length` is not `4` nor `16`
|
|
*/
|
|
fun toString(bytes: ByteArray, offset: Int = 0, length: Int = bytes.size): String {
|
|
return when (length) {
|
|
4 -> {
|
|
StringBuilder(15)
|
|
.append(bytes[offset].toInt())
|
|
.append('.')
|
|
.append(bytes[offset + 1].toInt())
|
|
.append('.')
|
|
.append(bytes[offset + 2].toInt())
|
|
.append('.')
|
|
.append(bytes[offset + 3].toInt()).toString()
|
|
}
|
|
16 -> IPv6.toString(bytes, offset)
|
|
else -> throw IllegalArgumentException("length: $length (expected: 4 or 16)")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the [String] representation of an [InetAddress].
|
|
*
|
|
* * Inet4Address results are identical to [InetAddress.getHostAddress]
|
|
* * Inet6Address results adhere to
|
|
* [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) if
|
|
* `ipv4Mapped` is false. If `ipv4Mapped` is true then "IPv4 mapped" format
|
|
* from [rfc 4291 section 2](http://tools.ietf.org/html/rfc4291#section-2.5.5) will be supported.
|
|
* The compressed result will always obey the compression rules defined in
|
|
* [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4)
|
|
*
|
|
*
|
|
* The output does not include Scope ID.
|
|
*
|
|
* @param ip [InetAddress] to be converted to an address string
|
|
* @param ipv4Mapped
|
|
*
|
|
* * `true` to stray from strict rfc 5952 and support the "IPv4 mapped" format
|
|
* defined in [rfc 4291 section 2](http://tools.ietf.org/html/rfc4291#section-2.5.5) while still
|
|
* following the updated guidelines in
|
|
* [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4)
|
|
*
|
|
* * `false` to strictly follow rfc 5952
|
|
*
|
|
* @return `String` containing the text-formatted IP address
|
|
*/
|
|
fun toString(ip: InetAddress, ipv4Mapped: Boolean = false): String {
|
|
if (ip is Inet4Address) {
|
|
return IPv4.toString(ip)
|
|
}
|
|
|
|
require(ip is Inet6Address) { "Unhandled type: $ip" }
|
|
|
|
return IPv6.toString(ip, ipv4Mapped)
|
|
}
|
|
|
|
/**
|
|
* Returns the [String] representation of an [InetSocketAddress].
|
|
*
|
|
* The output does not include Scope ID.
|
|
* @param addr [InetSocketAddress] to be converted to an address string
|
|
* @return `String` containing the text-formatted IP address
|
|
*/
|
|
fun toString(addr: InetSocketAddress): String {
|
|
val port = addr.port.toString()
|
|
val sb: StringBuilder
|
|
|
|
sb = if (addr.isUnresolved) {
|
|
val hostname = addr.hostString
|
|
newSocketAddressStringBuilder(hostname, port, !IPv6.isValid(hostname))
|
|
} else {
|
|
val address = addr.address
|
|
val hostString = toString(address)
|
|
newSocketAddressStringBuilder(hostString, port, address is Inet4Address)
|
|
}
|
|
return sb.append(':').append(port).toString()
|
|
}
|
|
|
|
/**
|
|
* Returns the [String] representation of a host port combo.
|
|
*/
|
|
fun toString(host: String, port: Int): String {
|
|
val portStr = port.toString()
|
|
return newSocketAddressStringBuilder(host, portStr, !IPv6.isValid(host)).append(':').append(portStr).toString()
|
|
}
|
|
|
|
private fun newSocketAddressStringBuilder(host: String, port: String, ipv4: Boolean): StringBuilder {
|
|
val hostLen = host.length
|
|
if (ipv4) {
|
|
// Need to include enough space for hostString:port.
|
|
return StringBuilder(hostLen + 1 + port.length).append(host)
|
|
}
|
|
|
|
// Need to include enough space for [hostString]:port.
|
|
val stringBuilder = StringBuilder(hostLen + 3 + port.length)
|
|
return if (hostLen > 1 && host[0] == '[' && host[hostLen - 1] == ']') {
|
|
stringBuilder.append(host)
|
|
} else {
|
|
stringBuilder.append('[').append(host).append(']')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the [InetAddress] representation of a [CharSequence] IP address.
|
|
*
|
|
* This method will treat all IPv4 type addresses as "IPv4 mapped" (see [.getByName])
|
|
*
|
|
* @param ip [CharSequence] IP address to be converted to a [InetAddress]
|
|
* @return [InetAddress] representation of the `ip` or `null` if not a valid IP address.
|
|
*/
|
|
fun toAddress(ip: String): InetAddress? {
|
|
return if (IPv4.isValid(ip)) {
|
|
IPv4.toAddressUnsafe(ip)
|
|
} else {
|
|
IPv6.toAddress(ip)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Truncates an address to the specified number of bits. For example,
|
|
* truncating the address 10.1.2.3 to 8 bits would yield 10.0.0.0.
|
|
*
|
|
* @param address The source address
|
|
* @param maskLength The number of bits to truncate the address to.
|
|
*/
|
|
fun truncate(address: InetAddress, maskLength: Int): InetAddress? {
|
|
return if (address is Inet4Address) {
|
|
IPv4.truncate(address, maskLength)
|
|
} else {
|
|
IPv6.truncate(address as Inet6Address, maskLength)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Takes a [String] and parses it to see if it is a valid IPv4/6 address.
|
|
*
|
|
* @return true, if the string represents an IPv4/6 address in dotted notation, false otherwise
|
|
*/
|
|
fun isValid(ip: String): Boolean {
|
|
return IPv4.isValid(ip) || IPv6.isValid(ip)
|
|
}
|
|
}
|