- Adds a new Settings page for Tor
- Adjusts all web calls to use Tor if that setting is enabled. - Allows .onion urls to use Tor - Blocks localhost from using Tor - Moves OTS web calls to use the Tor Proxy as well. - Starts to build all OkHttp clients from a main root node to keep the same thread pool - Refactors the URL Preview code - Changes ammolite to force proxy. - Changes NIP-47 implementation to force relay for the NWC connection.
This commit is contained in:
@@ -44,10 +44,10 @@ object Client : RelayPool.Listener {
|
||||
|
||||
@Synchronized
|
||||
fun reconnect(
|
||||
relays: Array<RelaySetupInfo>?,
|
||||
relays: Array<RelaySetupInfoToConnect>?,
|
||||
onlyIfChanged: Boolean = false,
|
||||
) {
|
||||
Log.d("Relay", "Relay Pool Reconnecting to ${relays?.size} relays: \n${relays?.joinToString("\n") { it.url + " " + it.read + " " + it.write + " " + it.feedTypes.joinToString(",") { it.name } }}")
|
||||
Log.d("Relay", "Relay Pool Reconnecting to ${relays?.size} relays: \n${relays?.joinToString("\n") { it.url + " " + it.forceProxy + " " + it.read + " " + it.write + " " + it.feedTypes.joinToString(",") { it.name } }}")
|
||||
checkNotInMainThread()
|
||||
|
||||
if (onlyIfChanged) {
|
||||
@@ -59,7 +59,7 @@ object Client : RelayPool.Listener {
|
||||
}
|
||||
|
||||
if (relays != null) {
|
||||
val newRelays = relays.map { Relay(it.url, it.read, it.write, it.feedTypes) }
|
||||
val newRelays = relays.map { Relay(it.url, it.read, it.write, it.forceProxy, it.feedTypes) }
|
||||
RelayPool.register(this)
|
||||
RelayPool.loadRelays(newRelays)
|
||||
RelayPool.requestAndWatch()
|
||||
@@ -74,7 +74,7 @@ object Client : RelayPool.Listener {
|
||||
}
|
||||
|
||||
if (relays != null) {
|
||||
val newRelays = relays.map { Relay(it.url, it.read, it.write, it.feedTypes) }
|
||||
val newRelays = relays.map { Relay(it.url, it.read, it.write, it.forceProxy, it.feedTypes) }
|
||||
RelayPool.register(this)
|
||||
RelayPool.loadRelays(newRelays)
|
||||
RelayPool.requestAndWatch()
|
||||
@@ -83,7 +83,7 @@ object Client : RelayPool.Listener {
|
||||
}
|
||||
}
|
||||
|
||||
fun isSameRelaySetConfig(newRelayConfig: Array<RelaySetupInfo>?): Boolean {
|
||||
fun isSameRelaySetConfig(newRelayConfig: Array<RelaySetupInfoToConnect>?): Boolean {
|
||||
if (relays.size != newRelayConfig?.size) return false
|
||||
|
||||
relays.forEach { oldRelayInfo ->
|
||||
@@ -137,6 +137,7 @@ object Client : RelayPool.Listener {
|
||||
suspend fun sendAndWaitForResponse(
|
||||
signedEvent: EventInterface,
|
||||
relay: String? = null,
|
||||
forceProxy: Boolean = false,
|
||||
feedTypes: Set<FeedType>? = null,
|
||||
relayList: List<RelaySetupInfo>? = null,
|
||||
onDone: (() -> Unit)? = null,
|
||||
@@ -201,7 +202,13 @@ object Client : RelayPool.Listener {
|
||||
|
||||
val job =
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
send(signedEvent, relay, feedTypes, relayList, onDone)
|
||||
if (relayList != null) {
|
||||
send(signedEvent, relayList)
|
||||
} else if (relay == null) {
|
||||
send(signedEvent)
|
||||
} else {
|
||||
sendSingle(signedEvent, RelaySetupInfoToConnect(relay, forceProxy, true, true, emptySet()), onDone ?: {})
|
||||
}
|
||||
}
|
||||
job.join()
|
||||
|
||||
@@ -224,34 +231,51 @@ object Client : RelayPool.Listener {
|
||||
RelayPool.connectAndSendFiltersIfDisconnected()
|
||||
}
|
||||
|
||||
fun send(
|
||||
fun sendIfExists(
|
||||
signedEvent: EventInterface,
|
||||
relay: String? = null,
|
||||
feedTypes: Set<FeedType>? = null,
|
||||
relayList: List<RelaySetupInfo>? = null,
|
||||
onDone: (() -> Unit)? = null,
|
||||
connectedRelay: Relay,
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (relayList != null) {
|
||||
RelayPool.sendToSelectedRelays(relayList, signedEvent)
|
||||
} else if (relay == null) {
|
||||
RelayPool.send(signedEvent)
|
||||
} else {
|
||||
RelayPool.getOrCreateRelay(relay, feedTypes, onDone) {
|
||||
it.send(signedEvent)
|
||||
}
|
||||
RelayPool.getRelays(connectedRelay.url).forEach {
|
||||
it.send(signedEvent)
|
||||
}
|
||||
}
|
||||
|
||||
fun sendSingle(
|
||||
signedEvent: EventInterface,
|
||||
relayTemplate: RelaySetupInfoToConnect,
|
||||
onDone: (() -> Unit),
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
|
||||
RelayPool.getOrCreateRelay(relayTemplate, onDone) {
|
||||
it.send(signedEvent)
|
||||
}
|
||||
}
|
||||
|
||||
fun send(signedEvent: EventInterface) {
|
||||
checkNotInMainThread()
|
||||
RelayPool.send(signedEvent)
|
||||
}
|
||||
|
||||
fun send(
|
||||
signedEvent: EventInterface,
|
||||
relayList: List<RelaySetupInfo>,
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
|
||||
RelayPool.sendToSelectedRelays(relayList, signedEvent)
|
||||
}
|
||||
|
||||
fun sendPrivately(
|
||||
signedEvent: EventInterface,
|
||||
relayList: List<String>,
|
||||
relayList: List<RelaySetupInfoToConnect>,
|
||||
) {
|
||||
checkNotInMainThread()
|
||||
|
||||
relayList.forEach { relayUrl ->
|
||||
RelayPool.getOrCreateRelay(relayUrl, emptySet(), { }) {
|
||||
relayList.forEach { relayTemplate ->
|
||||
RelayPool.getOrCreateRelay(relayTemplate, { }) {
|
||||
it.sendOverride(signedEvent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,9 @@ import com.vitorpamplona.quartz.encoders.RelayUrlFormatter
|
||||
object Constants {
|
||||
val activeTypes = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS)
|
||||
val activeTypesChats = setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS)
|
||||
val activeTypesGlobalChats =
|
||||
setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS, FeedType.GLOBAL)
|
||||
val activeTypesGlobalChats = setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS, FeedType.GLOBAL)
|
||||
val activeTypesSearch = setOf(FeedType.SEARCH)
|
||||
|
||||
fun convertDefaultRelays(): Array<Relay> = defaultRelays.map { Relay(it.url, it.read, it.write, it.feedTypes) }.toTypedArray()
|
||||
|
||||
val defaultRelays =
|
||||
arrayOf(
|
||||
// Free relays for only DMs, Chats and Follows due to the amount of spam
|
||||
|
||||
@@ -56,6 +56,7 @@ class Relay(
|
||||
val url: String,
|
||||
val read: Boolean = true,
|
||||
val write: Boolean = true,
|
||||
val forceProxy: Boolean = false,
|
||||
val activeTypes: Set<FeedType>,
|
||||
) {
|
||||
companion object {
|
||||
@@ -109,7 +110,7 @@ class Relay(
|
||||
private var connectingBlock = AtomicBoolean()
|
||||
|
||||
fun connectAndRun(onConnected: (Relay) -> Unit) {
|
||||
Log.d("Relay", "Relay.connect $url isAlreadyConnecting: ${connectingBlock.get()}")
|
||||
Log.d("Relay", "Relay.connect $url proxy: $forceProxy isAlreadyConnecting: ${connectingBlock.get()}")
|
||||
// BRB is crashing OkHttp Deflater object :(
|
||||
if (url.contains("brb.io")) return
|
||||
|
||||
@@ -131,11 +132,10 @@ class Relay(
|
||||
val request =
|
||||
Request
|
||||
.Builder()
|
||||
.header("User-Agent", HttpClientManager.getDefaultUserAgentHeader())
|
||||
.url(url.trim())
|
||||
.build()
|
||||
|
||||
socket = HttpClientManager.getHttpClientForUrl(url).newWebSocket(request, RelayListener(onConnected))
|
||||
socket = HttpClientManager.getHttpClient(forceProxy).newWebSocket(request, RelayListener(onConnected))
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
|
||||
@@ -558,8 +558,9 @@ class Relay(
|
||||
writeToSocket("""["CLOSE","$subscriptionId"]""")
|
||||
}
|
||||
|
||||
fun isSameRelayConfig(other: RelaySetupInfo): Boolean =
|
||||
fun isSameRelayConfig(other: RelaySetupInfoToConnect): Boolean =
|
||||
url == other.url &&
|
||||
forceProxy == other.forceProxy &&
|
||||
write == other.write &&
|
||||
read == other.read &&
|
||||
activeTypes == other.feedTypes
|
||||
|
||||
@@ -58,20 +58,22 @@ object RelayPool : Relay.Listener {
|
||||
fun getAll() = relays
|
||||
|
||||
fun getOrCreateRelay(
|
||||
url: String,
|
||||
feedTypes: Set<FeedType>? = null,
|
||||
relayTemplate: RelaySetupInfoToConnect,
|
||||
onDone: (() -> Unit)? = null,
|
||||
whenConnected: (Relay) -> Unit,
|
||||
) {
|
||||
synchronized(this) {
|
||||
val matching = getRelays(url)
|
||||
val matching = getRelays(relayTemplate.url)
|
||||
if (matching.isNotEmpty()) {
|
||||
matching.forEach { whenConnected(it) }
|
||||
} else {
|
||||
/** temporary connection */
|
||||
newSporadicRelay(
|
||||
url,
|
||||
feedTypes,
|
||||
relayTemplate.url,
|
||||
relayTemplate.read,
|
||||
relayTemplate.write,
|
||||
relayTemplate.forceProxy,
|
||||
relayTemplate.feedTypes,
|
||||
onConnected = whenConnected,
|
||||
onDone = onDone,
|
||||
)
|
||||
@@ -82,12 +84,15 @@ object RelayPool : Relay.Listener {
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun newSporadicRelay(
|
||||
url: String,
|
||||
read: Boolean,
|
||||
write: Boolean,
|
||||
forceProxy: Boolean,
|
||||
feedTypes: Set<FeedType>?,
|
||||
onConnected: (Relay) -> Unit,
|
||||
onDone: (() -> Unit)?,
|
||||
timeout: Long = 60000,
|
||||
) {
|
||||
val relay = Relay(url, true, true, feedTypes ?: emptySet())
|
||||
val relay = Relay(url, read, write, forceProxy, feedTypes ?: emptySet())
|
||||
addRelay(relay)
|
||||
|
||||
relay.connectAndRun {
|
||||
@@ -110,11 +115,8 @@ object RelayPool : Relay.Listener {
|
||||
}
|
||||
|
||||
fun loadRelays(relayList: List<Relay>) {
|
||||
if (!relayList.isNullOrEmpty()) {
|
||||
relayList.forEach { addRelay(it) }
|
||||
} else {
|
||||
Constants.convertDefaultRelays().forEach { addRelay(it) }
|
||||
}
|
||||
check(relayList.isNotEmpty()) { "Relay list should never be empty" }
|
||||
relayList.forEach { addRelay(it) }
|
||||
}
|
||||
|
||||
fun unloadRelays() {
|
||||
|
||||
@@ -29,3 +29,12 @@ data class RelaySetupInfo(
|
||||
val write: Boolean,
|
||||
val feedTypes: Set<FeedType>,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class RelaySetupInfoToConnect(
|
||||
val url: String,
|
||||
val forceProxy: Boolean,
|
||||
val read: Boolean,
|
||||
val write: Boolean,
|
||||
val feedTypes: Set<FeedType>,
|
||||
)
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.ammolite.service
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager.setDefaultProxy
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
@@ -29,37 +30,58 @@ import java.io.IOException
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.time.Duration
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class LoggingInterceptor : Interceptor {
|
||||
@Throws(IOException::class)
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request: Request = chain.request()
|
||||
val t1 = System.nanoTime()
|
||||
val port =
|
||||
(
|
||||
chain
|
||||
.connection()
|
||||
?.route()
|
||||
?.proxy
|
||||
?.address() as? InetSocketAddress
|
||||
)?.port
|
||||
val response: Response = chain.proceed(request)
|
||||
val t2 = System.nanoTime()
|
||||
|
||||
Log.d("OkHttpLog", "Req $port ${request.url} in ${(t2 - t1) / 1e6}ms")
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
object HttpClientManager {
|
||||
val rootClient =
|
||||
OkHttpClient
|
||||
.Builder()
|
||||
.followRedirects(true)
|
||||
.followSslRedirects(true)
|
||||
.build()
|
||||
|
||||
val DEFAULT_TIMEOUT_ON_WIFI: Duration = Duration.ofSeconds(10L)
|
||||
val DEFAULT_TIMEOUT_ON_MOBILE: Duration = Duration.ofSeconds(30L)
|
||||
|
||||
var proxyChangeListeners = ArrayList<() -> Unit>()
|
||||
private var defaultTimeout = DEFAULT_TIMEOUT_ON_WIFI
|
||||
private var defaultHttpClient: OkHttpClient? = null
|
||||
private var defaultHttpClientWithoutProxy: OkHttpClient? = null
|
||||
private var userAgent: String = "Amethyst"
|
||||
|
||||
// fires off every time value of the property changes
|
||||
private var internalProxy: Proxy? by
|
||||
Delegates.observable(null) { _, oldValue, newValue ->
|
||||
if (oldValue != newValue) {
|
||||
proxyChangeListeners.forEach { it() }
|
||||
}
|
||||
}
|
||||
private var currentProxy: Proxy? = null
|
||||
|
||||
fun setDefaultProxy(proxy: Proxy?) {
|
||||
if (internalProxy != proxy) {
|
||||
if (currentProxy != proxy) {
|
||||
Log.d("HttpClient", "Changing proxy to: ${proxy != null}")
|
||||
this.internalProxy = proxy
|
||||
this.currentProxy = proxy
|
||||
|
||||
// recreates singleton
|
||||
this.defaultHttpClient = buildHttpClient(internalProxy, defaultTimeout)
|
||||
this.defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
fun getDefaultProxy(): Proxy? = this.internalProxy
|
||||
fun getCurrentProxy(): Proxy? = this.currentProxy
|
||||
|
||||
fun setDefaultTimeout(timeout: Duration) {
|
||||
Log.d("HttpClient", "Changing timeout to: $timeout")
|
||||
@@ -67,33 +89,34 @@ object HttpClientManager {
|
||||
this.defaultTimeout = timeout
|
||||
|
||||
// recreates singleton
|
||||
this.defaultHttpClient = buildHttpClient(internalProxy, defaultTimeout)
|
||||
this.defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout)
|
||||
this.defaultHttpClientWithoutProxy = buildHttpClient(null, defaultTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
fun setDefaultUserAgent(userAgentHeader: String) {
|
||||
Log.d("HttpClient", "Changing userAgent")
|
||||
this.userAgent = userAgentHeader
|
||||
this.defaultHttpClient = buildHttpClient(internalProxy, defaultTimeout)
|
||||
if (userAgent != userAgentHeader) {
|
||||
this.userAgent = userAgentHeader
|
||||
this.defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout)
|
||||
this.defaultHttpClientWithoutProxy = buildHttpClient(null, defaultTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
fun getDefaultUserAgentHeader() = this.userAgent
|
||||
|
||||
private fun buildHttpClient(
|
||||
proxy: Proxy?,
|
||||
timeout: Duration,
|
||||
): OkHttpClient {
|
||||
val seconds = if (proxy != null) timeout.seconds * 3 else timeout.seconds
|
||||
val duration = Duration.ofSeconds(seconds)
|
||||
return OkHttpClient
|
||||
.Builder()
|
||||
return rootClient
|
||||
.newBuilder()
|
||||
.proxy(proxy)
|
||||
.readTimeout(duration)
|
||||
.connectTimeout(duration)
|
||||
.writeTimeout(duration)
|
||||
.addInterceptor(DefaultContentTypeInterceptor(userAgent))
|
||||
.followRedirects(true)
|
||||
.followSslRedirects(true)
|
||||
.addNetworkInterceptor(LoggingInterceptor())
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -112,20 +135,17 @@ object HttpClientManager {
|
||||
}
|
||||
}
|
||||
|
||||
fun getHttpClientForUrl(url: String): OkHttpClient {
|
||||
// TODO: How to identify relays on the local network?
|
||||
val isLocalHost = url.startsWith("ws://127.0.0.1") || url.startsWith("ws://localhost")
|
||||
return if (isLocalHost) {
|
||||
getHttpClient(false)
|
||||
fun getCurrentProxyPort(useProxy: Boolean): Int? =
|
||||
if (useProxy) {
|
||||
(currentProxy?.address() as? InetSocketAddress)?.port
|
||||
} else {
|
||||
getHttpClient()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getHttpClient(useProxy: Boolean = true): OkHttpClient =
|
||||
fun getHttpClient(useProxy: Boolean): OkHttpClient =
|
||||
if (useProxy) {
|
||||
if (this.defaultHttpClient == null) {
|
||||
this.defaultHttpClient = buildHttpClient(internalProxy, defaultTimeout)
|
||||
this.defaultHttpClient = buildHttpClient(currentProxy, defaultTimeout)
|
||||
}
|
||||
defaultHttpClient!!
|
||||
} else {
|
||||
@@ -135,9 +155,7 @@ object HttpClientManager {
|
||||
defaultHttpClientWithoutProxy!!
|
||||
}
|
||||
|
||||
fun initProxy(
|
||||
useProxy: Boolean,
|
||||
hostname: String,
|
||||
port: Int,
|
||||
): Proxy? = if (useProxy) Proxy(Proxy.Type.SOCKS, InetSocketAddress(hostname, port)) else null
|
||||
fun setDefaultProxyOnPort(port: Int) {
|
||||
setDefaultProxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", port)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 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.ammolite
|
||||
|
||||
import com.vitorpamplona.ammolite.service.HttpClientManager
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
class HttpClientHelperTest {
|
||||
@Test
|
||||
fun test() {
|
||||
val proxy = HttpClientManager.getDefaultProxy()
|
||||
assertEquals(null, proxy)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user