/* * Copyright 2020 Dorkbox, llc * Copyright 2012 The Netty Project * * 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: * * http://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 import java.math.BigInteger 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 IPv6 { /** * Gets the version number. */ const val version = Common.version // used for subnet mask info private val MINUS_ONE = BigInteger.valueOf(-1) /** * Maximum amount of value adding characters in between IPV6 separators */ private const val IPV6_MAX_CHAR_BETWEEN_SEPARATOR = 4 /** * Minimum number of separators that must be present in an IPv6 string */ private const val IPV6_MIN_SEPARATORS = 2 /** * Maximum number of separators that must be present in an IPv6 string */ private const val IPV6_MAX_SEPARATORS = 8 /** * Maximum amount of value adding characters in between IPV4 separators */ private const val IPV4_MAX_CHAR_BETWEEN_SEPARATOR = 3 /** * Number of separators that must be present in an IPv4 string */ private const val IPV4_SEPARATORS = 3 /** * Number of bytes needed to represent and IPV6 value */ private const val IPV6_BYTE_COUNT = 16 /** * This defines how many words (represented as ints) are needed to represent an IPv6 address */ private const val IPV6_WORD_COUNT = 8 /** * The maximum number of characters for an IPV6 string with no scope */ private const val IPV6_MAX_CHAR_COUNT = 39 /** * Returns `true` if an IPv6 address should be preferred when a host has both an IPv4 address and an IPv6 * address. The default value of this property is `false`. * * @see [Java SE * networking properties](https://docs.oracle.com/javase/8/docs/api/java/net/doc-files/net-properties.html) */ val isPreferred = Common.getBoolean("java.net.preferIPv6Addresses", false) /** * true if there is an IPv6 network interface available */ val isAvailable: Boolean by lazy { // 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) { Common.logger.warn("Failed to retrieve the list of available network interfaces", e) } // check if IPv4 is possible. var ipv6Possible = false loop@ for (iface in netInterfaces) { val i = SocketUtils.addressesFromNetworkInterface(iface) while (i.hasMoreElements()) { val addr: InetAddress = i.nextElement() if (addr is Inet6Address) { ipv6Possible = true break@loop } } } ipv6Possible } /** * The [Inet6Address] that represents the IPv6 loopback address '::1' */ val LOCALHOST: Inet6Address by lazy { // Create IPv6 address, this will ALWAYS work InetAddress.getByAddress("localhost", byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)) as Inet6Address } /** * The [String] that represents the IPv6 loopback address '::1' */ val LOCALHOST_STRING by lazy { toString(LOCALHOST) } /** * The [Inet4Address] that represents the IPv6 wildcard address '0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0' */ val WILDCARD: Inet6Address by lazy { // Create IPv6 address, this will ALWAYS work InetAddress.getByAddress(null, byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) as Inet6Address } /** * The [String] that represents the IPv6 wildcard address '0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0' */ val WILDCARD_STRING by lazy { toString(WILDCARD) } /** * the length of an address in this particular family. */ val length = 16 /** * @return if the specified address is of this family type */ fun isFamily(address: InetAddress) : Boolean { return address is Inet6Address } /** * Takes a [String] and parses it to see if it is a valid IPV6 address. * * @return true, if the string represents an IPV6 address, false otherwise */ fun isValid(ip: String): Boolean { var end = ip.length if (end < 2) { return false } // strip "[]" var start: Int var c = ip[0] if (c == '[') { end-- if (ip[end] != ']') { // must have a close ] return false } start = 1 c = ip[1] } else { start = 0 } var colons: Int var compressBegin: Int if (c == ':') { // an IPv6 address can start with "::" or with a number if (ip[start + 1] != ':') { return false } colons = 2 compressBegin = start start += 2 } else { colons = 0 compressBegin = -1 } var wordLen = 0 loop@ for (i in start until end) { c = ip[i] if (isValidHexChar(c)) { if (wordLen < 4) { wordLen++ continue } return false } when (c) { ':' -> { if (colons > 7) { return false } if (ip[i - 1] == ':') { if (compressBegin >= 0) { return false } compressBegin = i - 1 } else { wordLen = 0 } colons++ } '.' -> { // case for the last 32-bits represented as IPv4 x:x:x:x:x:x:d.d.d.d // check a normal case (6 single colons) if (compressBegin < 0 && colons != 6 || // a special case ::1:2:3:4:5:d.d.d.d allows 7 colons with an // IPv4 ending, otherwise 7 :'s is bad colons == 7 && compressBegin >= start || colons > 7) { return false } // Verify this address is of the correct structure to contain an IPv4 address. // It must be IPv4-Mapped or IPv4-Compatible // a valid one is, for example: 0000:0000:0000:0000:0000:0000:192.168.89.9 // (see https://tools.ietf.org/html/rfc4291#section-2.5.5). val ipv4Start = i - wordLen var j = ipv4Start - 2 // index of character before the previous ':'. if (IPv4.isValidIPv4MappedChar(ip[j])) { if (!IPv4.isValidIPv4MappedChar(ip[j - 1]) || !IPv4.isValidIPv4MappedChar(ip[j - 2]) || !IPv4.isValidIPv4MappedChar(ip[j - 3])) { return false } j -= 5 } while (j >= start) { val tmpChar = ip[j] if (tmpChar != '0' && tmpChar != ':') { return false } --j } // 7 - is minimum IPv4 address length var ipv4End = ip.indexOf('%', ipv4Start + 7) if (ipv4End < 0) { ipv4End = end } return IPv4.isValidIpV4Address(ip, ipv4Start, ipv4End) } '%' -> { // strip the interface name/index after the percent sign end = i break@loop } else -> return false } } // normal case without compression return if (compressBegin < 0) { colons == 7 && wordLen > 0 } else compressBegin + 2 == end || // 8 colons is valid only if compression in start or end wordLen > 0 && (colons < 8 || compressBegin <= start) } private fun isValidHexChar(c: Char): Boolean { return c in '0'..'9' || c in 'A'..'F' || c in 'a'..'f' } /** * Returns the [Inet6Address] 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 [Inet6Address] * @return [Inet6Address] representation of the `ip` or `null` if not a valid IP address. */ fun toAddress(ip: String): Inet6Address? { return toAddress(ip, true) } /** * Returns the [Inet6Address] representation of a [String] IP address. *

