mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-11-09 07:23:14 +00:00
248 lines
9.0 KiB
JavaScript
248 lines
9.0 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { gzipSync } from "node:zlib";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
APPROVED_FFMPEG_BINARIES,
|
|
assertRedistributableFfmpegOutput,
|
|
ensureRedistributableFfmpeg,
|
|
verifyFfmpegDistribution,
|
|
} from "../scripts/ffmpeg-distribution.mjs";
|
|
|
|
const fixtures = [];
|
|
function fixture(overrides = {}) {
|
|
const directory = mkdtempSync(path.join(os.tmpdir(), "recordly-ffmpeg-provenance-"));
|
|
fixtures.push(directory);
|
|
const binary = path.join(directory, "ffmpeg");
|
|
const bytes = Buffer.from("unapproved executable fixture");
|
|
writeFileSync(binary, bytes);
|
|
writeFileSync(
|
|
`${binary}.distribution.json`,
|
|
JSON.stringify({
|
|
schemaVersion: 1,
|
|
platform: "win32",
|
|
arch: "x64",
|
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
...overrides,
|
|
}),
|
|
);
|
|
return binary;
|
|
}
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
vi.restoreAllMocks();
|
|
for (const directory of fixtures.splice(0)) rmSync(directory, { recursive: true, force: true });
|
|
});
|
|
|
|
describe("FFmpeg redistribution checks", () => {
|
|
it("does not accept a content-hash override for an unapproved Linux executable", () => {
|
|
const binary = fixture({
|
|
platform: "linux",
|
|
sha256: APPROVED_FFMPEG_BINARIES["linux-x64"],
|
|
contentSha256: createHash("sha256")
|
|
.update("unapproved executable fixture")
|
|
.digest("hex"),
|
|
});
|
|
expect(() => verifyFfmpegDistribution(binary, { platform: "linux", arch: "x64" })).toThrow(
|
|
"does not match",
|
|
);
|
|
});
|
|
it("accepts GPL/LGPL builds with nonfree explicitly disabled", () => {
|
|
expect(() =>
|
|
assertRedistributableFfmpegOutput(
|
|
"--disable-nonfree\nGNU General Public License version 3",
|
|
),
|
|
).not.toThrow();
|
|
expect(() =>
|
|
assertRedistributableFfmpegOutput("GNU Lesser General Public License version 2.1"),
|
|
).not.toThrow();
|
|
});
|
|
it("rejects the real nonredistributable build declaration despite GPL text", () => {
|
|
expect(() =>
|
|
assertRedistributableFfmpegOutput(
|
|
"GNU General Public License\nThis version of ffmpeg has nonfree parts compiled in. Therefore it is not legally redistributable.",
|
|
),
|
|
).toThrow("nonfree");
|
|
expect(() =>
|
|
assertRedistributableFfmpegOutput(
|
|
"configuration: --enable-gpl --enable-nonfree\nGNU General Public License",
|
|
),
|
|
).toThrow("nonfree");
|
|
});
|
|
it("rejects missing or unrecognized license output", () => {
|
|
expect(() => assertRedistributableFfmpegOutput("ffmpeg version 8.1.2")).toThrow(
|
|
"recognized",
|
|
);
|
|
});
|
|
it("rejects a changed executable even with an existing provenance record", () => {
|
|
const binary = fixture();
|
|
writeFileSync(binary, "changed after verification");
|
|
expect(() => verifyFfmpegDistribution(binary, { platform: "win32", arch: "x64" })).toThrow(
|
|
"does not match",
|
|
);
|
|
});
|
|
it("rejects an artifact from another target architecture", () => {
|
|
expect(() =>
|
|
verifyFfmpegDistribution(fixture(), { platform: "win32", arch: "arm64" }),
|
|
).toThrow("does not match");
|
|
});
|
|
it("does not trust an arbitrary binary merely because its manifest has a matching checksum", () => {
|
|
expect(() =>
|
|
verifyFfmpegDistribution(fixture(), { platform: "win32", arch: "x64" }),
|
|
).toThrow("Unapproved");
|
|
});
|
|
it("rejects a manifest pretending to be a macOS source build", () => {
|
|
expect(() =>
|
|
verifyFfmpegDistribution(
|
|
fixture({ platform: "darwin", arch: "arm64", buildId: "made-up" }),
|
|
{ platform: "darwin", arch: "arm64" },
|
|
),
|
|
).toThrow("pinned redistributable sources");
|
|
});
|
|
});
|
|
|
|
const directRelease = "https://github.com/eugeneware/ffmpeg-static/releases/download/b6.1.1";
|
|
const invalidArtifact = gzipSync(Buffer.from("unapproved executable fixture"));
|
|
function installTarget(platform = "linux", arch = "x64") {
|
|
const root = mkdtempSync(path.join(os.tmpdir(), "recordly-ffmpeg-download-"));
|
|
fixtures.push(root);
|
|
return ensureRedistributableFfmpeg({ root, platform, arch });
|
|
}
|
|
function artifactResponse() {
|
|
return new Response(invalidArtifact);
|
|
}
|
|
|
|
describe("FFmpeg release transport", () => {
|
|
it("keeps a successful direct download and still rejects unapproved bytes", async () => {
|
|
const network = vi.fn().mockResolvedValue(artifactResponse());
|
|
vi.stubGlobal("fetch", network);
|
|
await expect(installTarget()).rejects.toThrow("pinned SHA-256 check");
|
|
expect(network).toHaveBeenCalledOnce();
|
|
expect(network.mock.calls[0][0]).toBe(`${directRelease}/ffmpeg-linux-x64.gz`);
|
|
expect(network.mock.calls[0][1]).toEqual({
|
|
signal: expect.any(AbortSignal),
|
|
credentials: "omit",
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
500, 502, 503, 504,
|
|
])("falls back once for retryable HTTP %i without accepting changed bytes", async (status) => {
|
|
const failed = new Response("temporarily unavailable", { status });
|
|
const cancel = vi.spyOn(failed.body, "cancel");
|
|
const network = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(failed)
|
|
.mockResolvedValueOnce(artifactResponse());
|
|
vi.stubGlobal("fetch", network);
|
|
await expect(installTarget()).rejects.toThrow("pinned SHA-256 check");
|
|
expect(cancel).toHaveBeenCalledOnce();
|
|
expect(network).toHaveBeenCalledTimes(2);
|
|
expect(network.mock.calls[1]).toEqual([
|
|
"https://api.github.com/repos/eugeneware/ffmpeg-static/releases/assets/316528626",
|
|
{
|
|
signal: expect.any(AbortSignal),
|
|
credentials: "omit",
|
|
headers: { Accept: "application/octet-stream" },
|
|
},
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
["linux", "arm64", 316528458],
|
|
["win32", "x64", 316528798],
|
|
])("uses the same pinned release asset for %s/%s", async (platform, arch, assetId) => {
|
|
const network = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(new Response(null, { status: 504 }))
|
|
.mockResolvedValueOnce(artifactResponse());
|
|
vi.stubGlobal("fetch", network);
|
|
await expect(installTarget(platform, arch)).rejects.toThrow("pinned SHA-256 check");
|
|
expect(network.mock.calls.map(([url]) => url)).toEqual([
|
|
`${directRelease}/ffmpeg-${platform}-${arch}.gz`,
|
|
`https://api.github.com/repos/eugeneware/ffmpeg-static/releases/assets/${assetId}`,
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
400, 401, 403, 404, 429,
|
|
])("fails closed without fallback for permanent HTTP %i", async (status) => {
|
|
const network = vi.fn().mockResolvedValue(new Response(null, { status }));
|
|
vi.stubGlobal("fetch", network);
|
|
await expect(installTarget()).rejects.toThrow(`download failed (${status})`);
|
|
expect(network).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("recovers a network exception through the exact same-provider asset API", async () => {
|
|
const network = vi
|
|
.fn()
|
|
.mockRejectedValueOnce(new TypeError("fetch failed"))
|
|
.mockResolvedValueOnce(artifactResponse());
|
|
vi.stubGlobal("fetch", network);
|
|
await expect(installTarget()).rejects.toThrow("pinned SHA-256 check");
|
|
expect(network).toHaveBeenCalledTimes(2);
|
|
expect(network.mock.calls[1][0]).toBe(
|
|
"https://api.github.com/repos/eugeneware/ffmpeg-static/releases/assets/316528626",
|
|
);
|
|
});
|
|
|
|
it("does not retry programming failures as network outages", async () => {
|
|
const network = vi.fn().mockRejectedValue(new Error("unexpected failure"));
|
|
vi.stubGlobal("fetch", network);
|
|
await expect(installTarget()).rejects.toThrow("unexpected failure");
|
|
expect(network).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("stops after a failed API fallback rather than looping", async () => {
|
|
const network = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(new Response(null, { status: 504 }))
|
|
.mockResolvedValueOnce(new Response(null, { status: 503 }));
|
|
vi.stubGlobal("fetch", network);
|
|
await expect(installTarget()).rejects.toThrow("download failed (503)");
|
|
expect(network).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("bounds both stalled requests with real abort signals", async () => {
|
|
const realTimeout = AbortSignal.timeout.bind(AbortSignal);
|
|
const deadlines = vi.spyOn(AbortSignal, "timeout").mockImplementation(() => realTimeout(1));
|
|
const network = vi.fn(
|
|
(_url, { signal }) =>
|
|
new Promise((_resolve, reject) => {
|
|
if (signal.aborted) reject(signal.reason);
|
|
else
|
|
signal.addEventListener("abort", () => reject(signal.reason), {
|
|
once: true,
|
|
});
|
|
}),
|
|
);
|
|
vi.stubGlobal("fetch", network);
|
|
await expect(installTarget()).rejects.toMatchObject({ name: "TimeoutError" });
|
|
expect(network).toHaveBeenCalledTimes(2);
|
|
expect(deadlines.mock.calls).toEqual([[180000], [180000]]);
|
|
expect(network.mock.calls[0][1].signal).not.toBe(network.mock.calls[1][1].signal);
|
|
});
|
|
|
|
it("includes response-body stalls in the same two-request bound", async () => {
|
|
const realTimeout = AbortSignal.timeout.bind(AbortSignal);
|
|
const deadlines = vi.spyOn(AbortSignal, "timeout").mockImplementation(() => realTimeout(1));
|
|
const network = vi.fn(async (_url, { signal }) => ({
|
|
ok: true,
|
|
arrayBuffer: () =>
|
|
new Promise((_resolve, reject) => {
|
|
if (signal.aborted) reject(signal.reason);
|
|
else
|
|
signal.addEventListener("abort", () => reject(signal.reason), {
|
|
once: true,
|
|
});
|
|
}),
|
|
}));
|
|
vi.stubGlobal("fetch", network);
|
|
await expect(installTarget()).rejects.toMatchObject({ name: "TimeoutError" });
|
|
expect(network).toHaveBeenCalledTimes(2);
|
|
expect(deadlines.mock.calls).toEqual([[180000], [180000]]);
|
|
});
|
|
});
|