2026-07-16 23:51:20 -05:00
|
|
|
import { webcrypto } from "node:crypto";
|
|
|
|
|
import {
|
|
|
|
|
expect,
|
|
|
|
|
request as requestFactory,
|
|
|
|
|
test,
|
|
|
|
|
type APIRequestContext,
|
|
|
|
|
} from "@playwright/test";
|
|
|
|
|
import { decodeMessage, encodeMessage } from "../../src/protocol";
|
|
|
|
|
import { ADMIN } from "./credentials";
|
|
|
|
|
|
|
|
|
|
const BASE_URL = "http://127.0.0.1:5193";
|
|
|
|
|
const AGENT_TOKEN = "e2e-agent-token-that-is-only-used-for-browser-tests";
|
|
|
|
|
const CLIENT_ID = "e2e-file-transfer-agent";
|
|
|
|
|
|
|
|
|
|
type AgentCommand = {
|
|
|
|
|
type: "command";
|
|
|
|
|
commandType: string;
|
|
|
|
|
id?: string;
|
|
|
|
|
payload?: { path?: string };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class TransferTestAgent {
|
|
|
|
|
private socket: WebSocket | null = null;
|
|
|
|
|
private downloads = new Map<string, Uint8Array>();
|
2026-08-24 10:06:19 -05:00
|
|
|
clientId = CLIENT_ID;
|
2026-07-16 23:51:20 -05:00
|
|
|
|
|
|
|
|
setDownload(path: string, bytes: Uint8Array): void {
|
|
|
|
|
this.downloads.set(path, bytes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async connect(): Promise<void> {
|
|
|
|
|
const keyPair = await webcrypto.subtle.generateKey(
|
|
|
|
|
{ name: "Ed25519" },
|
|
|
|
|
true,
|
|
|
|
|
["sign", "verify"],
|
|
|
|
|
);
|
|
|
|
|
const publicKey = new Uint8Array(
|
|
|
|
|
await webcrypto.subtle.exportKey("raw", keyPair.publicKey),
|
|
|
|
|
);
|
|
|
|
|
const socket = new WebSocket(
|
|
|
|
|
`ws://127.0.0.1:5193/api/clients/${CLIENT_ID}/stream/ws?token=${encodeURIComponent(AGENT_TOKEN)}`,
|
|
|
|
|
);
|
|
|
|
|
socket.binaryType = "arraybuffer";
|
|
|
|
|
this.socket = socket;
|
|
|
|
|
|
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
|
|
|
const timeout = setTimeout(() => reject(new Error("Agent enrollment timed out")), 10_000);
|
|
|
|
|
|
|
|
|
|
socket.addEventListener("error", () => {
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
reject(new Error("Agent WebSocket failed"));
|
|
|
|
|
});
|
|
|
|
|
socket.addEventListener("close", (event) => {
|
|
|
|
|
if (socket.readyState !== WebSocket.OPEN) {
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
reject(new Error(`Agent WebSocket closed during enrollment (${event.code}: ${event.reason})`));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
socket.addEventListener("message", async (event) => {
|
|
|
|
|
const message = decodeMessage(event.data as ArrayBuffer) as any;
|
|
|
|
|
if (message.type === "enrollment_challenge") {
|
|
|
|
|
const nonce = Buffer.from(message.nonce, "base64");
|
|
|
|
|
const signature = await webcrypto.subtle.sign("Ed25519", keyPair.privateKey, nonce);
|
|
|
|
|
socket.send(encodeMessage({
|
|
|
|
|
type: "hello",
|
|
|
|
|
id: CLIENT_ID,
|
|
|
|
|
host: "e2e-transfer-host",
|
|
|
|
|
os: "E2E",
|
|
|
|
|
arch: "x64",
|
|
|
|
|
version: "e2e",
|
|
|
|
|
user: "playwright",
|
|
|
|
|
monitors: 1,
|
|
|
|
|
hwid: CLIENT_ID,
|
|
|
|
|
publicKey: Buffer.from(publicKey).toString("base64"),
|
|
|
|
|
signature: Buffer.from(signature).toString("base64"),
|
|
|
|
|
}));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (message.type === "hello_ack") {
|
2026-08-24 10:06:19 -05:00
|
|
|
if (typeof message.id !== "string" || !message.id) {
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
reject(new Error("Agent hello acknowledgement did not include a client ID"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.clientId = message.id;
|
2026-07-16 23:51:20 -05:00
|
|
|
clearTimeout(timeout);
|
|
|
|
|
resolve();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (message.type === "command") {
|
|
|
|
|
this.handleCommand(message as AgentCommand);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
close(): void {
|
|
|
|
|
this.socket?.close(1000, "test_complete");
|
|
|
|
|
this.socket = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleCommand(command: AgentCommand): void {
|
|
|
|
|
if (command.commandType !== "file_download" || !command.id) return;
|
|
|
|
|
const path = command.payload?.path || "";
|
|
|
|
|
const bytes = this.downloads.get(path);
|
|
|
|
|
if (!bytes) {
|
|
|
|
|
this.socket?.send(encodeMessage({
|
|
|
|
|
type: "file_download",
|
|
|
|
|
commandId: command.id,
|
|
|
|
|
path,
|
|
|
|
|
data: new Uint8Array(),
|
|
|
|
|
offset: 0,
|
|
|
|
|
total: 0,
|
|
|
|
|
error: "E2E fixture not found",
|
|
|
|
|
}));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.socket?.send(encodeMessage({
|
|
|
|
|
type: "file_download",
|
|
|
|
|
commandId: command.id,
|
|
|
|
|
path,
|
|
|
|
|
data: bytes,
|
|
|
|
|
offset: 0,
|
|
|
|
|
total: bytes.byteLength,
|
|
|
|
|
chunkIndex: 0,
|
|
|
|
|
chunksTotal: 1,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test.describe("file transfer HTTP API runtime", () => {
|
|
|
|
|
let api: APIRequestContext;
|
|
|
|
|
let agent: TransferTestAgent;
|
|
|
|
|
|
|
|
|
|
test.beforeAll(async () => {
|
|
|
|
|
const loginApi = await requestFactory.newContext({ baseURL: BASE_URL });
|
|
|
|
|
const login = await loginApi.post("/api/login", {
|
|
|
|
|
data: { user: ADMIN.username, pass: ADMIN.password },
|
|
|
|
|
});
|
|
|
|
|
expect(login.ok()).toBeTruthy();
|
|
|
|
|
const { token } = await login.json();
|
|
|
|
|
await loginApi.dispose();
|
|
|
|
|
api = await requestFactory.newContext({
|
|
|
|
|
baseURL: BASE_URL,
|
|
|
|
|
extraHTTPHeaders: { Authorization: `Bearer ${token}` },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const enrollment = await api.post("/api/enrollment/settings", {
|
|
|
|
|
data: { requireApproval: false, autoApproveUnlessSuspicious: false },
|
|
|
|
|
});
|
|
|
|
|
expect(enrollment.ok()).toBeTruthy();
|
|
|
|
|
|
|
|
|
|
agent = new TransferTestAgent();
|
|
|
|
|
await agent.connect();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test.afterAll(async () => {
|
|
|
|
|
agent?.close();
|
|
|
|
|
if (api) {
|
|
|
|
|
await api.post("/api/enrollment/settings", {
|
|
|
|
|
data: { requireApproval: true, autoApproveUnlessSuspicious: false },
|
|
|
|
|
});
|
|
|
|
|
await api.dispose();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("uploads bytes for a one-time authenticated agent pull", async () => {
|
|
|
|
|
const remotePath = "C:\\Temp\\overlord-e2e-upload.bin";
|
|
|
|
|
const uploadBytes = Buffer.from("playwright upload payload\n\u0000with binary bytes", "utf8");
|
|
|
|
|
|
|
|
|
|
const requestUpload = await api.post("/api/file/upload/request", {
|
|
|
|
|
data: {
|
2026-08-24 10:06:19 -05:00
|
|
|
clientId: agent.clientId,
|
2026-07-16 23:51:20 -05:00
|
|
|
path: remotePath,
|
|
|
|
|
fileName: "overlord-e2e-upload.bin",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
expect(requestUpload.ok()).toBeTruthy();
|
|
|
|
|
const { uploadUrl } = await requestUpload.json();
|
|
|
|
|
|
|
|
|
|
const stageUpload = await api.put(uploadUrl, {
|
|
|
|
|
data: uploadBytes,
|
|
|
|
|
headers: { "Content-Type": "application/octet-stream" },
|
|
|
|
|
});
|
|
|
|
|
expect(stageUpload.ok()).toBeTruthy();
|
|
|
|
|
const staged = await stageUpload.json();
|
|
|
|
|
expect(staged).toMatchObject({
|
|
|
|
|
ok: true,
|
|
|
|
|
size: uploadBytes.byteLength,
|
|
|
|
|
path: remotePath,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const pullHeaders = {
|
|
|
|
|
"x-agent-token": AGENT_TOKEN,
|
2026-08-24 10:06:19 -05:00
|
|
|
"x-overlord-client-id": agent.clientId,
|
2026-07-16 23:51:20 -05:00
|
|
|
};
|
|
|
|
|
const pull = await api.get(staged.pullUrl, { headers: pullHeaders });
|
|
|
|
|
expect(pull.ok()).toBeTruthy();
|
|
|
|
|
expect(Buffer.from(await pull.body())).toEqual(uploadBytes);
|
|
|
|
|
|
|
|
|
|
const replay = await api.get(staged.pullUrl, { headers: pullHeaders });
|
|
|
|
|
expect(replay.status()).toBe(404);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("streams a requested agent file through the download API", async () => {
|
|
|
|
|
const remotePath = "C:\\Temp\\overlord-e2e-download.bin";
|
|
|
|
|
const downloadBytes = new Uint8Array([
|
|
|
|
|
0x00, 0x01, 0x02, 0x7f, 0x80, 0xfe, 0xff,
|
|
|
|
|
...Buffer.from("playwright download payload", "utf8"),
|
|
|
|
|
]);
|
|
|
|
|
agent.setDownload(remotePath, downloadBytes);
|
|
|
|
|
|
|
|
|
|
const requestDownload = await api.post("/api/file/download/request", {
|
2026-08-24 10:06:19 -05:00
|
|
|
data: { clientId: agent.clientId, path: remotePath },
|
2026-07-16 23:51:20 -05:00
|
|
|
});
|
|
|
|
|
expect(requestDownload.ok()).toBeTruthy();
|
|
|
|
|
const { downloadUrl } = await requestDownload.json();
|
|
|
|
|
|
|
|
|
|
const download = await api.get(downloadUrl);
|
|
|
|
|
expect(download.ok()).toBeTruthy();
|
|
|
|
|
expect(download.headers()["content-type"]).toContain("application/octet-stream");
|
|
|
|
|
expect(download.headers()["content-disposition"]).toContain("overlord-e2e-download.bin");
|
|
|
|
|
expect(Buffer.from(await download.body())).toEqual(Buffer.from(downloadBytes));
|
|
|
|
|
});
|
|
|
|
|
});
|