// Tests for server/audio.js — focused on the SSRF guard around // downloadPodcastAudio (a fully user-controlled outbound fetch). The // ffprobe/ffmpeg helpers need real media + binaries and aren't covered here. import { test, describe } from "node:test"; import { strict as assert } from "node:assert"; import { isBlockedAddress, downloadPodcastAudio } from "../audio.js"; const SINK = "/tmp/recap-audio-test-should-not-be-written"; describe("isBlockedAddress", () => { test("blocks IPv4 loopback / private / link-local / reserved", () => { for (const ip of [ "127.0.0.1", "127.1.2.3", "10.0.0.1", "172.16.0.1", "172.31.255.255", "192.168.1.1", "169.254.169.254", // cloud metadata "100.64.0.1", // CGNAT "0.0.0.0", "224.0.0.1", "255.255.255.255", ]) { assert.equal(isBlockedAddress(ip), true, `${ip} should be blocked`); } }); test("allows ordinary public IPv4 (incl. 172.x boundaries)", () => { for (const ip of ["8.8.8.8", "1.1.1.1", "93.184.216.34", "172.15.0.1", "172.32.0.1"]) { assert.equal(isBlockedAddress(ip), false, `${ip} should be allowed`); } }); test("blocks IPv6 loopback / ULA / link-local / multicast + IPv4-mapped privates", () => { for (const ip of [ "::1", "::", "fc00::1", "fd12:3456::1", "fe80::1", "ff02::1", "::ffff:127.0.0.1", "::ffff:169.254.169.254", ]) { assert.equal(isBlockedAddress(ip), true, `${ip} should be blocked`); } }); test("blocks hex-encoded embedded-IPv4 IPv6 forms (mapped/SIIT/NAT64/6to4)", () => { for (const ip of [ "::ffff:7f00:1", // IPv4-mapped 127.0.0.1, hex form "::ffff:0:7f00:1", // SIIT 127.0.0.1 "64:ff9b::7f00:1", // NAT64 well-known prefix of 127.0.0.1 "2002:7f00:1::", // 6to4 of 127.0.0.1 ]) { assert.equal(isBlockedAddress(ip), true, `${ip} should be blocked`); } }); test("allows ordinary public IPv6", () => { assert.equal(isBlockedAddress("2606:4700:4700::1111"), false); assert.equal(isBlockedAddress("::ffff:8.8.8.8"), false); }); test("blocks junk / empty / non-strings", () => { assert.equal(isBlockedAddress(""), true); assert.equal(isBlockedAddress(null), true); assert.equal(isBlockedAddress("not-an-ip"), true); }); }); describe("downloadPodcastAudio SSRF guard", () => { test("rejects non-HTTP(S) schemes", async () => { await assert.rejects(downloadPodcastAudio("file:///etc/passwd", SINK), /non-HTTP/); await assert.rejects(downloadPodcastAudio("ftp://example.com/x", SINK), /non-HTTP/); }); test("rejects internal / private destinations before connecting", async () => { // 127.0.0.1:1 would refuse instantly if we connected; we must reject at // the DNS-guard step instead (proving the guard fires before connect). await assert.rejects( downloadPodcastAudio("http://127.0.0.1:1/x", SINK), /disallowed address/, ); await assert.rejects( downloadPodcastAudio("http://169.254.169.254/latest/meta-data/", SINK), /disallowed address/, ); }); test("rejects a malformed URL", async () => { await assert.rejects( downloadPodcastAudio("not a url at all", SINK), /invalid podcast audio URL/, ); }); });