132 lines
4.2 KiB
TypeScript
132 lines
4.2 KiB
TypeScript
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
import type { AddressInfo } from "node:net";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import {
|
|
probeAuthenticatedOpenClawRelay,
|
|
resolveRelayAcceptedTokensForPort,
|
|
resolveRelayAuthTokenForPort,
|
|
} from "./extension-relay-auth.js";
|
|
import { getFreePort } from "./test-port.js";
|
|
|
|
async function withRelayServer(
|
|
handler: (req: IncomingMessage, res: ServerResponse) => void,
|
|
run: (params: { port: number }) => Promise<void>,
|
|
) {
|
|
const port = await getFreePort();
|
|
const server = createServer(handler);
|
|
await new Promise<void>((resolve, reject) => {
|
|
server.listen(port, "127.0.0.1", () => resolve());
|
|
server.once("error", reject);
|
|
});
|
|
try {
|
|
const actualPort = (server.address() as AddressInfo).port;
|
|
await run({ port: actualPort });
|
|
} finally {
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
}
|
|
}
|
|
|
|
function handleNonVersionRequest(req: IncomingMessage, res: ServerResponse): boolean {
|
|
if (req.url?.startsWith("/json/version")) {
|
|
return false;
|
|
}
|
|
res.writeHead(404);
|
|
res.end("not found");
|
|
return true;
|
|
}
|
|
|
|
async function probeRelay(baseUrl: string, relayAuthToken: string): Promise<boolean> {
|
|
return await probeAuthenticatedOpenClawRelay({
|
|
baseUrl,
|
|
relayAuthHeader: "x-openclaw-relay-token",
|
|
relayAuthToken,
|
|
});
|
|
}
|
|
|
|
describe("extension-relay-auth", () => {
|
|
const TEST_GATEWAY_TOKEN = "test-gateway-token";
|
|
let prevGatewayToken: string | undefined;
|
|
|
|
beforeEach(() => {
|
|
prevGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN;
|
|
process.env.OPENCLAW_GATEWAY_TOKEN = TEST_GATEWAY_TOKEN;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (prevGatewayToken === undefined) {
|
|
delete process.env.OPENCLAW_GATEWAY_TOKEN;
|
|
} else {
|
|
process.env.OPENCLAW_GATEWAY_TOKEN = prevGatewayToken;
|
|
}
|
|
});
|
|
|
|
it("derives deterministic relay tokens per port", async () => {
|
|
const tokenA1 = await resolveRelayAuthTokenForPort(18790);
|
|
const tokenA2 = await resolveRelayAuthTokenForPort(18790);
|
|
const tokenB = await resolveRelayAuthTokenForPort(18791);
|
|
expect(tokenA1).toBe(tokenA2);
|
|
expect(tokenA1).not.toBe(tokenB);
|
|
expect(tokenA1).not.toBe(TEST_GATEWAY_TOKEN);
|
|
});
|
|
|
|
it("accepts both relay-scoped and raw gateway tokens for compatibility", async () => {
|
|
const tokens = await resolveRelayAcceptedTokensForPort(18790);
|
|
expect(tokens).toContain(TEST_GATEWAY_TOKEN);
|
|
expect(tokens[0]).not.toBe(TEST_GATEWAY_TOKEN);
|
|
expect(tokens[0]).toBe(await resolveRelayAuthTokenForPort(18790));
|
|
});
|
|
|
|
it("accepts authenticated openclaw relay probe responses", async () => {
|
|
let seenToken: string | undefined;
|
|
await withRelayServer(
|
|
(req, res) => {
|
|
if (handleNonVersionRequest(req, res)) {
|
|
return;
|
|
}
|
|
const header = req.headers["x-openclaw-relay-token"];
|
|
seenToken = Array.isArray(header) ? header[0] : header;
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({ Browser: "OpenClaw/extension-relay" }));
|
|
},
|
|
async ({ port }) => {
|
|
const token = await resolveRelayAuthTokenForPort(port);
|
|
const ok = await probeRelay(`http://127.0.0.1:${port}`, token);
|
|
expect(ok).toBe(true);
|
|
expect(seenToken).toBe(token);
|
|
},
|
|
);
|
|
});
|
|
|
|
it("rejects unauthenticated probe responses", async () => {
|
|
await withRelayServer(
|
|
(req, res) => {
|
|
if (handleNonVersionRequest(req, res)) {
|
|
return;
|
|
}
|
|
res.writeHead(401);
|
|
res.end("Unauthorized");
|
|
},
|
|
async ({ port }) => {
|
|
const ok = await probeRelay(`http://127.0.0.1:${port}`, "irrelevant");
|
|
expect(ok).toBe(false);
|
|
},
|
|
);
|
|
});
|
|
|
|
it("rejects probe responses with wrong browser identity", async () => {
|
|
await withRelayServer(
|
|
(req, res) => {
|
|
if (handleNonVersionRequest(req, res)) {
|
|
return;
|
|
}
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({ Browser: "FakeRelay" }));
|
|
},
|
|
async ({ port }) => {
|
|
const ok = await probeRelay(`http://127.0.0.1:${port}`, "irrelevant");
|
|
expect(ok).toBe(false);
|
|
},
|
|
);
|
|
});
|
|
});
|