feat: scaffold I2P as a parallel privacy transport to Tor
Foundational types for offering I2P alongside the existing internal Tor.
No wiring yet — HTTP managers, RoleBasedHttpClientBuilder, the Android
I2P service and the Privacy settings UI follow in later commits.
Quartz (relay URL classifier):
- Add isI2p() and classifyHidden() to RelayUrlNormalizer
- Add HiddenServiceKind { CLEARNET, LOCALHOST, ONION, I2P }
- Add NormalizedRelayUrl.isI2p() / classifyHidden() extensions
- Extend the scheme-default branch so .i2p hosts default to ws:// like .onion
Commons (transport-agnostic types):
- PrivacyTransport enum { DIRECT, TOR, I2P }
- TransportChoice (UI-facing per-feature picker, screen-coded for persistence)
- FeatureRole + FeatureTransportChoices: per-feature picks for clearnet traffic
- PrivacySettings aggregate { tor, i2p, features }
- PrivacyRouter.route(url, role, settings): hostname pin for hidden services,
per-feature choice for clearnet, downgrades to DIRECT if backing transport is OFF
Commons (I2P settings model, mirrors tor/):
- I2pSettings, I2pType (OFF/INTERNAL/EXTERNAL), I2pRelaySettings
- I2pRelayEvaluation, I2pServiceStatus
- II2pManager, II2pSettingsPersistence (platform-agnostic interfaces)
- PrivacyRelayEvaluation composing TorRelayEvaluation + I2pRelayEvaluation
Tests:
- PrivacyRouterTest covers localhost bypass, onion pin, i2p pin, hostname-wins-over-picker,
per-feature picks routing independently, downgrade-when-transport-OFF, .b32.i2p
This commit is contained in:
+48
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.commons.i2p
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isI2p
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost
|
||||||
|
|
||||||
|
class I2pRelayEvaluation(
|
||||||
|
val i2pSettings: I2pRelaySettings,
|
||||||
|
val trustedRelayList: Set<NormalizedRelayUrl>,
|
||||||
|
val dmRelayList: Set<NormalizedRelayUrl>,
|
||||||
|
) {
|
||||||
|
fun useI2p(relay: NormalizedRelayUrl): Boolean =
|
||||||
|
if (i2pSettings.i2pType == I2pType.OFF) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
if (relay.isLocalHost()) {
|
||||||
|
false
|
||||||
|
} else if (relay.isI2p()) {
|
||||||
|
i2pSettings.i2pRelaysViaI2p
|
||||||
|
} else if (relay in dmRelayList) {
|
||||||
|
i2pSettings.dmRelaysViaI2p
|
||||||
|
} else if (relay in trustedRelayList) {
|
||||||
|
i2pSettings.trustedRelaysViaI2p
|
||||||
|
} else {
|
||||||
|
i2pSettings.newRelaysViaI2p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.commons.i2p
|
||||||
|
|
||||||
|
data class I2pRelaySettings(
|
||||||
|
val i2pType: I2pType = I2pType.OFF,
|
||||||
|
val i2pRelaysViaI2p: Boolean = true,
|
||||||
|
val dmRelaysViaI2p: Boolean = false,
|
||||||
|
val newRelaysViaI2p: Boolean = false,
|
||||||
|
val trustedRelaysViaI2p: Boolean = false,
|
||||||
|
)
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.commons.i2p
|
||||||
|
|
||||||
|
sealed class I2pServiceStatus {
|
||||||
|
data class Active(
|
||||||
|
val port: Int,
|
||||||
|
) : I2pServiceStatus()
|
||||||
|
|
||||||
|
data object Off : I2pServiceStatus()
|
||||||
|
|
||||||
|
data object Connecting : I2pServiceStatus()
|
||||||
|
|
||||||
|
data class Error(
|
||||||
|
val message: String,
|
||||||
|
) : I2pServiceStatus()
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.commons.i2p
|
||||||
|
|
||||||
|
// Mirror of TorSettings. Per-relay-class booleans answer "should I2P handle this
|
||||||
|
// kind of relay" — kept independent of TorSettings since the same questions can
|
||||||
|
// be answered differently for each transport. Per-feature picks (images,
|
||||||
|
// videos, etc.) live on PrivacySettings.features, not here.
|
||||||
|
data class I2pSettings(
|
||||||
|
val i2pType: I2pType = I2pType.OFF,
|
||||||
|
val externalSocksPort: Int = 4447,
|
||||||
|
val i2pRelaysViaI2p: Boolean = true,
|
||||||
|
val dmRelaysViaI2p: Boolean = false,
|
||||||
|
val newRelaysViaI2p: Boolean = false,
|
||||||
|
val trustedRelaysViaI2p: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class I2pType(
|
||||||
|
val screenCode: Int,
|
||||||
|
) {
|
||||||
|
OFF(0),
|
||||||
|
INTERNAL(1),
|
||||||
|
EXTERNAL(2),
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseI2pType(code: Int?): I2pType =
|
||||||
|
when (code) {
|
||||||
|
I2pType.INTERNAL.screenCode -> I2pType.INTERNAL
|
||||||
|
I2pType.EXTERNAL.screenCode -> I2pType.EXTERNAL
|
||||||
|
else -> I2pType.OFF
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.commons.i2p
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
// Platform-agnostic interface for the I2P daemon lifecycle.
|
||||||
|
// Android internal mode bundles net.i2p:router; external mode just opens a SOCKS
|
||||||
|
// proxy to a user-run daemon. activePortOrNull surfaces whichever local SOCKS
|
||||||
|
// port the HTTP manager should route I2P traffic through.
|
||||||
|
interface II2pManager {
|
||||||
|
val status: StateFlow<I2pServiceStatus>
|
||||||
|
val activePortOrNull: StateFlow<Int?>
|
||||||
|
|
||||||
|
suspend fun dormant()
|
||||||
|
|
||||||
|
suspend fun active()
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.commons.i2p
|
||||||
|
|
||||||
|
interface II2pSettingsPersistence {
|
||||||
|
fun load(): I2pSettings
|
||||||
|
|
||||||
|
fun save(settings: I2pSettings)
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.commons.privacy
|
||||||
|
|
||||||
|
// What the network call is for. Used by PrivacyRouter to pick a transport for
|
||||||
|
// clearnet traffic (hidden-service hostnames hard-pin and ignore the role).
|
||||||
|
enum class FeatureRole {
|
||||||
|
IMAGE,
|
||||||
|
VIDEO,
|
||||||
|
URL_PREVIEW,
|
||||||
|
PROFILE_PIC,
|
||||||
|
NIP05,
|
||||||
|
MONEY,
|
||||||
|
UPLOAD,
|
||||||
|
}
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.commons.privacy
|
||||||
|
|
||||||
|
data class FeatureTransportChoices(
|
||||||
|
val image: TransportChoice = TransportChoice.DIRECT,
|
||||||
|
val video: TransportChoice = TransportChoice.DIRECT,
|
||||||
|
val urlPreview: TransportChoice = TransportChoice.DIRECT,
|
||||||
|
val profilePic: TransportChoice = TransportChoice.DIRECT,
|
||||||
|
val nip05: TransportChoice = TransportChoice.DIRECT,
|
||||||
|
val money: TransportChoice = TransportChoice.DIRECT,
|
||||||
|
val upload: TransportChoice = TransportChoice.DIRECT,
|
||||||
|
) {
|
||||||
|
fun choiceFor(role: FeatureRole): TransportChoice =
|
||||||
|
when (role) {
|
||||||
|
FeatureRole.IMAGE -> image
|
||||||
|
FeatureRole.VIDEO -> video
|
||||||
|
FeatureRole.URL_PREVIEW -> urlPreview
|
||||||
|
FeatureRole.PROFILE_PIC -> profilePic
|
||||||
|
FeatureRole.NIP05 -> nip05
|
||||||
|
FeatureRole.MONEY -> money
|
||||||
|
FeatureRole.UPLOAD -> upload
|
||||||
|
}
|
||||||
|
|
||||||
|
fun with(
|
||||||
|
role: FeatureRole,
|
||||||
|
choice: TransportChoice,
|
||||||
|
): FeatureTransportChoices =
|
||||||
|
when (role) {
|
||||||
|
FeatureRole.IMAGE -> copy(image = choice)
|
||||||
|
FeatureRole.VIDEO -> copy(video = choice)
|
||||||
|
FeatureRole.URL_PREVIEW -> copy(urlPreview = choice)
|
||||||
|
FeatureRole.PROFILE_PIC -> copy(profilePic = choice)
|
||||||
|
FeatureRole.NIP05 -> copy(nip05 = choice)
|
||||||
|
FeatureRole.MONEY -> copy(money = choice)
|
||||||
|
FeatureRole.UPLOAD -> copy(upload = choice)
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.commons.privacy
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.commons.i2p.I2pRelayEvaluation
|
||||||
|
import com.vitorpamplona.amethyst.commons.tor.TorRelayEvaluation
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isI2p
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isOnion
|
||||||
|
|
||||||
|
// Per-relay transport pick. Onion always wins for Tor, I2P always wins for I2P
|
||||||
|
// (matching PrivacyRouter's hostname pin). For non-hidden hosts, prefer I2P if
|
||||||
|
// both transports want the relay — arbitrary tiebreak documented here so the
|
||||||
|
// behavior is testable.
|
||||||
|
class PrivacyRelayEvaluation(
|
||||||
|
private val tor: TorRelayEvaluation,
|
||||||
|
private val i2p: I2pRelayEvaluation,
|
||||||
|
) {
|
||||||
|
fun transport(relay: NormalizedRelayUrl): PrivacyTransport {
|
||||||
|
if (relay.isOnion()) return if (tor.useTor(relay)) PrivacyTransport.TOR else PrivacyTransport.DIRECT
|
||||||
|
if (relay.isI2p()) return if (i2p.useI2p(relay)) PrivacyTransport.I2P else PrivacyTransport.DIRECT
|
||||||
|
return when {
|
||||||
|
i2p.useI2p(relay) -> PrivacyTransport.I2P
|
||||||
|
tor.useTor(relay) -> PrivacyTransport.TOR
|
||||||
|
else -> PrivacyTransport.DIRECT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.commons.privacy
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.HiddenServiceKind
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||||
|
|
||||||
|
// Decides which PrivacyTransport carries a request. Hidden-service hostnames
|
||||||
|
// hard-pin (onion → Tor, i2p → I2P) regardless of the per-feature picker, since
|
||||||
|
// no other transport can reach them. If the pinned or chosen transport is OFF,
|
||||||
|
// downgrade to DIRECT — the caller decides whether to make the request anyway.
|
||||||
|
object PrivacyRouter {
|
||||||
|
fun route(
|
||||||
|
url: String,
|
||||||
|
role: FeatureRole,
|
||||||
|
settings: PrivacySettings,
|
||||||
|
): PrivacyTransport =
|
||||||
|
when (RelayUrlNormalizer.classifyHidden(url)) {
|
||||||
|
HiddenServiceKind.LOCALHOST -> PrivacyTransport.DIRECT
|
||||||
|
HiddenServiceKind.ONION -> if (settings.torAvailable) PrivacyTransport.TOR else PrivacyTransport.DIRECT
|
||||||
|
HiddenServiceKind.I2P -> if (settings.i2pAvailable) PrivacyTransport.I2P else PrivacyTransport.DIRECT
|
||||||
|
HiddenServiceKind.CLEARNET -> resolve(settings.features.choiceFor(role), settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolve(
|
||||||
|
choice: TransportChoice,
|
||||||
|
settings: PrivacySettings,
|
||||||
|
): PrivacyTransport =
|
||||||
|
when (choice) {
|
||||||
|
TransportChoice.DIRECT -> PrivacyTransport.DIRECT
|
||||||
|
TransportChoice.TOR -> if (settings.torAvailable) PrivacyTransport.TOR else PrivacyTransport.DIRECT
|
||||||
|
TransportChoice.I2P -> if (settings.i2pAvailable) PrivacyTransport.I2P else PrivacyTransport.DIRECT
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.commons.privacy
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.commons.i2p.I2pSettings
|
||||||
|
import com.vitorpamplona.amethyst.commons.i2p.I2pType
|
||||||
|
import com.vitorpamplona.amethyst.commons.tor.TorSettings
|
||||||
|
import com.vitorpamplona.amethyst.commons.tor.TorType
|
||||||
|
|
||||||
|
// Aggregate of all privacy configuration. TorSettings and I2pSettings stay
|
||||||
|
// independent (each owns its daemon-type + per-relay-class booleans, persisted
|
||||||
|
// under its own pref-key namespace). FeatureTransportChoices is the single
|
||||||
|
// source of truth for per-feature clearnet routing — UI presents one 3-way
|
||||||
|
// picker per role rather than two parallel toggles.
|
||||||
|
data class PrivacySettings(
|
||||||
|
val tor: TorSettings = TorSettings(),
|
||||||
|
val i2p: I2pSettings = I2pSettings(),
|
||||||
|
val features: FeatureTransportChoices = FeatureTransportChoices(),
|
||||||
|
) {
|
||||||
|
val torAvailable: Boolean get() = tor.torType != TorType.OFF
|
||||||
|
val i2pAvailable: Boolean get() = i2p.i2pType != I2pType.OFF
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.commons.privacy
|
||||||
|
|
||||||
|
enum class PrivacyTransport {
|
||||||
|
DIRECT,
|
||||||
|
TOR,
|
||||||
|
I2P,
|
||||||
|
}
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.commons.privacy
|
||||||
|
|
||||||
|
// UI-facing per-feature choice. Kept separate from PrivacyTransport so a future
|
||||||
|
// AUTO value can be added without churning the transport plumbing.
|
||||||
|
enum class TransportChoice(
|
||||||
|
val screenCode: Int,
|
||||||
|
) {
|
||||||
|
DIRECT(0),
|
||||||
|
TOR(1),
|
||||||
|
I2P(2),
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseTransportChoice(code: Int?): TransportChoice =
|
||||||
|
when (code) {
|
||||||
|
TransportChoice.TOR.screenCode -> TransportChoice.TOR
|
||||||
|
TransportChoice.I2P.screenCode -> TransportChoice.I2P
|
||||||
|
else -> TransportChoice.DIRECT
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TransportChoice.toTransport(): PrivacyTransport =
|
||||||
|
when (this) {
|
||||||
|
TransportChoice.DIRECT -> PrivacyTransport.DIRECT
|
||||||
|
TransportChoice.TOR -> PrivacyTransport.TOR
|
||||||
|
TransportChoice.I2P -> PrivacyTransport.I2P
|
||||||
|
}
|
||||||
+120
@@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.commons.privacy
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.commons.i2p.I2pSettings
|
||||||
|
import com.vitorpamplona.amethyst.commons.i2p.I2pType
|
||||||
|
import com.vitorpamplona.amethyst.commons.tor.TorSettings
|
||||||
|
import com.vitorpamplona.amethyst.commons.tor.TorType
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class PrivacyRouterTest {
|
||||||
|
private val clearnet = "wss://relay.damus.io/"
|
||||||
|
private val onion = "wss://abc123.onion/"
|
||||||
|
private val i2p = "wss://abc.i2p/"
|
||||||
|
private val localhost = "ws://127.0.0.1:8080/"
|
||||||
|
|
||||||
|
private fun settings(
|
||||||
|
torOn: Boolean = false,
|
||||||
|
i2pOn: Boolean = false,
|
||||||
|
features: FeatureTransportChoices = FeatureTransportChoices(),
|
||||||
|
) = PrivacySettings(
|
||||||
|
tor = TorSettings(torType = if (torOn) TorType.INTERNAL else TorType.OFF),
|
||||||
|
i2p = I2pSettings(i2pType = if (i2pOn) I2pType.INTERNAL else I2pType.OFF),
|
||||||
|
features = features,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun localhost_alwaysDirect() {
|
||||||
|
assertEquals(PrivacyTransport.DIRECT, PrivacyRouter.route(localhost, FeatureRole.IMAGE, settings(torOn = true, i2pOn = true)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onion_pinsToTor_whenAvailable() {
|
||||||
|
assertEquals(PrivacyTransport.TOR, PrivacyRouter.route(onion, FeatureRole.IMAGE, settings(torOn = true)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onion_downgradesToDirect_whenTorOff() {
|
||||||
|
assertEquals(PrivacyTransport.DIRECT, PrivacyRouter.route(onion, FeatureRole.IMAGE, settings(torOn = false, i2pOn = true)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun i2p_pinsToI2p_whenAvailable() {
|
||||||
|
assertEquals(PrivacyTransport.I2P, PrivacyRouter.route(i2p, FeatureRole.IMAGE, settings(i2pOn = true)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun i2p_downgradesToDirect_whenI2pOff() {
|
||||||
|
assertEquals(PrivacyTransport.DIRECT, PrivacyRouter.route(i2p, FeatureRole.IMAGE, settings(torOn = true, i2pOn = false)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun i2p_ignoresFeatureChoice() {
|
||||||
|
// Per-feature picker is irrelevant for hidden-service hosts.
|
||||||
|
val features = FeatureTransportChoices(image = TransportChoice.TOR)
|
||||||
|
assertEquals(PrivacyTransport.I2P, PrivacyRouter.route(i2p, FeatureRole.IMAGE, settings(torOn = true, i2pOn = true, features = features)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun clearnet_followsFeatureChoice_tor() {
|
||||||
|
val features = FeatureTransportChoices(image = TransportChoice.TOR)
|
||||||
|
assertEquals(PrivacyTransport.TOR, PrivacyRouter.route(clearnet, FeatureRole.IMAGE, settings(torOn = true, features = features)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun clearnet_followsFeatureChoice_i2p() {
|
||||||
|
val features = FeatureTransportChoices(video = TransportChoice.I2P)
|
||||||
|
assertEquals(PrivacyTransport.I2P, PrivacyRouter.route(clearnet, FeatureRole.VIDEO, settings(i2pOn = true, features = features)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun clearnet_choiceWithoutBackingTransport_downgradesToDirect() {
|
||||||
|
val features = FeatureTransportChoices(image = TransportChoice.TOR)
|
||||||
|
assertEquals(PrivacyTransport.DIRECT, PrivacyRouter.route(clearnet, FeatureRole.IMAGE, settings(torOn = false, features = features)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun clearnet_defaultChoice_isDirect() {
|
||||||
|
assertEquals(PrivacyTransport.DIRECT, PrivacyRouter.route(clearnet, FeatureRole.NIP05, settings(torOn = true, i2pOn = true)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun perFeatureRouting_routesIndependently() {
|
||||||
|
val features =
|
||||||
|
FeatureTransportChoices(
|
||||||
|
image = TransportChoice.TOR,
|
||||||
|
video = TransportChoice.I2P,
|
||||||
|
urlPreview = TransportChoice.DIRECT,
|
||||||
|
)
|
||||||
|
val s = settings(torOn = true, i2pOn = true, features = features)
|
||||||
|
assertEquals(PrivacyTransport.TOR, PrivacyRouter.route(clearnet, FeatureRole.IMAGE, s))
|
||||||
|
assertEquals(PrivacyTransport.I2P, PrivacyRouter.route(clearnet, FeatureRole.VIDEO, s))
|
||||||
|
assertEquals(PrivacyTransport.DIRECT, PrivacyRouter.route(clearnet, FeatureRole.URL_PREVIEW, s))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun b32_i2p_address_pinsToI2p() {
|
||||||
|
val b32 = "wss://abcdefghijklmnopqrstuvwxyz234567.b32.i2p/"
|
||||||
|
assertEquals(PrivacyTransport.I2P, PrivacyRouter.route(b32, FeatureRole.IMAGE, settings(i2pOn = true)))
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.quartz.nip01Core.relay.normalizer
|
||||||
|
|
||||||
|
enum class HiddenServiceKind {
|
||||||
|
CLEARNET,
|
||||||
|
LOCALHOST,
|
||||||
|
ONION,
|
||||||
|
I2P,
|
||||||
|
}
|
||||||
+4
@@ -46,4 +46,8 @@ fun NormalizedRelayUrl.toHttp() =
|
|||||||
|
|
||||||
fun NormalizedRelayUrl.isOnion() = url.contains(".onion/")
|
fun NormalizedRelayUrl.isOnion() = url.contains(".onion/")
|
||||||
|
|
||||||
|
fun NormalizedRelayUrl.isI2p() = RelayUrlNormalizer.isI2p(this.url)
|
||||||
|
|
||||||
fun NormalizedRelayUrl.isLocalHost() = RelayUrlNormalizer.isLocalHost(this.url)
|
fun NormalizedRelayUrl.isLocalHost() = RelayUrlNormalizer.isLocalHost(this.url)
|
||||||
|
|
||||||
|
fun NormalizedRelayUrl.classifyHidden(): HiddenServiceKind = RelayUrlNormalizer.classifyHidden(this.url)
|
||||||
|
|||||||
+11
-1
@@ -48,6 +48,16 @@ class RelayUrlNormalizer {
|
|||||||
|
|
||||||
fun isOnion(url: String) = url.endsWith(".onion") || url.contains(".onion/")
|
fun isOnion(url: String) = url.endsWith(".onion") || url.contains(".onion/")
|
||||||
|
|
||||||
|
fun isI2p(url: String) = url.endsWith(".i2p") || url.contains(".i2p/")
|
||||||
|
|
||||||
|
fun classifyHidden(url: String): HiddenServiceKind =
|
||||||
|
when {
|
||||||
|
isLocalHost(url) -> HiddenServiceKind.LOCALHOST
|
||||||
|
isOnion(url) -> HiddenServiceKind.ONION
|
||||||
|
isI2p(url) -> HiddenServiceKind.I2P
|
||||||
|
else -> HiddenServiceKind.CLEARNET
|
||||||
|
}
|
||||||
|
|
||||||
fun isRelaySchemePrefix(url: String) = url.length > 6 && url[0] == 'w' && url[1] == 's'
|
fun isRelaySchemePrefix(url: String) = url.length > 6 && url[0] == 'w' && url[1] == 's'
|
||||||
|
|
||||||
fun isRelaySchemePrefixSecure(url: String) = url[2] == 's' && url[3] == ':' && url[4] == '/' && url[5] == '/' && url[6] != '/'
|
fun isRelaySchemePrefixSecure(url: String) = url[2] == 's' && url[3] == ':' && url[4] == '/' && url[5] == '/' && url[6] != '/'
|
||||||
@@ -149,7 +159,7 @@ class RelayUrlNormalizer {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (isOnion(trimmed) || isLocalHost(trimmed)) {
|
return if (isOnion(trimmed) || isI2p(trimmed) || isLocalHost(trimmed)) {
|
||||||
"ws://$trimmed"
|
"ws://$trimmed"
|
||||||
} else {
|
} else {
|
||||||
"wss://$trimmed"
|
"wss://$trimmed"
|
||||||
|
|||||||
Reference in New Issue
Block a user