import assert from "node:assert/strict"; import crypto from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { execFileSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import { createDeploymentHttpServer, listenDeploymentHttpServer, } from "./deployment-http-server.mjs"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const releaseRoot = path.join(repoRoot, "release"); const sourceArchive = path.join(releaseRoot, "blender-web-corresponding-source.tar.gz"); const binaryArchive = path.join(releaseRoot, "blender-web-offline.tar.gz"); const sumsFile = path.join(releaseRoot, "SHA256SUMS.txt"); const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "blender-deployment-runbook-")); const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); function checksumEntries(value) { return Object.fromEntries(value.trim().split("\n").map((line) => { const match = line.match(/^([a-f0-9]{64}) ([^/]+)$/); assert.ok(match, `invalid SHA256SUMS entry: ${line}`); return [match[2], match[1]]; })); } function assertSafeArchive(file) { const verbose = execFileSync("tar", ["-tvzf", file], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }); for (const line of verbose.split("\n").filter(Boolean)) { assert.ok(["-", "d"].includes(line[0]), `deployment archive contains a link or special entry: ${line}`); } const entries = execFileSync("tar", ["-tzf", file], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }) .split("\n") .filter(Boolean); assert.ok(entries.length > 0, "deployment archive is empty"); for (const entry of entries) { assert.ok(!entry.startsWith("/"), `deployment archive contains an absolute path: ${entry}`); assert.ok(!entry.split("/").includes(".."), `deployment archive contains path traversal: ${entry}`); assert.ok(entry === "blender-web-offline/" || entry.startsWith("blender-web-offline/"), `unexpected deployment archive root: ${entry}`); } } function classifyOrigin(value, contract) { let origin; try { origin = new URL(value); } catch { return { status: "BLOCKED", reason: "INVALID_ORIGIN" }; } if (origin.username || origin.password || origin.pathname !== "/" || origin.search || origin.hash) { return { status: "BLOCKED", reason: "UNSUPPORTED_ORIGIN_SHAPE" }; } if (origin.protocol === `${contract.transport.production}:`) return { status: "READY", mode: "production-https" }; const local = new URL(contract.transport.localDevelopment); if (origin.protocol === local.protocol && origin.hostname === local.hostname) { return { status: "READY", mode: "loopback-http" }; } return { status: "BLOCKED", reason: "HTTPS_OR_EXPLICIT_LOOPBACK_REQUIRED" }; } function assertHeader(response, name, expected) { assert.equal(response.headers.get(name), expected, `${name} drifted at ${response.url}`); } let server; try { const delivery = path.join(workspace, "delivery"); const installRoot = path.join(workspace, "install"); fs.mkdirSync(delivery); fs.mkdirSync(installRoot); for (const file of [binaryArchive, sourceArchive, sumsFile]) { assert.ok(fs.statSync(file).isFile(), `release delivery is missing ${path.basename(file)}`); fs.copyFileSync(file, path.join(delivery, path.basename(file))); } const deliveredArchive = path.join(delivery, path.basename(binaryArchive)); const sums = checksumEntries(fs.readFileSync(path.join(delivery, path.basename(sumsFile)), "utf8")); assert.equal(sha256(deliveredArchive), sums[path.basename(binaryArchive)], "binary delivery checksum drifted"); assert.equal(sha256(path.join(delivery, path.basename(sourceArchive))), sums[path.basename(sourceArchive)], "source delivery checksum drifted"); assertSafeArchive(deliveredArchive); const releases = path.join(installRoot, "releases"); fs.mkdirSync(releases); const staging = fs.mkdtempSync(path.join(installRoot, ".install.")); execFileSync("tar", ["--no-same-owner", "--no-same-permissions", "-xzf", deliveredArchive, "-C", staging]); const stagedBundle = path.join(staging, "blender-web-offline"); const archiveSha256 = sha256(deliveredArchive); const installedRelease = path.join(releases, archiveSha256); assert.ok(!fs.existsSync(installedRelease), "fresh install unexpectedly contains a release"); fs.renameSync(stagedBundle, installedRelease); fs.rmdirSync(staging); const pendingLink = path.join(installRoot, ".current.next"); fs.symlinkSync(path.join("releases", archiveSha256), pendingLink); fs.renameSync(pendingLink, path.join(installRoot, "current")); assert.equal(fs.realpathSync(path.join(installRoot, "current")), installedRelease); assert.ok(!installedRelease.startsWith(`${repoRoot}${path.sep}`), "runbook installed inside the source workspace"); const bundleRoot = path.join(installRoot, "current"); const appRoot = path.join(bundleRoot, "app"); const contract = JSON.parse(fs.readFileSync(path.join(bundleRoot, "deployment-contract.json"), "utf8")); const engine = JSON.parse(fs.readFileSync(path.join(appRoot, "engine-manifest.json"), "utf8")); assert.equal(classifyOrigin("https://blender.example", contract).mode, "production-https"); assert.equal(classifyOrigin("https://blender.example:8443", contract).mode, "production-https"); assert.equal(classifyOrigin("http://127.0.0.1:8080", contract).mode, "loopback-http"); for (const blocked of [ "file:///tmp/blender/index.html", "http://localhost:8080", "http://0.0.0.0:8080", "http://192.168.1.10:8080", "https://user:secret@blender.example", "https://blender.example/subpath", ]) assert.equal(classifyOrigin(blocked, contract).status, "BLOCKED", `${blocked} must be blocked`); server = createDeploymentHttpServer({ root: appRoot, contract }); const origin = await listenDeploymentHttpServer(server); assert.equal(classifyOrigin(origin, contract).mode, "loopback-http"); const indexResponse = await fetch(`${origin}/`, { method: "HEAD" }); assert.equal(indexResponse.status, 200); for (const [name, value] of Object.entries(contract.responseHeaders.allResponses)) assertHeader(indexResponse, name, value); assertHeader(indexResponse, "cache-control", "no-cache"); assertHeader(indexResponse, "content-type", contract.mimeTypes[".html"]); assert.match(indexResponse.headers.get("etag") ?? "", /^"sha256-[a-f0-9]{64}"$/); const wasmPath = new URL(engine.variants.find((variant) => variant.id === "single").resources.wasm.url, origin); const rangeResponse = await fetch(wasmPath, { headers: { Range: "bytes=0-15" } }); assert.equal(rangeResponse.status, contract.rangeRequests.satisfiedStatus); assert.equal((await rangeResponse.arrayBuffer()).byteLength, 16); assertHeader(rangeResponse, "accept-ranges", "bytes"); assert.match(rangeResponse.headers.get("content-range") ?? "", /^bytes 0-15\/\d+$/); const report = { schemaVersion: 1, task: "M6-17A", status: "READY", archive: { bytes: fs.statSync(deliveredArchive).size, sha256: archiveSha256 }, engineReleaseId: engine.releaseId, install: { emptyRoot: true, outsideSourceWorkspace: true, immutableReleaseDirectory: path.relative(installRoot, installedRelease), currentTarget: fs.readlinkSync(path.join(installRoot, "current")), }, transport: { productionHttps: "READY", loopbackHttp: "READY", publicPlainHttp: "BLOCKED", fileProtocol: "BLOCKED", sameOriginRuntimeAssets: contract.transport.sameOriginRuntimeAssets, }, installedServer: { status: indexResponse.status, rangeStatus: rangeResponse.status, isolationHeaders: contract.responseHeaders.allResponses }, }; const reportRoot = path.join(releaseRoot, "operations-reports"); fs.mkdirSync(reportRoot, { recursive: true }); fs.writeFileSync(path.join(reportRoot, "deploy.json"), `${JSON.stringify(report, null, 2)}\n`); process.stdout.write(`deployment-runbook-ok release=${engine.releaseId} archive=${archiveSha256} install=empty https=ready loopback=ready public-http=blocked\n`); } finally { if (server) await new Promise((resolve) => server.close(resolve)); fs.rmSync(workspace, { recursive: true, force: true }); }