NetworkUtils/src/dorkbox/netUtil/IP.kt

276 lines
10 KiB
Kotlin

package dorkbox.netUtil
import dorkbox.netUtil.Common.logger
import java.net.*
/**
* 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.debug {
"-Djava.net.preferIPv4Stack: ${IPv4.isPreferred}\n" +
"-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.debug {
"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 ip.getHostAddress()
}
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 = getHostname(addr)
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.getByName(ip)
} else {
IPv6.getByName(ip)
}
}
/**
* Returns [InetSocketAddress.getHostString] if Java >= 7,
* or [InetSocketAddress.getHostName] otherwise.
* @param addr The address
* @return the host string
*/
fun getHostname(addr: InetSocketAddress): String {
return addr.hostString
}
}