= ArrayList()
+
+ while (end >= start) {
+ var maxsize = 32.toByte()
+ while (maxsize > 0) {
+ val mask = CIDR2MASK[maxsize - 1].toLong()
+ val maskedBase = start and mask
+ if (maskedBase != start) {
+ break
+ }
+ maxsize--
+ }
+
+ val x = ln(end - start + 1.toDouble()) / ln(2.0)
+ val maxDiff = (32 - floor(x)).toByte()
+ if (maxsize < maxDiff) {
+ maxsize = maxDiff
+ }
+
+ val ip = toString(start)
+ pairs.add("$ip/$maxsize")
+ start += 2.0.pow(32 - maxsize.toDouble()).toLong()
+ }
+ return pairs
+ }
+
+
+ /* Mask to convert unsigned int to a long (i.e. keep 32 bits) */
+ private const val UNSIGNED_INT_MASK = 0x0FFFFFFFFL
+
+ /**
+ * Check if the IP address is in the range of a specific IP/CIDR
+ *
+ * a prefix of 0 will ALWAYS return true
+ *
+ * @param address the address to check
+ * @param networkAddress the network address that will have the other address checked against
+ * @param networkPrefix 0-32 the network prefix (subnet) to use for the network address
+ *
+ * @return true if it is in range
+ */
+ fun isInRange(address: Int, networkAddress: Int, networkPrefix: Int): Boolean {
+ // System.err.println(" ip: " + IP.toString(address));
+ // System.err.println(" networkAddress: " + IP.toString(networkAddress));
+ // System.err.println(" networkSubnetPrefix: " + networkPrefix);
+ if (networkPrefix == 0) {
+ // a prefix of 0 means it is always true (even though the broadcast address is '-1'). So we short-cut it here
+ return true
+ }
+ val netmask = ((1 shl 32 - networkPrefix) - 1).inv()
+
+ // Calculate base network address
+ val network = (networkAddress and netmask and UNSIGNED_INT_MASK.toInt()).toLong()
+ // System.err.println(" network " + IP.toString(network));
+
+ // Calculate broadcast address
+ val broadcast = network or netmask.inv().toLong() and UNSIGNED_INT_MASK
+ // System.err.println(" broadcast " + IP.toString(broadcast));
+
+ val addressLong = (address and UNSIGNED_INT_MASK.toInt()).toLong()
+ return addressLong in network..broadcast
+ }
+
+
+ fun toInt(ipBytes: ByteArray): Int {
+ return ipBytes[0].toInt() shl 24 or
+ (ipBytes[1].toInt() shl 16) or
+ (ipBytes[2].toInt() shl 8) or
+ (ipBytes[3].toInt())
+ }
+
+ /**
+ * Converts a 32-bit integer into an IPv4 address.
+ */
+ fun toString(ipAddress: Int): String {
+ val buf = StringBuilder(15)
+ buf.append(ipAddress shr 24 and 0xFF)
+ buf.append('.')
+ buf.append(ipAddress shr 16 and 0xFF)
+ buf.append('.')
+ buf.append(ipAddress shr 8 and 0xF)
+ buf.append('.')
+ buf.append(ipAddress and 0xFF)
+ return buf.toString()
+ }
+
+ fun toString(ipBytes: ByteArray): String {
+ val buf = StringBuilder(15)
+ buf.append(ipBytes[0].toUByte())
+ buf.append('.')
+ buf.append(ipBytes[1].toUByte())
+ buf.append('.')
+ buf.append(ipBytes[2].toUByte())
+ buf.append('.')
+ buf.append(ipBytes[3].toUByte())
+ return buf.toString()
+ }
+
+ /**
+ * Returns the [String] representation of an [InetAddress]. Results are identical to [InetAddress.getHostAddress]
+ *
+ * @param ip [InetAddress] to be converted to an address string
+ *
+ * @return `String` containing the text-formatted IP address
+ */
+ fun toString(ip: InetAddress): String {
+ return (ip as Inet4Address).hostAddress
+ }
+
+
+ @Throws(Exception::class)
+ fun writeString(ipAddress: Int, writer: Writer) {
+ writer.write((ipAddress shr 24 and 0x000000FF).toString())
+ writer.write('.'.toInt())
+ writer.write((ipAddress shr 16 and 0x000000FF).toString())
+ writer.write('.'.toInt())
+ writer.write((ipAddress shr 8 and 0x000000FF).toString())
+ writer.write('.'.toInt())
+ writer.write((ipAddress and 0x000000FF).toString())
+ }
+
+ fun toString(ipAddress: Long): String {
+ val ipString = StringBuilder(15)
+ ipString.append(ipAddress shr 24 and 0x000000FF)
+ ipString.append('.')
+ ipString.append(ipAddress shr 16 and 0x000000FF)
+ ipString.append('.')
+ ipString.append(ipAddress shr 8 and 0x000000FF)
+ ipString.append('.')
+ ipString.append(ipAddress and 0x000000FF)
+ return ipString.toString()
+ }
+
+ fun toBytes(bytes: Int): ByteArray {
+ return byteArrayOf((bytes ushr 24 and 0xFF).toByte(),
+ (bytes ushr 16 and 0xFF).toByte(),
+ (bytes ushr 8 and 0xFF).toByte(),
+ (bytes and 0xFF).toByte())
+ }
+
+ fun toInt(ipAsString: String): Int {
+ return if (isValid(ipAsString)) {
+ val bytes = toBytes(ipAsString)
+
+ var address = 0
+ address = address or (bytes[0].toInt() shl 24)
+ address = address or (bytes[1].toInt() shl 16)
+ address = address or (bytes[2].toInt() shl 8)
+ address = address or bytes[3].toInt()
+
+ address
+ } else {
+ 0
+ }
+ }
+
+ /**
+ * Returns the [Inet4Address] representation of a [String] IP address.
+ *
+ * This method will treat all IPv4 type addresses as "IPv4 mapped" (see [.getByName])
+ *
+ * @param ip [String] IP address to be converted to a [Inet4Address]
+ * @return [Inet4Address] representation of the `ip` or `null` if not a valid IP address.
+ */
+ fun getByName(ip: String): Inet4Address? {
+ return if (isValid(ip)) {
+ val asBytes = toBytes(ip)
+ return Inet4Address.getByAddress(ip, asBytes) as Inet4Address
+ } else {
+ null
+ }
+ }
+}
diff --git a/src/dorkbox/netUtil/IPv6.kt b/src/dorkbox/netUtil/IPv6.kt
new file mode 100644
index 0000000..5563450
--- /dev/null
+++ b/src/dorkbox/netUtil/IPv6.kt
@@ -0,0 +1,691 @@
+/*
+ * 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.net.Inet6Address
+import java.net.InetAddress
+import java.net.UnknownHostException
+
+/**
+ * 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 {
+ /**
+ * 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)
+
+ /**
+ * The [Inet6Address] that represents the IPv6 loopback address '::1'
+ */
+ val LOCALHOST: Inet6Address by lazy {
+ // Create IPv6 loopback address.
+ // should never fail
+ InetAddress.getByAddress("localhost", byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)) as 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
+ // (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 getByName(ip: String): Inet6Address? {
+ return getByName(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
+ *
+ * - {@code true} To allow IPv4 mapped inputs to be translated into {@link Inet6Address}
+ * - {@code false} Consider IPv4 mapped addresses as invalid.
+ *
+ * @return [Inet6Address] representation of the [ip] or [null] if not a valid IP address.
+ */
+ fun getByName(ip: String, ipv4Mapped: Boolean): Inet6Address? {
+ val bytes = getIPv6ByName(ip, ipv4Mapped) ?: return null
+ return try {
+ Inet6Address.getByAddress(null, bytes, -1)
+ } catch (e: UnknownHostException) {
+ throw RuntimeException(e) // Should never happen
+ }
+
+// return NetUtil.getByName(ip, ipv4Mapped)
+ }
+
+ /**
+ * 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): 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 ipAddressString. No error handling is performed here.
+ */
+ fun toBytesOrNull(ipAddress: String): ByteArray? {
+ if (isValid(ipAddress)) {
+ return getByName(ipAddress)?.address
+ }
+
+ return null
+ }
+ /**
+ * Creates an byte[] based on an ipAddressString. No error handling is performed here.
+ */
+ 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 getByName(fixedIp)?.address ?: ByteArray(32)
+ }
+
+
+ /**
+ * 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 bytes [InetAddress] to be converted to an address string
+ *
+ * @return `String` containing the text-formatted IP address
+ */
+ fun toString(bytes: ByteArray, offset: Int): String {
+ return toAddressString(bytes, offset, false)
+ }
+
+ /**
+ * 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 {
+ return toAddressString(ip.address, 0, ipv4Mapped)
+ }
+
+ private fun toAddressString(bytes: ByteArray, offset: Int, ipv4Mapped: Boolean): String {
+ 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
+ val b = StringBuilder(IPV6_MAX_CHAR_COUNT)
+ if (shortestEnd < 0) { // Optimization when there is no compressing needed
+ b.append(Integer.toHexString(words[0]))
+ i = 1
+ while (i < words.size) {
+ b.append(':')
+ b.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)) {
+ b.append("::")
+ ipv4Mapped && shortestEnd == 5 && words[5] == 0xffff
+ }
+ else {
+ b.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) {
+ b.append(':')
+ }
+ else {
+ b.append('.')
+ }
+ }
+ if (isIpv4Mapped && i > 5) {
+ b.append(words[i] shr 8)
+ b.append('.')
+ b.append(words[i] and 0xff)
+ }
+ else {
+ b.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
+ b.append("::")
+ }
+ ++i
+ }
+ }
+ return b.toString()
+ }
+
+ /**
+ * 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
+ }
+}
diff --git a/src/dorkbox/netUtil/IfConfig.kt b/src/dorkbox/netUtil/IfConfig.kt
new file mode 100644
index 0000000..7c499f1
--- /dev/null
+++ b/src/dorkbox/netUtil/IfConfig.kt
@@ -0,0 +1,33 @@
+package dorkbox.netUtil
+
+import dorkbox.executor.Executor
+
+/**
+ *
+ */
+object IfConfig {
+ fun assignMac(interfaceName: String, interfaceMac: String) {
+ if (Common.OS_LINUX) {
+ Executor.run("/sbin/ifconfig", interfaceName, "hw", "ether", interfaceMac)
+ } else {
+ throw RuntimeException("NOT IMPL.")
+ }
+
+ }
+
+ fun up(interfaceName: String, interfaceCIDR: String) {
+ if (Common.OS_LINUX) {
+ up(interfaceName, "0.0.0.0", interfaceCIDR)
+ } else {
+ throw RuntimeException("NOT IMPL.")
+ }
+ }
+
+ fun up(interfaceName: String, interfaceIP: String, interfaceCIDR: String) {
+ if (Common.OS_LINUX) {
+ Executor.run("/sbin/ifconfig", interfaceName, "$interfaceIP/$interfaceCIDR", "up");
+ } else {
+ throw RuntimeException("NOT IMPL.")
+ }
+ }
+}
diff --git a/src/dorkbox/netUtil/Iface.kt b/src/dorkbox/netUtil/Iface.kt
new file mode 100644
index 0000000..cfa33fb
--- /dev/null
+++ b/src/dorkbox/netUtil/Iface.kt
@@ -0,0 +1,96 @@
+package dorkbox.netUtil
+
+import dorkbox.executor.Executor
+import java.util.*
+
+/**
+ *
+ */
+object Iface {
+ private val ifToIp: MutableMap = HashMap()
+
+ /**
+ * On disconnect, it will get the IP address for the interface from the cache, instead of from `ifconfig` (since the interface
+ * is down at this point)
+ */
+ fun getIpFromIf(interfaceName: String, isOnClientConnect: Boolean): String {
+ if (Common.OS_LINUX) {
+ if (isOnClientConnect) {
+ val ifaceInfo = Executor.run("/sbin/ifconfig", interfaceName)
+
+ val str = "inet addr:"
+ var index = ifaceInfo.indexOf(str)
+ if (index > -1) {
+ index += str.length
+ val possibleAddr = ifaceInfo.substring(index, ifaceInfo.indexOf(" ", index));
+
+ Common.logger.debug("Found on '{}' possible addr '{}' : ADD", interfaceName, possibleAddr);
+ synchronized(ifToIp) {
+ ifToIp.put(interfaceName, possibleAddr);
+ }
+ return possibleAddr;
+ }
+ }
+ else {
+ var possibleAddr: String?
+ synchronized(ifToIp) { possibleAddr = ifToIp.remove(interfaceName) }
+
+ Common.logger.debug("Found on '{}' possible addr '{}' : REMOVE", interfaceName, possibleAddr)
+ if (possibleAddr != null) {
+ return possibleAddr!!
+ }
+ }
+ }
+ else {
+ throw RuntimeException("NOT IMPL.")
+ }
+
+ return ""
+ }
+
+ fun down(interfaceName: String) {
+ if (Common.OS_LINUX) {
+ Executor.run("/sbin/ip", "link", "set", "dev", interfaceName, "down")
+ }
+ else {
+ throw RuntimeException("NOT IMPL.")
+ }
+ }
+
+ fun up(interfaceName: String) {
+ if (Common.OS_LINUX) {
+ Executor.run("/sbin/ip", "link", "set", "dev", interfaceName, "up")
+ }
+ else {
+ throw RuntimeException("NOT IMPL.")
+ }
+ }
+
+ fun setMac(interfaceName: String, interfaceMac: String) {
+ if (Common.OS_LINUX) {
+ Executor.run("/sbin/ip", "link", "set", "dev", interfaceName, "address", interfaceMac)
+ }
+ else {
+ throw RuntimeException("NOT IMPL.")
+ }
+ }
+
+ fun assignCIDR(interfaceName: String?, cidr: Int) {
+ if (Common.OS_LINUX) {
+ Executor.run("/sbin/ifconfig", "$interfaceName 0.0.0.0/$cidr up")
+ }
+ else {
+ throw RuntimeException("NOT IMPL.")
+ }
+ }
+
+ fun addLoopback() {
+ if (Common.OS_LINUX) {
+ Executor.Companion.run("/sbin/ip", "link", "set", "dev", "lo", "up")
+ Executor.Companion.run("/sbin/ip", "addr", "add", "127.0.0.1", "dev", "lo")
+ }
+ else {
+ throw RuntimeException("NOT IMPL.")
+ }
+ }
+}
diff --git a/src/dorkbox/netUtil/Mac.kt b/src/dorkbox/netUtil/Mac.kt
new file mode 100644
index 0000000..11b6a0c
--- /dev/null
+++ b/src/dorkbox/netUtil/Mac.kt
@@ -0,0 +1,414 @@
+package dorkbox.netUtil
+
+import dorkbox.executor.Executor
+import mu.KLogger
+import java.io.File
+import java.io.Writer
+import java.math.BigInteger
+import java.net.InetAddress
+import java.net.NetworkInterface
+import java.util.*
+import java.util.regex.Pattern
+
+/**
+ *
+ */
+object Mac {
+ enum class MacDelimiter(val delimiter: String) {
+ COLON(":"), PERIOD("."), SPACE(" ");
+ }
+
+ private const val MAC_ADDRESS_LENGTH = 6
+
+ private val random = Random()
+ private val fakeMacsInUse: MutableSet = HashSet()
+
+ private val MAC_ADDRESS_PATTERN by lazy {
+ Pattern.compile("^([a-fA-F0-9]{2}[:\\\\.-]?){5}[a-fA-F0-9]{2}$")
+ }
+
+ private val COLON_REGEX = ":".toRegex()
+
+ init {
+ synchronized(fakeMacsInUse) {}
+ }
+
+ fun getMacAddress(ip: String, logger: KLogger? = null): String {
+ try {
+ val mac = getMacAddressByte(ip, logger)
+ if (mac == null) {
+ logger?.error("Unable to get MAC address for IP '{}'", ip)
+ return ""
+ }
+
+ val s = StringBuilder(18)
+ for (b in mac) {
+ if (s.isNotEmpty()) {
+ s.append(':')
+ }
+ s.append(String.format("%02x", b))
+ }
+ return s.toString()
+ } catch (e: Exception) {
+ if (logger != null) {
+ logger.error("Unable to get MAC address for IP '{}'", ip, e)
+ } else {
+ e.printStackTrace()
+ }
+ }
+
+ logger?.error("Unable to get MAC address for IP '{}'", ip)
+
+ return ""
+ }
+
+ fun getMacAddressByte(ip: String, logger: KLogger? = null): ByteArray? {
+ try {
+ val addr = InetAddress.getByName(ip)
+ val networkInterface = NetworkInterface.getByInetAddress(addr)
+ if (networkInterface == null) {
+ logger?.error("Unable to get MAC address for IP '{}'", ip)
+ return null
+ }
+
+ val mac = networkInterface.hardwareAddress
+ if (mac == null) {
+ logger?.error("Unable to get MAC address for IP '{}'", ip)
+ return null
+ }
+
+ return mac
+ } catch (e: Exception) {
+ if (logger != null) {
+ logger.error("Unable to get MAC address for IP '{}'", ip, e)
+ } else {
+ e.printStackTrace()
+ }
+ }
+
+ return null
+ }
+
+ /**
+ * Removes a mac that was in use, to free it's use later on
+ */
+ fun freeFakeMac(fakeMac: String) {
+ synchronized(fakeMacsInUse) { fakeMacsInUse.remove(fakeMac) }
+ }
+
+ /**
+ * Removes a mac that was in use, to free it's use later on
+ */
+ fun freeFakeMac(vararg fakeMacs: String) {
+ synchronized(fakeMacsInUse) {
+ fakeMacs.forEach {
+ fakeMacsInUse.remove(it)
+ }
+ }
+ }
+
+ /**
+ * will also make sure that this mac doesn't already exist
+ *
+ * @return a unique MAC that can be used for VPN devices + setup. This is a LOWERCASE string!
+ */
+ val fakeMac: String
+ get() {
+ synchronized(fakeMacsInUse) {
+ var mac = fakeVpnMacUnsafe
+
+ // gotta make sure it doesn't already exist
+ while (fakeMacsInUse.contains(mac)) {
+ mac = fakeVpnMacUnsafe
+ }
+
+ fakeMacsInUse.add(mac)
+ return mac
+ }
+ }
+
+ /**
+ * will also make sure that this mac doesn't already exist
+ *
+ * @return a unique MAC that can be used for VPN devices + setup. This is a LOWERCASE string!
+ */
+ val fakeDockerMac: String
+ get() {
+ synchronized(fakeMacsInUse) {
+ var mac = fakeDockerMacUnsafe
+
+ // gotta make sure it doesn't already exist
+ while (fakeMacsInUse.contains(mac)) {
+ mac = fakeDockerMacUnsafe
+ }
+ fakeMacsInUse.add(mac)
+
+ return mac
+ }
+ }
+
+ /**
+ * http://serverfault.com/questions/40712/what-range-of-mac-addresses-can-i-safely-use-for-my-virtual-machines
+ *
+ * @return a mac that is safe to use for fake interfaces. THIS IS LOWERCASE!
+ */
+ val fakeVpnMacUnsafe: String
+ get() {
+ var vpnID = randHex
+ while (vpnID == "d0") {
+ // this is what is used for docker. NEVER assign it to a VPN mac!!
+ vpnID = randHex
+ }
+
+ return "02:$vpnID:$randHex:$randHex:$randHex:$randHex"
+ }
+
+ /**
+ * http://serverfault.com/questions/40712/what-range-of-mac-addresses-can-i-safely-use-for-my-virtual-machines
+ *
+ * @return a mac that is safe to use for fake interfaces. THIS IS LOWERCASE!
+ */
+ val fakeDockerMacUnsafe: String
+ get() {
+ return "02:d0:$randHex:$randHex:$randHex:$randHex"
+ }
+
+ /**
+ * will also make sure that this mac doesn't already exist
+ *
+ * @return a unique MAC that can be used for VPN devices + setup
+ */
+ fun getFakeVpnMac(vpnKeyDirForExistingVpnMacs: String): String {
+ synchronized(fakeMacsInUse) {
+ var mac = fakeVpnMacUnsafe
+
+ // gotta make sure it doesn't already exist
+ while (fakeMacsInUse.contains(mac) || File(vpnKeyDirForExistingVpnMacs, "$mac.crt").exists()) {
+ mac = fakeVpnMacUnsafe
+ }
+
+ fakeMacsInUse.add(mac)
+ return mac
+ }
+ }
+
+ private val randHex: String
+ get() {
+ val i = random.nextInt(255)
+
+ return if (i < 16) {
+ "0" + Integer.toHexString(i)
+ } else {
+ Integer.toHexString(i)
+ }
+ }
+
+ /**
+ * Converts a long into a properly formatted lower-case string
+ */
+ fun toStringLowerCase(mac: Long): String {
+ // we only use the right-most 6 bytes (of 8 bytes).
+ val macBytes = toBytes(mac)
+
+ // mac should have 6 bytes.
+ val buf = StringBuilder()
+ (0..8).forEach { index ->
+ val byte = macBytes[index]
+ if (buf.isNotEmpty()) {
+ buf.append(':');
+ }
+
+ if (byte in 0..15) {
+ buf.append('0');
+ }
+
+ buf.append(Integer.toHexString(byte.toUByte().toInt()))
+ }
+
+ return buf.toString()
+ }
+
+ @Throws(Exception::class)
+ fun writeStringLowerCase(mac: Long, writer: Writer) {
+ // we only use the right-most 6 bytes.
+ val macBytes = toBytes(mac)
+
+ // mac should have 6 bytes.
+ var bytesWritten = 0;
+ (0..8).forEach { index ->
+ val byte = macBytes[index]
+
+ if (bytesWritten != 0) {
+ writer.write(":");
+ bytesWritten++;
+ }
+
+ if (byte in 0..15) {
+ writer.append('0');
+ }
+
+ writer.write(Integer.toHexString(byte.toUByte().toInt()))
+ bytesWritten += 2;
+ }
+ }
+
+ private fun toBytes(x: Long): ByteArray {
+ return byteArrayOf((x shr 56).toByte(),
+ (x shr 48).toByte(),
+ (x shr 40).toByte(),
+ (x shr 32).toByte(),
+ (x shr 24).toByte(),
+ (x shr 16).toByte(),
+ (x shr 8).toByte(),
+ (x shr 0).toByte())
+ }
+
+
+ fun toStringLowerCase(mac: ByteArray): String {
+ // mac should have 6 bytes.
+ val buf = StringBuilder()
+ for (b in mac) {
+ if (buf.isNotEmpty()) {
+ buf.append(':')
+ }
+ if (b in 0..15) {
+ buf.append('0')
+ }
+
+ buf.append(Integer.toHexString(if (b < 0) b + 256 else b.toInt()))
+ }
+ return buf.toString()
+ }
+
+ fun toStringUpperCase(mac: ByteArray): String {
+ val buf = StringBuilder()
+ for (b in mac) {
+ if (buf.isNotEmpty()) {
+ buf.append(':')
+ }
+ if (b in 0..15) {
+ buf.append('0')
+ }
+
+ buf.append(Integer.toHexString(b.toUByte().toInt()).toUpperCase())
+ }
+ return buf.toString()
+ }
+
+ fun toBytes(mac: String): ByteArray {
+ val s = mac.replace(COLON_REGEX, "")
+ return BigInteger(s, 16).toByteArray()
+ }
+
+ fun toLong(mac: ByteArray): Long {
+ return ((mac[5].toLong() and 0xff)
+ + (mac[4].toLong() and 0xff shl 8)
+ + (mac[3].toLong() and 0xff shl 16)
+ + (mac[2].toLong() and 0xff shl 24)
+ + (mac[1].toLong() and 0xff shl 32)
+ + (mac[0].toLong() and 0xff shl 40))
+ }
+
+ fun toLong(mac: String, delimiter: MacDelimiter? = MacDelimiter.COLON): Long {
+ val addressInBytes = ByteArray(MAC_ADDRESS_LENGTH)
+ try {
+ val elements: Array
+
+ if (delimiter != null) {
+ elements = mac.split(delimiter.delimiter.toRegex()).toTypedArray()
+ } else {
+ elements = arrayOfNulls(MAC_ADDRESS_LENGTH)
+ var index = 0
+ var substringPos = 0
+ while (index < MAC_ADDRESS_LENGTH) {
+ elements[index] = mac.substring(substringPos, substringPos + 2)
+ index++
+ substringPos += 2
+ }
+ }
+
+ for (i in 0 until MAC_ADDRESS_LENGTH) {
+ val element = elements[i]
+ addressInBytes[i] = element!!.toInt(16).toByte()
+ }
+ } catch (e: Exception) {
+ Common.logger.error("Error parsing MAC address '{}'", mac, e)
+ }
+
+ return toLong(addressInBytes)
+ }
+
+ fun isValid(macAsString: String): Boolean {
+ if (macAsString.isEmpty()) {
+ return false
+ }
+
+ // Check whether mac is separated by a colon, period, space, or nothing at all.
+ // Must standardize to a colon.
+ var normalizedMac: String = macAsString
+ if (normalizedMac.split(COLON_REGEX).toTypedArray().size != 6) {
+
+ // Does not already use colons, must modify the mac to use colons.
+ when {
+ normalizedMac.contains(".") -> {
+ // Period notation
+ normalizedMac = normalizedMac.replace(".", ":")
+ }
+ normalizedMac.contains(" ") -> {
+ // Space notation
+ normalizedMac = normalizedMac.replace(" ", ":")
+ }
+ else -> {
+ // No delimiter, manually add colons.
+ normalizedMac = ""
+ var index = 0
+ var substringPos = 0
+
+ // We ensure that the substring function will not exceed the length of the given string if the string is not at least
+ //(MAC_ADDRESS_LENGTH * 2) characters long.
+ while (index < MAC_ADDRESS_LENGTH && macAsString.length >= substringPos + 2) {
+ // Reconstruct the string, adding colons.
+ normalizedMac = if (index != MAC_ADDRESS_LENGTH - 1) {
+ normalizedMac + macAsString.substring(substringPos, substringPos + 2) + ":"
+ } else {
+ normalizedMac + macAsString.substring(substringPos, substringPos + 2)
+ }
+
+ index++
+ substringPos += 2
+ }
+ }
+ }
+ }
+ val matcher = MAC_ADDRESS_PATTERN.matcher(normalizedMac)
+ return matcher.matches()
+ }
+
+ /**
+ * Returns the type of delmiter based on how a mac address is constructed. Returns null if no delimiter.
+ * @return a MacDelimiter if there is one, or null if the mac address is one long string.
+ */
+ fun getMacDelimiter(macAsString: String): MacDelimiter? {
+ when {
+ macAsString.contains(":") -> {
+ return MacDelimiter.COLON
+ }
+ macAsString.contains(".") -> {
+ return MacDelimiter.PERIOD
+ }
+ macAsString.contains(" ") -> {
+ return MacDelimiter.SPACE
+ }
+ else -> return null
+ }
+ }
+
+ fun assign(interfaceName: String, macAddress: String) {
+ if (Common.OS_LINUX) {
+ Executor.run("/sbin/ifconfig", interfaceName, "hw", "ether", macAddress);
+ throw RuntimeException("NOT IMPL.")
+ } else {
+ throw RuntimeException("NOT IMPL.")
+ }
+ }
+}
diff --git a/src/dorkbox/netUtil/NameSpace.kt b/src/dorkbox/netUtil/NameSpace.kt
new file mode 100644
index 0000000..384f65c
--- /dev/null
+++ b/src/dorkbox/netUtil/NameSpace.kt
@@ -0,0 +1,127 @@
+package dorkbox.netUtil
+
+import dorkbox.executor.Executor
+import java.io.File
+import java.util.*
+
+/**
+ *
+ */
+object NameSpace {
+ private val nameSpaceToIifToIp: MutableMap> = HashMap()
+
+ fun add(nameSpace: String) {
+ if (Common.OS_LINUX) {
+ Executor.run("/sbin/ip", "netns", "add", nameSpace)
+ } else {
+ throw RuntimeException("NOT IMPL.")
+ }
+ }
+
+ fun delete(nameSpace: String) {
+ if (Common.OS_LINUX) {
+ Executor.run("/sbin/ip", "netns", "del", nameSpace);
+ } else {
+ throw RuntimeException("NOT IMPL.")
+ }
+ }
+
+ fun dhcpStart(nameSpace: String, id: String, interfaceName: String) {
+ if (Common.OS_LINUX) {
+ dhcpStop(nameSpace, id, interfaceName)
+ val dhcpPidFile = "/var/run/dhclient-$id.pid"
+ run(nameSpace, "/sbin/dhclient", "-pf", dhcpPidFile, interfaceName)
+ } else {
+ throw RuntimeException("NOT IMPL.")
+ }
+
+ }
+
+ fun dhcpStop(nameSpace: String, id: String, interfaceName: String) {
+ if (Common.OS_LINUX) {
+ val dhcpPidFile = "/var/run/dhclient-$id.pid"
+
+ // close the dhclient if it was already running (based on pid file), and delete the pid file
+ run(nameSpace, "/sbin/dhclient", "-r -pf", dhcpPidFile, interfaceName)
+
+ // short break
+ try {
+ Thread.sleep(500)
+ } catch (e: InterruptedException) {
+ e.printStackTrace()
+ }
+ File(dhcpPidFile).delete()
+ } else {
+ throw RuntimeException("NOT IMPL.")
+ }
+ }
+
+ fun run(nameSpace: String, vararg args: String): String {
+ if (Common.OS_LINUX) {
+ val command = mutableListOf()
+ command.add("/sbin/ip")
+ command.add("netns")
+ command.add("exec")
+ command.add(nameSpace)
+ command.addAll(listOf(*args))
+
+ return Executor.run(command)
+ } else {
+ throw RuntimeException("NOT IMPL.")
+ }
+ }
+
+ /**
+ * On client disconnect, it will get the IP address for the interface from the cache, instead of from `ifconfig` (since the
+ * interface
+ * is down at this point)
+ */
+ fun getIpFromIf(nameSpace: String, interfaceName: String, isOnClientConnect: Boolean): String {
+ if (Common.OS_LINUX) {
+ if (isOnClientConnect) {
+ val ifaceInfo = run(nameSpace, "/sbin/ifconfig", interfaceName)
+ val str = "inet addr:"
+ var index = ifaceInfo.indexOf(str)
+ if (index > -1) {
+ index += str.length
+ val possibleAddr = ifaceInfo.substring(index, ifaceInfo.indexOf(" ", index))
+ Common.logger.debug("Found on '{}' possible addr '{}' : ADD", interfaceName, possibleAddr)
+ synchronized(nameSpaceToIifToIp) {
+ var ifToIp = nameSpaceToIifToIp[nameSpace]
+ if (ifToIp == null) {
+ ifToIp = HashMap()
+ nameSpaceToIifToIp[nameSpace] = ifToIp
+ }
+ ifToIp.put(interfaceName, possibleAddr)
+ }
+ return possibleAddr
+ }
+ } else {
+ var possibleAddr: String? = ""
+ synchronized(nameSpaceToIifToIp) {
+ val ifToIp = nameSpaceToIifToIp[nameSpace]
+ if (ifToIp != null) {
+ possibleAddr = ifToIp.remove(interfaceName)
+ }
+ }
+ Common.logger.debug("Found on '{}' possible addr '{}' : REMOVE", interfaceName, possibleAddr)
+ if (possibleAddr != null) {
+ return possibleAddr!!
+ }
+ }
+ return ""
+ } else {
+ throw RuntimeException("NOT IMPL.")
+ }
+
+ }
+
+ fun addLoopback(nameSpace: String) {
+ if (Common.OS_LINUX) {
+ run(nameSpace, "/sbin/ip link set dev lo up")
+ run(nameSpace, "/sbin/ip addr add 127.0.0.1 dev lo")
+ } else {
+ throw RuntimeException("NOT IMPL.")
+ }
+ }
+}
diff --git a/src/dorkbox/netUtil/Route.kt b/src/dorkbox/netUtil/Route.kt
new file mode 100644
index 0000000..88c5ad1
--- /dev/null
+++ b/src/dorkbox/netUtil/Route.kt
@@ -0,0 +1,24 @@
+package dorkbox.netUtil
+
+import dorkbox.executor.Executor
+
+/**
+ *
+ */
+object Route {
+ fun flush(nameSpace: String) {
+ if (Common.OS_LINUX) {
+ NameSpace.run(nameSpace, "/sbin/ip route flush cache")
+ } else {
+ throw RuntimeException("NOT IMPL.")
+ }
+ }
+
+ fun add(targetIpAndCidr: String, hostIP: String, hostInterface: String) {
+ if (Common.OS_LINUX) {
+ Executor.run("/sbin/ip", "route", "add", targetIpAndCidr, "via", hostIP, "dev", hostInterface);
+ } else {
+ throw RuntimeException("NOT IMPL.")
+ }
+ }
+}
diff --git a/src/dorkbox/netUtil/Sntp.kt b/src/dorkbox/netUtil/Sntp.kt
new file mode 100644
index 0000000..dae0a1e
--- /dev/null
+++ b/src/dorkbox/netUtil/Sntp.kt
@@ -0,0 +1,410 @@
+package dorkbox.netUtil
+
+import java.net.DatagramPacket
+import java.net.DatagramSocket
+import java.net.InetAddress
+import java.util.*
+import java.util.concurrent.TimeUnit
+
+/**
+ *
+ */
+object Sntp {
+ /**
+ * Simple SNTP client class for retrieving network time.
+ *
+ * https://tools.ietf.org/html/rfc2030
+ *
+ * Sample usage:
+ * SntpClient client = new SntpClient();
+ * if (client.requestTime("time.foo.com")) {
+ * long now = client.getNtpTime() + SystemClock.elapsedRealtime() - client.getNtpTimeReference();
+ * }
+
*
+ */
+ class SntpClient {
+ companion object {
+ private class InvalidServerReplyException(message: String) : Exception(message)
+
+ private const val TAG = "SntpClient"
+ private const val DBG = true
+ private const val REFERENCE_TIME_OFFSET = 16
+ private const val ORIGINATE_TIME_OFFSET = 24
+ private const val RECEIVE_TIME_OFFSET = 32
+ private const val TRANSMIT_TIME_OFFSET = 40
+ private const val NTP_PACKET_SIZE = 48
+ private const val NTP_PORT = 123
+ private const val NTP_MODE_CLIENT = 3
+ private const val NTP_MODE_SERVER = 4
+ private const val NTP_MODE_BROADCAST = 5
+ private const val NTP_VERSION = 3
+ private const val NTP_LEAP_NOSYNC = 3
+ private const val NTP_STRATUM_DEATH = 0
+ private const val NTP_STRATUM_MAX = 15
+
+ // Number of seconds between Jan 1, 1900 and Jan 1, 1970
+ // 70 years plus 17 leap days
+ private const val OFFSET_1900_TO_1970 = (365L * 70L + 17L) * 24L * 60L * 60L
+
+ @Throws(InvalidServerReplyException::class)
+ private fun checkValidServerReply(leap: Int, mode: Int, stratum: Int, transmitTime: Long) {
+ if (leap == NTP_LEAP_NOSYNC) {
+ throw InvalidServerReplyException("unsynchronized server")
+ }
+ if (mode != NTP_MODE_SERVER && mode != NTP_MODE_BROADCAST) {
+ throw InvalidServerReplyException("untrusted mode: $mode")
+ }
+ if (stratum == NTP_STRATUM_DEATH || stratum > NTP_STRATUM_MAX) {
+ throw InvalidServerReplyException("untrusted stratum: $stratum")
+ }
+ if (transmitTime == 0L) {
+ throw InvalidServerReplyException("zero transmitTime")
+ }
+ }
+ }
+
+ /**
+ * This is a two-bit code warning of an impending leap second to be
+ * inserted/deleted in the last minute of the current day. It's values
+ * may be as follows:
+ *
+ * Value Meaning
+ * ----- -------
+ * 0 no warning
+ * 1 last minute has 61 seconds
+ * 2 last minute has 59 seconds)
+ * 3 alarm condition (clock not synchronized)
+ */
+ var leap: Int = 0
+ private set
+
+
+ /**
+ * This value indicates the NTP/SNTP version number. The version number
+ * is 3 for Version 3 (IPv4 only) and 4 for Version 4 (IPv4, IPv6 and OSI).
+ * If necessary to distinguish between IPv4, IPv6 and OSI, the
+ * encapsulating context must be inspected.
+ */
+ var version: Int = 0
+ private set
+
+ /**
+ * This value indicates the mode, with values defined as follows:
+ *
+ * Mode Meaning
+ * ---- -------
+ * 0 reserved
+ * 1 symmetric active
+ * 2 symmetric passive
+ * 3 client
+ * 4 server
+ * 5 broadcast
+ * 6 reserved for NTP control message
+ * 7 reserved for private use
+ *
+ * In unicast and anycast modes, the client sets this field to 3 (client)
+ * in the request and the server sets it to 4 (server) in the reply. In
+ * multicast mode, the server sets this field to 5 (broadcast).
+ */
+ var mode: Int = 0
+ private set
+
+ /**
+ * This value indicates the stratum level of the local clock, with values
+ * defined as follows:
+ *
+ * Stratum Meaning
+ * ----------------------------------------------
+ * 0 unspecified or unavailable
+ * 1 primary reference (e.g., radio clock)
+ * 2-15 secondary reference (via NTP or SNTP)
+ * 16-255 reserved
+ */
+ var stratum: Int = 0
+ private set
+
+ /**
+ * This value indicates the maximum interval between successive messages,
+ * in seconds to the nearest power of two. The values that can appear in
+ * this field presently range from 4 (16 s) to 14 (16284 s); however, most
+ * applications use only the sub-range 6 (64 s) to 10 (1024 s).
+ */
+ var pollInterval: Int = 0
+ private set
+
+ /**
+ * This value indicates the precision of the local clock, in seconds to
+ * the nearest power of two. The values that normally appear in this field
+ * range from -6 for mains-frequency clocks to -20 for microsecond clocks
+ * found in some workstations.
+ */
+ var precision: Int = 0
+ private set
+
+ /**
+ * This value indicates the total roundtrip delay to the primary reference
+ * source, in seconds. Note that this variable can take on both positive
+ * and negative values, depending on the relative time and frequency
+ * offsets. The values that normally appear in this field range from
+ * negative values of a few milliseconds to positive values of several
+ * hundred milliseconds.
+ */
+ var rootDelay = 0.0
+
+
+ /**
+ * This value indicates the nominal error relative to the primary reference
+ * source, in seconds. The values that normally appear in this field
+ * range from 0 to several hundred milliseconds.
+ */
+ var rootDispersion = 0.0
+
+ /**
+ * This is the time at which the local clock was last set or corrected, in
+ * seconds since 00:00 1-Jan-1900.
+ */
+ var referenceTimestamp: Long = 0L
+ private set
+
+ /**
+ * This is the time at which the request departed the client for the
+ * server, in seconds since 00:00 1-Jan-1900.
+ */
+ var originateTimestamp: Long = 0L
+ private set
+
+ /**
+ * This is the time at which the request arrived at the server, in seconds
+ * since 00:00 1-Jan-1900.
+ */
+ var receiveTimestamp: Long = 0L
+ private set
+
+ /**
+ * This is the time at which the reply departed the server for the client,
+ * in seconds since 00:00 1-Jan-1900.
+ */
+ var transmitTimestamp: Long = 0L
+ private set
+
+ /**
+ * This is the time at which the client received the NTP server response
+ */
+ var destinationTimestamp: Long = 0L
+ private set
+
+
+ /**
+ * @return time value computed from NTP server response.
+ */
+ var ntpTime: Long = 0
+ private set
+
+ /**
+ * Returns the round trip time of the NTP transaction
+ *
+ * @return round trip time in milliseconds.
+ */
+ var roundTripDelay: Long = 0
+ private set
+
+ var localClockOffset: Long = 0
+ private set
+
+ /**
+ * Sends an SNTP request to the given host and processes the response.
+ *
+ * @param host host name of the server.
+ * @param timeoutMS network timeout in milliseconds to wait for a response.
+ *
+ * @return true if the transaction was successful.
+ */
+ internal fun requestTime(host: String, timeoutMS: Int): SntpClient {
+ var socket: DatagramSocket? = null
+ var address: InetAddress? = null
+
+ try {
+ socket = DatagramSocket()
+ socket.soTimeout = timeoutMS
+ address = InetAddress.getByName(host)
+
+ val buffer = ByteArray(NTP_PACKET_SIZE)
+ val request = DatagramPacket(buffer, buffer.size, address, NTP_PORT)
+
+ // set mode = 3 (client) and version = 3
+ // mode is in low 3 bits of first byte
+ // version is in bits 3-5 of first byte
+ buffer[0] = (NTP_MODE_CLIENT or (NTP_VERSION shl 3)).toByte()
+
+
+ // don't want random number generation polluting the timing
+ val randomNumber = kotlin.random.Random.nextInt(255).toByte()
+
+ // get current time and write it to the request packet
+ val timeAtSend = System.currentTimeMillis() // this is UTC from epic
+ writeTimeStamp(buffer, TRANSMIT_TIME_OFFSET, timeAtSend, randomNumber)
+
+ socket.send(request)
+
+ // read the response
+ val response = DatagramPacket(buffer, buffer.size)
+ socket.receive(response)
+
+ destinationTimestamp = System.currentTimeMillis() // this is UTC from epic
+
+ // we don't want the socket close operation time to pollute the NTP response timing, so close it afterwards
+ socket.close()
+
+ // extract the results
+
+ // See the packet format diagram in RFC 2030 for more information
+ leap = buffer[0].toInt() shr 6 and 0x3
+ version = buffer[0].toInt() shr 3 and 0x7
+ mode = buffer[0].toInt() and 0x7
+ stratum = buffer[1].toInt()
+ pollInterval = buffer[2].toInt()
+ precision = buffer[3].toInt()
+
+ rootDelay = (buffer[4] * 256.0) +
+ buffer[5].toInt() +
+ (buffer[6].toInt() / 256.0) +
+ (buffer[7].toInt() / 65536.0)
+
+ rootDispersion = (buffer[8].toInt() * 256.0) +
+ buffer[9].toInt() +
+ (buffer[10].toInt() / 256.0) +
+ (buffer[11].toInt() / 65536.0)
+
+
+ referenceTimestamp = readTimeStamp(buffer, REFERENCE_TIME_OFFSET)
+ originateTimestamp = readTimeStamp(buffer, ORIGINATE_TIME_OFFSET)
+ receiveTimestamp = readTimeStamp(buffer, RECEIVE_TIME_OFFSET)
+ transmitTimestamp = readTimeStamp(buffer, TRANSMIT_TIME_OFFSET)
+
+ checkValidServerReply(leap, mode, stratum, transmitTimestamp)
+
+ // Formula for delay according to the RFC2030
+ // Timestamp Name ID When Generated
+ // ------------------------------------------------------------
+ // Originate Timestamp T1 time request sent by client
+ // Receive Timestamp T2 time request received by server
+ // Transmit Timestamp T3 time reply sent by server
+ // Destination Timestamp T4 time reply received by client
+ //
+ // The roundtrip delay d and local clock offset t are defined as follows:
+ //
+ // delay = (T4 - T1) - (T3 - T2)
+ // offset = ((T2 - T1) + (T3 - T4)) / 2
+
+ roundTripDelay = (destinationTimestamp - originateTimestamp) - (transmitTimestamp - receiveTimestamp)
+ localClockOffset = ((receiveTimestamp - originateTimestamp) + (transmitTimestamp - destinationTimestamp)) / 2L
+ } catch (e: Exception) {
+ System.err.println("Error with NTP to $address. $e")
+ } finally {
+ socket?.close()
+ }
+
+ return this
+ }
+
+ /**
+ * Reads an unsigned 32 bit big endian number from the given offset in the buffer.
+ */
+ private fun read32(buffer: ByteArray, offset: Int): Long {
+ val b0 = buffer[offset].toInt()
+ val b1 = buffer[offset + 1].toInt()
+ val b2 = buffer[offset + 2].toInt()
+ val b3 = buffer[offset + 3].toInt()
+
+ // convert signed bytes to unsigned values
+ val i0 = (if (b0 and 0x80 == 0x80) (b0 and 0x7F) + 0x80 else b0).toLong()
+ val i1 = (if (b1 and 0x80 == 0x80) (b1 and 0x7F) + 0x80 else b1).toLong()
+ val i2 = (if (b2 and 0x80 == 0x80) (b2 and 0x7F) + 0x80 else b2).toLong()
+ val i3 = (if (b3 and 0x80 == 0x80) (b3 and 0x7F) + 0x80 else b3).toLong()
+ return (i0 shl 24) + (i1 shl 16) + (i2 shl 8) + i3
+ }
+
+ /**
+ * Reads the NTP time stamp at the given offset in the buffer and returns
+ * it as a system time (milliseconds since January 1, 1970).
+ */
+ private fun readTimeStamp(buffer: ByteArray, offset: Int): Long {
+ val seconds = read32(buffer, offset)
+ val fraction = read32(buffer, offset + 4)
+
+ // Special case: zero means zero.
+ return if (seconds == 0L && fraction == 0L) {
+ 0
+ } else (seconds - OFFSET_1900_TO_1970) * 1000 + fraction * 1000L / 0x100000000L
+ }
+
+ /**
+ * Writes system time (milliseconds since January 1, 1970) as an NTP time stamp
+ * at the given offset in the buffer.
+ */
+ private fun writeTimeStamp(buffer: ByteArray, @Suppress("SameParameterValue") offset_: Int, time: Long, randomNumber: Byte) {
+ var offset = offset_
+
+ if (time == 0L) {
+ // Special case: zero means zero.
+ Arrays.fill(buffer, offset, offset + 8, 0x00.toByte())
+ return
+ }
+
+ var seconds: Long = TimeUnit.MILLISECONDS.toSeconds(time)
+ val remainingNotSeconds = time - TimeUnit.SECONDS.toMillis(seconds)
+
+ // now apply the required offset (NTP is based on 1900)
+ seconds += OFFSET_1900_TO_1970
+
+ val fraction = remainingNotSeconds * 0x100000000L / 1000L
+
+ // write seconds in big endian format
+ buffer[offset++] = (seconds shr 24).toByte()
+ buffer[offset++] = (seconds shr 16).toByte()
+ buffer[offset++] = (seconds shr 8).toByte()
+ buffer[offset++] = (seconds shr 0).toByte()
+
+ // write fraction in big endian format
+ buffer[offset++] = (fraction shr 24).toByte()
+ buffer[offset++] = (fraction shr 16).toByte()
+ buffer[offset++] = (fraction shr 8).toByte()
+
+ // From RFC 2030: It is advisable to fill the non-significant
+ // low order bits of the timestamp with a random, unbiased
+ // bitstring, both to avoid systematic roundoff errors and as
+ // a means of loop detection and replay detection.
+ buffer[offset] = randomNumber
+ }
+ }
+
+ /**
+ * Sends an SNTP request to the given host and processes the response.
+ *
+ * @param server host name of the SNTP server.
+ * @param timeoutInMS network timeout in milliseconds to wait for a response.
+ */
+ fun update(server: String, timeoutInMS: Int = 10_000): SntpClient {
+ return SntpClient().requestTime(server, timeoutInMS)
+ }
+
+// @JvmStatic
+// fun main(args: Array) {
+// val ntp = update("time.mit.edu")
+//
+// // Display response
+// val now = System.currentTimeMillis()
+// val cor = now + ntp.localClockOffset
+//
+// System.out.printf ("Originate time: %1\$ta, %1\$td %1\$tb %1\$tY, %1\$tI:%1\$tm:%1\$tS.%1\$tL %1\$tp %1\$tZ%n", ntp.originateTimestamp);
+// System.out.printf ("Receive time: %1\$ta, %1\$td %1\$tb %1\$tY, %1\$tI:%1\$tm:%1\$tS.%1\$tL %1\$tp %1\$tZ%n", ntp.receiveTimestamp)
+// System.out.printf ("Transmit time: %1\$ta, %1\$td %1\$tb %1\$tY, %1\$tI:%1\$tm:%1\$tS.%1\$tL %1\$tp %1\$tZ%n", ntp.transmitTimestamp)
+// System.out.printf ("Destination time: %1\$ta, %1\$td %1\$tb %1\$tY, %1\$tI:%1\$tm:%1\$tS.%1\$tL %1\$tp %1\$tZ%n", ntp.destinationTimestamp)
+//
+// System.out.println("RoundTripDelay : ${ntp.roundTripDelay}")
+// System.out.println("ClockOffset : ${ntp.localClockOffset}")
+//
+// System.out.printf ("Local time: %1\$ta, %1\$td %1\$tb %1\$tY, %1\$tI:%1\$tm:%1\$tS.%1\$tL %1\$tp %1\$tZ%n", now)
+// System.out.printf ("Corrected time: %1\$ta, %1\$td %1\$tb %1\$tY, %1\$tI:%1\$tm:%1\$tS.%1\$tL %1\$tp %1\$tZ%n", cor)
+// }
+}
diff --git a/src/dorkbox/netUtil/SocketUtils.kt b/src/dorkbox/netUtil/SocketUtils.kt
new file mode 100644
index 0000000..87585f3
--- /dev/null
+++ b/src/dorkbox/netUtil/SocketUtils.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2016 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.io.IOException
+import java.net.*
+import java.nio.channels.DatagramChannel
+import java.nio.channels.ServerSocketChannel
+import java.nio.channels.SocketChannel
+import java.security.AccessController
+import java.security.PrivilegedAction
+import java.security.PrivilegedActionException
+import java.security.PrivilegedExceptionAction
+import java.util.*
+
+/**
+ * Provides socket operations with privileges enabled. This is necessary for applications that use the
+ * [SecurityManager] to restrict [SocketPermission] to their application.
+ *
+ * By asserting that these operations are privileged, the operations can proceed even if some code in the calling chain lacks
+ * the appropriate [SocketPermission].
+ */
+object SocketUtils {
+ private val EMPTY = Collections.enumeration(emptyList())
+ private fun empty(): Enumeration {
+ return EMPTY as Enumeration
+ }
+
+ @Throws(IOException::class)
+ fun connect(socket: Socket, remoteAddress: SocketAddress?, timeout: Int) {
+ try {
+ AccessController.doPrivileged(PrivilegedExceptionAction {
+ socket.connect(remoteAddress, timeout)
+ null
+ })
+ } catch (e: PrivilegedActionException) {
+ throw (e.cause as IOException?)!!
+ }
+ }
+
+ @Throws(IOException::class)
+ fun bind(socket: Socket, bindpoint: SocketAddress?) {
+ try {
+ AccessController.doPrivileged(PrivilegedExceptionAction {
+ socket.bind(bindpoint)
+ null
+ })
+ } catch (e: PrivilegedActionException) {
+ throw (e.cause as IOException?)!!
+ }
+ }
+
+ @Throws(IOException::class)
+ fun connect(socketChannel: SocketChannel, remoteAddress: SocketAddress?): Boolean {
+ return try {
+ AccessController.doPrivileged(PrivilegedExceptionAction { socketChannel.connect(remoteAddress) })
+ } catch (e: PrivilegedActionException) {
+ throw (e.cause as IOException?)!!
+ }
+ }
+
+ @Throws(IOException::class)
+ fun bind(socketChannel: SocketChannel, address: SocketAddress?) {
+ try {
+ AccessController.doPrivileged(PrivilegedExceptionAction {
+ socketChannel.bind(address)
+ null
+ })
+ } catch (e: PrivilegedActionException) {
+ throw (e.cause as IOException?)!!
+ }
+ }
+
+ @Throws(IOException::class)
+ fun accept(serverSocketChannel: ServerSocketChannel): SocketChannel {
+ return try {
+ AccessController.doPrivileged(PrivilegedExceptionAction { serverSocketChannel.accept() })
+ } catch (e: PrivilegedActionException) {
+ throw (e.cause as IOException?)!!
+ }
+ }
+
+ @Throws(IOException::class)
+ fun bind(networkChannel: DatagramChannel, address: SocketAddress?) {
+ try {
+ AccessController.doPrivileged(PrivilegedExceptionAction {
+ networkChannel.bind(address)
+ null
+ })
+ } catch (e: PrivilegedActionException) {
+ throw (e.cause as IOException?)!!
+ }
+ }
+
+ fun localSocketAddress(socket: ServerSocket): SocketAddress {
+ return AccessController.doPrivileged(PrivilegedAction { socket.localSocketAddress })
+ }
+
+ @Throws(UnknownHostException::class)
+ fun addressByName(hostname: String?): InetAddress {
+ return try {
+ AccessController.doPrivileged(PrivilegedExceptionAction { InetAddress.getByName(hostname) })
+ } catch (e: PrivilegedActionException) {
+ throw (e.cause as UnknownHostException?)!!
+ }
+ }
+
+ @Throws(UnknownHostException::class)
+ fun allAddressesByName(hostname: String?): Array {
+ return try {
+ AccessController.doPrivileged(PrivilegedExceptionAction { InetAddress.getAllByName(hostname) })
+ } catch (e: PrivilegedActionException) {
+ throw (e.cause as UnknownHostException?)!!
+ }
+ }
+
+ fun socketAddress(hostname: String?, port: Int): InetSocketAddress {
+ return AccessController.doPrivileged(PrivilegedAction { InetSocketAddress(hostname, port) })
+ }
+
+ fun addressesFromNetworkInterface(intf: NetworkInterface): Enumeration {
+ // Android seems to sometimes return null even if this is not a valid return value by the api docs.
+ // Just return an empty Enumeration in this case.
+ // See https://github.com/netty/netty/issues/10045
+ return AccessController.doPrivileged(PrivilegedAction { intf.inetAddresses })
+ ?: return empty()
+ }
+
+ fun loopbackAddress(): InetAddress {
+ return AccessController.doPrivileged(PrivilegedAction {
+ return@PrivilegedAction InetAddress.getLoopbackAddress()
+ })
+ }
+
+ @Throws(SocketException::class)
+ fun hardwareAddressFromNetworkInterface(intf: NetworkInterface): ByteArray {
+ return try {
+ AccessController.doPrivileged(PrivilegedExceptionAction { intf.hardwareAddress })
+ } catch (e: PrivilegedActionException) {
+ throw (e.cause as SocketException?)!!
+ }
+ }
+}
diff --git a/src/dorkbox/netUtil/VirtualEth.kt b/src/dorkbox/netUtil/VirtualEth.kt
new file mode 100644
index 0000000..b2842d6
--- /dev/null
+++ b/src/dorkbox/netUtil/VirtualEth.kt
@@ -0,0 +1,21 @@
+package dorkbox.netUtil
+
+/**
+ *
+ */
+object VirtualEth {
+ fun add(host: String?, guest: String?) {
+ // ShellExecutor.Companion.run("/sbin/ip", "link add name " + host + " type veth peer name " + guest);
+ throw RuntimeException("NOT IMPL.")
+ }
+
+ fun delete(host: String?) {
+ // ShellExecutor.Companion.run("/sbin/ip", "link del " + host);
+ throw RuntimeException("NOT IMPL.")
+ }
+
+ fun assignNameSpace(nameSpace: String?, guest: String?) {
+ // ShellExecutor.Companion.run("/sbin/ip", "link set " + guest + " netns " + nameSpace);
+ throw RuntimeException("NOT IMPL.")
+ }
+}
diff --git a/src/dorkbox/netUtil/ping/Ping.kt b/src/dorkbox/netUtil/ping/Ping.kt
new file mode 100644
index 0000000..c6c792d
--- /dev/null
+++ b/src/dorkbox/netUtil/ping/Ping.kt
@@ -0,0 +1,84 @@
+package dorkbox.netUtil.ping
+
+import dorkbox.executor.Executor
+import dorkbox.netUtil.Common
+import org.slf4j.LoggerFactory
+import java.io.IOException
+import java.time.Duration
+
+/**
+ *
+ */
+class Ping {
+ companion object {
+ private val logger = LoggerFactory.getLogger(Ping::class.java.simpleName)
+ }
+
+ private val count = 4
+ private val host = "1.1.1.1"
+ private val waitTime = Duration.ofSeconds(4)
+ private val deadline: Duration? = null
+ private val ttl: Short? = null
+
+ @Throws(IOException::class)
+ fun run(): PingResult {
+ val ping = Executor()
+ .command("ping")
+
+ if (Common.OS_WINDOWS) {
+ ping.addCommand("-n")
+ ping.addCommand("$count")
+ }
+ else {
+ ping.addCommand("-q")
+ ping.addCommand("-c $count")
+ }
+
+ if (waitTime != null) {
+ when {
+ Common.OS_MAC -> {
+ ping.addCommand("-W " + waitTime.toMillis())
+ }
+ Common.OS_WINDOWS -> {
+ ping.addCommand("-w " + waitTime.toMillis())
+ }
+ else -> {
+ ping.addCommand("-W " + waitTime.seconds)
+ }
+ }
+ }
+
+ if (deadline != null) {
+ when {
+ Common.OS_MAC -> {
+ ping.addCommand("-t " + deadline.seconds)
+ }
+ Common.OS_WINDOWS -> {
+ logger.info("Deadline is not supported on Windows")
+ }
+ else -> {
+ ping.addCommand("-w " + deadline.seconds)
+ }
+ }
+ }
+
+ if (ttl != null) {
+ when {
+ Common.OS_MAC -> {
+ ping.addCommand("-m $ttl")
+ }
+ Common.OS_WINDOWS -> {
+ ping.addCommand("-i $ttl")
+ }
+ else -> {
+ ping.addCommand("-t $ttl")
+ }
+ }
+ }
+ ping.command("ping $host")
+
+ // wait for it to finish running
+ val output: String = ping.readOutput().startAsShellBlocking().output.utf8()
+ return PingResultBuilder.fromOutput(output)
+ }
+}
diff --git a/src/dorkbox/netUtil/ping/PingResult.kt b/src/dorkbox/netUtil/ping/PingResult.kt
new file mode 100644
index 0000000..dbf46c6
--- /dev/null
+++ b/src/dorkbox/netUtil/ping/PingResult.kt
@@ -0,0 +1,28 @@
+package dorkbox.netUtil.ping
+
+import java.time.Duration
+import java.util.*
+
+class PingResult {
+ var host: String? = null
+ var ip: String? = null
+ var responses: MutableList = LinkedList()
+ var transmittedPackets = 0
+ var receivedPackets = 0
+ var packetLoss = 0.0
+ var time: Duration? = null
+ var minRoundTripTime: Duration? = null
+ var avgRoundTripTime: Duration? = null
+ var maxRoundTripTime: Duration? = null
+ var mdevRoundTripTime: Duration? = null
+
+ class Response(val bytes: Int,
+ val host: String,
+ val icmpSeq: Int,
+ val ttl: Int,
+ val time: Duration)
+
+ override fun toString(): String {
+ return "PingResult(host=$host, ip=$ip, responses=$responses, transmittedPackets=$transmittedPackets, receivedPackets=$receivedPackets, packetLoss=$packetLoss, time=$time, minRoundTripTime=$minRoundTripTime, avgRoundTripTime=$avgRoundTripTime, maxRoundTripTime=$maxRoundTripTime, mdevRoundTripTime=$mdevRoundTripTime)"
+ }
+}
diff --git a/src/dorkbox/netUtil/ping/PingResultBuilder.kt b/src/dorkbox/netUtil/ping/PingResultBuilder.kt
new file mode 100644
index 0000000..eb51e42
--- /dev/null
+++ b/src/dorkbox/netUtil/ping/PingResultBuilder.kt
@@ -0,0 +1,129 @@
+package dorkbox.netUtil.ping
+
+import dorkbox.netUtil.Common
+import dorkbox.netUtil.IPv4
+import java.time.Duration
+import java.time.temporal.ChronoUnit
+import java.util.regex.Matcher
+
+internal object PingResultBuilder {
+
+ private val roundTripTimeParser: (PingResult, Matcher) -> PingResult = { result, matcher ->
+ val minRTT = matcher.group(1).toDouble()
+ val avgRTT = matcher.group(2).toDouble()
+ val maxRTT = matcher.group(3).toDouble()
+ val mdevRTT = matcher.group(4).toDouble()
+
+ result.minRoundTripTime = Duration.ofNanos((1000 * 1000 * minRTT).toLong())
+ result.avgRoundTripTime = Duration.ofNanos((1000 * 1000 * avgRTT).toLong())
+ result.maxRoundTripTime = Duration.ofNanos((1000 * 1000 * maxRTT).toLong())
+ result.mdevRoundTripTime = Duration.ofNanos((1000 * 1000 * mdevRTT).toLong())
+
+ result
+ }
+
+
+ private val resultParsers =
+ when {
+ Common.OS_MAC -> {
+ listOf(
+ /* BSD Ping (MacOS) */
+ ResultParser.of("(.*) packets transmitted, (.*) packets received, (.*)% packet loss") { result, matcher ->
+ val transmittedPackets: Int = matcher.group(1).toInt()
+ result.transmittedPackets = transmittedPackets
+
+ val receivedPackets: Int = matcher.group(2).toInt()
+ result.receivedPackets = receivedPackets
+
+ val packetLoss: Double = 0.01 * matcher.group(3).toDouble()
+ result.packetLoss = packetLoss
+
+ result
+ },
+ ResultParser.of("round-trip min\\/avg\\/max\\/stddev = (.*)\\/(.*)\\/(.*)\\/(.*) ms", roundTripTimeParser),
+ ResultParser.of("(.*) bytes from (.*): icmp_seq=(.*) ttl=(.*) time=(.*) ms") { result, matcher ->
+ val bytes: Int = matcher.group(1).toInt()
+ val host: String = matcher.group(2)
+ val icmpSeq: Int = matcher.group(3).toInt()
+ val ttl: Int = matcher.group(4).toInt()
+ val time = Duration.ofNanos((1000 * 1000 * matcher.group(5).toDouble()).toLong())
+ val response = PingResult.Response(bytes, host, icmpSeq, ttl, time)
+
+ result.responses.add(response)
+ result
+ }
+ )
+ }
+ Common.OS_WINDOWS -> {
+ listOf(
+ /* Windows */
+ ResultParser.of("Pinging (.*) with") { result, matcher ->
+ result.host = IPv4.WILDCARD // note: this is REALLY the host used for default traffic
+ result.ip = matcher.group(1)
+ result
+ },
+ ResultParser.of("(.*) packets transmitted, (.*) packets received, (.*)% packet loss") { result, matcher ->
+ val transmittedPackets: Int = matcher.group(1).toInt()
+ result.transmittedPackets = transmittedPackets
+
+ val receivedPackets: Int = matcher.group(2).toInt()
+ result.receivedPackets = receivedPackets
+
+ val packetLoss: Double = 0.01 * matcher.group(3).toDouble()
+ result.packetLoss = packetLoss
+
+ result
+ },
+ ResultParser.of("round-trip min\\/avg\\/max\\/stddev = (.*)\\/(.*)\\/(.*)\\/(.*) ms", roundTripTimeParser),
+ ResultParser.of("(.*) bytes from (.*): icmp_seq=(.*) ttl=(.*) time=(.*) ms") { result, matcher ->
+ val bytes: Int = matcher.group(1).toInt()
+ val host: String = matcher.group(2)
+ val icmpSeq: Int = matcher.group(3).toInt()
+ val ttl: Int = matcher.group(4).toInt()
+ val time = Duration.ofNanos((1000 * 1000 * matcher.group(5).toDouble()) as Long)
+ val response = PingResult.Response(bytes, host, icmpSeq, ttl, time)
+
+ result.responses.add(response)
+ result
+ }
+ )
+ }
+ else -> {
+ listOf(
+ /* GNU Ping (Debian, etc.) */
+ ResultParser.of("PING (.*) \\((.*?)\\)") { result, matcher ->
+ result.host = matcher.group(1)
+ result.ip = matcher.group(2)
+ result
+ },
+ ResultParser.of("(.*) packets transmitted, (.*) received, (.*)% packet loss, time (.*)ms") { result, matcher ->
+ val transmittedPackets: Int = matcher.group(1).toInt()
+ result.transmittedPackets = transmittedPackets
+
+ val receivedPackets: Int = matcher.group(2).toInt()
+ result.receivedPackets = receivedPackets
+
+ val packetLoss: Double = 0.01 * Integer.valueOf(matcher.group(3))
+ result.packetLoss = packetLoss
+
+ val time = Duration.of(matcher.group(4).toLong(), ChronoUnit.MILLIS)
+ result.time = time
+
+ result
+ },
+ ResultParser.of("rtt min\\/avg\\/max\\/mdev = (.*)\\/(.*)\\/(.*)\\/(.*) ms", roundTripTimeParser)
+ )
+ }
+ }
+
+
+ fun fromOutput(output: String): PingResult {
+ val pingResult = PingResult()
+
+ resultParsers.forEach { rp: ResultParser ->
+ rp.fill(pingResult, output)
+ }
+
+ return pingResult
+ }
+}
diff --git a/src/dorkbox/netUtil/ping/ResultParser.kt b/src/dorkbox/netUtil/ping/ResultParser.kt
new file mode 100644
index 0000000..76b7d74
--- /dev/null
+++ b/src/dorkbox/netUtil/ping/ResultParser.kt
@@ -0,0 +1,23 @@
+package dorkbox.netUtil.ping
+
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+class ResultParser(private val pattern: Pattern, private val reader: (PingResult, Matcher) -> PingResult) {
+ fun fill(result: PingResult, output: String): PingResult {
+ var result = result
+ val matcher = pattern.matcher(output)
+ while (matcher.find()) {
+ result = reader(result, matcher)
+ }
+
+ return result
+ }
+
+ companion object {
+ fun of(regex: String, reader: (PingResult, Matcher) -> PingResult): ResultParser {
+ val compile = Pattern.compile(regex)
+ return ResultParser(compile, reader)
+ }
+ }
+}
diff --git a/test/dorkbox/netUtil/NetUtilTest.kt b/test/dorkbox/netUtil/NetUtilTest.kt
new file mode 100644
index 0000000..825b2c4
--- /dev/null
+++ b/test/dorkbox/netUtil/NetUtilTest.kt
@@ -0,0 +1,749 @@
+/*
+ * 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 dorkbox.util.Sys
+import org.junit.Assert
+import org.junit.Assert.*
+import org.junit.Test
+import java.net.Inet6Address
+import java.net.InetAddress
+import java.net.InetSocketAddress
+import java.net.UnknownHostException
+
+class NetUtilTest {
+ companion object {
+ private val validIpV4Hosts = mapOf(
+ "192.168.1.0" to "c0a80100",
+ "10.255.255.254" to "0afffffe",
+ "172.18.5.4" to "ac120504",
+ "0.0.0.0" to "00000000",
+ "127.0.0.1" to "7f000001",
+ "255.255.255.255" to "ffffffff",
+ "1.2.3.4" to "01020304")
+
+ private val invalidIpV4Hosts = mapOf(
+ "1.256.3.4" to null,
+ "256.0.0.1" to null,
+ "1.1.1.1.1" to null,
+ "x.255.255.255" to null,
+ "0.1:0.0" to null,
+ "0.1.0.0:" to null,
+ "127.0.0." to null,
+ "1.2..4" to null,
+ "192.0.1" to null,
+ "192.0.1.1.1" to null,
+ "192.0.1.a" to null,
+ "19a.0.1.1" to null,
+ "a.0.1.1" to null,
+ ".0.1.1" to null,
+ "127.0.0" to null,
+ "192.0.1.256" to null,
+ "0.0.200.259" to null,
+ "1.1.-1.1" to null,
+ "1.1. 1.1" to null,
+ "1.1.1.1 " to null,
+ "1.1.+1.1" to null,
+ "0.0x1.0.255" to null,
+ "0.01x.0.255" to null,
+ "0.x01.0.255" to null,
+ "0.-.0.0" to null,
+ "0..0.0" to null,
+ "0.A.0.0" to null,
+ "0.1111.0.0" to null,
+ "..." to null)
+
+ private val validIpV6Hosts = mapOf(
+ "::ffff:5.6.7.8" to "00000000000000000000ffff05060708",
+ "fdf8:f53b:82e4::53" to "fdf8f53b82e400000000000000000053",
+ "fe80::200:5aee:feaa:20a2" to "fe8000000000000002005aeefeaa20a2",
+ "2001::1" to "20010000000000000000000000000001",
+ "2001:0000:4136:e378:8000:63bf:3fff:fdd2" to "200100004136e378800063bf3ffffdd2",
+ "2001:0002:6c::430" to "20010002006c00000000000000000430",
+ "2001:10:240:ab::a" to "20010010024000ab000000000000000a",
+ "2002:cb0a:3cdd:1::1" to "2002cb0a3cdd00010000000000000001",
+ "2001:db8:8:4::2" to "20010db8000800040000000000000002",
+ "ff01:0:0:0:0:0:0:2" to "ff010000000000000000000000000002",
+ "[fdf8:f53b:82e4::53]" to "fdf8f53b82e400000000000000000053",
+ "[fe80::200:5aee:feaa:20a2]" to "fe8000000000000002005aeefeaa20a2",
+ "[2001::1]" to "20010000000000000000000000000001",
+ "[2001:0000:4136:e378:8000:63bf:3fff:fdd2]" to "200100004136e378800063bf3ffffdd2",
+ "0:1:2:3:4:5:6:789a" to "0000000100020003000400050006789a",
+ "0:1:2:3::f" to "0000000100020003000000000000000f",
+ "0:0:0:0:0:0:10.0.0.1" to "00000000000000000000ffff0a000001",
+ "0:0:0:0:0::10.0.0.1" to "00000000000000000000ffff0a000001",
+ "0:0:0:0::10.0.0.1" to "00000000000000000000ffff0a000001",
+ "::0:0:0:0:0:10.0.0.1" to "00000000000000000000ffff0a000001",
+ "0::0:0:0:0:10.0.0.1" to "00000000000000000000ffff0a000001",
+ "0:0::0:0:0:10.0.0.1" to "00000000000000000000ffff0a000001",
+ "0:0:0::0:0:10.0.0.1" to "00000000000000000000ffff0a000001",
+ "0:0:0:0::0:10.0.0.1" to "00000000000000000000ffff0a000001",
+ "0:0:0:0:0:ffff:10.0.0.1" to "00000000000000000000ffff0a000001",
+ "::ffff:192.168.0.1" to "00000000000000000000ffffc0a80001",
+ // Test if various interface names after the percent sign are recognized.
+ "[::1%1]" to "00000000000000000000000000000001",
+ "[::1%eth0]" to "00000000000000000000000000000001",
+ "[::1%%]" to "00000000000000000000000000000001",
+ "0:0:0:0:0:ffff:10.0.0.1%" to "00000000000000000000ffff0a000001",
+ "0:0:0:0:0:ffff:10.0.0.1%1" to "00000000000000000000ffff0a000001",
+ "[0:0:0:0:0:ffff:10.0.0.1%1]" to "00000000000000000000ffff0a000001",
+ "[0:0:0:0:0::10.0.0.1%1]" to "00000000000000000000ffff0a000001",
+ "[::0:0:0:0:ffff:10.0.0.1%1]" to "00000000000000000000ffff0a000001",
+ "::0:0:0:0:ffff:10.0.0.1%1" to "00000000000000000000ffff0a000001",
+ "::1%1" to "00000000000000000000000000000001",
+ "::1%eth0" to "00000000000000000000000000000001",
+ "::1%%" to "00000000000000000000000000000001",
+ // Tests with leading or trailing compression
+ "0:0:0:0:0:0:0::" to "00000000000000000000000000000000",
+ "0:0:0:0:0:0::" to "00000000000000000000000000000000",
+ "0:0:0:0:0::" to "00000000000000000000000000000000",
+ "0:0:0:0::" to "00000000000000000000000000000000",
+ "0:0:0::" to "00000000000000000000000000000000",
+ "0:0::" to "00000000000000000000000000000000",
+ "0::" to "00000000000000000000000000000000",
+ "::" to "00000000000000000000000000000000",
+ "::0" to "00000000000000000000000000000000",
+ "::0:0" to "00000000000000000000000000000000",
+ "::0:0:0" to "00000000000000000000000000000000",
+ "::0:0:0:0" to "00000000000000000000000000000000",
+ "::0:0:0:0:0" to "00000000000000000000000000000000",
+ "::0:0:0:0:0:0" to "00000000000000000000000000000000",
+ "::0:0:0:0:0:0:0" to "00000000000000000000000000000000")
+
+
+ private val invalidIpV6Hosts = mapOf(
+ // Test method with garbage.
+ "Obvious Garbage" to null,
+ // Test method with preferred style, too many :
+ "0:1:2:3:4:5:6:7:8" to null,
+ // Test method with preferred style, not enough :
+ "0:1:2:3:4:5:6" to null,
+ // Test method with preferred style, bad digits.
+ "0:1:2:3:4:5:6:x" to null,
+ // Test method with preferred style, adjacent :
+ "0:1:2:3:4:5:6::7" to null,
+ // Too many : separators trailing
+ "0:1:2:3:4:5:6:7::" to null,
+ // Too many : separators leading
+ "::0:1:2:3:4:5:6:7" to null,
+ // Too many : separators trailing
+ "1:2:3:4:5:6:7:" to null,
+ // Too many : separators leading
+ ":1:2:3:4:5:6:7" to null,
+ // Compression with : separators trailing
+ "0:1:2:3:4:5::7:" to null,
+ "0:1:2:3:4::7:" to null,
+ "0:1:2:3::7:" to null,
+ "0:1:2::7:" to null,
+ "0:1::7:" to null,
+ "0::7:" to null,
+ // Compression at start with : separators trailing
+ "::0:1:2:3:4:5:7:" to null,
+ "::0:1:2:3:4:7:" to null,
+ "::0:1:2:3:7:" to null,
+ "::0:1:2:7:" to null,
+ "::0:1:7:" to null,
+ "::7:" to null,
+ // The : separators leading and trailing
+ ":1:2:3:4:5:6:7:" to null,
+ ":1:2:3:4:5:6:" to null,
+ ":1:2:3:4:5:" to null,
+ ":1:2:3:4:" to null,
+ ":1:2:3:" to null,
+ ":1:2:" to null,
+ ":1:" to null,
+ // Compression with : separators leading
+ ":1::2:3:4:5:6:7" to null,
+ ":1::3:4:5:6:7" to null,
+ ":1::4:5:6:7" to null,
+ ":1::5:6:7" to null,
+ ":1::6:7" to null,
+ ":1::7" to null,
+ ":1:2:3:4:5:6::7" to null,
+ ":1:3:4:5:6::7" to null,
+ ":1:4:5:6::7" to null,
+ ":1:5:6::7" to null,
+ ":1:6::7" to null,
+ ":1::" to null,
+ // Compression trailing with : separators leading
+ ":1:2:3:4:5:6:7::" to null,
+ ":1:3:4:5:6:7::" to null,
+ ":1:4:5:6:7::" to null,
+ ":1:5:6:7::" to null,
+ ":1:6:7::" to null,
+ ":1:7::" to null,
+ // Double compression
+ "1::2:3:4:5:6::" to null,
+ "::1:2:3:4:5::6" to null,
+ "::1:2:3:4:5:6::" to null,
+ "::1:2:3:4:5::" to null,
+ "::1:2:3:4::" to null,
+ "::1:2:3::" to null,
+ "::1:2::" to null,
+ "::0::" to null,
+ "12::0::12" to null,
+ // Too many : separators leading 0
+ "0::1:2:3:4:5:6:7" to null,
+ // Test method with preferred style, too many digits.
+ "0:1:2:3:4:5:6:789abcdef" to null,
+ // Test method with compressed style, bad digits.
+ "0:1:2:3::x" to null,
+ // Test method with compressed style to too many adjacent :
+ "0:1:2:::3" to null,
+ // Test method with compressed style, too many digits.
+ "0:1:2:3::abcde" to null,
+ // Test method with compressed style, not enough :
+ "0:1" to null,
+ // Test method with ipv4 style, bad ipv6 digits.
+ "0:0:0:0:0:x:10.0.0.1" to null,
+ // Test method with ipv4 style, bad ipv4 digits.
+ "0:0:0:0:0:0:10.0.0.x" to null,
+ // Test method with ipv4 style, too many ipv6 digits.
+ "0:0:0:0:0:00000:10.0.0.1" to null,
+ // Test method with ipv4 style, too many :
+ "0:0:0:0:0:0:0:10.0.0.1" to null,
+ // Test method with ipv4 style, not enough :
+ "0:0:0:0:0:10.0.0.1" to null,
+ // Test method with ipv4 style, too many .
+ "0:0:0:0:0:0:10.0.0.0.1" to null,
+ // Test method with ipv4 style, not enough .
+ "0:0:0:0:0:0:10.0.1" to null,
+ // Test method with ipv4 style, adjacent .
+ "0:0:0:0:0:0:10..0.0.1" to null,
+ // Test method with ipv4 style, leading .
+ "0:0:0:0:0:0:.0.0.1" to null,
+ // Test method with ipv4 style, leading .
+ "0:0:0:0:0:0:.10.0.0.1" to null,
+ // Test method with ipv4 style, trailing .
+ "0:0:0:0:0:0:10.0.0." to null,
+ // Test method with ipv4 style, trailing .
+ "0:0:0:0:0:0:10.0.0.1." to null,
+ // Test method with compressed ipv4 style, bad ipv6 digits.
+ "::fffx:192.168.0.1" to null,
+ // Test method with compressed ipv4 style, bad ipv4 digits.
+ "::ffff:192.168.0.x" to null,
+ // Test method with compressed ipv4 style, too many adjacent :
+ ":::ffff:192.168.0.1" to null,
+ // Test method with compressed ipv4 style, too many ipv6 digits.
+ "::fffff:192.168.0.1" to null,
+ // Test method with compressed ipv4 style, too many ipv4 digits.
+ "::ffff:1923.168.0.1" to null,
+ // Test method with compressed ipv4 style, not enough :
+ ":ffff:192.168.0.1" to null,
+ // Test method with compressed ipv4 style, too many .
+ "::ffff:192.168.0.1.2" to null,
+ // Test method with compressed ipv4 style, not enough .
+ "::ffff:192.168.0" to null,
+ // Test method with compressed ipv4 style, adjacent .
+ "::ffff:192.168..0.1" to null,
+ // Test method, bad ipv6 digits.
+ "x:0:0:0:0:0:10.0.0.1" to null,
+ // Test method, bad ipv4 digits.
+ "0:0:0:0:0:0:x.0.0.1" to null,
+ // Test method, too many ipv6 digits.
+ "00000:0:0:0:0:0:10.0.0.1" to null,
+ // Test method, too many ipv4 digits.
+ "0:0:0:0:0:0:10.0.0.1000" to null,
+ // Test method, too many :
+ "0:0:0:0:0:0:0:10.0.0.1" to null,
+ // Test method, not enough :
+ "0:0:0:0:0:10.0.0.1" to null,
+ // Test method, out of order trailing :
+ "0:0:0:0:0:10.0.0.1:" to null,
+ // Test method, out of order leading :
+ ":0:0:0:0:0:10.0.0.1" to null,
+ // Test method, out of order leading :
+ "0:0:0:0::10.0.0.1:" to null,
+ // Test method, out of order trailing :
+ ":0:0:0:0::10.0.0.1" to null,
+ // Test method, too many .
+ "0:0:0:0:0:0:10.0.0.0.1" to null,
+ // Test method, not enough .
+ "0:0:0:0:0:0:10.0.1" to null,
+ // Test method, adjacent .
+ "0:0:0:0:0:0:10.0.0..1" to null,
+ // Empty contents
+ "" to null,
+ // Invalid single compression
+ ":" to null,
+ ":::" to null,
+ // Trailing : (max number of : = 8)
+ "2001:0:4136:e378:8000:63bf:3fff:fdd2:" to null,
+ // Leading : (max number of : = 8)
+ ":aaaa:bbbb:cccc:dddd:eeee:ffff:1111:2222" to null,
+ // Invalid character
+ "1234:2345:3456:4567:5678:6789::X890" to null,
+ // Trailing . in IPv4
+ "::ffff:255.255.255.255." to null,
+ // To many characters in IPv4
+ "::ffff:0.0.1111.0" to null,
+ // Test method, adjacent .
+ "::ffff:0.0..0" to null,
+ // Not enough IPv4 entries trailing .
+ "::ffff:127.0.0." to null,
+ // Invalid trailing IPv4 character
+ "::ffff:127.0.0.a" to null,
+ // Invalid leading IPv4 character
+ "::ffff:a.0.0.1" to null,
+ // Invalid middle IPv4 character
+ "::ffff:127.a.0.1" to null,
+ // Invalid middle IPv4 character
+ "::ffff:127.0.a.1" to null,
+ // Not enough IPv4 entries no trailing .
+ "::ffff:1.2.4" to null,
+ // Extra IPv4 entry
+ "::ffff:192.168.0.1.255" to null,
+ // Not enough IPv6 content
+ ":ffff:192.168.0.1.255" to null,
+ // Intermixed IPv4 and IPv6 symbols
+ "::ffff:255.255:255.255." to null,
+ // Invalid IPv4 mapped address - invalid ipv4 separator
+ "0:0:0::0:0:00f.0.0.1" to null,
+ // Invalid IPv4 mapped address - not enough f's
+ "0:0:0:0:0:fff:1.0.0.1" to null,
+ // Invalid IPv4 mapped address - not IPv4 mapped, not IPv4 compatible
+ "0:0:0:0:0:ff00:1.0.0.1" to null,
+ // Invalid IPv4 mapped address - not IPv4 mapped, not IPv4 compatible
+ "0:0:0:0:0:ff:1.0.0.1" to null,
+ // Invalid IPv4 mapped address - too many f's
+ "0:0:0:0:0:fffff:1.0.0.1" to null,
+ // Invalid IPv4 mapped address - too many bytes (too many 0's)
+ "0:0:0:0:0:0:ffff:1.0.0.1" to null,
+ // Invalid IPv4 mapped address - too many bytes (too many 0's)
+ "::0:0:0:0:0:ffff:1.0.0.1" to null,
+ // Invalid IPv4 mapped address - too many bytes (too many 0's)
+ "0:0:0:0:0:0::1.0.0.1" to null,
+ // Invalid IPv4 mapped address - too many bytes (too many 0's)
+ "0:0:0:0:0:00000:1.0.0.1" to null,
+ // Invalid IPv4 mapped address - too few bytes (not enough 0's)
+ "0:0:0:0:ffff:1.0.0.1" to null,
+ // Invalid IPv4 mapped address - too few bytes (not enough 0's)
+ "ffff:192.168.0.1" to null,
+ // Invalid IPv4 mapped address - 0's after the mapped ffff indicator
+ "0:0:0:0:0:ffff::10.0.0.1" to null,
+ // Invalid IPv4 mapped address - 0's after the mapped ffff indicator
+ "0:0:0:0:ffff::10.0.0.1" to null,
+ // Invalid IPv4 mapped address - 0's after the mapped ffff indicator
+ "0:0:0:ffff::10.0.0.1" to null,
+ // Invalid IPv4 mapped address - 0's after the mapped ffff indicator
+ "0:0:ffff::10.0.0.1" to null,
+ // Invalid IPv4 mapped address - 0's after the mapped ffff indicator
+ "0:ffff::10.0.0.1" to null,
+ // Invalid IPv4 mapped address - 0's after the mapped ffff indicator
+ "ffff::10.0.0.1" to null,
+ // Invalid IPv4 mapped address - not all 0's before the mapped separator
+ "1:0:0:0:0:ffff:10.0.0.1" to null,
+ // Address that is similar to IPv4 mapped, but is invalid
+ "0:0:0:0:ffff:ffff:1.0.0.1" to null,
+ // Valid number of separators, but invalid IPv4 format
+ "::1:2:3:4:5:6.7.8.9" to null,
+ // Too many digits
+ "0:0:0:0:0:0:ffff:10.0.0.1" to null,
+ // Invalid IPv4 format
+ ":1.2.3.4" to null,
+ // Invalid IPv4 format
+ "::.2.3.4" to null,
+ // Invalid IPv4 format
+ "::ffff:0.1.2." to null)
+
+
+ private val ipv6ToAddressStrings = mapOf(
+ // From the RFC 5952 http://tools.ietf.org/html/rfc5952#section-4
+ byteArrayOf(
+ 32, 1, 13, -72,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0, 1
+ ) to "2001:db8::1",
+
+ byteArrayOf(
+ 32, 1, 13, -72,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 2, 0, 1
+ ) to "2001:db8::2:1",
+
+ byteArrayOf(
+ 32, 1, 13, -72,
+ 0, 0, 0, 1,
+ 0, 1, 0, 1,
+ 0, 1, 0, 1
+ ) to "2001:db8:0:1:1:1:1:1",
+
+ // Other examples
+ byteArrayOf(
+ 32, 1, 13, -72,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 2, 0, 1
+ ) to "2001:db8::2:1",
+
+ byteArrayOf(
+ 32, 1, 0, 0,
+ 0, 0, 0, 1,
+ 0, 0, 0, 0,
+ 0, 0, 0, 1
+ ) to "2001:0:0:1::1",
+
+ byteArrayOf(
+ 32, 1, 13, -72,
+ 0, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 0, 1
+ ) to "2001:db8::1:0:0:1",
+
+ byteArrayOf(
+ 32, 1, 13, -72,
+ 0, 0, 0, 0,
+ 0, 1, 0, 0,
+ 0, 0, 0, 0
+ ) to "2001:db8:0:0:1::",
+
+ byteArrayOf(
+ 32, 1, 13, -72,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 2, 0, 0
+ ) to "2001:db8::2:0",
+
+ byteArrayOf(
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0, 1
+ ) to "::1",
+
+ byteArrayOf(
+ 0, 0, 0, 0,
+ 0, 0, 0, 1,
+ 0, 0, 0, 0,
+ 0, 0, 0, 1
+ ) to "::1:0:0:0:1",
+
+ byteArrayOf(
+ 0, 0, 0, 0,
+ 1, 0, 0, 1,
+ 0, 0, 0, 0,
+ 1, 0, 0, 0
+ ) to "::100:1:0:0:100:0",
+
+ byteArrayOf(
+ 32, 1, 0, 0,
+ 65, 54, -29, 120,
+ -128, 0, 99, -65,
+ 63, -1, -3, -46
+ ) to "2001:0:4136:e378:8000:63bf:3fff:fdd2",
+
+ byteArrayOf(
+ -86, -86, -69, -69,
+ -52, -52, -35, -35,
+ -18, -18, -1, -1,
+ 17, 17, 34, 34
+ ) to "aaaa:bbbb:cccc:dddd:eeee:ffff:1111:2222",
+
+ byteArrayOf(
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0,
+ 0, 0, 0, 0
+ ) to "::"
+ )
+
+
+ private val ipv4MappedToIPv6AddressStrings = mapOf(
+ // IPv4 addresses
+ "255.255.255.255" to "::ffff:255.255.255.255",
+ "0.0.0.0" to "::ffff:0.0.0.0",
+ "127.0.0.1" to "::ffff:127.0.0.1",
+ "1.2.3.4" to "::ffff:1.2.3.4",
+ "192.168.0.1" to "::ffff:192.168.0.1",
+ // IPv4 compatible addresses are deprecated [1], so we don't support outputting them, but we do support
+ // parsing them into IPv4 mapped addresses. These values are treated the same as a plain IPv4 address above.
+ // [1] https://tools.ietf.org/html/rfc4291#section-2.5.5.1
+ "0:0:0:0:0:0:255.254.253.252" to "::ffff:255.254.253.252",
+ "0:0:0:0:0::1.2.3.4" to "::ffff:1.2.3.4",
+ "0:0:0:0::1.2.3.4" to "::ffff:1.2.3.4",
+ "::0:0:0:0:0:1.2.3.4" to "::ffff:1.2.3.4",
+ "0::0:0:0:0:1.2.3.4" to "::ffff:1.2.3.4",
+ "0:0::0:0:0:1.2.3.4" to "::ffff:1.2.3.4",
+ "0:0:0::0:0:1.2.3.4" to "::ffff:1.2.3.4",
+ "0:0:0:0::0:1.2.3.4" to "::ffff:1.2.3.4",
+ "0:0:0:0:0::1.2.3.4" to "::ffff:1.2.3.4",
+ "::0:0:0:0:1.2.3.4" to "::ffff:1.2.3.4",
+ "0::0:0:0:1.2.3.4" to "::ffff:1.2.3.4",
+ "0:0::0:0:1.2.3.4" to "::ffff:1.2.3.4",
+ "0:0:0::0:1.2.3.4" to "::ffff:1.2.3.4",
+ "0:0:0:0::1.2.3.4" to "::ffff:1.2.3.4",
+ "::0:0:0:0:1.2.3.4" to "::ffff:1.2.3.4",
+ "0::0:0:0:1.2.3.4" to "::ffff:1.2.3.4",
+ "0:0::0:0:1.2.3.4" to "::ffff:1.2.3.4",
+ "0:0:0::0:1.2.3.4" to "::ffff:1.2.3.4",
+ "0:0:0:0::1.2.3.4" to "::ffff:1.2.3.4",
+ "::0:0:0:1.2.3.4" to "::ffff:1.2.3.4",
+ "0::0:0:1.2.3.4" to "::ffff:1.2.3.4",
+ "0:0::0:1.2.3.4" to "::ffff:1.2.3.4",
+ "0:0:0::1.2.3.4" to "::ffff:1.2.3.4",
+ "::0:0:1.2.3.4" to "::ffff:1.2.3.4",
+ "0::0:1.2.3.4" to "::ffff:1.2.3.4",
+ "0:0::1.2.3.4" to "::ffff:1.2.3.4",
+ "::0:1.2.3.4" to "::ffff:1.2.3.4",
+ "::1.2.3.4" to "::ffff:1.2.3.4",
+ // IPv4 mapped (fully specified)
+ "0:0:0:0:0:ffff:1.2.3.4" to "::ffff:1.2.3.4",
+ // IPv6 addresses
+ // Fully specified
+ "2001:0:4136:e378:8000:63bf:3fff:fdd2" to "2001:0:4136:e378:8000:63bf:3fff:fdd2",
+ "aaaa:bbbb:cccc:dddd:eeee:ffff:1111:2222" to "aaaa:bbbb:cccc:dddd:eeee:ffff:1111:2222",
+ "0:0:0:0:0:0:0:0" to "::",
+ "0:0:0:0:0:0:0:1" to "::1",
+ // Compressing at the beginning
+ "::1:0:0:0:1" to "::1:0:0:0:1",
+ "::1:ffff:ffff" to "::1:ffff:ffff",
+ "::" to "::",
+ "::1" to "::1",
+ "::ffff" to "::ffff",
+ "::ffff:0" to "::ffff:0",
+ "::ffff:ffff" to "::ffff:ffff",
+ "::0987:9876:8765" to "::987:9876:8765",
+ "::0987:9876:8765:7654" to "::987:9876:8765:7654",
+ "::0987:9876:8765:7654:6543" to "::987:9876:8765:7654:6543",
+ "::0987:9876:8765:7654:6543:5432" to "::987:9876:8765:7654:6543:5432",
+ // Note the compression is removed (rfc 5952 section 4.2.2)
+ "::0987:9876:8765:7654:6543:5432:3210" to "0:987:9876:8765:7654:6543:5432:3210",
+ // Compressing at the end
+ // Note the compression is removed (rfc 5952 section 4.2.2)
+ "2001:db8:abcd:bcde:cdef:def1:ef12::" to "2001:db8:abcd:bcde:cdef:def1:ef12:0",
+ "2001:db8:abcd:bcde:cdef:def1::" to "2001:db8:abcd:bcde:cdef:def1::",
+ "2001:db8:abcd:bcde:cdef::" to "2001:db8:abcd:bcde:cdef::",
+ "2001:db8:abcd:bcde::" to "2001:db8:abcd:bcde::",
+ "2001:db8:abcd::" to "2001:db8:abcd::",
+ "2001:1234::" to "2001:1234::",
+ "2001::" to "2001::",
+ "0::" to "::",
+ // Compressing in the middle
+ "1234:2345::7890" to "1234:2345::7890",
+ "1234::2345:7890" to "1234::2345:7890",
+ "1234:2345:3456::7890" to "1234:2345:3456::7890",
+ "1234:2345::3456:7890" to "1234:2345::3456:7890",
+ "1234::2345:3456:7890" to "1234::2345:3456:7890",
+ "1234:2345:3456:4567::7890" to "1234:2345:3456:4567::7890",
+ "1234:2345:3456::4567:7890" to "1234:2345:3456::4567:7890",
+ "1234:2345::3456:4567:7890" to "1234:2345::3456:4567:7890",
+ "1234::2345:3456:4567:7890" to "1234::2345:3456:4567:7890",
+ "1234:2345:3456:4567:5678::7890" to "1234:2345:3456:4567:5678::7890",
+ "1234:2345:3456:4567::5678:7890" to "1234:2345:3456:4567::5678:7890",
+ "1234:2345:3456::4567:5678:7890" to "1234:2345:3456::4567:5678:7890",
+ "1234:2345::3456:4567:5678:7890" to "1234:2345::3456:4567:5678:7890",
+ "1234::2345:3456:4567:5678:7890" to "1234::2345:3456:4567:5678:7890",
+ // Note the compression is removed (rfc 5952 section 4.2.2)
+ "1234:2345:3456:4567:5678:6789::7890" to "1234:2345:3456:4567:5678:6789:0:7890",
+ // Note the compression is removed (rfc 5952 section 4.2.2)
+ "1234:2345:3456:4567:5678::6789:7890" to "1234:2345:3456:4567:5678:0:6789:7890",
+ // Note the compression is removed (rfc 5952 section 4.2.2)
+ "1234:2345:3456:4567::5678:6789:7890" to "1234:2345:3456:4567:0:5678:6789:7890",
+ // Note the compression is removed (rfc 5952 section 4.2.2)
+ "1234:2345:3456::4567:5678:6789:7890" to "1234:2345:3456:0:4567:5678:6789:7890",
+ // Note the compression is removed (rfc 5952 section 4.2.2)
+ "1234:2345::3456:4567:5678:6789:7890" to "1234:2345:0:3456:4567:5678:6789:7890",
+ // Note the compression is removed (rfc 5952 section 4.2.2)
+ "1234::2345:3456:4567:5678:6789:7890" to "1234:0:2345:3456:4567:5678:6789:7890",
+ // IPv4 mapped addresses
+ "::ffff:255.255.255.255" to "::ffff:255.255.255.255",
+ "::ffff:0.0.0.0" to "::ffff:0.0.0.0",
+ "::ffff:127.0.0.1" to "::ffff:127.0.0.1",
+ "::ffff:1.2.3.4" to "::ffff:1.2.3.4",
+ "::ffff:192.168.0.1" to "::ffff:192.168.0.1")
+
+
+ private fun assertHexDumpEquals(expected: String?, actual: ByteArray?, message: String) {
+ assertEquals(message, expected, if (actual == null) null else hex(actual))
+ }
+
+ private fun hex(value: ByteArray): String {
+ return Sys.bytesToHex(value).toLowerCase()
+ }
+
+ private fun unhex(value: String?): ByteArray? {
+ return if (value != null) Sys.hexToBytes(value) else null
+ }
+ }
+
+ @Test
+ fun testLocalhost() {
+ Assert.assertNotNull(IP.LOCALHOST)
+ }
+
+ @Test
+ fun testLoopback() {
+ Assert.assertNotNull(IP.LOOPBACK_IF)
+ }
+
+ @Test
+ fun testIsValidIpV4Address() {
+ for (host in validIpV4Hosts.keys) {
+ Assert.assertTrue(host, IPv4.isValid(host))
+ }
+ for (host in invalidIpV4Hosts.keys) {
+ Assert.assertFalse(host, IPv4.isValid(host))
+ }
+ }
+
+ @Test
+ fun testIsValidIpV6Address() {
+ for (host in validIpV6Hosts.keys) {
+ Assert.assertTrue(host, IPv6.isValid(host))
+ if (host[0] != '[' && !host.contains("%")) {
+ Assert.assertNotNull(host, IPv6.getByName(host, true))
+ var hostMod = "[$host]"
+ Assert.assertTrue(hostMod, IPv6.isValid(hostMod))
+ hostMod = "$host%"
+ Assert.assertTrue(hostMod, IPv6.isValid(hostMod))
+ hostMod = "$host%eth1"
+ Assert.assertTrue(hostMod, IPv6.isValid(hostMod))
+ hostMod = "[$host%]"
+ Assert.assertTrue(hostMod, IPv6.isValid(hostMod))
+ hostMod = "[$host%1]"
+ Assert.assertTrue(hostMod, IPv6.isValid(hostMod))
+ hostMod = "[$host]%"
+ Assert.assertFalse(hostMod, IPv6.isValid(hostMod))
+ hostMod = "[$host]%1"
+ Assert.assertFalse(hostMod, IPv6.isValid(hostMod))
+ }
+ }
+ for (host in invalidIpV6Hosts.keys) {
+ Assert.assertFalse(host, IPv6.isValid(host))
+ Assert.assertNull(host, IPv6.getByName(host))
+ var hostMod = "[$host]"
+ Assert.assertFalse(hostMod, IPv6.isValid(hostMod))
+ hostMod = "$host%"
+ Assert.assertFalse(hostMod, IPv6.isValid(hostMod))
+ hostMod = "$host%eth1"
+ Assert.assertFalse(hostMod, IPv6.isValid(hostMod))
+ hostMod = "[$host%]"
+ Assert.assertFalse(hostMod, IPv6.isValid(hostMod))
+ hostMod = "[$host%1]"
+ Assert.assertFalse(hostMod, IPv6.isValid(hostMod))
+ hostMod = "[$host]%"
+ Assert.assertFalse(hostMod, IPv6.isValid(hostMod))
+ hostMod = "[$host]%1"
+ Assert.assertFalse(hostMod, IPv6.isValid(hostMod))
+ hostMod = "$host]"
+ Assert.assertFalse(hostMod, IPv6.isValid(hostMod))
+ hostMod = "[$host"
+ Assert.assertFalse(hostMod, IPv6.isValid(hostMod))
+ }
+ }
+
+ @Test
+ fun testCreateByteArrayFromIpAddressString() {
+ for ((ip, value) in validIpV4Hosts) {
+ assertHexDumpEquals(value, IPv4.toBytes(ip), ip)
+ }
+ for ((ip, value) in invalidIpV4Hosts) {
+ assertHexDumpEquals(value, IPv4.toBytesorNull(ip), ip)
+ }
+ for ((ip, value) in validIpV6Hosts) {
+ assertHexDumpEquals(value, IPv6.toBytes(ip), ip)
+ }
+ for ((ip, value) in invalidIpV6Hosts) {
+ assertHexDumpEquals(value, IPv6.toBytesOrNull(ip), ip)
+ }
+ }
+
+ @Test
+ @Throws(UnknownHostException::class)
+ fun testBytesToIpAddress() {
+ for ((key) in validIpV4Hosts) {
+ assertEquals(key, IPv4.toString(IPv4.toBytes(key)))
+ assertEquals(key, IPv4.toString(IPv4.toBytes(key)))
+ }
+ for ((key, value) in ipv6ToAddressStrings) {
+ assertEquals(value, IPv6.toString(key))
+ }
+ }
+
+ @Test
+ @Throws(UnknownHostException::class)
+ fun testIp6AddressToString() {
+ for ((key, value) in ipv6ToAddressStrings) {
+ assertEquals(value, IPv6.toString(InetAddress.getByAddress(key)))
+ }
+ }
+
+ @Test
+ @Throws(UnknownHostException::class)
+ fun testIp4AddressToString() {
+ for ((key, value) in validIpV4Hosts) {
+ assertEquals(key, IPv4.toString(InetAddress.getByAddress(unhex(value))))
+ }
+ }
+
+ @Test
+ fun testIpv4MappedIp6GetByName() {
+ for ((srcIp, dstIp) in ipv4MappedToIPv6AddressStrings) {
+ val inet6Address: Inet6Address? = IPv6.getByName(srcIp, true)
+ Assert.assertNotNull("$srcIp, $dstIp", inet6Address)
+ assertEquals(srcIp, dstIp, IPv6.toString(inet6Address!!, true))
+ }
+ }
+
+ @Test
+ fun testInvalidIpv4MappedIp6GetByName() {
+ for (host in invalidIpV4Hosts.keys) {
+ Assert.assertNull(host, IPv4.getByName(host))
+ }
+ for (host in invalidIpV6Hosts.keys) {
+ Assert.assertNull(host, IPv6.getByName(host, true))
+ }
+ }
+
+ @Test
+ @Throws(UnknownHostException::class)
+ fun testIp6InetSocketAddressToString() {
+ for ((key, value) in ipv6ToAddressStrings) {
+ assertEquals("[$value]:9999", IP.toString(InetSocketAddress(InetAddress.getByAddress(key), 9999)))
+ }
+ }
+
+ @Test
+ @Throws(UnknownHostException::class)
+ fun testIp4SocketAddressToString() {
+ for ((key, value) in validIpV4Hosts) {
+ assertEquals("$key:9999", IP.toString(InetSocketAddress(InetAddress.getByAddress(unhex(value)), 9999)))
+ }
+ }
+//
+// @Test
+// fun testPing() {
+// println(Ping().run())
+//// println(Executor().command("ping 1.1.1.1").readOutput().startAsShellBlocking().output.utf8())
+// }
+
+ @Test
+ fun testIp4Range() {
+ assertTrue("", IPv4.isInRange(IPv4.toInt("10.10.10.5"), IPv4.toInt("10.10.10.10"), 24))
+ assertTrue("", IPv4.isInRange(IPv4.toInt("10.0.0.5"), IPv4.toInt("10.10.10.10"), 8))
+ assertFalse("", IPv4.isInRange(IPv4.toInt("11.0.0.5"), IPv4.toInt("10.10.10.10"), 8))
+ assertTrue("", IPv4.isInRange(IPv4.toInt("11.0.0.5"), IPv4.toInt("10.10.10.10"), 1))
+ assertTrue("", IPv4.isInRange(IPv4.toInt("11.0.0.5"), IPv4.toInt("10.10.10.10"), 0))
+ assertFalse("", IPv4.isInRange(IPv4.toInt("11.0.0.5"), IPv4.toInt("10.10.10.10"), 32))
+ assertTrue("", IPv4.isInRange(IPv4.toInt("10.10.10.10"), IPv4.toInt("10.10.10.10"), 32))
+ assertTrue("", IPv4.isInRange(IPv4.toInt("10.10.10.10"), IPv4.toInt("10.10.10.10"), 31))
+ assertTrue("", IPv4.isInRange(IPv4.toInt("10.10.10.10"), IPv4.toInt("10.10.10.10"), 30))
+ assertTrue("", IPv4.isInRange(IPv4.toInt("192.168.42.14"), IPv4.toInt("192.168.0.0"), 16))
+ assertTrue("", IPv4.isInRange(IPv4.toInt("192.168.0.0"), IPv4.toInt("192.168.0.0"), 16))
+ }
+}