Files
Recordly/scripts/ffmpeg-distribution.mjs
T

330 lines
12 KiB
JavaScript

import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
import { chmod, copyFile, mkdir, rename, writeFile } from "node:fs/promises";
import path from "node:path";
import { gunzipSync } from "node:zlib";
import {
buildMacFfmpeg,
MAC_BUILD_ID,
MAC_SOURCE_ARCHIVES,
} from "./build-redistributable-ffmpeg.mjs";
import { machoContentHash } from "./macho-content-hash.mjs";
import { peContentHash } from "./pe-content-hash.mjs";
const upstreamRelease = "https://github.com/eugeneware/ffmpeg-static/releases/download/b6.1.1";
// Exact assets from that same pinned release. The API route avoids release-page outages.
const upstreamAssetIds = Object.freeze({
"ffmpeg-linux-arm64.gz": 316528458,
"ffmpeg-linux-x64.gz": 316528626,
"ffmpeg-win32-x64.gz": 316528798,
"ffprobe-win32-x64.gz": 316529830,
"linux-arm64.README": 316529906,
"linux-x64.README": 316529927,
"win32-x64.README": 316529965,
});
// Verified raw release digests. The upstream darwin-arm64 artifact enables nonfree.
export const APPROVED_FFMPEG_BINARIES = Object.freeze({
"win32-x64": "04e1307997530f9cf2fe35cba2ca7e8875ca91da02f89d6c7243df819c94ad00",
"linux-x64": "e7e7fb30477f717e6f55f9180a70386c62677ef8a4d4d1a5d948f4098aa3eb99",
"linux-arm64": "6bb182d0d75d23028db82e9e4f723ca69b853d055698486e6984ddb2c06fb8ce",
});
const windowsFfprobeSha256 = "3a7e2dc003dc2cd1472827e4c7c4f056ae1ae0ae7c5bbc580c99b49827351ba4";
const windowsFfmpegContentSha256 =
"e7d76a7bfd32b60f20c7e9f2fba1c3059d2edc629ccb730310d7a000be0a2b37";
const windowsFfprobeContentSha256 =
"f2e535164943b58f6dad618f75c8ad00eb2c6c232caad899d77a28ce37d54edb";
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
const contentHash = (bytes, platform) =>
platform === "darwin"
? machoContentHash(bytes)
: platform === "win32"
? peContentHash(bytes)
: sha256(bytes);
function matchesBinary(bytes, manifest, platform, expectedSignedContent) {
if (manifest.sha256 === sha256(bytes)) return true;
if (platform !== "darwin" && platform !== "win32") return false;
if (!manifest.contentSha256) return false;
if (expectedSignedContent && manifest.contentSha256 !== expectedSignedContent) return false;
return manifest.contentSha256 === contentHash(bytes, platform);
}
export function assertRedistributableFfmpegOutput(output) {
if (
/--enable-nonfree(?:[\s\0]|$)/i.test(output) ||
/not legally redistributable|unredistributable/i.test(output)
) {
throw new Error("FFmpeg contains nonfree parts and cannot be packaged for redistribution.");
}
if (!/GNU (?:General|Lesser General) Public License/i.test(output)) {
throw new Error("FFmpeg did not report a recognized GPL/LGPL license.");
}
}
export function verifyFfmpegDistribution(
binaryPath,
{
platform = process.platform,
arch = process.arch,
metadataPath = binaryPath,
sourcesPath = `${metadataPath}.sources`,
} = {},
) {
const bytes = readFileSync(binaryPath);
const manifest = JSON.parse(readFileSync(`${metadataPath}.distribution.json`, "utf8"));
if (
manifest.schemaVersion !== 1 ||
manifest.platform !== platform ||
manifest.arch !== arch ||
!matchesBinary(
bytes,
manifest,
platform,
platform === "win32" ? windowsFfmpegContentSha256 : undefined,
)
) {
throw new Error("FFmpeg distribution provenance does not match the binary or target.");
}
if (platform === "darwin") {
if (
manifest.buildId !== MAC_BUILD_ID ||
!MAC_SOURCE_ARCHIVES.every((source) =>
manifest.sources?.some(
(entry) => entry.name === source.name && entry.sha256 === source.sha256,
),
)
) {
throw new Error(
"macOS FFmpeg was not built from Recordly's pinned redistributable sources.",
);
}
for (const source of MAC_SOURCE_ARCHIVES) {
if (sha256(readFileSync(path.join(sourcesPath, source.name))) !== source.sha256) {
throw new Error(
`FFmpeg corresponding source is missing or changed: ${source.name}`,
);
}
}
} else if (APPROVED_FFMPEG_BINARIES[`${platform}-${arch}`] !== manifest.sha256) {
throw new Error(`Unapproved FFmpeg distribution for ${platform}/${arch}.`);
}
for (const suffix of [".LICENSE", ".README"]) {
if (!readFileSync(`${metadataPath}${suffix}`, "utf8").trim())
throw new Error(`FFmpeg distribution is missing ${suffix}.`);
}
if (platform === process.platform && arch === process.arch) {
const license = execFileSync(binaryPath, ["-L"], {
encoding: "utf8",
timeout: 15000,
windowsHide: true,
stdio: ["ignore", "pipe", "pipe"],
});
assertRedistributableFfmpegOutput(license);
}
return manifest;
}
export function verifyFfprobeDistribution(
binaryPath,
{ platform = process.platform, arch = process.arch, metadataPath = binaryPath } = {},
) {
const manifest = JSON.parse(readFileSync(`${metadataPath}.distribution.json`, "utf8"));
if (
platform !== "win32" ||
arch !== "x64" ||
manifest.schemaVersion !== 1 ||
manifest.platform !== platform ||
manifest.arch !== arch ||
manifest.sha256 !== windowsFfprobeSha256 ||
!matchesBinary(readFileSync(binaryPath), manifest, platform, windowsFfprobeContentSha256)
) {
throw new Error(
"FFprobe distribution provenance does not match the approved Windows binary.",
);
}
for (const suffix of [".LICENSE", ".README"]) {
if (!readFileSync(`${metadataPath}${suffix}`, "utf8").trim())
throw new Error(`FFprobe distribution is missing ${suffix}.`);
}
if (platform === process.platform && arch === process.arch) {
assertRedistributableFfmpegOutput(
execFileSync(binaryPath, ["-L"], {
encoding: "utf8",
timeout: 15000,
windowsHide: true,
stdio: ["ignore", "pipe", "pipe"],
}),
);
}
return manifest;
}
async function download(url) {
const assetName = url.startsWith(`${upstreamRelease}/`)
? url.slice(upstreamRelease.length + 1)
: undefined;
const assetId = upstreamAssetIds[assetName];
if (!Number.isSafeInteger(assetId)) throw new Error("Unapproved FFmpeg asset URL.");
for (let attempt = 0; attempt < 2; attempt += 1) {
let response;
try {
response = await fetch(
attempt === 0
? url
: `https://api.github.com/repos/eugeneware/ffmpeg-static/releases/assets/${assetId}`,
{
signal: AbortSignal.timeout(180000),
credentials: "omit",
...(attempt === 0 ? {} : { headers: { Accept: "application/octet-stream" } }),
},
);
if (!response.ok) {
await response.body?.cancel().catch(() => {
// Preserve the HTTP failure if releasing its body also fails.
});
throw new Error(`FFmpeg download failed (${response.status}): ${url}`);
}
return Buffer.from(await response.arrayBuffer());
} catch (error) {
const retryable =
response && !response.ok
? response.status >= 500 && response.status <= 599
: error instanceof TypeError ||
error?.name === "TimeoutError" ||
error?.name === "AbortError";
if (attempt !== 0 || !retryable) throw error;
}
}
}
async function ensureWindowsFfprobe(root) {
const binaryPath = path.join(root, "node_modules/ffprobe-static/bin/win32/x64/ffprobe.exe");
try {
const manifest = {
...verifyFfprobeDistribution(binaryPath, { platform: "win32", arch: "x64" }),
contentSha256: windowsFfprobeContentSha256,
};
await writeFile(
`${binaryPath}.distribution.json`,
`${JSON.stringify(manifest, null, 2)}\n`,
);
return manifest;
} catch {
/* Replace legacy/missing binaries. */
}
const url = `${upstreamRelease}/ffprobe-win32-x64.gz`;
const bytes = gunzipSync(await download(url), { maxOutputLength: 200 * 1024 * 1024 });
if (sha256(bytes) !== windowsFfprobeSha256)
throw new Error("Downloaded FFprobe failed its pinned SHA-256 check.");
await mkdir(path.dirname(binaryPath), { recursive: true });
await writeFile(`${binaryPath}.download`, bytes, { mode: 0o755 });
await rename(`${binaryPath}.download`, binaryPath);
await copyFile(
path.join(root, "src/assets/cursors/pixel-glove/LICENSE.GPL"),
`${binaryPath}.LICENSE`,
);
const readme = (await download(`${upstreamRelease}/win32-x64.README`)).toString("utf8");
await writeFile(
`${binaryPath}.README`,
`FFprobe, GPL-3.0-or-later. Artifact: ${url}\nUpstream build/source information:\n${readme}`,
);
await writeFile(
`${binaryPath}.distribution.json`,
`${JSON.stringify({ schemaVersion: 1, platform: "win32", arch: "x64", sha256: windowsFfprobeSha256, contentSha256: windowsFfprobeContentSha256, artifactUrl: url }, null, 2)}\n`,
);
return verifyFfprobeDistribution(binaryPath, { platform: "win32", arch: "x64" });
}
export async function ensureRedistributableFfmpeg({
root = process.cwd(),
platform = process.env.npm_config_platform || process.platform,
arch = process.env.npm_config_arch || process.arch,
} = {}) {
const binaryPath = path.join(
root,
"node_modules",
"ffmpeg-static",
platform === "win32" ? "ffmpeg.exe" : "ffmpeg",
);
let existingManifest;
try {
existingManifest = verifyFfmpegDistribution(binaryPath, { platform, arch });
} catch {
// Missing, stale, nonfree or changed artifact: install a verified replacement.
}
if (existingManifest) {
const manifest = {
...existingManifest,
contentSha256: contentHash(readFileSync(binaryPath), platform),
};
await writeFile(
`${binaryPath}.distribution.json`,
`${JSON.stringify(manifest, null, 2)}\n`,
);
console.log(
`[ffmpeg] Verified redistributable ${platform}/${arch} binary (${manifest.sha256.slice(0, 12)}).`,
);
if (platform === "win32" && arch === "x64") await ensureWindowsFfprobe(root);
return manifest;
}
await mkdir(path.dirname(binaryPath), { recursive: true });
if (existsSync(binaryPath)) {
const backup = path.join(
root,
".tmp",
"ffmpeg-distribution",
"previous",
`${platform}-${arch}-${sha256(readFileSync(binaryPath))}`,
);
await mkdir(path.dirname(backup), { recursive: true });
if (!existsSync(backup)) await copyFile(binaryPath, backup);
}
let provenance;
if (platform === "darwin") {
provenance = await buildMacFfmpeg({ root, arch, destination: binaryPath });
} else {
const expected = APPROVED_FFMPEG_BINARIES[`${platform}-${arch}`];
if (!expected)
throw new Error(
`No redistributable FFmpeg artifact configured for ${platform}/${arch}.`,
);
const url = `${upstreamRelease}/ffmpeg-${platform}-${arch}.gz`;
const bytes = gunzipSync(await download(url), { maxOutputLength: 200 * 1024 * 1024 });
if (sha256(bytes) !== expected)
throw new Error("Downloaded FFmpeg failed its pinned SHA-256 check.");
const temporaryPath = `${binaryPath}.download`;
await writeFile(temporaryPath, bytes, { mode: 0o755 });
await rename(temporaryPath, binaryPath);
await chmod(binaryPath, 0o755);
await copyFile(
path.join(root, "src", "assets", "cursors", "pixel-glove", "LICENSE.GPL"),
`${binaryPath}.LICENSE`,
);
const upstreamReadme = (
await download(`${upstreamRelease}/${platform}-${arch}.README`)
).toString("utf8");
await writeFile(
`${binaryPath}.README`,
`Recordly bundles this separate FFmpeg executable under GPL-3.0-or-later.\nArtifact: ${url}\nSHA-256: ${expected}\nBuild/source information supplied by the artifact provider follows.\n\n${upstreamReadme}`,
);
provenance = {
buildId: "ffmpeg-static-b6.1.1",
artifactUrl: url,
sourceInstructions: upstreamReadme,
};
}
const manifest = {
schemaVersion: 1,
platform,
arch,
...provenance,
sha256: sha256(readFileSync(binaryPath)),
contentSha256: contentHash(readFileSync(binaryPath), platform),
};
await writeFile(`${binaryPath}.distribution.json`, `${JSON.stringify(manifest, null, 2)}\n`);
verifyFfmpegDistribution(binaryPath, { platform, arch });
if (platform === "win32" && arch === "x64") await ensureWindowsFfprobe(root);
return manifest;
}