mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
fix(napplet): route brokered resource fetches by the applet's own Tor mode
The consolidation passed `useProxy = true` for every brokered `resource.bytes` fetch, forcing them through Tor whenever Tor was active — regardless of the napplet/nSite's actual network mode. That overrides the user's explicit choice: an nSite running in "open web" mode would still have its blob fetches tunneled, inconsistent with how its own WebView page loads. The authoritative per-applet preference already exists main-side in NappletNetworkRegistry.useTor(coordinate) (locked napplets pinned to Tor; nSites follow the persisted per-site toggle, which relaunches on change) — the same source NappletLauncher reads to set the WebView proxy. Thread the calling applet's coordinate through NappletResourceGateway.fetch so the broker can resolve it, and pick the shared client with getHttpClient(useProxy = NappletNetworkRegistry.useTor(coordinate)). This mirrors the host's own `effectiveProxy = if (useTor) proxyPort else -1` exactly, so a brokered fetch now routes like the applet's page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c797033ba5
commit
1dbfd78b44
@@ -320,8 +320,9 @@ class NappletBrokerService : Service() {
|
||||
context = applicationContext,
|
||||
ledger = ledger,
|
||||
storage = storage,
|
||||
// Prefer Tor when active; the shared manager falls back to clearnet when it isn't.
|
||||
httpClient = { Amethyst.instance.okHttpClients.getHttpClient(useProxy = true) },
|
||||
// Per-applet Tor decision (see NappletResourceFetcher): the shared manager routes
|
||||
// through Tor when asked + active, and falls back to clearnet otherwise.
|
||||
httpClient = { useProxy -> Amethyst.instance.okHttpClients.getHttpClient(useProxy) },
|
||||
).broker()
|
||||
cachedBroker = account to broker
|
||||
return broker
|
||||
|
||||
+2
-2
@@ -71,7 +71,7 @@ class AccountNappletGateways(
|
||||
private val context: Context,
|
||||
private val ledger: NappletPermissionLedger,
|
||||
private val storage: NappletStorage,
|
||||
private val httpClient: () -> OkHttpClient,
|
||||
private val httpClient: (useProxy: Boolean) -> OkHttpClient,
|
||||
) {
|
||||
private val consentSummary = NappletConsentSummary(context)
|
||||
|
||||
@@ -102,7 +102,7 @@ class AccountNappletGateways(
|
||||
}
|
||||
|
||||
val wallet = NappletWalletGateway { invoice -> payInvoiceViaNwc(invoice) }
|
||||
val resource = NappletResourceGateway { url -> resourceFetcher.fetch(url) }
|
||||
val resource = NappletResourceGateway { url, coordinate -> resourceFetcher.fetch(url, coordinate) }
|
||||
val identityReads = NappletIdentityGateway { method, argument -> identityReader.read(method, argument) }
|
||||
val upload = NappletUploadGateway { bytes, contentType, filename -> uploadBlob(bytes, contentType, filename) }
|
||||
val theme = NappletThemeGateway { currentThemeColors() }
|
||||
|
||||
+26
-21
@@ -23,6 +23,7 @@ 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.napplet.NappletNetworkRegistry
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchAll
|
||||
@@ -50,23 +51,32 @@ import java.net.URLDecoder
|
||||
*
|
||||
* 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.
|
||||
* standing up a private client — means napplet blob fetches inherit the same passive
|
||||
* `Onion-Location` discovery + `.onion` rewriting, local Blossom cache redirect, connection pool
|
||||
* and DNS as every other HTTP role. Tor-or-clearnet is chosen per request from the calling
|
||||
* applet's [NappletNetworkRegistry] mode (locked napplets are pinned to Tor; nSites follow the
|
||||
* user's per-site toggle), so a brokered fetch routes exactly like that applet's own page loads.
|
||||
* 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 httpClient: () -> OkHttpClient,
|
||||
private val httpClient: (useProxy: Boolean) -> OkHttpClient,
|
||||
) {
|
||||
/** Fetches an https/data/blossom resource, or null if unsupported/unavailable. */
|
||||
suspend fun fetch(url: String): NappletResource? =
|
||||
/** Fetches an https/data/blossom resource for the applet at [coordinate], or null if unsupported/unavailable. */
|
||||
suspend fun fetch(
|
||||
url: String,
|
||||
coordinate: String,
|
||||
): NappletResource? =
|
||||
withContext(Dispatchers.IO) {
|
||||
// Route like the applet's own page: Tor when its network mode is Tor, clearnet otherwise.
|
||||
NappletNetworkRegistry.awaitReady()
|
||||
val client = httpClient(NappletNetworkRegistry.useTor(coordinate))
|
||||
when {
|
||||
url.startsWith("data:") -> decodeDataUrl(url)
|
||||
url.startsWith("https://") -> {
|
||||
runCatching {
|
||||
client()
|
||||
client
|
||||
.newCall(
|
||||
Request
|
||||
.Builder()
|
||||
@@ -82,7 +92,7 @@ class NappletResourceFetcher(
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
url.startsWith("blossom:") -> fetchBlossom(url)
|
||||
url.startsWith("blossom:") -> fetchBlossom(url, client)
|
||||
url.startsWith("nostr:") -> resolveNostr(url)
|
||||
else -> null
|
||||
}
|
||||
@@ -139,20 +149,16 @@ class NappletResourceFetcher(
|
||||
.maxByOrNull { it.createdAt }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private fun client(): OkHttpClient = httpClient()
|
||||
|
||||
/**
|
||||
* Fetches a `blossom:<sha256>` (or `blossom://<sha256>`) blob from the user's Blossom servers
|
||||
* (kind:10063), verifying the sha256 before returning — content-addressed, so a wrong server
|
||||
* can never substitute the blob. Returns null for a malformed hash or if no server serves it.
|
||||
* (kind:10063) over [client], verifying the sha256 before returning — content-addressed, so a
|
||||
* wrong server can never substitute the blob. Returns null for a malformed hash or if no server
|
||||
* serves it.
|
||||
*/
|
||||
private fun fetchBlossom(url: String): NappletResource? {
|
||||
private fun fetchBlossom(
|
||||
url: String,
|
||||
client: OkHttpClient,
|
||||
): NappletResource? {
|
||||
val hash =
|
||||
url
|
||||
.removePrefix("blossom://")
|
||||
@@ -168,7 +174,6 @@ class NappletResourceFetcher(
|
||||
.getBlossomServersList()
|
||||
?.servers()
|
||||
.orEmpty()
|
||||
val client = client()
|
||||
for (candidate in StaticSiteResolver.candidateUrls(servers, hash)) {
|
||||
val bytes =
|
||||
runCatching {
|
||||
|
||||
+1
-1
@@ -274,7 +274,7 @@ class NappletBroker(
|
||||
|
||||
is NappletRequest.ResourceBytes -> {
|
||||
val gateway = resource ?: return NappletResponse.Unsupported("resource.bytes")
|
||||
val fetched = gateway.fetch(request.url) ?: return NappletResponse.Failed("Could not fetch the resource.")
|
||||
val fetched = gateway.fetch(request.url, identity.coordinate) ?: return NappletResponse.Failed("Could not fetch the resource.")
|
||||
NappletResponse.Bytes(fetched.bytes, fetched.contentType)
|
||||
}
|
||||
|
||||
|
||||
+8
-1
@@ -161,9 +161,16 @@ class NappletResource(
|
||||
* (`resource.bytes`). The host fetches https/blossom/nostr/data URLs on the applet's behalf —
|
||||
* the applet itself has no direct network (CSP `connect-src 'none'`). Returns `null` for an
|
||||
* unsupported scheme or a failed fetch.
|
||||
*
|
||||
* [coordinate] is the calling applet's identity coordinate (`author:identifier`), so the host can
|
||||
* route the fetch the same way the applet's own page loads — through Tor or the open web — per that
|
||||
* applet's/site's network mode.
|
||||
*/
|
||||
fun interface NappletResourceGateway {
|
||||
suspend fun fetch(url: String): NappletResource?
|
||||
suspend fun fetch(
|
||||
url: String,
|
||||
coordinate: String,
|
||||
): NappletResource?
|
||||
}
|
||||
|
||||
/** A completed upload: where the blob lives plus NIP-94-ish metadata. */
|
||||
|
||||
Reference in New Issue
Block a user