Merge pull request #2919 from greenart7c3/claude/fix-cache-server-url-IbSg8
Fix Blossom blob detection to reject non-compliant filenames
This commit is contained in:
+9
-8
@@ -72,16 +72,17 @@ class LocalBlossomCacheRedirectInterceptor(
|
||||
}
|
||||
|
||||
private fun findSha256AndExtensionInPath(url: HttpUrl): Triple<Int, String, String>? {
|
||||
// Per Blossom (BUD-01) the blob is always the last path segment. If the
|
||||
// last segment isn't a sha256, this isn't a Blossom URL and the bridge
|
||||
// must leave it alone — even if an earlier path segment happens to be
|
||||
// a 64-char hex (e.g. a per-user cache prefix). The prefix segments
|
||||
// are preserved verbatim via `buildServerBase`.
|
||||
// Per Blossom (BUD-01) the last path segment must be exactly
|
||||
// `<sha256>` or `<sha256>.<ext>`. URLs whose filename merely embeds a
|
||||
// 64-char hex (e.g. "nostr.build_<sha>.jpg") aren't Blossom blobs and
|
||||
// the bridge must leave them alone — rewriting them would point the
|
||||
// local cache at a fallback `xs=` server that doesn't host the blob.
|
||||
// Prefix segments are preserved verbatim via `buildServerBase`.
|
||||
val lastIndex = url.pathSegments.lastIndex
|
||||
if (lastIndex < 0) return null
|
||||
val segment = url.pathSegments[lastIndex]
|
||||
val match = SHA256_SEGMENT_REGEX.find(segment) ?: return null
|
||||
val sha = match.value.lowercase()
|
||||
val match = BLOSSOM_LAST_SEGMENT_REGEX.matchEntire(segment) ?: return null
|
||||
val sha = match.groupValues[1].lowercase()
|
||||
val ext = guessExtensionFrom(segment, sha) ?: "bin"
|
||||
return Triple(lastIndex, sha, ext)
|
||||
}
|
||||
@@ -119,6 +120,6 @@ class LocalBlossomCacheRedirectInterceptor(
|
||||
const val LOCAL_CACHE_HOST = "127.0.0.1"
|
||||
const val LOCAL_CACHE_PORT = 24242
|
||||
const val LOCAL_CACHE_BASE = "http://$LOCAL_CACHE_HOST:$LOCAL_CACHE_PORT"
|
||||
private val SHA256_SEGMENT_REGEX = Regex("(?<![0-9a-fA-F])[0-9a-fA-F]{64}(?![0-9a-fA-F])")
|
||||
private val BLOSSOM_LAST_SEGMENT_REGEX = Regex("^([0-9a-fA-F]{64})(?:\\.[^./]+)?$")
|
||||
}
|
||||
}
|
||||
|
||||
+25
@@ -163,6 +163,31 @@ class LocalBlossomCacheRedirectInterceptorTest {
|
||||
response.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bridgeOnSkipsWhenLastSegmentHasNonHexPrefixBeforeSha() {
|
||||
// nostr.build /i/ layout: <prefix>_<sha>.<ext>. The hex inside the
|
||||
// filename isn't a Blossom blob — rewriting to the local cache and
|
||||
// sending xs=https://nostr.build/i would 404 because the real blob
|
||||
// is at /i/nostr.build_<sha>.jpg, not /i/<sha>.
|
||||
val interceptor = LocalBlossomCacheRedirectInterceptor { true }
|
||||
val captured = mutableListOf<String>()
|
||||
val url = "https://nostr.build/i/nostr.build_$sha.jpg"
|
||||
val response = interceptor.intercept(fakeChain(url, captured))
|
||||
assertEquals(url, captured.single())
|
||||
response.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bridgeOnSkipsWhenLastSegmentHasSuffixAfterSha() {
|
||||
// A filename like <sha>_thumb.jpg isn't a BUD-01 blob URL either.
|
||||
val interceptor = LocalBlossomCacheRedirectInterceptor { true }
|
||||
val captured = mutableListOf<String>()
|
||||
val url = "https://example.com/${sha}_thumb.jpg"
|
||||
val response = interceptor.intercept(fakeChain(url, captured))
|
||||
assertEquals(url, captured.single())
|
||||
response.close()
|
||||
}
|
||||
|
||||
private fun fakeChain(
|
||||
url: String,
|
||||
captured: MutableList<String>,
|
||||
|
||||
+8
-7
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.commons.richtext
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomUri
|
||||
|
||||
private val sha256HexRegex = Regex("[0-9a-f]{64}")
|
||||
private val sha256InPathRegex = Regex("(?<![0-9a-fA-F])[0-9a-fA-F]{64}(?![0-9a-fA-F])")
|
||||
private val blossomLastSegmentRegex = Regex("^([0-9a-fA-F]{64})(?:\\.[^./]+)?$")
|
||||
|
||||
/**
|
||||
* Converts this media content into a Coil/ExoPlayer-friendly model string.
|
||||
@@ -144,14 +144,15 @@ private fun percentEncode(input: String): String {
|
||||
}
|
||||
|
||||
private fun extractSha256FromUrlPath(url: String): String? {
|
||||
// Per Blossom (BUD-01) the blob is always the last path segment. If the
|
||||
// last segment isn't a sha256, this isn't a Blossom URL and the bridge
|
||||
// must leave it alone — even if an earlier path segment happens to be
|
||||
// a 64-char hex (e.g. a per-user cache prefix).
|
||||
// Per Blossom (BUD-01) the last path segment must be exactly
|
||||
// `<sha256>` or `<sha256>.<ext>`. URLs whose filename merely embeds a
|
||||
// 64-char hex (e.g. "nostr.build_<sha>.jpg") aren't Blossom blobs and
|
||||
// the bridge must leave them alone — rewriting them would point the
|
||||
// local cache at a fallback `xs=` server that doesn't host the blob.
|
||||
val pathPart = url.substringBefore('?').substringBefore('#')
|
||||
val lastSegment = pathPart.substringAfterLast('/')
|
||||
val match = sha256InPathRegex.find(lastSegment) ?: return null
|
||||
return match.value.lowercase()
|
||||
val match = blossomLastSegmentRegex.matchEntire(lastSegment) ?: return null
|
||||
return match.groupValues[1].lowercase()
|
||||
}
|
||||
|
||||
private fun guessExtension(
|
||||
|
||||
+23
@@ -213,4 +213,27 @@ class MediaUrlContentExtTest {
|
||||
val url = "https://example.com/$sha/avatar.jpg"
|
||||
assertEquals(url, bridgeProfilePictureUrl(url, useBridge = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bridgeOnSkipsWhenLastSegmentHasNonHexPrefixBeforeSha() {
|
||||
// nostr.build /i/ layout: <prefix>_<sha>.<ext>. The hex inside the
|
||||
// filename isn't a Blossom blob per BUD-01 — the last segment must
|
||||
// be exactly <sha> or <sha>.<ext>.
|
||||
val url = "https://nostr.build/i/nostr.build_$sha.jpg"
|
||||
val image = MediaUrlImage(url = url, hash = null)
|
||||
assertEquals(url, image.toCoilModel(useLocalBlossomBridge = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bridgeProfilePictureUrlSkipsWhenLastSegmentHasNonHexPrefixBeforeSha() {
|
||||
val url = "https://nostr.build/i/nostr.build_$sha.jpg"
|
||||
assertEquals(url, bridgeProfilePictureUrl(url, useBridge = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bridgeOnSkipsWhenLastSegmentHasSuffixAfterSha() {
|
||||
val url = "https://example.com/${sha}_thumb.jpg"
|
||||
val image = MediaUrlImage(url = url, hash = null)
|
||||
assertEquals(url, image.toCoilModel(useLocalBlossomBridge = true))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user