openvibers/openvibe.live Commits

Live: a channel's own OpenVibe avatar in its social cards loads again (the image proxy refuses SVG) (#121)

OP OpenVibers committed 30 days ago · 73a06c3835 · 2 files, +10 −3

The browser check after #118 failed on /@iZoR_One:
`/api/img-proxy?url=https://openvibe.network/avatar/iZoR_One?s=160` answered 415. That avatar is generated, so it is SVG, and the proxy rightly refuses SVG: served from openvibe.live's origin it could run script.

Since #120 every card picture went through the proxy, including OpenVibe's own. Pictures from openvibe.network, openvibe.media and openvibe.community now stay direct; those hosts are already in Live's img-src. Every other platform's picture still goes through the proxy. The match is on exact hosts only.

test/social-links.test.js adds the avatar case and a look-alike host.

Changes

ddiff --git a/server/social/preview.js b/server/social/preview.js
iindex a3473a53..02f8f47a 100644
---- a/server/social/preview.js
++++ b/server/social/preview.js
@@@ -39,13 +39,17 @@ const RARITY = { common: 'Common', uncommon: 'Uncommon', rare: 'Rare', epic: 'Ep
const httpsOnly = (u) => (typeof u === 'string' && /^https:\/\//i.test(u) ? u : null);
/**
-- * A preview as the channel page shows it: every picture (avatar, live thumbnail, post image) through Live's image proxy
-- * (/api/img-proxy), so a viewer's browser never asks the platform itself. That keeps viewers private, and the page's
++ * A preview as the channel page shows it: every picture (avatar, live thumbnail, post image) from another platform goes
++ * through Live's image proxy (/api/img-proxy), so a viewer's browser never asks the platform itself. That keeps viewers private, and the page's
* img-src needs no list of every platform's CDN (a Twitch avatar on static-cdn.jtvnw.net was blocked).
*/
++// OpenVibe's own image hosts are in the page's img-src and serve generated avatars as SVG, which the image proxy refuses
++// (415): their pictures stay direct.
++const FIRST_PARTY_IMAGES = new Set(['openvibe.network', 'openvibe.media', 'openvibe.community']);
function forPage(p) {
if (!p || typeof p !== 'object') return p;
-- const via = (u) => (httpsOnly(u) ? `/api/img-proxy?url=${encodeURIComponent(u)}` : null);
++ const firstParty = (u) => { try { return FIRST_PARTY_IMAGES.has(new URL(u).hostname); } catch { return false; } };
++ const via = (u) => (!httpsOnly(u) ? null : firstParty(u) ? u : `/api/img-proxy?url=${encodeURIComponent(u)}`);
const out = { ...p, image: via(p.image) };
if (p.live) out.live = { ...p.live, thumbnail: via(p.live.thumbnail) };
if (Array.isArray(p.items)) out.items = p.items.map((it) => ({ ...it, image: via(it.image) }));
ddiff --git a/test/social-links.test.js b/test/social-links.test.js
iindex 84df4c1c..c22aa9ea 100644
---- a/test/social-links.test.js
++++ b/test/social-links.test.js
@@@ -127,6 +127,9 @@ async function check(name, fn) { await fn(); passed++; console.log(` ✓ ${name
assert.ok(p.live.thumbnail.startsWith('/api/img-proxy?url=https%3A%2F%2Fstatic-cdn.jtvnw.net'));
assert.deepStrictEqual(p.items.map((i) => i.image && i.image.slice(0, 15)), ['/api/img-proxy?', null]);
assert.strictEqual(forPage({ title: 'x', image: 'http://plain.example/a.png' }).image, null, 'never a plain-http picture');
++ // OpenVibe's own avatar (a generated SVG, which the proxy refuses with 415) stays direct: its host is in img-src.
++ assert.strictEqual(forPage({ title: 'iZoR_One', image: 'https://openvibe.network/avatar/iZoR_One?s=160' }).image, 'https://openvibe.network/avatar/iZoR_One?s=160');
++ assert.strictEqual(forPage({ title: 'x', image: 'https://openvibe.network.evil.example/a.png' }).image.slice(0, 15), '/api/img-proxy?', 'only the exact hosts');
assert.match(require('fs').readFileSync(require('path').join(__dirname, '..', 'server', 'social', 'routes.js'), 'utf8'), /res\.json\(forPage\(p\)\)/);
});
console.log(`social-links: ${passed} checks passed`);