* The [ipv4Mapped] parameter specifies how IPv4 addresses should be treated. The "IPv4 mapped" format is * defined in rfc 4291 section 2 is supported. * * @param ip [String] IP address to be converted to a [Inet6Address] * @param ipv4Mapped *

* @return [Inet6Address] representation of the [ip] or [null] if not a valid IP address. */ fun toAddress(ip: String, ipv4Mapped: Boolean = true): Inet6Address? { val bytes = getIPv6ByName(ip, ipv4Mapped) ?: return null return try { Inet6Address.getByAddress(null, bytes, -1) } catch (e: UnknownHostException) { throw RuntimeException(e) // Should never happen } } /** * Returns the byte array representation of a [CharSequence] IP address. * * * The `ipv4Mapped` parameter specifies how IPv4 addresses should be treated. * "IPv4 mapped" format as * defined in [rfc 4291 section 2](http://tools.ietf.org/html/rfc4291#section-2.5.5) is supported. * @param ip [CharSequence] IP address to be converted to a [Inet6Address] * @param ipv4Mapped * * `true` To allow IPv4 mapped inputs to be translated into [Inet6Address] * * `false` Consider IPv4 mapped addresses as invalid. * * @return byte array representation of the `ip` or `null` if not a valid IP address. */ private fun getIPv6ByName(ip: CharSequence, ipv4Mapped: Boolean = true): ByteArray? { val bytes = ByteArray(IPV6_BYTE_COUNT) val ipLength = ip.length var compressBegin = 0 var compressLength = 0 var currentIndex = 0 var value = 0 var begin = -1 var i = 0 var ipv6Separators = 0 var ipv4Separators = 0 var tmp: Int var needsShift = false while (i < ipLength) { val c = ip[i] when (c) { ':' -> { ++ipv6Separators if (i - begin > IPV6_MAX_CHAR_BETWEEN_SEPARATOR || ipv4Separators > 0 || ipv6Separators > IPV6_MAX_SEPARATORS || currentIndex + 1 >= bytes.size) { return null } value = value shl (IPV6_MAX_CHAR_BETWEEN_SEPARATOR - (i - begin) shl 2) if (compressLength > 0) { compressLength -= 2 } // The value integer holds at most 4 bytes from right (most significant) to left (least significant). // The following bit shifting is used to extract and re-order the individual bytes to achieve a // left (most significant) to right (least significant) ordering. bytes[currentIndex++] = (value and 0xf shl 4 or (value shr 4 and 0xf)).toByte() bytes[currentIndex++] = (value shr 8 and 0xf shl 4 or (value shr 12 and 0xf)).toByte() tmp = i + 1 if (tmp < ipLength && ip[tmp] == ':') { ++tmp if (compressBegin != 0 || tmp < ipLength && ip[tmp] == ':') { return null } ++ipv6Separators needsShift = ipv6Separators == 2 && value == 0 compressBegin = currentIndex compressLength = bytes.size - compressBegin - 2 ++i } value = 0 begin = -1 } '.' -> { ++ipv4Separators tmp = i - begin // tmp is the length of the current segment. if (tmp > IPV4_MAX_CHAR_BETWEEN_SEPARATOR || begin < 0 || ipv4Separators > IPV4_SEPARATORS || ipv6Separators > 0 && currentIndex + compressLength < 12 || i + 1 >= ipLength || currentIndex >= bytes.size || ipv4Separators == 1 && // We also parse pure IPv4 addresses as IPv4-Mapped for ease of use. (!ipv4Mapped || currentIndex != 0 && !isValidIPv4Mapped(bytes, currentIndex, compressBegin, compressLength) || tmp == 3 && (!IPv4.isValidNumericChar( ip[i - 1]) || !IPv4.isValidNumericChar(ip[i - 2]) || !IPv4.isValidNumericChar(ip[i - 3])) || tmp == 2 && (!IPv4.isValidNumericChar( ip[i - 1]) || !IPv4.isValidNumericChar(ip[i - 2])) || tmp == 1 && !IPv4.isValidNumericChar(ip[i - 1]))) { return null } value = value shl (IPV4_MAX_CHAR_BETWEEN_SEPARATOR - tmp shl 2) // The value integer holds at most 3 bytes from right (most significant) to left (least significant). // The following bit shifting is to restructure the bytes to be left (most significant) to // right (least significant) while also accounting for each IPv4 digit is base 10. begin = (value and 0xf) * 100 + (value shr 4 and 0xf) * 10 + (value shr 8 and 0xf) if (begin < 0 || begin > 255) { return null } bytes[currentIndex++] = begin.toByte() value = 0 begin = -1 } else -> { if (!isValidHexChar(c) || ipv4Separators > 0 && !IPv4.isValidNumericChar(c)) { return null } if (begin < 0) { begin = i } else if (i - begin > IPV6_MAX_CHAR_BETWEEN_SEPARATOR) { return null } // The value is treated as a sort of array of numbers because we are dealing with // at most 4 consecutive bytes we can use bit shifting to accomplish this. // The most significant byte will be encountered first, and reside in the right most // position of the following integer value += Common.decodeHexNibble(c) shl (i - begin shl 2) } } ++i } val isCompressed = compressBegin > 0 // Finish up last set of data that was accumulated in the loop (or before the loop) if (ipv4Separators > 0) { if (begin > 0 && i - begin > IPV4_MAX_CHAR_BETWEEN_SEPARATOR || ipv4Separators != IPV4_SEPARATORS || currentIndex >= bytes.size) { return null } if (ipv6Separators == 0) { compressLength = 12 } else if (ipv6Separators >= IPV6_MIN_SEPARATORS && (!isCompressed && ipv6Separators == 6 && ip[0] != ':' || isCompressed && ipv6Separators < IPV6_MAX_SEPARATORS && (ip[0] != ':' || compressBegin <= 2))) { compressLength -= 2 } else { return null } value = value shl (IPV4_MAX_CHAR_BETWEEN_SEPARATOR - (i - begin) shl 2) // The value integer holds at most 3 bytes from right (most significant) to left (least significant). // The following bit shifting is to restructure the bytes to be left (most significant) to // right (least significant) while also accounting for each IPv4 digit is base 10. begin = (value and 0xf) * 100 + (value shr 4 and 0xf) * 10 + (value shr 8 and 0xf) if (begin < 0 || begin > 255) { return null } bytes[currentIndex++] = begin.toByte() } else { tmp = ipLength - 1 if (begin > 0 && i - begin > IPV6_MAX_CHAR_BETWEEN_SEPARATOR || ipv6Separators < IPV6_MIN_SEPARATORS || !isCompressed && (ipv6Separators + 1 != IPV6_MAX_SEPARATORS || ip[0] == ':' || ip[tmp] == ':') || isCompressed && (ipv6Separators > IPV6_MAX_SEPARATORS || ipv6Separators == IPV6_MAX_SEPARATORS && (compressBegin <= 2 && ip[0] != ':' || compressBegin >= 14 && ip[tmp] != ':')) || currentIndex + 1 >= bytes.size || begin < 0 && ip[tmp - 1] != ':' || compressBegin > 2 && ip[0] == ':') { return null } if (begin >= 0 && i - begin <= IPV6_MAX_CHAR_BETWEEN_SEPARATOR) { value = value shl (IPV6_MAX_CHAR_BETWEEN_SEPARATOR - (i - begin) shl 2) } // The value integer holds at most 4 bytes from right (most significant) to left (least significant). // The following bit shifting is used to extract and re-order the individual bytes to achieve a // left (most significant) to right (least significant) ordering. bytes[currentIndex++] = (value and 0xf shl 4 or (value shr 4 and 0xf)).toByte() bytes[currentIndex++] = (value shr 8 and 0xf shl 4 or (value shr 12 and 0xf)).toByte() } i = currentIndex + compressLength if (needsShift || i >= bytes.size) { // Right shift array if (i >= bytes.size) { ++compressBegin } i = currentIndex while (i < bytes.size) { begin = bytes.size - 1 while (begin >= compressBegin) { bytes[begin] = bytes[begin - 1] --begin } bytes[begin] = 0 ++compressBegin ++i } } else { // Selectively move elements i = 0 while (i < compressLength) { begin = i + compressBegin currentIndex = begin + compressLength if (currentIndex < bytes.size) { bytes[currentIndex] = bytes[begin] bytes[begin] = 0 } else { break } ++i } } if (ipv4Separators > 0) { // We only support IPv4-Mapped addresses [1] because IPv4-Compatible addresses are deprecated [2]. // [1] https://tools.ietf.org/html/rfc4291#section-2.5.5.2 // [2] https://tools.ietf.org/html/rfc4291#section-2.5.5.1 bytes[11] = 0xff.toByte() bytes[10] = bytes[11] } return bytes } private fun isValidIPv4Mapped(bytes: ByteArray, currentIndex: Int, compressBegin: Int, compressLength: Int): Boolean { val mustBeZero = compressBegin + compressLength >= 14 return currentIndex <= 12 && currentIndex >= 2 && (!mustBeZero || compressBegin < 12) && isValidIPv4MappedSeparators(bytes[currentIndex - 1], bytes[currentIndex - 2], mustBeZero) && isZero(bytes, 0, currentIndex - 3) } private fun isZero(bytes: ByteArray, startPos: Int, length: Int): Boolean { var start = startPos val end = start + length val zeroByteByte = 0.toByte() while (start < end) { if (bytes[start++] != zeroByteByte) { return false } } return true } private fun isValidIPv4MappedSeparators(b0: Byte, b1: Byte, mustBeZero: Boolean): Boolean { // We allow IPv4 Mapped (https://tools.ietf.org/html/rfc4291#section-2.5.5.1) // and IPv4 compatible (https://tools.ietf.org/html/rfc4291#section-2.5.5.1). // The IPv4 compatible is deprecated, but it allows parsing of plain IPv4 addressed into IPv6-Mapped addresses. return b0 == b1 && (b0.toInt() == 0 || !mustBeZero && b1.toInt() == -1) } /** * Creates an byte[] based on an ip string. No error handling is performed here. */ fun toBytesOrNull(ipAddress: String): ByteArray? { if (isValid(ipAddress)) { return toAddress(ipAddress)?.address } return null } /** * Creates an byte[] based on an ip string. */ fun toBytes(ipAddress: String): ByteArray { // always return a byte array var fixedIp = ipAddress if (fixedIp[0] == '[') { fixedIp = fixedIp.substring(1, fixedIp.length - 1) } val percentPos: Int = fixedIp.indexOf('%') if (percentPos >= 0) { fixedIp = fixedIp.substring(0, percentPos) } return toAddress(fixedIp)?.address ?: ByteArray(32) } /** * Converts a byte array into a 128-bit BigInteger */ fun toInt(ipBytes: ByteArray): BigInteger { return BigInteger(ipBytes) } /** * Converts an IP address into a 128-bit BigInteger */ fun toInt(ipAsString: String): BigInteger { return if (isValid(ipAsString)) { toIntUnsafe(ipAsString) } else { BigInteger(ByteArray(0)) } } /** * Converts an IP address into a 128-bit BigInteger, no validity checks are performed */ fun toIntUnsafe(ipAsString: String): BigInteger { val bytes = toBytes(ipAsString) return BigInteger(bytes) } /** * 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 bytes [InetAddress] to be converted to an address string * * @return `String` containing the text-formatted IP address */ fun toString(bytes: ByteArray): String { return toString(bytes, 0) } /** * 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 ints [InetAddress] to be converted to an address string * * @return `String` containing the text-formatted IP address */ fun toString(ints: IntArray): String { return toString(ints, 0) } /** * 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 bytes [InetAddress] to be converted to an address string * @param buf the StringBuilder to build the address into */ fun toString(bytes: ByteArray, buf: StringBuilder) { toString(bytes, 0, buf) } /** * 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 ints [InetAddress] to be converted to an address string * @param buf the StringBuilder to build the address into */ fun toString(ints: IntArray, buf: StringBuilder) { toString(ints, 0, buf) } /** * 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 bytes [InetAddress] to be converted to an address string * * @return `String` containing the text-formatted IP address */ fun toString(bytes: ByteArray, offset: Int): String { val buf = StringBuilder(IPV6_MAX_CHAR_COUNT) toAddressString(bytes, offset, false, buf) return buf.toString() } /** * 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 ints [InetAddress] to be converted to an address string * * @return `String` containing the text-formatted IP address */ fun toString(ints: IntArray, offset: Int): String { val buf = StringBuilder(IPV6_MAX_CHAR_COUNT) toAddressString(ints, offset, false, buf) return buf.toString() } /** * 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 bytes [InetAddress] to be converted to an address string * @param offset the offset into the array to start * @param buf the StringBuilder to build the address into */ fun toString(bytes: ByteArray, offset: Int, buf: StringBuilder) { toAddressString(bytes, offset, false, buf) } /** * 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 ints [InetAddress] to be converted to an address string * @param offset the offset into the array to start * @param buf the StringBuilder to build the address into */ fun toString(ints: IntArray, offset: Int, buf: StringBuilder) { toAddressString(ints, offset, false, buf) } /** * 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: Inet6Address, ipv4Mapped: Boolean = false): String { val buf = StringBuilder(IPV6_MAX_CHAR_COUNT) toAddressString(ip.address, 0, ipv4Mapped, buf) return buf.toString() } /** * 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 * * @param buf the StringBuilder to build the address into */ fun toString(ip: Inet6Address, ipv4Mapped: Boolean = false, buf: StringBuilder) { toAddressString(ip.address, 0, ipv4Mapped, buf) } private fun toAddressString(bytes: ByteArray, offset: Int, ipv4Mapped: Boolean, buf: StringBuilder) { val words = IntArray(IPV6_WORD_COUNT) var i: Int val end = offset + words.size i = offset while (i < end) { words[i] = bytes[i shl 1].toInt() and 0xff shl 8 or (bytes[(i shl 1) + 1].toInt() and 0xff) ++i } // Find longest run of 0s, tie goes to first found instance var currentStart = -1 var currentLength: Int var shortestStart = -1 var shortestLength = 0 i = 0 while (i < words.size) { if (words[i] == 0) { if (currentStart < 0) { currentStart = i } } else if (currentStart >= 0) { currentLength = i - currentStart if (currentLength > shortestLength) { shortestStart = currentStart shortestLength = currentLength } currentStart = -1 } ++i } // If the array ends on a streak of zeros, make sure we account for it if (currentStart >= 0) { currentLength = i - currentStart if (currentLength > shortestLength) { shortestStart = currentStart shortestLength = currentLength } } // Ignore the longest streak if it is only 1 long if (shortestLength == 1) { shortestLength = 0 shortestStart = -1 } // Translate to string taking into account longest consecutive 0s val shortestEnd = shortestStart + shortestLength if (shortestEnd < 0) { // Optimization when there is no compressing needed buf.append(Integer.toHexString(words[0])) i = 1 while (i < words.size) { buf.append(':') buf.append(Integer.toHexString(words[i])) ++i } } else { // General case that can handle compressing (and not compressing) // Loop unroll the first index (so we don't constantly check i==0 cases in loop) val isIpv4Mapped: Boolean isIpv4Mapped = if (inRangeEndExclusive(0, shortestStart, shortestEnd)) { buf.append("::") ipv4Mapped && shortestEnd == 5 && words[5] == 0xffff } else { buf.append(Integer.toHexString(words[0])) false } i = 1 while (i < words.size) { if (!inRangeEndExclusive(i, shortestStart, shortestEnd)) { if (!inRangeEndExclusive(i - 1, shortestStart, shortestEnd)) { // If the last index was not part of the shortened sequence if (!isIpv4Mapped || i == 6) { buf.append(':') } else { buf.append('.') } } if (isIpv4Mapped && i > 5) { buf.append(words[i] shr 8) buf.append('.') buf.append(words[i] and 0xff) } else { buf.append(Integer.toHexString(words[i])) } } else if (!inRangeEndExclusive(i - 1, shortestStart, shortestEnd)) { // If we are in the shortened sequence and the last index was not buf.append("::") } ++i } } } private fun toAddressString(ints: IntArray, offset: Int, ipv4Mapped: Boolean, buf: StringBuilder) { val words = IntArray(IPV6_WORD_COUNT) var i: Int val end = offset + words.size i = offset while (i < end) { words[i] = ints[i shl 1] shl 8 or (ints[(i shl 1) + 1]) ++i } // Find longest run of 0s, tie goes to first found instance var currentStart = -1 var currentLength: Int var shortestStart = -1 var shortestLength = 0 i = 0 while (i < words.size) { if (words[i] == 0) { if (currentStart < 0) { currentStart = i } } else if (currentStart >= 0) { currentLength = i - currentStart if (currentLength > shortestLength) { shortestStart = currentStart shortestLength = currentLength } currentStart = -1 } ++i } // If the array ends on a streak of zeros, make sure we account for it if (currentStart >= 0) { currentLength = i - currentStart if (currentLength > shortestLength) { shortestStart = currentStart shortestLength = currentLength } } // Ignore the longest streak if it is only 1 long if (shortestLength == 1) { shortestLength = 0 shortestStart = -1 } // Translate to string taking into account longest consecutive 0s val shortestEnd = shortestStart + shortestLength if (shortestEnd < 0) { // Optimization when there is no compressing needed buf.append(Integer.toHexString(words[0])) i = 1 while (i < words.size) { buf.append(':') buf.append(Integer.toHexString(words[i])) ++i } } else { // General case that can handle compressing (and not compressing) // Loop unroll the first index (so we don't constantly check i==0 cases in loop) val isIpv4Mapped: Boolean isIpv4Mapped = if (inRangeEndExclusive(0, shortestStart, shortestEnd)) { buf.append("::") ipv4Mapped && shortestEnd == 5 && words[5] == 0xffff } else { buf.append(Integer.toHexString(words[0])) false } i = 1 while (i < words.size) { if (!inRangeEndExclusive(i, shortestStart, shortestEnd)) { if (!inRangeEndExclusive(i - 1, shortestStart, shortestEnd)) { // If the last index was not part of the shortened sequence if (!isIpv4Mapped || i == 6) { buf.append(':') } else { buf.append('.') } } if (isIpv4Mapped && i > 5) { buf.append(words[i] shr 8) buf.append('.') buf.append(words[i] and 0xff) } else { buf.append(Integer.toHexString(words[i])) } } else if (!inRangeEndExclusive(i - 1, shortestStart, shortestEnd)) { // If we are in the shortened sequence and the last index was not buf.append("::") } ++i } } } /** * Does a range check on `value` if is within `start` (inclusive) and `end` (exclusive). * @param value The value to checked if is within `start` (inclusive) and `end` (exclusive) * @param start The start of the range (inclusive) * @param end The end of the range (exclusive) * @return * * * `true` if `value` if is within `start` (inclusive) and `end` (exclusive) * * `false` otherwise * */ private fun inRangeEndExclusive(value: Int, start: Int, end: Int): Boolean { return value >= start && value < end } /** * IPv6 address reserved for loopback use is 0000:0000:0000:0000:0000:0000:0000:0001/128. * * This loopback address is so lengthy and can be further simplified as ::1/128, or just ::1 * * /128 means EXACTLY this address */ fun isLoopback(ipAsString: String): Boolean { var oneCount = 0 // can only be with one "1", and nothing else ipAsString.forEach { char -> when (char) { ':' -> { // this is always ok } '1' -> { oneCount++ if (oneCount > 1) { return false } } else -> { // not OK return false } } } return oneCount == 1 } /** * 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: Inet6Address, maskLength: Int): InetAddress? { val maxMaskLength = length * 8 require(!(maskLength < 0 || maskLength > maxMaskLength)) { "invalid mask length" } if (maskLength == maxMaskLength) { return address } val bytes = address.address for (i in maskLength / 8 + 1 until bytes.size) { bytes[i] = 0 } val maskBits = maskLength % 8 var bitmask = 0 for (i in 0 until maskBits) { bitmask = bitmask or (1 shl 7 - i) } bytes[maskLength / 8] = (bytes[maskLength / 8].toInt() and bitmask).toByte() return try { InetAddress.getByAddress(bytes) } catch (e: UnknownHostException) { throw IllegalArgumentException("invalid address") } } /** * @param cidrPrefix the CIDR notation, ie: /24, /16, etc that we want to convert into a netmask * * @return the netmask, or if there were errors, the default /0 netmask */ fun cidrPrefixToSubnetMask(cidrPrefix: Int): BigInteger { return MINUS_ONE.shiftLeft(128 - cidrPrefix) } }