openvibers/openvibe.locker Commits

Media downloads signed on the public origin are fetched over loopback (#2)

OP OpenVibers committed 5 days ago · 3e3e9f4402 · 3 files, +27 −2

In production Locker reaches Media at 127.0.0.1:4100 while Media signs download URLs on https://openvibe.media, so
every download failed with locker.media.origin (502). The same signed path is now fetched on the loopback origin;
any other origin is still refused, and no service credentials follow a signed URL.

Changes

ddiff --git a/server/config.js b/server/config.js
iindex fe8f848..3b75286 100644
---- a/server/config.js
++++ b/server/config.js
@@@ -14,6 +14,9 @@ function load(env = process.env) {
networkUrl: env.OV_NETWORK_URL || 'https://openvibe.network',
networkInternalUrl: env.OV_NETWORK_INTERNAL_URL || 'http://127.0.0.1:4000',
mediaUrl: env.OV_MEDIA_URL || 'https://openvibe.media',
++ // Media signs download URLs on its public origin; when Locker reaches Media over loopback, the same signed path
++ // is fetched there (no service credentials follow either way).
++ mediaPublicUrl: env.OV_MEDIA_PUBLIC_URL || 'https://openvibe.media',
eventsUrl: env.OV_EVENTS_URL || 'https://openvibe.events',
clientId: 'locker', clientSecret: env.OV_OAUTH_CLIENT_SECRET || '', eventsSecret: env.LOCKER_EVENTS_SECRET || '',
dbUrl: env.DATABASE_URL, dbDirectUrl: env.DATABASE_DIRECT_URL,
ddiff --git a/server/media.js b/server/media.js
iindex ac53e3e..91de317 100644
---- a/server/media.js
++++ b/server/media.js
@@@ -42,8 +42,12 @@ function createMedia(config, fetchImpl = fetch) {
async read(id) {
// Keep private signed URLs on the server; never redirect a limited Send to Media.
const { url } = await call('GET', `/${encodeURIComponent(id)}/download?format=json`);
-- const target = new URL(url);
-- if (target.origin !== new URL(config.mediaUrl).origin) fail(502, 'locker.media.origin', 'Unexpected encrypted storage origin.');
++ let target = new URL(url);
++ const internal = new URL(config.mediaUrl);
++ if (config.mediaPublicUrl && target.origin === new URL(config.mediaPublicUrl).origin && target.origin !== internal.origin) {
++ target = new URL(`${target.pathname}${target.search}`, internal.origin);
++ }
++ if (target.origin !== internal.origin) fail(502, 'locker.media.origin', 'Unexpected encrypted storage origin.');
// Media may redirect its private signed URL to a signed storage-provider URL; no service credentials follow.
const r = await fetchImpl(target, { signal: AbortSignal.timeout(30 * 60 * 1000), redirect: 'follow' });
if (!r.ok || !r.body) fail(502, 'locker.media.read', 'Encrypted storage is temporarily unavailable.');
ddiff --git a/test/media.test.js b/test/media.test.js
iindex 49dd587..6bb4d25 100644
---- a/test/media.test.js
++++ b/test/media.test.js
@@@ -52,3 +52,21 @@ test('Network key proxy preserves reauthentication errors without fetching servi
await assert.rejects(network.json({ method: 'PUT', path: '/api/v1/me/keys', json: {}, token: 'test-session', preserveProblem: true }),
(e) => e.status === 403 && e.code === 'locker.reauth_required');
});
++
++test('a download signed on the public Media origin is fetched over the loopback origin; any other origin is refused', async () => {
++ const seen = [];
++ const json = (body) => Response.json(body);
++ let signed = 'https://media.public/o/med_x?sig=abc';
++ const fakeFetch = async (url, options) => {
++ const u = new URL(url); seen.push(u.href);
++ if (u.pathname === '/oauth/token') return json({ access_token: 'test-only', expires_in: 300 });
++ if (u.pathname.endsWith('/download')) return json({ url: signed });
++ assert.equal(options.headers, undefined, 'no service credentials follow a signed URL');
++ return new Response(Buffer.from('ciphertext'));
++ };
++ const media = createMedia({ networkInternalUrl: 'https://network.test', mediaUrl: 'http://127.0.0.1:4100', mediaPublicUrl: 'https://media.public', clientId: 'locker', clientSecret: '' }, fakeFetch);
++ assert.equal(Buffer.from(await (await media.read('med_x')).arrayBuffer()).toString(), 'ciphertext');
++ assert.ok(seen.includes('http://127.0.0.1:4100/o/med_x?sig=abc'), 'the signed path went to loopback');
++ signed = 'https://elsewhere.example/o/med_x?sig=abc';
++ await assert.rejects(media.read('med_x'), (e) => e.code === 'locker.media.origin');
++});