329 lines
12 KiB
JavaScript
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), []);
|
|
});
|
|
});
|