import { OpenAPIHono } from "@hono/zod-openapi"; import { describe, expect, it } from "vitest"; import type { AuthEnv, Principal, SessionVar } from "../../src/http/auth.ts"; import { ApiException, errorBody } from "../../src/http/errors.ts"; import { basicAuth, type DesktopDeps, type DesktopRole, deriveDesktopPasswords, desktopUpstream, registerDesktopRoutes, sameOrigin, } from "../../src/modules/desktop/desktop.ts"; // 桌面模式反代:口令派生与容器 init 脚本一致、角色 → 口令注入、只接受控制台会话、响应头与路径约束。 const SID = "7d3c1a52-9b0e-4b7a-8a51-0c2f7f7f0a11"; const KEY = "p0key"; function harness( opts: { principal?: Partial; role?: DesktopRole; check?: DesktopDeps["checkSession"] } = {}, ) { const calls: { url: string; headers: Headers; method: string }[] = []; const app = new OpenAPIHono(); app.onError((err, c) => { if (err instanceof ApiException) return c.json(errorBody(err.code, err.message), err.status as 400); throw err; }); app.use("*", async (c, next) => { const p: Principal = { kind: "admin_session", id: "u1", name: "admin", scopes: ["admin"], ...opts.principal }; c.set("principal", p); c.set("session", p.kind === "admin_session" ? ({ id: "s1" } as SessionVar) : undefined); await next(); }); registerDesktopRoutes(app, { upstream: "http://camofox:3000", accessKey: KEY, checkSession: opts.check ?? (async (_id, holder) => { expect(holder).toBe("admin_session:admin"); return { ok: true, role: opts.role ?? "viewer", leaseExpiresAt: null }; }), consoleSessionValid: async () => true, probeTtlMs: 0, fetch: (async (input: string | URL | Request, init?: RequestInit) => { const url = String(input); calls.push({ url, headers: new Headers(init?.headers), method: init?.method ?? "GET" }); if (url.endsWith("/api/secret-401")) return new Response("no", { status: 401, headers: { "www-authenticate": "Basic" } }); return new Response("ok", { status: 200, headers: { "content-type": "text/html", "www-authenticate": "Basic", "x-frame-options": "DENY" }, }); }) as typeof fetch, }); const root = new OpenAPIHono(); root.onError((err, c) => { if (err instanceof ApiException) return c.json(errorBody(err.code, err.message), err.status as 400); throw err; }); root.route("/api/v1", app); return { app: root, calls }; } describe("桌面口令与上游地址", () => { it("口令与容器 init-umt-desktop 的 openssl HMAC 派生一致", () => { // printf '%s' umt-desktop/control | openssl dgst -sha256 -hmac p0key -r | cut -c1-48 const p = deriveDesktopPasswords(KEY); expect(p.controller).toBe("0b29251f95456b1e4612e3f4c45c2e353fd24e5ecbdf4959"); expect(p.viewer).toHaveLength(48); expect(p.viewer).not.toBe(p.controller); }); it("缺省由浏览器服务地址推出(端口换 3000),可显式覆盖", () => { expect(desktopUpstream({ providerUrl: "http://camofox:9377" })).toBe("http://camofox:3000"); expect(desktopUpstream({ providerUrl: "http://camofox:9377", desktopUrl: "http://desk:3000/" })).toBe( "http://desk:3000", ); expect(desktopUpstream({})).toBeNull(); }); it("WebSocket 只接受同源页面", () => { const req = (origin: string | null, host = "media.example.com") => new Request("https://media.example.com/x", { headers: { host, ...(origin ? { origin } : {}) } }); expect(sameOrigin(req("https://media.example.com"))).toBe(true); expect(sameOrigin(req("https://evil.example"))).toBe(false); expect(sameOrigin(req(null))).toBe(false); }); }); describe("桌面反代", () => { it("外部 API Key 不能用桌面模式", async () => { const { app } = harness({ principal: { kind: "api_key" } }); const r = await app.request(`/api/v1/browser/desktop/${SID}/`); expect(r.status).toBe(403); }); it("状态:按租约给角色,并返回 iframe 地址", async () => { const { app } = harness({ role: "controller" }); const r = await app.request(`/api/v1/browser/desktop/${SID}/_status`); const body = (await r.json()) as { data: { available: boolean; role: string; path: string } }; expect(body.data).toMatchObject({ available: true, role: "controller", path: `/api/v1/browser/desktop/${SID}/` }); }); it("静态资源:不带口令、不转发 cookie;响应只允许本站嵌入,不外露认证质询", async () => { const { app, calls } = harness(); const r = await app.request(`/api/v1/browser/desktop/${SID}/assets/index.js?v=1`, { headers: { cookie: "umt_session=secret" }, }); expect(r.status).toBe(200); expect(calls.at(-1)?.url).toBe("http://camofox:3000/assets/index.js?v=1"); expect(calls.at(-1)?.headers.get("authorization")).toBeNull(); expect(calls.at(-1)?.headers.get("cookie")).toBeNull(); expect(r.headers.get("content-security-policy")).toBe("frame-ancestors 'self'"); expect(r.headers.get("x-frame-options")).toBe("SAMEORIGIN"); expect(r.headers.get("www-authenticate")).toBeNull(); }); it("Selkies 接口:只读 / 控制口令按角色注入", async () => { const pw = deriveDesktopPasswords(KEY); for (const role of ["viewer", "controller"] as const) { const { app, calls } = harness({ role }); await app.request(`/api/v1/browser/desktop/${SID}/api/websockets`); expect(calls.at(-1)?.headers.get("authorization")).toBe(basicAuth(pw[role])); } }); it("上游 401 不透传质询,报口令不匹配", async () => { const { app } = harness(); const r = await app.request(`/api/v1/browser/desktop/${SID}/api/secret-401`); expect(r.status).toBe(502); expect(r.headers.get("www-authenticate")).toBeNull(); }); it("会话不属于自带浏览器服务 → 409;非法会话 id → 404;WebSocket 跨源 → 403", async () => { const { app } = harness({ check: async () => ({ ok: false, code: "unsupported", message: "只有部署自带的浏览器服务支持桌面模式" }), }); expect((await app.request(`/api/v1/browser/desktop/${SID}/api/websockets`)).status).toBe(409); expect((await app.request("/api/v1/browser/desktop/not-a-uuid/")).status).toBe(404); const ws = await harness().app.request(`/api/v1/browser/desktop/${SID}/api/websockets`, { headers: { upgrade: "websocket", origin: "https://evil.example", host: "media.example.com" }, }); expect(ws.status).toBe(403); }); });