From bbc09400ba36fda6ab231c25101a15ec68e5b1bd Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Thu, 6 Jun 2024 06:08:40 -0400 Subject: [PATCH 1/3] Code clean up --- .../kmp/tor/runtime/core/address/IPAddress.kt | 82 ++++++++++--------- .../core/address/IPAddressV6UnitTest.kt | 30 +++++-- 2 files changed, 68 insertions(+), 44 deletions(-) diff --git a/library/runtime-core/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/core/address/IPAddress.kt b/library/runtime-core/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/core/address/IPAddress.kt index bb1124875..41df14918 100644 --- a/library/runtime-core/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/core/address/IPAddress.kt +++ b/library/runtime-core/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/core/address/IPAddress.kt @@ -109,13 +109,15 @@ public sealed class IPAddress private constructor( /** * Holder for an IPv4 address + * + * @see [AnyHost] * */ public open class V4 private constructor(bytes: ByteArray, value: String): IPAddress(bytes, value) { /** - * `0.0.0.0` + * Static instance for `0.0.0.0` * */ - public object AnyHost: V4(ByteArray(4) { 0 }, "0.0.0.0") + public object AnyHost: V4(ByteArray(4), "0.0.0.0") public companion object { @@ -238,6 +240,7 @@ public sealed class IPAddress private constructor( * * @param [scope] The network interface name or index * number, or null if no scope was expressed. + * @see [AnyHost] * */ public open class V6 private constructor( @JvmField @@ -247,7 +250,7 @@ public sealed class IPAddress private constructor( ): IPAddress(bytes, value + if (scope == null) "" else "%$scope") { /** - * `::0` + * Holder for `::0` * * @see [of] * @see [NoScope] @@ -263,7 +266,7 @@ public sealed class IPAddress private constructor( * */ public companion object NoScope: AnyHost( scope = null, - bytes = ByteArray(16) { 0 }, + bytes = ByteArray(16), value = "0:0:0:0:0:0:0:0" ) { @@ -358,20 +361,21 @@ public sealed class IPAddress private constructor( // Square brackets run { - val iClosing = stripped.indexOfLast { it == ']' } - val startBracket = stripped.startsWith('[') - - // No start bracket, yes closing bracket. Invalid. - if (!startBracket && iClosing != -1) return null + val hasOpenBracket = stripped.startsWith('[') + val iClosingBracket = stripped.indexOfLast { it == ']' } - if (iClosing == -1) { + if (iClosingBracket == -1) { // Yes start bracket, no closing bracket. Invalid. - if (startBracket) return null + if (hasOpenBracket) return null // No start bracket, no closing bracket. Valid. + // stripped = stripped } else { + // No start bracket, yes closing bracket. Invalid + if (!hasOpenBracket) return null + // Yes start bracket, yes closing bracket. Strip. - stripped = stripped.substring(1, iClosing) + stripped = stripped.substring(1, iClosingBracket) } } @@ -382,8 +386,10 @@ public sealed class IPAddress private constructor( @Suppress("LocalVariableName") val _scope = stripped.substring(iPct + 1) + val msg = _scope.isValidScopeOrErrorMessage() + // Interface name or index number bad. Invalid. - if (_scope.isValidScopeOrErrorMessage() != null) return null + if (msg != null) return null stripped = stripped.substring(0, iPct) _scope @@ -397,33 +403,36 @@ public sealed class IPAddress private constructor( else -> null }?.let { return it } - val blocks8: List = stripped.split(':', limit = 10).let { split -> - // min (3) to max (9) - // *:: or ::* to ::*:*:*:*:*:*:* or *:*:*:*:*:*:*:: - if (split.size !in 3..9) return@let emptyList() + val blocks8: List = stripped.split(':', limit = 10).let { splits -> + // min (3) to max (9) + // *:: or ::* to ::*:*:*:*:*:*:* or *:*:*:*:*:*:*:: + if (splits.size !in 3..9) return@let emptyList() var iExpand = -1 val blocks: MutableList = run { - val emptyFirst = split.first().isEmpty() - val emptyLast = split.last().isEmpty() + // Will be empty if started with `:` + val emptyFirst = splits.first().isEmpty() + // Will be empty if ended with `:` + val emptyLast = splits.last().isEmpty() // Started and ended with `:`, but `::` was eliminated. Invalid. if (emptyFirst && emptyLast) return@let emptyList() @Suppress("LocalVariableName") - val _blocks = (split as? MutableList) ?: split.toMutableList() + val _blocks = (splits as? MutableList) ?: splits.toMutableList() - // Replace first/last empty block with `0` + // Replace first or last empty block with `0` so that + // parsing for `::` results in the proper iExpand check. if (emptyFirst) { - // Must start with ::, otherwise invalid. + // Must start with `::`, otherwise invalid. iExpand = 1 _blocks.removeFirst() _blocks.add(0, "0") } if (emptyLast) { - // Must end with ::, otherwise invalid. - iExpand = split.lastIndex - 1 + // Must end with `::`, otherwise invalid. + iExpand = splits.lastIndex - 1 _blocks.removeLast() _blocks.add("0") } @@ -449,18 +458,16 @@ public sealed class IPAddress private constructor( // No expression of `::` if (iExpand == -1) return@let blocks - // Have single `::` expression. Deal with it. - - // Indicates that first or last block was - // empty at start and replaced with `0` + had - // iExpanded set to the expected index, but - // parsing all blocks did not observe any empty - // blocks at all. - // - // So, started or ended with single `:` instead - // of expected `::`. Invalid. + // If the first or last block was empty at (started or + // ended with `:`) those blocks were replaced with `0` + // and iExpanded was set to the expected index. If when + // all blocks were checked for emptiness resulted in none + // being found, then the expected iExpand value was not + // confirmed. This indicates that it started with single + // `:` instead of expected `::`. Invalid. if (!hasEmptyBlock) return@let emptyList() + // Have single `::` expression. Deal with it. blocks.removeAt(iExpand) while (blocks.size < 8) { blocks.add(iExpand, "0") } blocks @@ -468,12 +475,12 @@ public sealed class IPAddress private constructor( if (blocks8.size != 8) return null + // 8 non-empty blocks. Decode. var iB = 0 val bytes = ByteArray(16) - // 8 non-empty blocks. Decode. try { - BASE_16.newDecoderFeed { byte -> bytes[iB++] = byte }.use { feed -> + BASE_16.newDecoderFeed(out = { byte -> bytes[iB++] = byte }).use { feed -> for (block in blocks8) { val iNonZero = block.indexOfFirst { it != '0' } @@ -573,6 +580,7 @@ public sealed class IPAddress private constructor( private val BASE_16 = Base16 { strict(); encodeToLowercase = true } } + // Typical IPv4 loopback address of `::1` private open class Loopback private constructor( scope: String?, bytes: ByteArray, @@ -581,7 +589,7 @@ public sealed class IPAddress private constructor( companion object NoScope: Loopback( scope = null, - bytes = ByteArray(16) { i -> if (i == 15) 1 else 0 }, + bytes = ByteArray(16).apply { this[15] = 1 }, value = "0:0:0:0:0:0:0:1", ) { diff --git a/library/runtime-core/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/core/address/IPAddressV6UnitTest.kt b/library/runtime-core/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/core/address/IPAddressV6UnitTest.kt index 2236d423f..9b791803d 100644 --- a/library/runtime-core/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/core/address/IPAddressV6UnitTest.kt +++ b/library/runtime-core/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/core/address/IPAddressV6UnitTest.kt @@ -17,10 +17,26 @@ package io.matthewnelson.kmp.tor.runtime.core.address import io.matthewnelson.kmp.tor.runtime.core.address.IPAddress.V6.Companion.isLoopback import io.matthewnelson.kmp.tor.runtime.core.address.IPAddress.V6.Companion.toIPAddressV6 +import io.matthewnelson.kmp.tor.runtime.core.address.IPAddress.V6.Companion.toIPAddressV6OrNull import kotlin.test.* class IPAddressV6UnitTest { + @Test + fun givenBlock_whenInvalidLength_thenReturnsNull() { + val valid = "::1000" + assertNotNull(valid.toIPAddressV6OrNull()) + assertNull("${valid}${valid.last()}".toIPAddressV6OrNull()) + } + + @Test + fun givenEnclosingBrackets_whenEitherOrNotBoth_thenReturnsNull() { + val valid = "[1::1]" + assertNotNull(valid.toIPAddressV6OrNull()) + assertNull(valid.drop(1).toIPAddressV6OrNull()) + assertNull(valid.dropLast(1).toIPAddressV6OrNull()) + } + @Test fun givenIPAddressV6_whenAnyHost_thenIsInstance() { assertIs("::".toIPAddressV6()) @@ -33,14 +49,14 @@ class IPAddressV6UnitTest { @Test fun givenIPAddressV6_whenTypicalLoopback_thenIsInstance() { - val noScope = "::1".toIPAddressV6() - assertTrue(noScope.isLoopback()) - assertNull(noScope.scope) + val notScoped = "::1".toIPAddressV6() + assertTrue(notScoped.isLoopback()) + assertNull(notScoped.scope) - val scope = "::1%1".toIPAddressV6() - assertTrue(scope.isLoopback()) - assertNotNull(scope.scope) - assertNotEquals(noScope, scope) + val yesScoped = "::1%1".toIPAddressV6() + assertTrue(yesScoped.isLoopback()) + assertNotNull(yesScoped.scope) + assertNotEquals(notScoped, yesScoped) } @Test From 426446c51b81b86b9f007281be6b4175974705e6 Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Thu, 6 Jun 2024 10:12:04 -0400 Subject: [PATCH 2/3] Implement TorCmd.MapAddress --- library/runtime-core/api/runtime-core.api | 43 +++- .../tor/runtime/core/ctrl/AddressMapping.kt | 232 +++++++++++++++++- .../core/ctrl/AddressMappingUnitTest.kt | 36 +++ .../kmp/tor/runtime/ctrl/internal/-TorCmd.kt | 37 ++- .../tor/runtime/ctrl/internal/-TorCmdJob.kt | 33 ++- .../kmp/tor/runtime/ctrl/TestUtils.kt | 2 +- .../kmp/tor/runtime/TorCmdUnitTest.kt | 75 +++++- .../kmp/tor/runtime/test/TestUtils.kt | 12 +- 8 files changed, 436 insertions(+), 34 deletions(-) create mode 100644 library/runtime-core/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMappingUnitTest.kt diff --git a/library/runtime-core/api/runtime-core.api b/library/runtime-core/api/runtime-core.api index 6e114175f..eb3bc9e6a 100644 --- a/library/runtime-core/api/runtime-core.api +++ b/library/runtime-core/api/runtime-core.api @@ -3422,11 +3422,50 @@ public final class io/matthewnelson/kmp/tor/runtime/core/builder/UnixSocketBuild } public final class io/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping { - public fun ()V + public static final field Companion Lio/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping$Companion; + public final field from Ljava/lang/String; + public final field to Ljava/lang/String; + public fun (Lio/matthewnelson/kmp/tor/runtime/core/address/IPAddress;Lio/matthewnelson/kmp/tor/runtime/core/address/IPAddress;)V + public fun (Lio/matthewnelson/kmp/tor/runtime/core/address/IPAddress;Lio/matthewnelson/kmp/tor/runtime/core/address/OnionAddress;)V + public fun (Lio/matthewnelson/kmp/tor/runtime/core/address/IPAddress;Ljava/lang/String;)V + public fun (Ljava/lang/String;Lio/matthewnelson/kmp/tor/runtime/core/address/IPAddress;)V + public fun (Ljava/lang/String;Ljava/lang/String;)V + public static final fun anyHostIPv4To (Lio/matthewnelson/kmp/tor/runtime/core/address/OnionAddress;)Lio/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping; + public static final fun anyHostIPv4To (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping; + public static final fun anyHostIPv6To (Lio/matthewnelson/kmp/tor/runtime/core/address/OnionAddress;)Lio/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping; + public static final fun anyHostIPv6To (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping; + public static final fun anyHostTo (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping; + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun copy ()Lio/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping; + public final fun copy (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping; + public static synthetic fun copy$default (Lio/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; + public static final fun unmapFrom (Lio/matthewnelson/kmp/tor/runtime/core/address/IPAddress;)Lio/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping; + public static final fun unmapFrom (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping; +} + +public final class io/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping$Companion { + public final fun anyHostIPv4To (Lio/matthewnelson/kmp/tor/runtime/core/address/OnionAddress;)Lio/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping; + public final fun anyHostIPv4To (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping; + public final fun anyHostIPv6To (Lio/matthewnelson/kmp/tor/runtime/core/address/OnionAddress;)Lio/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping; + public final fun anyHostIPv6To (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping; + public final fun anyHostTo (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping; + public final fun unmapFrom (Lio/matthewnelson/kmp/tor/runtime/core/address/IPAddress;)Lio/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping; + public final fun unmapFrom (Ljava/lang/String;)Lio/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping; } public final class io/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping$Result { - public fun ()V + public final field from Ljava/lang/String; + public final field isUnmapping Z + public final field to Ljava/lang/String; + public fun (Ljava/lang/String;Ljava/lang/String;)V + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; } public final class io/matthewnelson/kmp/tor/runtime/core/ctrl/ClientAuthEntry { diff --git a/library/runtime-core/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping.kt b/library/runtime-core/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping.kt index 2aa0e89b0..5528c285f 100644 --- a/library/runtime-core/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping.kt +++ b/library/runtime-core/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping.kt @@ -13,14 +13,238 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ +@file:Suppress("JoinDeclarationAndAssignment") + package io.matthewnelson.kmp.tor.runtime.core.ctrl -public class AddressMapping { +import io.matthewnelson.kmp.tor.runtime.core.address.IPAddress +import io.matthewnelson.kmp.tor.runtime.core.address.OnionAddress +import kotlin.jvm.JvmField +import kotlin.jvm.JvmName +import kotlin.jvm.JvmOverloads +import kotlin.jvm.JvmStatic + +/** + * Holder for address mapping requests. + * + * @see [TorCmd.MapAddress] + * @see [AddressMapping.Result] + * */ +public class AddressMapping(from: String, to: String) { + + public constructor(from: IPAddress, to: IPAddress): this(from.value, to.value) + public constructor(from: IPAddress, to: String): this(from.value, to) + public constructor(from: String, to: IPAddress): this(from, to.value) + public constructor(from: IPAddress, to: OnionAddress): this(from.value, to.canonicalHostName()) + + /** + * The "original", or "old", address that will be mapped to [to]. + * */ + @JvmField + public val from: String + + /** + * The "replacement", or "new", address that [from] will be mapped to. + * */ + @JvmField + public val to: String + + public operator fun component1(): String = from + public operator fun component2(): String = to + + @JvmOverloads + public fun copy( + from: String = this.from, + to: String = this.to, + ): AddressMapping { + if (from == this.from && to == this.to) return this + return AddressMapping(from, to) + } + + public companion object { + + /** + * Creates a [AddressMapping] that instructs tor to generate + * a random host value (e.g. 4lr2xdqckbl4nttj.virtual) and + * map the provided string (host name) to it. + * + * If the string (host name) is already mapped, tor will return + * that mapping for [AddressMapping.Result]. + * */ + @JvmStatic + @JvmName("anyHostTo") + public fun String.mappingToAnyHost(): AddressMapping { + return AddressMapping(".", this) + } + + /** + * Creates a [AddressMapping] that instructs tor to generate + * a virtual [IPAddress.V4] address and map the provided string + * (host name) to it. + * + * If the string (host name) is already mapped, tor will return + * that mapping for [AddressMapping.Result]. + * */ + @JvmStatic + @JvmName("anyHostIPv4To") + public fun String.mappingToAnyHostIPv4(): AddressMapping { + return AddressMapping(IPAddress.V4.AnyHost, this) + } - // TODO + /** + * Creates a [AddressMapping] that instructs tor to generate + * a virtual [IPAddress.V4] address and map the provided + * [OnionAddress] to it. + * + * If the [OnionAddress] is already mapped, tor will return + * that mapping for [AddressMapping.Result]. + * */ + @JvmStatic + @JvmName("anyHostIPv4To") + public fun OnionAddress.mappingToAnyHostIPv4(): AddressMapping { + return AddressMapping(IPAddress.V4.AnyHost, this) + } - public class Result { + /** + * Creates a [AddressMapping] that instructs tor to generate + * a virtual [IPAddress.V6] address and map the provided string + * (host name) to it. + * + * If the string (host name) is already mapped, tor will return + * that mapping for [AddressMapping.Result]. + * */ + @JvmStatic + @JvmName("anyHostIPv6To") + public fun String.mappingToAnyHostIPv6(): AddressMapping { + return AddressMapping(IPAddress.V6.AnyHost.NoScope, this) + } + + /** + * Creates a [AddressMapping] that instructs tor to generate + * a virtual [IPAddress.V4] address and map the provided + * [OnionAddress] to it. + * + * If the [OnionAddress] is already mapped, tor will return + * that mapping for [AddressMapping.Result]. + * */ + @JvmStatic + @JvmName("anyHostIPv6To") + public fun OnionAddress.mappingToAnyHostIPv6(): AddressMapping { + return AddressMapping(IPAddress.V6.AnyHost.NoScope, this) + } + + /** + * Creates a [AddressMapping] that instruct tor to unmap any + * addresses associated with the provided string (host name). + * */ + @JvmStatic + @JvmName("unmapFrom") + public fun String.unmappingFrom(): AddressMapping { + return AddressMapping(this, this) + } + + /** + * Creates a [AddressMapping] that instruct tor to unmap any + * addresses associated with the provided [IPAddress]. + * */ + @JvmStatic + @JvmName("unmapFrom") + public fun IPAddress.unmappingFrom(): AddressMapping { + return AddressMapping(this, this) + } + } + + /** + * Holder for response from [TorCmd.MapAddress] + * */ + public class Result( + + /** + * The "original", or "old", address that + * has been mapped to [to]. + * */ + @JvmField + public val from: String, + + /** + * The "replacement", or "new", address that + * [from] has been mapped to. + * */ + @JvmField + public val to: String, + ) { + + /** + * Indicates that this [Result] was an "unmapping" + * of the address (i.e. tor removed the mapping from + * its indices). + * + * @see [unmappingFrom] + * */ + @JvmField + public val isUnmapping: Boolean = from == to + + public override fun equals(other: Any?): Boolean { + return other is Result + && other.from == from + && other.to == to + } + + public override fun hashCode(): Int { + var result = 17 + result = result * 42 + from.hashCode() + result = result * 42 + to.hashCode() + return result + } + + public override fun toString(): String = buildString { + appendLine("AddressMapping.Result: [") + append(" from: ") + appendLine(from) + append(" to: ") + appendLine(to) + append(" isUnmapping: ") + appendLine(isUnmapping) + append(']') + } + } + + init { + this.from = when (from) { + // For mapping to IPv6 any host, tor expects + // unbracketed `::` and nothing else. + // + // Because constructor allows for IPAddress, + // and the implementation for IPAddress.V6 + // **always** expands addresses to 8 blocks, + // this swaps it out for the expected `::` + // any host value. + "::", + "::0", + IPAddress.V6.AnyHost.value, + "[::]", + "[::0]", + IPAddress.V6.AnyHost.canonicalHostName() -> "::" + else -> from + } + + this.to = to + } + + public override fun equals(other: Any?): Boolean { + return other is AddressMapping + && other.from == from + && other.to == to + } + + public override fun hashCode(): Int { + var result = 15 + result = result * 42 + from.hashCode() + result = result * 42 + to.hashCode() + return result + } - // TODO + public override fun toString(): String { + return "AddressMapping[from=$from, to=$to]" } } diff --git a/library/runtime-core/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMappingUnitTest.kt b/library/runtime-core/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMappingUnitTest.kt new file mode 100644 index 000000000..b6c02578c --- /dev/null +++ b/library/runtime-core/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMappingUnitTest.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 Matthew Nelson + * + * Licensed 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 + * + * https://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 io.matthewnelson.kmp.tor.runtime.core.ctrl + +import io.matthewnelson.kmp.tor.runtime.core.address.IPAddress +import kotlin.test.Test +import kotlin.test.assertEquals + +class AddressMappingUnitTest { + + @Test + fun givenFrom_whenIPv6AnyHost_thenReplacesWithAddressTorExpects() { + listOf( + AddressMapping("[::]", "something"), + AddressMapping("::0", "something"), + AddressMapping("[::0]", "something"), + AddressMapping(IPAddress.V6.AnyHost, "something"), + AddressMapping(IPAddress.V6.AnyHost.canonicalHostName(), "something"), + ).forEach { mapping -> + assertEquals("::", mapping.from) + } + } +} diff --git a/library/runtime-ctrl/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/internal/-TorCmd.kt b/library/runtime-ctrl/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/internal/-TorCmd.kt index 9fcaf1402..0f6dac558 100644 --- a/library/runtime-ctrl/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/internal/-TorCmd.kt +++ b/library/runtime-ctrl/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/internal/-TorCmd.kt @@ -3,7 +3,6 @@ package io.matthewnelson.kmp.tor.runtime.ctrl.internal import io.matthewnelson.kmp.file.SysDirSep -import io.matthewnelson.kmp.tor.runtime.core.TorConfig import io.matthewnelson.kmp.tor.runtime.core.TorConfig.Keyword.Attribute import io.matthewnelson.kmp.tor.runtime.core.ctrl.TorCmd import io.matthewnelson.kmp.tor.runtime.ctrl.internal.Debugger.Companion.d @@ -199,9 +198,10 @@ private fun TorCmd.Hs.Fetch.encode(LOG: Debugger?): ByteArray { return StringBuilder(keyword).apply { SP().append(address) - for (server in servers) { - require(server.isNotEmpty()) { "servers cannot contain empty values" } - require(!server.hasWhitespace()) { "server values cannot contain whitespace" } + servers.forEach { server -> + require(!server.isEmptyOrHasWhitespace()) { + "server[$server] cannot be empty or contain whitespace" + } SP().append("SERVER=").append(server) } @@ -216,9 +216,10 @@ private fun TorCmd.Info.Get.encode(LOG: Debugger?): ByteArray { require(keywords.isNotEmpty()) { "A minimum of 1 keyword is required" } return StringBuilder(keyword).apply { - for (word in keywords) { - require(word.isNotEmpty()) { "keywords cannot contain empty values" } - require(!word.hasWhitespace()) { "keyword values cannot contain whitespace" } + keywords.forEach { word -> + require(!word.isEmptyOrHasWhitespace()) { + "keyword[$word] cannot be empty or contain whitespace" + } SP().append(word) } @@ -232,7 +233,21 @@ private fun TorCmd.Info.Get.encode(LOG: Debugger?): ByteArray { private fun TorCmd.MapAddress.encode(LOG: Debugger?): ByteArray { require(mappings.isNotEmpty()) { "A minimum of 1 mapping is required" } - TODO("Issue #418") + return StringBuilder(keyword).apply { + mappings.forEach { mapping -> + require(!mapping.from.isEmptyOrHasWhitespace()) { + "AddressMapping.from[${mapping.from}] cannot be empty or contain whitespace" + } + require(!mapping.to.isEmptyOrHasWhitespace()) { + "AddressMapping.to[${mapping.to}] cannot be empty or contain whitespace" + } + + SP().append(mapping.from).append('=').append(mapping.to) + } + + LOG.d { ">> ${toString()}" } + CRLF() + }.encodeToByteArray() } private fun TorCmd.Onion.Add.encode(LOG: Debugger?): ByteArray { @@ -286,7 +301,7 @@ private fun TorCmd.Ownership.Take.encode(LOG: Debugger?): ByteArray { @Throws(IllegalArgumentException::class) private fun TorCmd.Resolve.encode(LOG: Debugger?): ByteArray { require(hostname.isNotEmpty()) { "hostname cannot be empty" } - require(!hostname.hasWhitespace()) { "hostname cannot contain whitespace" } + require(!hostname.isEmptyOrHasWhitespace()) { "hostname cannot contain whitespace" } return StringBuilder(keyword).apply { if (reverse) { @@ -300,7 +315,7 @@ private fun TorCmd.Resolve.encode(LOG: Debugger?): ByteArray { private fun TorCmd.SetEvents.encode(LOG: Debugger?): ByteArray { return StringBuilder(keyword).apply { - for (event in events) { + events.forEach { event -> SP().append(event.name) } LOG.d { ">> ${toString()}" } @@ -340,4 +355,4 @@ private inline fun StringBuilder.encodeToByteArray(fill: Boolean = false): ByteA } @Suppress("NOTHING_TO_INLINE") -private inline fun String.hasWhitespace(): Boolean = indexOfFirst { it.isWhitespace() } != -1 +private inline fun String.isEmptyOrHasWhitespace(): Boolean = indexOfFirst { it.isWhitespace() } != -1 diff --git a/library/runtime-ctrl/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/internal/-TorCmdJob.kt b/library/runtime-ctrl/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/internal/-TorCmdJob.kt index 5147ee1ee..6333b7e7d 100644 --- a/library/runtime-ctrl/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/internal/-TorCmdJob.kt +++ b/library/runtime-ctrl/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/internal/-TorCmdJob.kt @@ -19,7 +19,9 @@ package io.matthewnelson.kmp.tor.runtime.ctrl.internal import io.matthewnelson.immutable.collections.toImmutableList import io.matthewnelson.immutable.collections.toImmutableMap +import io.matthewnelson.immutable.collections.toImmutableSet import io.matthewnelson.kmp.file.InterruptedException +import io.matthewnelson.kmp.tor.runtime.core.ctrl.AddressMapping import io.matthewnelson.kmp.tor.runtime.core.ctrl.ConfigEntry import io.matthewnelson.kmp.tor.runtime.core.ctrl.Reply import io.matthewnelson.kmp.tor.runtime.core.ctrl.Reply.Error.Companion.toError @@ -92,7 +94,7 @@ private fun TorCmd.Config.Get.complete(job: TorCmdJob<*>, replies: ArrayList(replies.size) for (reply in replies) { - if (reply is Reply.Success.OK) continue + if (reply.isOK) continue val kvp = reply.message val i = kvp.indexOf('=') @@ -119,12 +121,11 @@ private fun TorCmd.Config.Get.complete(job: TorCmdJob<*>, replies: ArrayList>().completion(entries.toImmutableList()) } -@Suppress("NOTHING_TO_INLINE") -private inline fun TorCmd.Info.Get.complete(job: TorCmdJob<*>, replies: ArrayList) { +private fun TorCmd.Info.Get.complete(job: TorCmdJob<*>, replies: ArrayList) { val map = LinkedHashMap(keywords.size, 1.0f) for (reply in replies) { - if (reply is Reply.Success.OK) continue + if (reply.isOK) continue val kvp = reply.message val i = kvp.indexOf('=') @@ -136,18 +137,28 @@ private inline fun TorCmd.Info.Get.complete(job: TorCmdJob<*>, replies: ArrayLis job.unsafeCast>().completion(map.toImmutableMap()) } -@Suppress("NOTHING_TO_INLINE") -private inline fun TorCmd.MapAddress.complete(job: TorCmdJob<*>, replies: ArrayList) { - TODO("Issue #418") +private fun TorCmd.MapAddress.complete(job: TorCmdJob<*>, replies: ArrayList) { + val set = LinkedHashSet(mappings.size, 1.0f) + + for (reply in replies) { + if (reply.isOK) continue + + val kvp = reply.message + val i = kvp.indexOf('=') + if (i == -1) continue + + val result = AddressMapping.Result(kvp.substring(0, i), kvp.substring(i + 1)) + set.add(result) + } + + job.unsafeCast>().completion(set.toImmutableSet()) } -@Suppress("NOTHING_TO_INLINE") -private inline fun TorCmd.Onion.Add.complete(job: TorCmdJob<*>, replies: ArrayList) { +private fun TorCmd.Onion.Add.complete(job: TorCmdJob<*>, replies: ArrayList) { TODO("Issue #419") } -@Suppress("NOTHING_TO_INLINE") -private inline fun TorCmd.OnionClientAuth.View.complete(job: TorCmdJob<*>, replies: ArrayList) { +private fun TorCmd.OnionClientAuth.View.complete(job: TorCmdJob<*>, replies: ArrayList) { TODO("Issue #421") } diff --git a/library/runtime-ctrl/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/TestUtils.kt b/library/runtime-ctrl/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/TestUtils.kt index b9b1b3ed4..748320c52 100644 --- a/library/runtime-ctrl/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/TestUtils.kt +++ b/library/runtime-ctrl/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/ctrl/TestUtils.kt @@ -102,6 +102,6 @@ public object TestUtils { } public val INSTALLER by lazy { - TorResources(installationDir = SysTempDir.resolve("kmp_tor_ctrl")) + TorResources(SysTempDir.resolve("kmp_tor_ctrl")) } } diff --git a/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/TorCmdUnitTest.kt b/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/TorCmdUnitTest.kt index 48616f1a8..334712274 100644 --- a/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/TorCmdUnitTest.kt +++ b/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/TorCmdUnitTest.kt @@ -19,9 +19,18 @@ import io.matthewnelson.kmp.tor.core.api.annotation.InternalKmpTorApi import io.matthewnelson.kmp.tor.core.resource.SynchronizedObject import io.matthewnelson.kmp.tor.core.resource.synchronized import io.matthewnelson.kmp.tor.runtime.Action.Companion.startDaemonAsync +import io.matthewnelson.kmp.tor.runtime.Action.Companion.stopDaemonAsync import io.matthewnelson.kmp.tor.runtime.core.OnFailure import io.matthewnelson.kmp.tor.runtime.core.OnSuccess import io.matthewnelson.kmp.tor.runtime.core.TorConfig +import io.matthewnelson.kmp.tor.runtime.core.TorEvent +import io.matthewnelson.kmp.tor.runtime.core.address.IPAddress +import io.matthewnelson.kmp.tor.runtime.core.address.IPAddress.V4.Companion.toIPAddressV4OrNull +import io.matthewnelson.kmp.tor.runtime.core.address.IPAddress.V6.Companion.toIPAddressV6OrNull +import io.matthewnelson.kmp.tor.runtime.core.ctrl.AddressMapping.Companion.mappingToAnyHost +import io.matthewnelson.kmp.tor.runtime.core.ctrl.AddressMapping.Companion.mappingToAnyHostIPv4 +import io.matthewnelson.kmp.tor.runtime.core.ctrl.AddressMapping.Companion.mappingToAnyHostIPv6 +import io.matthewnelson.kmp.tor.runtime.core.ctrl.AddressMapping.Companion.unmappingFrom import io.matthewnelson.kmp.tor.runtime.core.ctrl.ConfigEntry import io.matthewnelson.kmp.tor.runtime.core.ctrl.TorCmd import io.matthewnelson.kmp.tor.runtime.core.util.executeAsync @@ -29,8 +38,7 @@ import io.matthewnelson.kmp.tor.runtime.test.TestUtils import io.matthewnelson.kmp.tor.runtime.test.TestUtils.ensureStoppedOnTestCompletion import io.matthewnelson.kmp.tor.runtime.test.TestUtils.testEnv import kotlinx.coroutines.test.runTest -import kotlin.test.Test -import kotlin.test.assertEquals +import kotlin.test.* @OptIn(InternalKmpTorApi::class) class TorCmdUnitTest { @@ -81,4 +89,67 @@ class TorCmdUnitTest { assertEquals(2, resultMulti.size) } + + @Test + fun givenMapAddress_whenMapping_thenIsAsExpected() = runTest { + val runtime = TorRuntime.Builder(testEnv("cmd_mapaddress_test")) { + required(TorEvent.ADDRMAP) +// observerStatic(RuntimeEvent.LOG.DEBUG) { println(it) } +// observerStatic(RuntimeEvent.PROCESS.STDOUT) { println(it) } + }.ensureStoppedOnTestCompletion() + + runtime.startDaemonAsync() + + val expected = "torproject.org" + + val results = runtime.executeAsync( + TorCmd.MapAddress( + expected.mappingToAnyHost(), + expected.mappingToAnyHostIPv4(), + expected.mappingToAnyHostIPv6(), + ), + ) + + listOf<(from: String) -> Unit>( + { from -> + assertTrue(from.endsWith(".virtual")) + }, + { from -> + val a = from.toIPAddressV4OrNull() + assertNotNull(a) + assertIsNot(a) + }, + { from -> + val a = from.toIPAddressV6OrNull() + assertNotNull(a) + assertIsNot(a) + } + ).forEachIndexed { index, assertFrom -> + val result = results.elementAt(index) + assertEquals(expected, result.to) + assertFrom(result.from) + assertFalse(result.isUnmapping) + } + + runtime.executeAsync( + TorCmd.Info.Get("address-mappings/control") + ).let { mappings -> + assertEquals(results.size, mappings.entries.first().value.lines().size) + } + + runtime.executeAsync( + TorCmd.MapAddress(results.map { result -> result.from.unmappingFrom() }) + ).forEach { result -> + assertTrue(result.isUnmapping) + } + + runtime.executeAsync( + TorCmd.Info.Get("address-mappings/control") + ).let { mappings -> + // Should all have been un mapped + assertTrue(mappings.entries.first().value.isEmpty()) + } + + runtime.stopDaemonAsync() + } } diff --git a/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/test/TestUtils.kt b/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/test/TestUtils.kt index 4a94f4eeb..59b7993f7 100644 --- a/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/test/TestUtils.kt +++ b/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/test/TestUtils.kt @@ -31,13 +31,19 @@ import kotlin.time.Duration.Companion.milliseconds object TestUtils { + private val TMP_TEST_DIR = SysTempDir.resolve("kmp_tor_test") + + private val INSTALLER by lazy { + TorResources(TMP_TEST_DIR.resolve("resources")) + } + fun testEnv( dirName: String, block: ThisBlock = ThisBlock {} ): TorRuntime.Environment = TorRuntime.Environment.Builder( - workDirectory = SysTempDir.resolve("kmp_tor_test/$dirName/work"), - cacheDirectory = SysTempDir.resolve("kmp_tor_test/$dirName/cache"), - installer = { dir -> TorResources(dir) }, + workDirectory = TMP_TEST_DIR.resolve("$dirName/work"), + cacheDirectory = TMP_TEST_DIR.resolve("$dirName/cache"), + installer = { INSTALLER }, block = block ).also { it.debug = true } From 4721fb44ad59e3f8d05ee1b7f04568050d761311 Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Thu, 6 Jun 2024 11:04:56 -0400 Subject: [PATCH 3/3] Always use bracketed IPAddress.V6 addresses --- library/runtime-core/api/runtime-core.api | 1 + .../tor/runtime/core/ctrl/AddressMapping.kt | 62 +++++++++++++++++-- .../kmp/tor/runtime/TorCmdUnitTest.kt | 2 +- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/library/runtime-core/api/runtime-core.api b/library/runtime-core/api/runtime-core.api index eb3bc9e6a..7ed2bfb0a 100644 --- a/library/runtime-core/api/runtime-core.api +++ b/library/runtime-core/api/runtime-core.api @@ -3466,6 +3466,7 @@ public final class io/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping$Res public fun equals (Ljava/lang/Object;)Z public fun hashCode ()I public fun toString ()Ljava/lang/String; + public final fun toUnmapping ()Lio/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping; } public final class io/matthewnelson/kmp/tor/runtime/core/ctrl/ClientAuthEntry { diff --git a/library/runtime-core/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping.kt b/library/runtime-core/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping.kt index 5528c285f..ee4ec8f9b 100644 --- a/library/runtime-core/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping.kt +++ b/library/runtime-core/src/commonMain/kotlin/io/matthewnelson/kmp/tor/runtime/core/ctrl/AddressMapping.kt @@ -27,15 +27,62 @@ import kotlin.jvm.JvmStatic /** * Holder for address mapping requests. * + * e.g. + * + * val results = runtime.executeAsync( + * TorCmd.MapAddress( + * "torproject.org".mappingToAnyHost(), + * "torproject.org".mappingToAnyHostIPv4(), + * "torproject.org".mappingToAnyHostIPv6(), + * ) + * ) + * + * val unmappings = results.map { result -> + * println(result) + * result.toUnmapping() + * } + * + * // AddressMapping.Result: [ + * // from: wsvidzeicnyrlruo.virtual + * // to: torproject.org + * // isUnmapping: false + * // ] + * // AddressMapping.Result: [ + * // from: 127.240.73.168 + * // to: torproject.org + * // isUnmapping: false + * // ] + * // AddressMapping.Result: [ + * // from: [fe85:2bcd:7057:b3f9:ffb2:be48:1e7b:884] + * // to: torproject.org + * // isUnmapping: false + * // ] + * + * runtime.executeAsync( + * TorCmd.Info.Get("address-mappings/control") + * ).let { mappings -> println(mappings.values.first()) } + * + * // wsvidzeicnyrlruo.virtual torproject.org NEVER + * // 127.240.73.168 torproject.org NEVER + * // [fe85:2bcd:7057:b3f9:ffb2:be48:1e7b:884] torproject.org NEVER + * + * runtime.executeAsync(TorCmd.MapAddress(unmappings)) + * + * runtime.executeAsync( + * TorCmd.Info.Get("address-mappings/control") + * ).let { mappings -> println(mappings) } + * + * // {address-mappings/control=} + * * @see [TorCmd.MapAddress] * @see [AddressMapping.Result] * */ public class AddressMapping(from: String, to: String) { - public constructor(from: IPAddress, to: IPAddress): this(from.value, to.value) - public constructor(from: IPAddress, to: String): this(from.value, to) - public constructor(from: String, to: IPAddress): this(from, to.value) - public constructor(from: IPAddress, to: OnionAddress): this(from.value, to.canonicalHostName()) + public constructor(from: IPAddress, to: IPAddress): this(from.canonicalHostName(), to.canonicalHostName()) + public constructor(from: IPAddress, to: String): this(from.canonicalHostName(), to) + public constructor(from: String, to: IPAddress): this(from, to.canonicalHostName()) + public constructor(from: IPAddress, to: OnionAddress): this(from.canonicalHostName(), to.canonicalHostName()) /** * The "original", or "old", address that will be mapped to [to]. @@ -184,6 +231,13 @@ public class AddressMapping(from: String, to: String) { @JvmField public val isUnmapping: Boolean = from == to + /** + * Creates a new [AddressMapping] request using [from]. + * + * @see [unmappingFrom] + * */ + public fun toUnmapping(): AddressMapping = from.unmappingFrom() + public override fun equals(other: Any?): Boolean { return other is Result && other.from == from diff --git a/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/TorCmdUnitTest.kt b/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/TorCmdUnitTest.kt index 334712274..d1fcee6e0 100644 --- a/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/TorCmdUnitTest.kt +++ b/library/runtime/src/commonTest/kotlin/io/matthewnelson/kmp/tor/runtime/TorCmdUnitTest.kt @@ -138,7 +138,7 @@ class TorCmdUnitTest { } runtime.executeAsync( - TorCmd.MapAddress(results.map { result -> result.from.unmappingFrom() }) + TorCmd.MapAddress(results.map { result -> result.toUnmapping() }) ).forEach { result -> assertTrue(result.isUnmapping) }