From 566a4bf4524bf48ec274fa49245770b1a3879a0f Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Wed, 1 Apr 2026 14:03:13 +0300 Subject: [PATCH] fix(tor): wire Coil image loading through Tor via custom Fetcher.Factory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port Android's OkHttpFactory pattern to desktop — creates a custom Fetcher.Factory that calls DesktopHttpClient.currentClient() per image request, routing all image loads through the SOCKS proxy when Tor is active. Avoids the OkHttpNetworkFetcher.factory() import issue by directly constructing NetworkFetcher with the Tor-aware Call.Factory, matching the proven Android implementation. This closes the last network leak — ALL egress paths now route through Tor when enabled. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../service/images/DesktopImageLoaderSetup.kt | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/images/DesktopImageLoaderSetup.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/images/DesktopImageLoaderSetup.kt index 0d53e31e5..0a72468f1 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/images/DesktopImageLoaderSetup.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/service/images/DesktopImageLoaderSetup.kt @@ -23,11 +23,22 @@ package com.vitorpamplona.amethyst.desktop.service.images import coil3.ImageLoader import coil3.PlatformContext import coil3.SingletonImageLoader +import coil3.Uri import coil3.annotation.DelicateCoilApi +import coil3.annotation.ExperimentalCoilApi import coil3.disk.DiskCache +import coil3.fetch.Fetcher import coil3.memory.MemoryCache +import coil3.network.CacheStrategy +import coil3.network.ConcurrentRequestStrategy +import coil3.network.ConnectivityChecker +import coil3.network.NetworkFetcher +import coil3.network.okhttp.asNetworkClient +import coil3.request.Options import coil3.size.Precision import coil3.svg.SvgDecoder +import com.vitorpamplona.amethyst.desktop.network.DesktopHttpClient +import okhttp3.Call import okio.Path.Companion.toOkioPath import java.io.File @@ -44,8 +55,7 @@ object DesktopImageLoaderSetup { .diskCache { newDiskCache() } .precision(Precision.INEXACT) .components { - // TODO: Wire Coil through Tor — OkHttpNetworkFetcher.factory() not resolving in JVM module - // add(coil3.network.okhttp.OkHttpNetworkFetcher.factory { DesktopHttpClient.currentClient() }) + add(TorAwareOkHttpFactory { DesktopHttpClient.currentClient() }) add(SvgDecoder.Factory()) add(SkiaGifDecoder.Factory()) add(DesktopBase64Fetcher.Factory) @@ -94,3 +104,35 @@ object DesktopImageLoaderSetup { } } } + +/** + * Custom Coil Fetcher.Factory that routes all image requests through the Tor-aware client. + * Pattern ported from Android's OkHttpFactory in ImageLoaderSetup.kt. + * + * Each image request calls [clientProvider] to get the current OkHttpClient, + * which returns the SOCKS proxy client when Tor is active or the direct client when off. + */ +@OptIn(ExperimentalCoilApi::class) +private class TorAwareOkHttpFactory( + val clientProvider: () -> Call.Factory, +) : Fetcher.Factory { + private val cacheStrategyLazy = lazy { CacheStrategy.DEFAULT } + + override fun create( + data: Uri, + options: Options, + imageLoader: ImageLoader, + ): Fetcher? { + if (data.scheme != "http" && data.scheme != "https") return null + + return NetworkFetcher( + url = data.toString(), + options = options, + networkClient = lazy { clientProvider().asNetworkClient() }, + diskCache = lazy { imageLoader.diskCache }, + cacheStrategy = cacheStrategyLazy, + connectivityChecker = lazy { ConnectivityChecker(options.context) }, + concurrentRequestStrategy = lazy { ConcurrentRequestStrategy.UNCOORDINATED }, + ) + } +}