mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
perf: preemptively sign Blossom reads for known auth-gated hosts
The retry-on-401 interceptor re-probed anonymously on every blob, so each image from an auth-gated host (e.g. a Buzz community feed, where nearly all media is on one host) paid a wasted 401 round trip before the signed retry. Remember hosts that answered 401 and attach the cached token up front on their subsequent blobs — one round trip instead of two in steady state. The first blob per host still costs the probe; the learned set falls back to an anonymous request when no signer is available so a logged-out user never loops. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ao9w26c2gAm4gjJdhgvLyp
This commit is contained in:
+36
-8
@@ -22,7 +22,9 @@ package com.vitorpamplona.amethyst.service.okhttp
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Retries a Blossom blob download with a BUD-01 `t=get` authorization when the
|
||||
@@ -51,10 +53,24 @@ import okhttp3.Response
|
||||
* signer is available or signing times out, and is only consulted on a real
|
||||
* `401`, so an unauthenticated user simply keeps seeing the broken image
|
||||
* rather than paying any signing cost.
|
||||
*
|
||||
* The first blob from an auth-gated host costs an extra round trip (anonymous
|
||||
* `GET` → `401` → signed retry), but that host is then remembered in
|
||||
* [knownAuthHosts] so every later blob from it is signed **up front** — one
|
||||
* round trip, not two. This matters on a Buzz community feed where nearly every
|
||||
* image comes from the same gated host: without it each image would keep paying
|
||||
* the wasted 401 probe. The learned host also short-circuits to anonymous when
|
||||
* no signer is available, so a logged-out user never re-probes needlessly.
|
||||
*/
|
||||
class BlossomReadAuthInterceptor(
|
||||
private val authHeaderProvider: (host: String, sha256: HexKey) -> String?,
|
||||
) : Interceptor {
|
||||
// Hosts observed to answer 401 to an anonymous Blossom GET. Small (a user
|
||||
// follows a handful of auth-gated servers at most) and shared across all
|
||||
// clients derived from the same factory. newKeySet() is thread-safe for the
|
||||
// concurrent reads/writes of parallel feed downloads.
|
||||
private val knownAuthHosts: MutableSet<String> = ConcurrentHashMap.newKeySet()
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
|
||||
@@ -65,24 +81,36 @@ class BlossomReadAuthInterceptor(
|
||||
}
|
||||
|
||||
val sha256 = blossomHashOrNull(request.url.encodedPath) ?: return chain.proceed(request)
|
||||
val host = request.url.host
|
||||
|
||||
// Known-gated host: skip the anonymous probe and sign the first attempt.
|
||||
// Falls through to anonymous only when we can't produce a token (no
|
||||
// signer / timeout) — the server would 401 either way.
|
||||
if (host in knownAuthHosts) {
|
||||
authHeaderProvider(host, sha256)?.let { header ->
|
||||
return chain.proceed(request.withAuth(header))
|
||||
}
|
||||
}
|
||||
|
||||
val response = chain.proceed(request)
|
||||
if (response.code != 401) return response
|
||||
|
||||
val header = authHeaderProvider(request.url.host, sha256) ?: return response
|
||||
// Learn the host so its next blob is signed up front.
|
||||
knownAuthHosts.add(host)
|
||||
|
||||
val header = authHeaderProvider(host, sha256) ?: return response
|
||||
|
||||
// Close the 401 body before replaying so the connection can be reused.
|
||||
response.close()
|
||||
|
||||
val authed =
|
||||
request
|
||||
.newBuilder()
|
||||
.header("Authorization", header)
|
||||
.build()
|
||||
|
||||
return chain.proceed(authed)
|
||||
return chain.proceed(request.withAuth(header))
|
||||
}
|
||||
|
||||
private fun Request.withAuth(header: String) =
|
||||
newBuilder()
|
||||
.header("Authorization", header)
|
||||
.build()
|
||||
|
||||
companion object {
|
||||
private val SHA256_HEX = Regex("^[0-9a-f]{64}$")
|
||||
|
||||
|
||||
+40
@@ -163,6 +163,46 @@ class BlossomReadAuthInterceptorTest {
|
||||
response.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun learnsHostThenSignsSubsequentBlobsUpFront() {
|
||||
val provider = RecordingProvider(header = "Nostr token")
|
||||
val interceptor = BlossomReadAuthInterceptor(provider::header)
|
||||
val otherSha = "b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553"
|
||||
|
||||
// First blob learns the host via the 401 probe + signed retry.
|
||||
val first = fakeChain("https://$host/media/$sha.png", codes = listOf(401, 200))
|
||||
interceptor.intercept(first.asChain()).close()
|
||||
assertEquals(2, first.requests.size)
|
||||
|
||||
// Second, different blob on the same host is signed on the first attempt —
|
||||
// no anonymous probe, so a single request.
|
||||
val second = fakeChain("https://$host/media/$otherSha.png", codes = listOf(200))
|
||||
val response = interceptor.intercept(second.asChain())
|
||||
|
||||
assertEquals(200, response.code)
|
||||
assertEquals("no anonymous probe on a known-gated host", 1, second.requests.size)
|
||||
assertEquals("Nostr token", second.requests.single().header("Authorization"))
|
||||
response.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun learnedHostWithoutSignerDoesNotLoop() {
|
||||
val provider = RecordingProvider(header = null)
|
||||
val interceptor = BlossomReadAuthInterceptor(provider::header)
|
||||
|
||||
val first = fakeChain("https://$host/media/$sha.png", codes = listOf(401))
|
||||
interceptor.intercept(first.asChain()).close()
|
||||
|
||||
// Host is now known, but with no signer the preemptive path must fall
|
||||
// back to a single anonymous request rather than retrying endlessly.
|
||||
val second = fakeChain("https://$host/media/$sha.png", codes = listOf(401))
|
||||
val response = interceptor.intercept(second.asChain())
|
||||
|
||||
assertEquals(401, response.code)
|
||||
assertEquals(1, second.requests.size)
|
||||
response.close()
|
||||
}
|
||||
|
||||
private class RecordingProvider(
|
||||
private val header: String?,
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user