package dorkbox.netUtil import dorkbox.netUtil.Common.logger import java.net.Inet4Address import java.net.Inet6Address import java.net.InetAddress import java.net.InetSocketAddress import java.net.NetworkInterface import java.net.SocketException /** * 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 { /** * 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 /** * The loopback address */ const val LOOPBACK = "127.0.0.1" 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() 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!! } /** * 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 getByName(ip: String): InetAddress? { return if (IPv4.isValid(ip)) { IPv4.getByNameUnsafe(ip) } else { IPv6.getByName(ip) } } }