From c797033ba5bbcb7a653036defb4648d416509714 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 26 Jun 2026 16:34:52 -0400 Subject: [PATCH] refactor(napplet): route blob fetches through the shared OkHttpClientFactory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The t4 onion-location proposal hand-wired OnionLocationInterceptor + OnionUrlRewriteInterceptor into NappletResourceFetcher's private, torPort-keyed OkHttpClient. That reached onion-routing parity but duplicated the exact wiring OkHttpClientFactory already does, and the private client still missed the local Blossom cache redirect, the shared connection pool / HTTP-2 keepalive, and SurgeDns. Inject the app-wide client instead: NappletResourceFetcher now takes a () -> OkHttpClient and the broker supplies `okHttpClients.getHttpClient(useProxy = true)` — the same DualHttpClientManager path the image pipeline uses. Behavior-preserving for Tor (proxied when Tor is active, clearnet when not) and, since these are sha256 blobs, the shared Blossom-cache redirect is now a feature, not a loss. Drops the private client + its cache and the hand-wired interceptors. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../amethyst/napplet/NappletBrokerService.kt | 3 +- .../gateways/AccountNappletGateways.kt | 11 ++-- .../gateways/NappletResourceFetcher.kt | 52 +++++-------------- 3 files changed, 20 insertions(+), 46 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt index d27bd89e3d..391773cbc5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletBrokerService.kt @@ -320,7 +320,8 @@ class NappletBrokerService : Service() { context = applicationContext, ledger = ledger, storage = storage, - torPort = { Amethyst.instance.torManager.activePortOrNull.value ?: -1 }, + // Prefer Tor when active; the shared manager falls back to clearnet when it isn't. + httpClient = { Amethyst.instance.okHttpClients.getHttpClient(useProxy = true) }, ).broker() cachedBroker = account to broker return broker diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/gateways/AccountNappletGateways.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/gateways/AccountNappletGateways.kt index 60ba78a6d8..ee56c95694 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/gateways/AccountNappletGateways.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/gateways/AccountNappletGateways.kt @@ -56,6 +56,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse import com.vitorpamplona.quartz.utils.sha256.sha256 import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.withTimeout +import okhttp3.OkHttpClient import java.io.ByteArrayInputStream import kotlin.time.Duration.Companion.seconds @@ -70,14 +71,14 @@ class AccountNappletGateways( private val context: Context, private val ledger: NappletPermissionLedger, private val storage: NappletStorage, - private val torPort: () -> Int, + private val httpClient: () -> OkHttpClient, ) { private val consentSummary = NappletConsentSummary(context) - // Share the app-wide OnionLocationCache so any `Onion-Location` learned - // elsewhere (NIP-11 docs, relay handshakes, image hosts, money endpoints) - // also benefits napplet HTTP blob fetches over Tor — and vice versa. - private val resourceFetcher = NappletResourceFetcher(account, torPort, Amethyst.instance.onionLocationCache) + // Reuse the app-wide HTTP client so napplet blob fetches inherit the same Tor + // routing, Onion-Location discovery/rewriting, Blossom cache and pool as the + // rest of the app, instead of a private client that has to re-wire all of it. + private val resourceFetcher = NappletResourceFetcher(account, httpClient) private val identityReader = AccountIdentityReader(account) fun broker(): NappletBroker { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/gateways/NappletResourceFetcher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/gateways/NappletResourceFetcher.kt index 052b27cfdb..b8ce3fd977 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/gateways/NappletResourceFetcher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/gateways/NappletResourceFetcher.kt @@ -23,9 +23,6 @@ package com.vitorpamplona.amethyst.napplet.gateways import android.util.Base64 import com.vitorpamplona.amethyst.commons.napplet.NappletResource import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.service.okhttp.OnionLocationCache -import com.vitorpamplona.amethyst.service.okhttp.OnionLocationInterceptor -import com.vitorpamplona.amethyst.service.okhttp.OnionUrlRewriteInterceptor import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchAll @@ -43,8 +40,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request -import java.net.InetSocketAddress -import java.net.Proxy import java.net.URLDecoder /** @@ -53,22 +48,17 @@ import java.net.URLDecoder * `https:`, and `blossom:` URLs; blossom blobs are content-addressed and **sha256-verified** before * returning, so a wrong server can never substitute the blob. * - * Owns a Tor-routed [OkHttpClient], cached and rebuilt only when the active Tor port ([torPort]) - * changes. Built per account (so it reads the right Blossom server list); consent is enforced by the - * broker before [fetch] ever runs. + * Network goes through the app-wide [OkHttpClient] supplied by [httpClient] (the shared + * [com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager]). Reusing it — rather than + * standing up a private client — means napplet blob fetches inherit the same Tor routing, + * passive `Onion-Location` discovery + `.onion` rewriting, local Blossom cache redirect, + * connection pool and DNS as every other HTTP role. Built per account (so it reads the right + * Blossom server list); consent is enforced by the broker before [fetch] ever runs. */ class NappletResourceFetcher( private val account: Account, - private val torPort: () -> Int, - // Shared with the rest of the app so an `Onion-Location` learned via any - // OkHttp client (e.g. an NIP-11 fetch on a relay socket) transparently - // applies to napplet blob fetches too — and vice versa. Optional so unit - // tests can construct this without standing up the cache. - private val onionCache: OnionLocationCache? = null, + private val httpClient: () -> OkHttpClient, ) { - // Reused blob HTTP client, keyed by the active Tor port (see client()). - private var cachedHttp: Pair? = null - /** Fetches an https/data/blossom resource, or null if unsupported/unavailable. */ suspend fun fetch(url: String): NappletResource? = withContext(Dispatchers.IO) { @@ -150,30 +140,12 @@ class NappletResourceFetcher( } /** - * Tor-routed OkHttp client for host-side blob fetches (the applet has no direct network). - * Cached and reused for connection pooling; rebuilt only when the Tor proxy port changes. + * The app-wide OkHttp client for host-side blob fetches (the applet has no direct network). + * The shared manager already routes through Tor when active, captures `Onion-Location` and + * rewrites `.onion`s, bridges the local Blossom cache, and pools connections — so there is no + * private client to build or cache here. */ - @Synchronized - private fun client(): OkHttpClient { - val port = torPort() - cachedHttp?.let { (cachedPort, client) -> if (cachedPort == port) return client } - // Both variants passively capture `Onion-Location` headers into the - // shared cache. The Tor-routed variant additionally rewrites outbound - // URLs to known `.onion`s so applet blob fetches over Tor avoid exit - // nodes when the destination has advertised an onion. Clearnet variant - // never rewrites (DNS would fail on `.onion`). - val builder = OkHttpClient.Builder() - if (port > 0) { - builder.proxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", port))) - } - onionCache?.let { builder.addInterceptor(OnionLocationInterceptor(it)) } - if (port > 0) { - onionCache?.let { builder.addInterceptor(OnionUrlRewriteInterceptor(it)) } - } - val client = builder.build() - cachedHttp = port to client - return client - } + private fun client(): OkHttpClient = httpClient() /** * Fetches a `blossom:` (or `blossom://`) blob from the user's Blossom servers