// Podcast proxy: only the allowed Podgrab calls get through, the Podgrab password is added server-side, audio ranges and redirects work. const http = require('http'), assert = require('assert'), fs = require('fs'), os = require('os'), path = require('path'); process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-pod-')); const seen = []; const fake = http.createServer((req, res) => { let b = ''; req.on('data', c => b += c); req.on('end', () => { seen.push({ m: req.method, u: req.url, auth: req.headers.authorization, range: req.headers.range, b }); if (req.url === '/podcasts' && req.method === 'GET') { res.writeHead(200, { 'content-type': 'application/json' }); return res.end(JSON.stringify([{ ID: 'p1', Title: 'Show', URL: 'https://a.test/feed' }, { ID: 'p9', Title: 'Other', URL: 'https://b.test/feed' }])); } if (req.url === '/podcasts' && req.method === 'POST') { const u = JSON.parse(b).url; if (u === 'https://a.test/feed') { res.writeHead(409, { 'content-type': 'application/json' }); return res.end('{"message":"exists"}'); } res.writeHead(200, { 'content-type': 'application/json' }); return res.end('{"ID":"p2","Title":"New","URL":"' + u + '"}'); } if (req.url === '/podcastitems/e1/file') { res.writeHead(206, { 'content-type': 'audio/mpeg', 'content-range': 'bytes 0-3/10', 'content-length': '4' }); return res.end('abcd'); } if (req.url === '/podcastitems/e2/file') { res.writeHead(302, { location: 'https://cdn.example.com/a.mp3' }); return res.end(); } res.writeHead(200, { 'content-type': 'application/json' }); res.end('{}'); }); }).listen(0, async () => { process.env.PODCAST_URL = 'http://127.0.0.1:' + fake.address().port + '/'; process.env.PODGRAB_PASSWORD = 'secret'; const podcast = require('../podcast'), users = require('../users'); const people = { admin: users.create({ name: 'Ann', role: 'admin', handle: 'YQ' }), other: users.create({ name: 'Bob', role: 'user', tabs: ['podcast'], handle: 'Yg' }) }; const hub = http.createServer((req, res) => users.runAs(req.headers['x-user'] === 'other' ? people.other : people.admin, () => podcast.handle(req, res))).listen(0, async () => { const base = 'http://127.0.0.1:' + hub.address().port, get = (p, o = {}) => fetch(base + '/api/podcast' + p, { redirect: 'manual', ...o }); try { let r = await get('/podcasts'); assert.equal(r.status, 200); assert.deepEqual((await r.json()).map(x => x.ID), ['p1', 'p9'], 'the admin starts with everything already in Podgrab'); assert.equal(seen[0].auth, 'Basic ' + Buffer.from('podgrab:secret').toString('base64'), 'password added by the server'); r = await get('/podcastitems/e1/file', { headers: { range: 'bytes=0-3' } }); assert.equal(r.status, 206); assert.equal(await r.text(), 'abcd'); assert.equal(seen.at(-1).range, 'bytes=0-3'); r = await get('/podcastitems/e2/file'); assert.equal(r.status, 302); assert.equal(r.headers.get('location'), 'https://cdn.example.com/a.mp3'); const other = { 'x-user': 'other' }; r = await get('/podcasts', { headers: other }); assert.deepEqual(await r.json(), [], 'a new person starts with their own empty list'); r = await get('/podcasts/p1/items', { headers: other }); assert.equal(r.status, 404, 'a show not on your list is not reachable'); r = await get('/podcasts', { method: 'POST', headers: other, body: '{"url":"https://a.test/feed"}' }); assert.equal(r.status, 200); // Podgrab already has it (409): found by its address and put on their list assert.deepEqual((await (await get('/podcasts', { headers: other })).json()).map(x => x.ID), ['p1']); r = await get('/podcasts', { method: 'POST', body: '{"url":"https://x.test/feed"}' }); assert.equal(r.status, 200); assert.equal((await r.json()).ID, 'p2'); assert.deepEqual((await (await get('/podcasts')).json()).map(x => x.ID), ['p1', 'p9'], 'the other list did not change (fake Podgrab lists only its own two)'); let dels = seen.filter(x => x.m === 'DELETE').length; r = await get('/podcasts/p1', { method: 'DELETE', headers: other }); assert.equal((await r.json()).removed, false, 'the admin still follows p1, so Podgrab keeps it'); assert.equal(seen.filter(x => x.m === 'DELETE').length, dels); r = await get('/podcasts/p9', { method: 'DELETE' }); assert.equal((await r.json()).removed, true, 'nobody else follows p9, so Podgrab drops it'); assert.equal(seen.filter(x => x.m === 'DELETE').length, dels + 1); const post = b => get('/progress', { method: 'POST', body: JSON.stringify(b) }).then(x => x.json()); await post({ e1: { pos: 90, dur: 600, t: 10 } }); const pr = await post({ e1: { pos: 5, dur: 600, t: 3 }, e2: { pos: 7, dur: 300, t: 4 } }); assert.equal(pr.e1.pos, 90, 'the newest position wins'); assert.equal((await (await get('/progress')).json()).e2.pos, 7); const n = seen.length; for (const [m, p] of [['GET', '/settings'], ['POST', '/settings'], ['GET', '/opml'], ['DELETE', '/tags/1'], ['GET', '/backups'], ['GET', '/podcasts/../settings'], ['DELETE', '/podcastitems/e1']]) { r = await get(p, { method: m }); assert.equal(r.status, 404, m + ' ' + p); } assert.equal(seen.length, n, 'blocked calls never reach Podgrab'); console.log('podcast ok'); } catch (e) { console.error(e); process.exitCode = 1; } hub.close(); fake.close(); }); });