fix(desktop): use IPv4 loopback for media proxy URLs (#1245)

Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Tyler
2026-06-24 20:20:27 -04:00
committed by GitHub
co-authored by npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta
parent 856815994c
commit cee2c5f263
4 changed files with 30 additions and 8 deletions
+13
View File
@@ -0,0 +1,13 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { mediaProxyUrl } from "./mediaUrl.ts";
const HASH = "a".repeat(64);
test("mediaProxyUrl: uses the IPv4 loopback literal for the localhost proxy", () => {
assert.equal(
mediaProxyUrl(54321, `${HASH}.png`),
`http://127.0.0.1:54321/media/${HASH}.png`,
);
});
+11 -2
View File
@@ -80,9 +80,18 @@ export function resetMediaCaches(): void {
cachedRelayOrigin = null;
}
/**
* Build the local proxy URL with an IPv4 literal. The Rust proxy binds
* `127.0.0.1:0`, not `::1`, and some WebViews resolve `localhost` to IPv6
* first. Matching the bind address avoids machine-dependent image failures.
*/
export function mediaProxyUrl(port: number, mediaPath: string): string {
return `http://127.0.0.1:${port}/media/${mediaPath}`;
}
/**
* If `url` is a Blossom media URL hosted on the Buzz relay, rewrite it
* to go through the localhost streaming proxy. External Blossom URLs and
* to go through the local streaming proxy. External Blossom URLs and
* non-Blossom URLs are returned unchanged.
*
* Falls back to buzz-media:// if the proxy port isn't available yet.
@@ -100,7 +109,7 @@ export function rewriteRelayUrl(url: string): string {
}
if (cachedPort && cachedPort > 0) {
return `http://localhost:${cachedPort}/media/${m[1]}`;
return mediaProxyUrl(cachedPort, m[1]);
}
// Kick off fetch if we haven't yet.
+1 -1
View File
@@ -674,7 +674,7 @@ const KIND_REACTION = 7; // NIP-25 reaction
const KIND_DELETION = 5; // NIP-09 deletion
// Fake media-proxy port the mock answers for `get_media_proxy_port`, so
// `rewriteRelayUrl()` produces a real `http://localhost:<port>/media/...` src
// `rewriteRelayUrl()` produces a real `http://127.0.0.1:<port>/media/...` src
// in e2e (instead of the `buzz-media://` fallback). The reaction guard
// asserts against this exact port.
const MOCK_MEDIA_PROXY_PORT = 54321;
+5 -5
View File
@@ -141,14 +141,14 @@ test("native emoji-only messages leave space below the author metadata", async (
//
// This drives the real interactive react flow (hover -> Open reactions ->
// emoji-mart custom category) so it exercises the add_reaction Tauri command,
// then asserts the rendered reaction <img> src points at the localhost media
// then asserts the rendered reaction <img> src points at the loopback media
// proxy. On the pre-fix code the src would be the raw relay URL, so this test
// fails there — exactly the assertion that would have caught the bug.
//
// `:react:` is a relay-hosted fixture emoji (URL on the relay origin matching
// rewriteRelayUrl()'s /media/{64-hex}.{ext} pattern), and the mock bridge
// answers get_media_proxy_port with port 54321 so the rewrite resolves to a
// real localhost URL rather than the buzz-media:// fallback.
// real 127.0.0.1 URL rather than the buzz-media:// fallback.
const REACTION_SHORTCODE = "react";
const MOCK_MEDIA_PROXY_PORT = 54321;
@@ -214,7 +214,7 @@ test("message quick reaction tray stays neutral after selecting a tray emoji", a
);
});
test("reacting with a custom emoji renders via the localhost media proxy", async ({
test("reacting with a custom emoji renders via the loopback media proxy", async ({
page,
}) => {
await openGeneral(page);
@@ -236,7 +236,7 @@ test("reacting with a custom emoji renders via the localhost media proxy", async
.click();
// The reaction pill renders the custom emoji as an <img alt=":react:">. Its
// src must be the localhost proxy URL — proving rewriteRelayUrl() ran. A raw
// src must be the loopback proxy URL — proving rewriteRelayUrl() ran. A raw
// relay URL here is the bug.
const reactionPill = row.getByLabel(
`Toggle :${REACTION_SHORTCODE}: reaction`,
@@ -248,7 +248,7 @@ test("reacting with a custom emoji renders via the localhost media proxy", async
await expect(reactionImg).toHaveAttribute(
"src",
new RegExp(
`^http://localhost:${MOCK_MEDIA_PROXY_PORT}/media/[\\da-f]{64}\\.png$`,
`^http://127\\.0\\.0\\.1:${MOCK_MEDIA_PROXY_PORT}/media/[\\da-f]{64}\\.png$`,
),
);