refactor(channels): dedupe transport and gateway test scaffolds

This commit is contained in:
Peter Steinberger
2026-02-16 14:52:15 +00:00
parent f717a13039
commit 93ca0ed54f
95 changed files with 4068 additions and 5221 deletions

View File

@@ -1,6 +1,3 @@
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, test } from "vitest";
import { WebSocket, WebSocketServer } from "ws";
import type { CanvasHostHandler } from "../canvas-host/server.js";
@@ -9,34 +6,7 @@ import type { GatewayWsClient } from "./server/ws-types.js";
import { A2UI_PATH, CANVAS_HOST_PATH, CANVAS_WS_PATH } from "../canvas-host/a2ui.js";
import { createAuthRateLimiter } from "./auth-rate-limit.js";
import { attachGatewayUpgradeHandler, createGatewayHttpServer } from "./server-http.js";
async function withTempConfig(params: { cfg: unknown; run: () => Promise<void> }): Promise<void> {
const prevConfigPath = process.env.OPENCLAW_CONFIG_PATH;
const prevDisableCache = process.env.OPENCLAW_DISABLE_CONFIG_CACHE;
const dir = await mkdtemp(path.join(os.tmpdir(), "openclaw-canvas-auth-test-"));
const configPath = path.join(dir, "openclaw.json");
process.env.OPENCLAW_CONFIG_PATH = configPath;
process.env.OPENCLAW_DISABLE_CONFIG_CACHE = "1";
try {
await writeFile(configPath, JSON.stringify(params.cfg, null, 2), "utf-8");
await params.run();
} finally {
if (prevConfigPath === undefined) {
delete process.env.OPENCLAW_CONFIG_PATH;
} else {
process.env.OPENCLAW_CONFIG_PATH = prevConfigPath;
}
if (prevDisableCache === undefined) {
delete process.env.OPENCLAW_DISABLE_CONFIG_CACHE;
} else {
process.env.OPENCLAW_DISABLE_CONFIG_CACHE = prevDisableCache;
}
await rm(dir, { recursive: true, force: true });
}
}
import { withTempConfig } from "./test-temp-config.js";
async function listen(server: ReturnType<typeof createGatewayHttpServer>): Promise<{
port: number;
@@ -80,6 +50,64 @@ async function expectWsRejected(
});
}
async function withCanvasGatewayHarness(params: {
resolvedAuth: ResolvedGatewayAuth;
rateLimiter?: ReturnType<typeof createAuthRateLimiter>;
handleHttpRequest: CanvasHostHandler["handleHttpRequest"];
run: (ctx: {
listener: Awaited<ReturnType<typeof listen>>;
clients: Set<GatewayWsClient>;
}) => Promise<void>;
}) {
const clients = new Set<GatewayWsClient>();
const canvasWss = new WebSocketServer({ noServer: true });
const canvasHost: CanvasHostHandler = {
rootDir: "test",
close: async () => {},
handleUpgrade: (req, socket, head) => {
const url = new URL(req.url ?? "/", "http://localhost");
if (url.pathname !== CANVAS_WS_PATH) {
return false;
}
canvasWss.handleUpgrade(req, socket, head, (ws) => ws.close());
return true;
},
handleHttpRequest: params.handleHttpRequest,
};
const httpServer = createGatewayHttpServer({
canvasHost,
clients,
controlUiEnabled: false,
controlUiBasePath: "/__control__",
openAiChatCompletionsEnabled: false,
openResponsesEnabled: false,
handleHooksRequest: async () => false,
resolvedAuth: params.resolvedAuth,
rateLimiter: params.rateLimiter,
});
const wss = new WebSocketServer({ noServer: true });
attachGatewayUpgradeHandler({
httpServer,
wss,
canvasHost,
clients,
resolvedAuth: params.resolvedAuth,
rateLimiter: params.rateLimiter,
});
const listener = await listen(httpServer);
try {
await params.run({ listener, clients });
} finally {
await listener.close();
params.rateLimiter?.dispose();
canvasWss.close();
wss.close();
}
}
describe("gateway canvas host auth", () => {
test("allows canvas IP fallback for private/CGNAT addresses and denies public fallback", async () => {
const resolvedAuth: ResolvedGatewayAuth = {
@@ -95,23 +123,10 @@ describe("gateway canvas host auth", () => {
trustedProxies: ["127.0.0.1"],
},
},
prefix: "openclaw-canvas-auth-test-",
run: async () => {
const clients = new Set<GatewayWsClient>();
const canvasWss = new WebSocketServer({ noServer: true });
const canvasHost: CanvasHostHandler = {
rootDir: "test",
close: async () => {},
handleUpgrade: (req, socket, head) => {
const url = new URL(req.url ?? "/", "http://localhost");
if (url.pathname !== CANVAS_WS_PATH) {
return false;
}
canvasWss.handleUpgrade(req, socket, head, (ws) => {
ws.close();
});
return true;
},
await withCanvasGatewayHarness({
resolvedAuth,
handleHttpRequest: async (req, res) => {
const url = new URL(req.url ?? "/", "http://localhost");
if (
@@ -125,125 +140,102 @@ describe("gateway canvas host auth", () => {
res.end("ok");
return true;
},
};
run: async ({ listener, clients }) => {
const privateIpA = "192.168.1.10";
const privateIpB = "192.168.1.11";
const publicIp = "203.0.113.10";
const cgnatIp = "100.100.100.100";
const httpServer = createGatewayHttpServer({
canvasHost,
clients,
controlUiEnabled: false,
controlUiBasePath: "/__control__",
openAiChatCompletionsEnabled: false,
openResponsesEnabled: false,
handleHooksRequest: async () => false,
resolvedAuth,
});
const unauthCanvas = await fetch(
`http://127.0.0.1:${listener.port}${CANVAS_HOST_PATH}/`,
{
headers: { "x-forwarded-for": privateIpA },
},
);
expect(unauthCanvas.status).toBe(401);
const wss = new WebSocketServer({ noServer: true });
attachGatewayUpgradeHandler({
httpServer,
wss,
canvasHost,
clients,
resolvedAuth,
});
const listener = await listen(httpServer);
try {
const privateIpA = "192.168.1.10";
const privateIpB = "192.168.1.11";
const publicIp = "203.0.113.10";
const cgnatIp = "100.100.100.100";
const unauthCanvas = await fetch(
`http://127.0.0.1:${listener.port}${CANVAS_HOST_PATH}/`,
{
headers: { "x-forwarded-for": privateIpA },
},
);
expect(unauthCanvas.status).toBe(401);
const unauthA2ui = await fetch(`http://127.0.0.1:${listener.port}${A2UI_PATH}/`, {
headers: { "x-forwarded-for": privateIpA },
});
expect(unauthA2ui.status).toBe(401);
await expectWsRejected(`ws://127.0.0.1:${listener.port}${CANVAS_WS_PATH}`, {
"x-forwarded-for": privateIpA,
});
clients.add({
socket: {} as unknown as WebSocket,
connect: {} as never,
connId: "c1",
clientIp: privateIpA,
});
const authCanvas = await fetch(`http://127.0.0.1:${listener.port}${CANVAS_HOST_PATH}/`, {
headers: { "x-forwarded-for": privateIpA },
});
expect(authCanvas.status).toBe(200);
expect(await authCanvas.text()).toBe("ok");
const otherIpStillBlocked = await fetch(
`http://127.0.0.1:${listener.port}${CANVAS_HOST_PATH}/`,
{
headers: { "x-forwarded-for": privateIpB },
},
);
expect(otherIpStillBlocked.status).toBe(401);
clients.add({
socket: {} as unknown as WebSocket,
connect: {} as never,
connId: "c-public",
clientIp: publicIp,
});
const publicIpStillBlocked = await fetch(
`http://127.0.0.1:${listener.port}${CANVAS_HOST_PATH}/`,
{
headers: { "x-forwarded-for": publicIp },
},
);
expect(publicIpStillBlocked.status).toBe(401);
await expectWsRejected(`ws://127.0.0.1:${listener.port}${CANVAS_WS_PATH}`, {
"x-forwarded-for": publicIp,
});
clients.add({
socket: {} as unknown as WebSocket,
connect: {} as never,
connId: "c-cgnat",
clientIp: cgnatIp,
});
const cgnatAllowed = await fetch(
`http://127.0.0.1:${listener.port}${CANVAS_HOST_PATH}/`,
{
headers: { "x-forwarded-for": cgnatIp },
},
);
expect(cgnatAllowed.status).toBe(200);
await new Promise<void>((resolve, reject) => {
const ws = new WebSocket(`ws://127.0.0.1:${listener.port}${CANVAS_WS_PATH}`, {
const unauthA2ui = await fetch(`http://127.0.0.1:${listener.port}${A2UI_PATH}/`, {
headers: { "x-forwarded-for": privateIpA },
});
const timer = setTimeout(() => reject(new Error("timeout")), 10_000);
ws.once("open", () => {
clearTimeout(timer);
ws.terminate();
resolve();
expect(unauthA2ui.status).toBe(401);
await expectWsRejected(`ws://127.0.0.1:${listener.port}${CANVAS_WS_PATH}`, {
"x-forwarded-for": privateIpA,
});
ws.once("unexpected-response", (_req, res) => {
clearTimeout(timer);
reject(new Error(`unexpected response ${res.statusCode}`));
clients.add({
socket: {} as unknown as WebSocket,
connect: {} as never,
connId: "c1",
clientIp: privateIpA,
});
ws.once("error", reject);
});
} finally {
await listener.close();
canvasWss.close();
wss.close();
}
const authCanvas = await fetch(
`http://127.0.0.1:${listener.port}${CANVAS_HOST_PATH}/`,
{
headers: { "x-forwarded-for": privateIpA },
},
);
expect(authCanvas.status).toBe(200);
expect(await authCanvas.text()).toBe("ok");
const otherIpStillBlocked = await fetch(
`http://127.0.0.1:${listener.port}${CANVAS_HOST_PATH}/`,
{
headers: { "x-forwarded-for": privateIpB },
},
);
expect(otherIpStillBlocked.status).toBe(401);
clients.add({
socket: {} as unknown as WebSocket,
connect: {} as never,
connId: "c-public",
clientIp: publicIp,
});
const publicIpStillBlocked = await fetch(
`http://127.0.0.1:${listener.port}${CANVAS_HOST_PATH}/`,
{
headers: { "x-forwarded-for": publicIp },
},
);
expect(publicIpStillBlocked.status).toBe(401);
await expectWsRejected(`ws://127.0.0.1:${listener.port}${CANVAS_WS_PATH}`, {
"x-forwarded-for": publicIp,
});
clients.add({
socket: {} as unknown as WebSocket,
connect: {} as never,
connId: "c-cgnat",
clientIp: cgnatIp,
});
const cgnatAllowed = await fetch(
`http://127.0.0.1:${listener.port}${CANVAS_HOST_PATH}/`,
{
headers: { "x-forwarded-for": cgnatIp },
},
);
expect(cgnatAllowed.status).toBe(200);
await new Promise<void>((resolve, reject) => {
const ws = new WebSocket(`ws://127.0.0.1:${listener.port}${CANVAS_WS_PATH}`, {
headers: { "x-forwarded-for": privateIpA },
});
const timer = setTimeout(() => reject(new Error("timeout")), 10_000);
ws.once("open", () => {
clearTimeout(timer);
ws.terminate();
resolve();
});
ws.once("unexpected-response", (_req, res) => {
clearTimeout(timer);
reject(new Error(`unexpected response ${res.statusCode}`));
});
ws.once("error", reject);
});
},
});
},
});
}, 60_000);
@@ -263,74 +255,39 @@ describe("gateway canvas host auth", () => {
},
},
run: async () => {
const clients = new Set<GatewayWsClient>();
const rateLimiter = createAuthRateLimiter({
maxAttempts: 1,
windowMs: 60_000,
lockoutMs: 60_000,
exemptLoopback: false,
});
const canvasWss = new WebSocketServer({ noServer: true });
const canvasHost: CanvasHostHandler = {
rootDir: "test",
close: async () => {},
handleUpgrade: (req, socket, head) => {
const url = new URL(req.url ?? "/", "http://localhost");
if (url.pathname !== CANVAS_WS_PATH) {
return false;
}
canvasWss.handleUpgrade(req, socket, head, (ws) => ws.close());
return true;
await withCanvasGatewayHarness({
resolvedAuth,
rateLimiter,
handleHttpRequest: async () => false,
run: async ({ listener }) => {
const headers = {
authorization: "Bearer wrong",
"x-forwarded-for": "203.0.113.99",
};
const first = await fetch(`http://127.0.0.1:${listener.port}${CANVAS_HOST_PATH}/`, {
headers,
});
expect(first.status).toBe(401);
const second = await fetch(`http://127.0.0.1:${listener.port}${CANVAS_HOST_PATH}/`, {
headers,
});
expect(second.status).toBe(429);
expect(second.headers.get("retry-after")).toBeTruthy();
await expectWsRejected(
`ws://127.0.0.1:${listener.port}${CANVAS_WS_PATH}`,
headers,
429,
);
},
handleHttpRequest: async (_req, _res) => false,
};
const httpServer = createGatewayHttpServer({
canvasHost,
clients,
controlUiEnabled: false,
controlUiBasePath: "/__control__",
openAiChatCompletionsEnabled: false,
openResponsesEnabled: false,
handleHooksRequest: async () => false,
resolvedAuth,
rateLimiter,
});
const wss = new WebSocketServer({ noServer: true });
attachGatewayUpgradeHandler({
httpServer,
wss,
canvasHost,
clients,
resolvedAuth,
rateLimiter,
});
const listener = await listen(httpServer);
try {
const headers = {
authorization: "Bearer wrong",
"x-forwarded-for": "203.0.113.99",
};
const first = await fetch(`http://127.0.0.1:${listener.port}${CANVAS_HOST_PATH}/`, {
headers,
});
expect(first.status).toBe(401);
const second = await fetch(`http://127.0.0.1:${listener.port}${CANVAS_HOST_PATH}/`, {
headers,
});
expect(second.status).toBe(429);
expect(second.headers.get("retry-after")).toBeTruthy();
await expectWsRejected(`ws://127.0.0.1:${listener.port}${CANVAS_WS_PATH}`, headers, 429);
} finally {
await listener.close();
rateLimiter.dispose();
canvasWss.close();
wss.close();
}
},
});
}, 60_000);