'use strict'; const test = require('node:test'); const assert = require('node:assert'); const fs = require('fs'); const os = require('os'); const path = require('path'); const http = require('http'); const crypto = require('crypto'); const { Readable } = require('stream'); // Isolate state BEFORE requiring the app: temp DATA_DIR (throwaway DB + cache) and a tiny size cap. const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'st-media-')); process.env.DATA_DIR = TMP; process.env.MEDIA_PROXY_MAX_BYTES = '1024'; const express = require('express'); const media = require('../routes/media'); const { db } = require('../db/database'); const { sniffMedia, consumeToCache, ensureCached, fetchOrRevalidate, isFresh, dataFile, metaFile, readMeta } = media.__test; const png = Buffer.concat([Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]), Buffer.alloc(40)]); const jpeg = Buffer.concat([Buffer.from([0xFF, 0xD8, 0xFF, 0xE0]), Buffer.alloc(40)]); const gif = Buffer.concat([Buffer.from('GIF89a'), Buffer.alloc(40)]); const webp = Buffer.concat([Buffer.from('RIFF'), Buffer.alloc(4), Buffer.from('WEBP'), Buffer.alloc(40)]); const mp4 = Buffer.concat([Buffer.alloc(4), Buffer.from('ftypisom'), Buffer.alloc(40)]); const avif = Buffer.concat([Buffer.alloc(4), Buffer.from('ftypavif'), Buffer.alloc(40)]); const webm = Buffer.concat([Buffer.from([0x1A, 0x45, 0xDF, 0xA3]), Buffer.alloc(40)]); const ogg = Buffer.concat([Buffer.from('OggS'), Buffer.alloc(40)]); const html = Buffer.from('xhi'); const doctype = Buffer.from(' '); const pdf = Buffer.concat([Buffer.from('%PDF-1.7'), Buffer.alloc(40)]); test('sniffMedia: real media accepted, everything else rejected (XSS defense)', () => { assert.equal(sniffMedia(png), 'image/png'); assert.equal(sniffMedia(jpeg), 'image/jpeg'); assert.equal(sniffMedia(gif), 'image/gif'); assert.equal(sniffMedia(webp), 'image/webp'); assert.equal(sniffMedia(mp4), 'video/mp4'); assert.equal(sniffMedia(avif), 'image/avif'); assert.equal(sniffMedia(webm), 'video/webm'); assert.equal(sniffMedia(ogg), 'video/ogg'); assert.equal(sniffMedia(html), null); // the whole point: HTML never passes assert.equal(sniffMedia(doctype), null); assert.equal(sniffMedia(pdf), null); assert.equal(sniffMedia(Buffer.alloc(0)), null); assert.equal(sniffMedia(Buffer.from([0x89])), null); }); test('consumeToCache: valid image is cached with sniffed type', async () => { const key = 'okpng'; const meta = await consumeToCache(Readable.from([png]), key); assert.equal(meta.type, 'image/png'); assert.equal(meta.size, png.length); assert.ok(fs.existsSync(dataFile(key))); assert.deepEqual(readMeta(key), meta); }); test('consumeToCache: HTML body rejected even under the size cap, nothing persisted', async () => { const key = 'badhtml'; await assert.rejects(consumeToCache(Readable.from([Buffer.concat([html, Buffer.alloc(40)])]), key), (e) => /unsupported-content/.test(e.message)); assert.ok(!fs.existsSync(dataFile(key)), 'no cache file for rejected content'); }); test('consumeToCache: stream over the byte cap is aborted (Content-Length not trusted)', async () => { const key = 'toobig'; await assert.rejects(consumeToCache(Readable.from([Buffer.alloc(2048)]), key), // > 1024 cap (e) => /too-large/.test(e.message)); assert.ok(!fs.existsSync(dataFile(key))); }); test('ensureCached: single-flight collapses concurrent misses to one fetch', async () => { let calls = 0; const fetcher = (url, key) => { calls++; return new Promise((r) => setImmediate(() => r({ type: 'image/png', size: 1 }))); }; const key = 'sf-' + crypto.randomBytes(3).toString('hex'); const [a, b] = await Promise.all([ ensureCached('http://x/1', key, fetcher), ensureCached('http://x/1', key, fetcher), ]); assert.equal(calls, 1, 'fifty panels -> one upstream fetch'); assert.deepEqual(a, b); }); test('route: 400 on malformed content id, 404 on unknown content', async () => { const { port, close } = await listen(); try { assert.equal((await get(port, '/media/proxy/bad.id')).status, 400, 'dot is not a valid id char'); assert.equal((await get(port, '/media/proxy/ghostcontent')).status, 404, 'well-formed but no such content'); } finally { await close(); } }); test('route: cache hit serves sniffed type with hardening headers', async () => { db.pragma('foreign_keys = OFF'); db.prepare("INSERT INTO content (id, filename, mime_type, remote_url) VALUES ('c1','x.png','image/png','http://example.test/x')").run(); const key = crypto.createHash('sha256').update('http://example.test/x').digest('hex'); fs.writeFileSync(dataFile(key), png); fs.writeFileSync(metaFile(key), JSON.stringify({ type: 'image/png', size: png.length, fetchedAt: Date.now() })); const { port, close } = await listen(); try { const r = await get(port, '/media/proxy/c1'); assert.equal(r.status, 200); assert.equal(r.headers['content-type'], 'image/png'); assert.equal(r.headers['x-content-type-options'], 'nosniff'); assert.equal(r.headers['content-security-policy'], 'sandbox'); assert.equal(r.headers['access-control-allow-origin'], '*'); assert.equal(r.headers['cross-origin-resource-policy'], 'cross-origin'); assert.equal(r.body.length, png.length); } finally { await close(); } }); test('route: hostile remote_url pointing at loopback is blocked end-to-end (403)', async () => { db.pragma('foreign_keys = OFF'); db.prepare("INSERT INTO content (id, filename, mime_type, remote_url) VALUES ('c2','y.png','image/png','http://127.0.0.1:9/x')").run(); const { port, close } = await listen(); try { const r = await get(port, '/media/proxy/c2'); assert.equal(r.status, 403, 'SSRF guard refuses the live fetch to a private IP'); } finally { await close(); } }); test('consumeToCache: captures ETag/Last-Modified + fetchedAt for revalidation', async () => { const key = 'validators'; const s = Readable.from([png]); s.headers = { etag: '"abc123"', 'last-modified': 'Wed, 21 Oct 2025 07:28:00 GMT' }; const meta = await consumeToCache(s, key); assert.equal(meta.etag, '"abc123"'); assert.equal(meta.lastModified, 'Wed, 21 Oct 2025 07:28:00 GMT'); assert.equal(typeof meta.fetchedAt, 'number'); }); test('ensureCached: fresh hit served without refetch, stale hit triggers revalidation', async () => { const key = 'freshness'; fs.writeFileSync(dataFile(key), png); let called = 0; const fetcher = () => { called++; return Promise.resolve({ type: 'image/png', size: png.length, fetchedAt: Date.now() }); }; fs.writeFileSync(metaFile(key), JSON.stringify({ type: 'image/png', size: png.length, fetchedAt: Date.now() })); assert.equal(isFresh(readMeta(key)), true); await ensureCached('http://x/a', key, fetcher); assert.equal(called, 0, 'fresh hit does not refetch'); fs.writeFileSync(metaFile(key), JSON.stringify({ type: 'image/png', size: png.length, fetchedAt: 1 })); // ancient assert.equal(isFresh(readMeta(key)), false); await ensureCached('http://x/a', key, fetcher); assert.equal(called, 1, 'stale hit revalidates'); }); test('fetchOrRevalidate: revalidation failure serves the STALE copy (never blank)', async () => { const key = 'stale-serve'; fs.writeFileSync(dataFile(key), png); const existing = { type: 'image/png', size: png.length, fetchedAt: 1, etag: '"x"' }; fs.writeFileSync(metaFile(key), JSON.stringify(existing)); // loopback remote_url -> SSRF guard rejects the revalidation fetch -> must fall back to stale bytes const meta = await fetchOrRevalidate('http://127.0.0.1:9/x', key, existing); assert.equal(meta.type, 'image/png'); assert.ok(meta.fetchedAt > 1, 'freshness stamp bumped so we do not hammer a down upstream'); }); test('fetchOrRevalidate: hard miss to a blocked host throws (no stale to fall back on)', async () => { await assert.rejects(fetchOrRevalidate('http://127.0.0.1:9/x', 'nomiss', null)); }); test('consumeToCache: refuses to write when free space is below the floor (disk-fill guard)', async () => { const key = 'diskfull'; const prev = process.env.MEDIA_PROXY_FREE_FLOOR_BYTES; process.env.MEDIA_PROXY_FREE_FLOOR_BYTES = String(Number.MAX_SAFE_INTEGER); // floor above any real free space try { await assert.rejects(consumeToCache(Readable.from([png]), key), (e) => /disk-full/.test(e.message)); assert.ok(!fs.existsSync(dataFile(key))); } finally { if (prev === undefined) delete process.env.MEDIA_PROXY_FREE_FLOOR_BYTES; else process.env.MEDIA_PROXY_FREE_FLOOR_BYTES = prev; } }); // ---- helpers ---- function listen() { const app = express(); app.use('/media', media); return new Promise((resolve) => { const srv = app.listen(0, '127.0.0.1', () => resolve({ port: srv.address().port, close: () => new Promise((r) => srv.close(r)), })); }); } function get(port, p) { return new Promise((resolve, reject) => { http.get({ host: '127.0.0.1', port, path: p }, (res) => { const chunks = []; res.on('data', (c) => chunks.push(c)); res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks) })); }).on('error', reject); }); } test.after(() => { try { fs.rmSync(TMP, { recursive: true, force: true }); } catch (e) {} });