mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 07:05:24 +00:00
feat: finish the remote-desktop plugin (B14)
This commit is contained in:
1 parent
0af2c94983
commit
14eb558b85
215 files changed
+30029
-3921
No files matched your search
@@ -0,0 +1,82 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
normalizeImportedHost,
|
||||
userDefaultParams,
|
||||
} from "../../src/backend/host-settings.js";
|
||||
|
||||
describe("normalizeImportedHost", () => {
|
||||
it("reads a Termix export's pluginSettings", () => {
|
||||
expect(
|
||||
normalizeImportedHost({
|
||||
pluginSettings: {
|
||||
"remote-desktop": {
|
||||
enableVnc: true,
|
||||
vncPort: 5901,
|
||||
guacamoleConfig: { cursor: "local" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
enableVnc: true,
|
||||
vncPort: 5901,
|
||||
guacamoleConfig: { cursor: "local" },
|
||||
});
|
||||
});
|
||||
|
||||
it("reads the flat fields hosts had before 2.9.0", () => {
|
||||
expect(
|
||||
normalizeImportedHost({
|
||||
enableRdp: true,
|
||||
rdpPort: "3390",
|
||||
security: "nla",
|
||||
ignoreCert: 1,
|
||||
guacamoleConfig: JSON.stringify({ dpi: 120 }),
|
||||
enableTerminalToolbar: false,
|
||||
}),
|
||||
).toEqual({
|
||||
enableRdp: true,
|
||||
rdpPort: 3390,
|
||||
rdpSecurity: "nla",
|
||||
rdpIgnoreCert: true,
|
||||
guacamoleConfig: { dpi: 120 },
|
||||
enableToolbar: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("turns a connectionType-only host into its switch and port", () => {
|
||||
expect(
|
||||
normalizeImportedHost({ connectionType: "telnet", port: 2323 }),
|
||||
).toEqual({ enableTelnet: true, telnetPort: 2323 });
|
||||
});
|
||||
|
||||
it("writes nothing for a plain SSH row", () => {
|
||||
expect(
|
||||
normalizeImportedHost({ connectionType: "ssh", port: 22 }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("drops a port out of range", () => {
|
||||
expect(normalizeImportedHost({ enableRdp: true, rdpPort: 70000 })).toEqual({
|
||||
enableRdp: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("userDefaultParams", () => {
|
||||
it("maps the user settings onto guacd parameters, skipping inherit", () => {
|
||||
expect(
|
||||
userDefaultParams({
|
||||
colorDepth: "24",
|
||||
resizeMethod: "reconnect",
|
||||
disableCopy: "on",
|
||||
enableWallpaper: "off",
|
||||
enableDrive: "inherit",
|
||||
}),
|
||||
).toEqual({
|
||||
"color-depth": 24,
|
||||
"resize-method": "reconnect",
|
||||
"disable-copy": true,
|
||||
"enable-wallpaper": false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildRdpSettings,
|
||||
resolveRdpAuthTypeForConnect,
|
||||
resolveRdpDomain,
|
||||
} from "../../src/backend/rdp-settings.js";
|
||||
|
||||
@@ -53,32 +52,3 @@ describe("buildRdpSettings", () => {
|
||||
expect(resolveRdpDomain("none", undefined, "SAVED")).toBe("SAVED");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveRdpAuthTypeForConnect", () => {
|
||||
it("keeps prompt-on-connect authentication for a secretless recipient", () => {
|
||||
expect(
|
||||
resolveRdpAuthTypeForConnect({
|
||||
storedAuthType: "none",
|
||||
sharedResolution: { source: "secretless" },
|
||||
}),
|
||||
).toBe("none");
|
||||
});
|
||||
|
||||
it("uses a recipient override instead of prompting", () => {
|
||||
expect(
|
||||
resolveRdpAuthTypeForConnect({
|
||||
storedAuthType: "none",
|
||||
sharedResolution: { source: "personal-override" },
|
||||
}),
|
||||
).toBe("credential");
|
||||
});
|
||||
|
||||
it("uses authentication shared by the owner", () => {
|
||||
expect(
|
||||
resolveRdpAuthTypeForConnect({
|
||||
storedAuthType: "credential",
|
||||
sharedResolution: { source: "owner-shared", authType: "credential" },
|
||||
}),
|
||||
).toBe("credential");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,318 @@
|
||||
import http from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import express, { type Router } from "express";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
createMockCtx,
|
||||
type MockPluginContext,
|
||||
} from "@termix/plugin-sdk/testing";
|
||||
import type {
|
||||
PluginContext,
|
||||
PluginProtocolTarget,
|
||||
} from "@termix/plugin-sdk/backend";
|
||||
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
|
||||
import manifestJson from "../../manifest.json";
|
||||
import { activate } from "../../src/backend/index.js";
|
||||
import { GuacamoleTokenService } from "../../src/backend/token-service.js";
|
||||
|
||||
const manifest = manifestJson as unknown as PluginManifest;
|
||||
|
||||
// The token key comes from JWT_SECRET; without one each service picks its own.
|
||||
process.env.JWT_SECRET = "remote-desktop-test-secret";
|
||||
|
||||
function target(
|
||||
overrides: Partial<PluginProtocolTarget["auth"]> = {},
|
||||
): PluginProtocolTarget {
|
||||
return {
|
||||
host: {
|
||||
id: 7,
|
||||
name: "Win box",
|
||||
ip: "10.0.0.7",
|
||||
port: 22,
|
||||
ownerUserId: "user-1",
|
||||
jumpHosts: [],
|
||||
},
|
||||
shared: false,
|
||||
auth: {
|
||||
authType: "direct",
|
||||
username: "admin",
|
||||
password: "secret",
|
||||
domain: "CORP",
|
||||
...overrides,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
interface Server {
|
||||
mock: MockPluginContext;
|
||||
request: (
|
||||
method: string,
|
||||
path: string,
|
||||
body?: unknown,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
) => Promise<{ status: number; body: any }>;
|
||||
close: () => Promise<void>;
|
||||
}
|
||||
|
||||
let server: Server | null = null;
|
||||
|
||||
afterEach(async () => {
|
||||
await server?.close();
|
||||
server = null;
|
||||
});
|
||||
|
||||
async function start(
|
||||
options: {
|
||||
protocolTargets?: Record<string, PluginProtocolTarget>;
|
||||
services?: Record<string, object>;
|
||||
capabilities?: string[];
|
||||
} = {},
|
||||
): Promise<Server> {
|
||||
let router: Router | null = null;
|
||||
const mock = createMockCtx({
|
||||
pluginId: manifest.id,
|
||||
manifest,
|
||||
capabilities: options.capabilities ?? manifest.capabilities,
|
||||
router: () => (router = express.Router()),
|
||||
protocolTargets: options.protocolTargets,
|
||||
services: options.services,
|
||||
});
|
||||
await activate(mock.ctx as PluginContext);
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use((req, _res, next) => {
|
||||
mock.setActor(req.header("x-test-user") ?? "user-1");
|
||||
next();
|
||||
});
|
||||
app.use((req, res, next) => router!(req, res, next));
|
||||
const httpServer = http.createServer(app);
|
||||
await new Promise<void>((resolve) => httpServer.listen(0, resolve));
|
||||
const { port } = httpServer.address() as AddressInfo;
|
||||
|
||||
return {
|
||||
mock,
|
||||
async request(method, path, body) {
|
||||
const response = await fetch(`http://127.0.0.1:${port}${path}`, {
|
||||
method,
|
||||
headers: body ? { "content-type": "application/json" } : {},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
const text = await response.text();
|
||||
return { status: response.status, body: text ? JSON.parse(text) : null };
|
||||
},
|
||||
async close() {
|
||||
await new Promise<void>((resolve) => httpServer.close(() => resolve()));
|
||||
for (const dispose of [...mock.disposals].reverse()) await dispose();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const tokens = new GuacamoleTokenService();
|
||||
|
||||
describe("POST /connect-host/:hostId", () => {
|
||||
it("mints a token from the resolved login and the host's own settings", async () => {
|
||||
server = await start({ protocolTargets: { "7:rdp": target() } });
|
||||
await server.mock.ctx.settings.setHost(7, "enableRdp", true);
|
||||
await server.mock.ctx.settings.setHost(7, "rdpPort", 3390);
|
||||
await server.mock.ctx.settings.setHost(7, "rdpSecurity", "nla");
|
||||
await server.mock.ctx.settings.setHost(7, "guacamoleConfig", {
|
||||
"color-depth": 16,
|
||||
"enable-wallpaper": "auto",
|
||||
});
|
||||
|
||||
const response = await server.request("POST", "/connect-host/7", {
|
||||
protocol: "rdp",
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.termixConnectId).toEqual(expect.any(String));
|
||||
const token = tokens.decryptToken(response.body.token);
|
||||
expect(token?.connection.settings).toMatchObject({
|
||||
hostname: "10.0.0.7",
|
||||
port: 3390,
|
||||
username: "admin",
|
||||
password: "secret",
|
||||
domain: "CORP",
|
||||
security: "nla",
|
||||
"color-depth": 16,
|
||||
});
|
||||
expect(token?.connection.settings).not.toHaveProperty("enable-wallpaper");
|
||||
expect(token?.termixMeta).toMatchObject({
|
||||
hostId: 7,
|
||||
hostName: "Win box",
|
||||
protocol: "rdp",
|
||||
});
|
||||
expect(server.mock.audits.at(-1)).toMatchObject({
|
||||
action: "rdp_connect",
|
||||
success: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("puts the user's RDP defaults under the host's own values", async () => {
|
||||
server = await start({ protocolTargets: { "7:rdp": target() } });
|
||||
await server.mock.ctx.settings.setHost(7, "enableRdp", true);
|
||||
await server.mock.ctx.settings.setHost(7, "guacamoleConfig", {
|
||||
"color-depth": 32,
|
||||
});
|
||||
await server.mock.ctx.settings.setUser("user-1", "colorDepth", "16");
|
||||
await server.mock.ctx.settings.setUser("user-1", "disableCopy", "on");
|
||||
|
||||
const response = await server.request("POST", "/connect-host/7", {
|
||||
protocol: "rdp",
|
||||
});
|
||||
const settings = tokens.decryptToken(response.body.token)?.connection
|
||||
.settings;
|
||||
expect(settings?.["color-depth"]).toBe(32);
|
||||
expect(settings?.["disable-copy"]).toBe(true);
|
||||
});
|
||||
|
||||
it("uses the prompted login when the host asks at connect time", async () => {
|
||||
server = await start({
|
||||
protocolTargets: { "7:rdp": target({ authType: "none", username: "" }) },
|
||||
});
|
||||
await server.mock.ctx.settings.setHost(7, "enableRdp", true);
|
||||
|
||||
const response = await server.request("POST", "/connect-host/7", {
|
||||
protocol: "rdp",
|
||||
promptedUsername: "bob",
|
||||
promptedPassword: "pw",
|
||||
promptedDomain: "",
|
||||
});
|
||||
const settings = tokens.decryptToken(response.body.token)?.connection
|
||||
.settings;
|
||||
expect(settings).toMatchObject({ username: "bob", password: "pw" });
|
||||
});
|
||||
|
||||
it("refuses a protocol the host has switched off", async () => {
|
||||
server = await start({ protocolTargets: { "7:vnc": target() } });
|
||||
const response = await server.request("POST", "/connect-host/7", {
|
||||
protocol: "vnc",
|
||||
});
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it("answers 404 when the caller cannot reach the host", async () => {
|
||||
server = await start();
|
||||
await server.mock.ctx.settings.setHost(7, "enableRdp", true);
|
||||
const response = await server.request("POST", "/connect-host/7", {
|
||||
protocol: "rdp",
|
||||
});
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
it("refuses everything while an admin has turned it off", async () => {
|
||||
server = await start({ protocolTargets: { "7:rdp": target() } });
|
||||
await server.mock.ctx.settings.set("enabled", false);
|
||||
await server.mock.ctx.settings.setHost(7, "enableRdp", true);
|
||||
|
||||
expect(
|
||||
(await server.request("POST", "/connect-host/7", { protocol: "rdp" }))
|
||||
.status,
|
||||
).toBe(403);
|
||||
expect(
|
||||
(
|
||||
await server.request("POST", "/token", {
|
||||
type: "rdp",
|
||||
hostname: "10.0.0.9",
|
||||
})
|
||||
).status,
|
||||
).toBe(403);
|
||||
const status = await server.request("GET", "/status?probe=0");
|
||||
expect(status.body.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it("records only when session recording is on for the host", async () => {
|
||||
const enabledFor = async (hostId: number) => hostId === 7;
|
||||
server = await start({
|
||||
protocolTargets: { "7:rdp": target() },
|
||||
services: {
|
||||
"recordings.writer": {
|
||||
enabledFor,
|
||||
createFinished: async () => ({ id: 1 }),
|
||||
},
|
||||
},
|
||||
});
|
||||
await server.mock.ctx.settings.setHost(7, "enableRdp", true);
|
||||
const response = await server.request("POST", "/connect-host/7", {
|
||||
protocol: "rdp",
|
||||
});
|
||||
const token = tokens.decryptToken(response.body.token);
|
||||
expect(token?.recording).toMatchObject({ hostId: 7, protocol: "rdp" });
|
||||
expect(token?.connection.settings["recording-name"]).toEqual(
|
||||
expect.any(String),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the rest of the routes", () => {
|
||||
it("mints a quick connect token with the caller's defaults", async () => {
|
||||
server = await start();
|
||||
await server.mock.ctx.settings.setUser("user-1", "colorDepth", "24");
|
||||
const response = await server.request("POST", "/token", {
|
||||
type: "rdp",
|
||||
hostname: "10.0.0.9",
|
||||
username: "u",
|
||||
password: "p",
|
||||
});
|
||||
expect(response.status).toBe(200);
|
||||
expect(
|
||||
tokens.decryptToken(response.body.token)?.connection.settings,
|
||||
).toMatchObject({ hostname: "10.0.0.9", port: 3389, "color-depth": 24 });
|
||||
});
|
||||
|
||||
it("offers native RDP only in the desktop app", async () => {
|
||||
server = await start();
|
||||
expect((await server.request("GET", "/native-rdp")).body).toEqual({
|
||||
available: false,
|
||||
});
|
||||
expect(
|
||||
(await server.request("POST", "/native-rdp", { host: "10.0.0.7" }))
|
||||
.status,
|
||||
).toBe(404);
|
||||
});
|
||||
|
||||
it("does not hand out another user's session id", async () => {
|
||||
server = await start();
|
||||
const response = await server.request("GET", "/connection/unknown");
|
||||
expect(response.body).toEqual({ guacamoleConnectionId: null });
|
||||
});
|
||||
});
|
||||
|
||||
describe("activate", () => {
|
||||
it("fails closed without network:serve", async () => {
|
||||
await expect(
|
||||
start({
|
||||
capabilities: manifest.capabilities.filter(
|
||||
(capability) => capability !== "network:serve",
|
||||
),
|
||||
}),
|
||||
).rejects.toThrow(/network:serve/);
|
||||
});
|
||||
|
||||
it("cannot read a host's login without credentials:read", async () => {
|
||||
server = await start({
|
||||
protocolTargets: { "7:rdp": target() },
|
||||
capabilities: manifest.capabilities.filter(
|
||||
(capability) => capability !== "credentials:read",
|
||||
),
|
||||
});
|
||||
await server.mock.ctx.settings.setHost(7, "enableRdp", true);
|
||||
const response = await server.request("POST", "/connect-host/7", {
|
||||
protocol: "rdp",
|
||||
});
|
||||
expect(response.status).toBe(500);
|
||||
expect(server.mock.checked).toContain("credentials:read");
|
||||
});
|
||||
|
||||
it("provides sessions.live for rdp, vnc and telnet", async () => {
|
||||
server = await start();
|
||||
expect([...server.mock.services.keys()].sort()).toEqual(
|
||||
expect.arrayContaining([
|
||||
"sessions.live#rdp",
|
||||
"sessions.live#telnet",
|
||||
"sessions.live#vnc",
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { RemoteSessions } from "../../src/backend/sessions.js";
|
||||
import { createLiveSessions } from "../../src/backend/live-sessions.js";
|
||||
import { GuacamoleTokenService } from "../../src/backend/token-service.js";
|
||||
|
||||
const meta = {
|
||||
termixConnectId: "c1",
|
||||
hostId: 7,
|
||||
hostName: "Win box",
|
||||
ownerUserId: "user-1",
|
||||
protocol: "rdp" as const,
|
||||
tabInstanceId: "tab-1",
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("RemoteSessions", () => {
|
||||
it("moves parked resources onto the session and releases them on close", () => {
|
||||
const sessions = new RemoteSessions();
|
||||
const tunnel = vi.fn();
|
||||
const tracking = vi.fn();
|
||||
sessions.park("c1", [tunnel]);
|
||||
sessions.opened(meta, "g1", [tracking]);
|
||||
|
||||
expect(sessions.byConnect("c1")?.guacamoleConnectionId).toBe("g1");
|
||||
expect(sessions.byGuacamole("g1")?.hostName).toBe("Win box");
|
||||
expect(tunnel).not.toHaveBeenCalled();
|
||||
|
||||
sessions.closed("c1");
|
||||
expect(tunnel).toHaveBeenCalledOnce();
|
||||
expect(tracking).toHaveBeenCalledOnce();
|
||||
expect(sessions.byGuacamole("g1")).toBeNull();
|
||||
});
|
||||
|
||||
it("releases a tunnel guacd never used", () => {
|
||||
vi.useFakeTimers();
|
||||
const sessions = new RemoteSessions(1000);
|
||||
const tunnel = vi.fn();
|
||||
sessions.park("c1", [tunnel]);
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(tunnel).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("releases everything on clear, and one failing close does not stop the rest", () => {
|
||||
const sessions = new RemoteSessions();
|
||||
const failing = vi.fn(() => {
|
||||
throw new Error("already closed");
|
||||
});
|
||||
const pending = vi.fn();
|
||||
const open = vi.fn();
|
||||
sessions.park("c2", [failing, pending]);
|
||||
sessions.park("c1", [open]);
|
||||
sessions.opened(meta, "g1");
|
||||
sessions.clear();
|
||||
expect(pending).toHaveBeenCalledOnce();
|
||||
expect(open).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe("sessions.live providers", () => {
|
||||
const tokens = new GuacamoleTokenService();
|
||||
|
||||
it("describes a live session of its own protocol only", () => {
|
||||
const sessions = new RemoteSessions();
|
||||
sessions.opened(meta, "g1");
|
||||
const deps = { sessions, tokens, endSession: vi.fn(() => true) };
|
||||
const rdp = createLiveSessions("rdp", deps);
|
||||
const vnc = createLiveSessions("vnc", deps);
|
||||
|
||||
expect(rdp.getSession("g1")).toMatchObject({
|
||||
id: "g1",
|
||||
userId: "user-1",
|
||||
hostId: 7,
|
||||
hostName: "Win box",
|
||||
isConnected: true,
|
||||
tabInstanceId: "tab-1",
|
||||
});
|
||||
expect(vnc.getSession("g1")).toBeNull();
|
||||
});
|
||||
|
||||
it("mints a join token for a viewer and refuses an unknown session", () => {
|
||||
const sessions = new RemoteSessions();
|
||||
sessions.opened(meta, "g1");
|
||||
const rdp = createLiveSessions("rdp", {
|
||||
sessions,
|
||||
tokens,
|
||||
endSession: vi.fn(() => true),
|
||||
});
|
||||
|
||||
const token = tokens.decryptToken(rdp.createViewerToken("g1", true));
|
||||
expect(token?.connection).toMatchObject({ join: "g1", readOnly: true });
|
||||
expect(token?.recording).toBeUndefined();
|
||||
expect(() => rdp.createViewerToken("nope", false)).toThrow();
|
||||
});
|
||||
|
||||
it("ends the session for its owner", () => {
|
||||
const sessions = new RemoteSessions();
|
||||
sessions.opened(meta, "g1");
|
||||
const endSession = vi.fn(() => true);
|
||||
const rdp = createLiveSessions("rdp", { sessions, tokens, endSession });
|
||||
rdp.ownerEndSession("g1", "owner ended");
|
||||
expect(endSession).toHaveBeenCalledWith("g1");
|
||||
});
|
||||
});
|
||||
@@ -1,98 +0,0 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { runInNewContext } from "node:vm";
|
||||
import ts from "typescript";
|
||||
import { expect, it, vi } from "vitest";
|
||||
|
||||
it("returns the minted token without waiting for a connection that needs that token", async () => {
|
||||
const source = readFileSync(
|
||||
new URL("../../src/backend/routes.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const ast = ts.createSourceFile(
|
||||
"routes.ts",
|
||||
source,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
);
|
||||
const route = ast.statements.find(
|
||||
(s) =>
|
||||
ts.isExpressionStatement(s) &&
|
||||
ts.isCallExpression(s.expression) &&
|
||||
s.expression.arguments[0]?.getText(ast) === '"/connect-host/:hostId"',
|
||||
) as ts.ExpressionStatement;
|
||||
const callback = (route.expression as ts.CallExpression)
|
||||
.arguments[1] as ts.ArrowFunction;
|
||||
const block = (callback.body as ts.Block).statements[0] as ts.TryStatement;
|
||||
const index = block.tryBlock.statements
|
||||
.map((s, i) => (ts.isSwitchStatement(s) ? i : -1))
|
||||
.filter((i) => i >= 0)
|
||||
.at(-1)!;
|
||||
expect(index).toBeGreaterThan(-1);
|
||||
const tail = block.tryBlock.statements
|
||||
.slice(index + 1)
|
||||
.map((s) => s.getText(ast))
|
||||
.join("\n");
|
||||
const json = vi.fn();
|
||||
const wait = vi.fn(() => {
|
||||
throw new Error("Client has not received the token yet");
|
||||
});
|
||||
await runInNewContext(
|
||||
ts.transpileModule(`(async () => { ${tail} })()`, {
|
||||
compilerOptions: { target: ts.ScriptTarget.ES2022 },
|
||||
}).outputText,
|
||||
{
|
||||
waitForGuacdOpen: wait,
|
||||
token: "minted-token",
|
||||
termixConnectId: "correlation-id",
|
||||
req: {},
|
||||
getRequestMeta: () => ({}),
|
||||
logAudit: async () => {},
|
||||
getAuditUsername: async () => "owner",
|
||||
userId: "owner",
|
||||
connectionType: "rdp",
|
||||
hostId: 7,
|
||||
hostname: "host",
|
||||
port: 3389,
|
||||
res: { json },
|
||||
},
|
||||
);
|
||||
expect(wait).not.toHaveBeenCalled();
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
token: "minted-token",
|
||||
termixConnectId: "correlation-id",
|
||||
guacamoleConnectionId: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("only reveals the live session ID to its owner", () => {
|
||||
const source = readFileSync(
|
||||
new URL("../../src/backend/guacamole-server.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const ast = ts.createSourceFile(
|
||||
"server.ts",
|
||||
source,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
);
|
||||
const fn = ast.statements.find(
|
||||
(s) =>
|
||||
ts.isFunctionDeclaration(s) &&
|
||||
s.name?.text === "getGuacSessionByConnectId",
|
||||
)!;
|
||||
const info = { ownerUserId: "owner", guacamoleConnectionId: "guacd-id" };
|
||||
const context = {
|
||||
exports: {} as Record<string, (id: string, user: string) => unknown>,
|
||||
guacSessionByConnectId: new Map([["connect-id", info]]),
|
||||
};
|
||||
runInNewContext(
|
||||
ts.transpileModule(fn.getText(ast), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS },
|
||||
}).outputText,
|
||||
context,
|
||||
);
|
||||
const lookup = context.exports.getGuacSessionByConnectId;
|
||||
expect(lookup("connect-id", "owner")).toEqual(info);
|
||||
expect(lookup("connect-id", "other")).toBeNull();
|
||||
expect(lookup("pending", "owner")).toBeNull();
|
||||
});
|
||||
@@ -1,20 +1,9 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
vi.mock("../../../../src/backend/utils/logger.js", () => ({
|
||||
guacLogger: {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const { GuacamoleTokenService } =
|
||||
await import("../../src/backend/token-service.js");
|
||||
import { GuacamoleTokenService } from "../../src/backend/token-service.js";
|
||||
|
||||
describe("GuacamoleTokenService", () => {
|
||||
const tokenService = GuacamoleTokenService.getInstance();
|
||||
const tokenService = new GuacamoleTokenService();
|
||||
|
||||
it("disables RDP pre-authentication when no credentials are configured", () => {
|
||||
const token = tokenService.createRdpToken("windows.example.test", "", "");
|
||||
|
||||
Reference in new issue
Block a user