feat: serve plugin http and websockets from one mount instead of ports

This commit is contained in:
LukeGus committed 2026-09-22 16:39:41 -05:00
1 parent c496a093ca
commit 8431570ff7
102 files changed
+2894 -2308

No files matched your search

@@ -24,7 +24,6 @@ function readGuacdOptions(): { host: string; port: number } {
return resolveGuacdOptions(dbUrl);
}
export const GUAC_WS_PORT = 30008;
const DATA_DIR = process.env.DATA_DIR || "./db/data";
const GUACAMOLE_RECORDINGS_DIR =
process.env.GUACD_RECORDING_BACKEND_PATH ||
@@ -121,9 +120,12 @@ async function persistGuacamoleRecording(
});
}
// noServer: core owns the listening socket. guacamole-lite passes this object
// straight to ws, and exposes the WebSocketServer it builds, so the plugin can
// hand it an upgrade from /plugin-ws/remote-desktop/display. There is no
// guacamole port any more.
const websocketOptions = {
host: "127.0.0.1",
port: GUAC_WS_PORT,
noServer: true,
};
const clientOptions = {
@@ -253,13 +255,48 @@ function createGuacServer(): GuacamoleLite {
let guacServer: GuacamoleLite | null = null;
/**
* Starts the guacamole-lite WebSocket server. Called from this plugin's
* activate() -- see plugins/remote-desktop/backend/index.mjs.
* Builds the guacamole-lite server. Called from this plugin's activate().
* Nothing binds a port: handleGuacamoleUpgrade below feeds it connections.
*/
export async function startGuacamoleService(): Promise<void> {
guacServer = createGuacServer();
}
/**
* Hands one upgrade to guacamole-lite's own WebSocketServer.
*
* guacamole-lite constructs that server internally and has no "attach to this
* http server" option, so giving it the upgrade is the only way in. Core has
* already authenticated the request by the time this runs.
*/
export function handleGuacamoleUpgrade(
request: import("http").IncomingMessage,
socket: import("stream").Duplex,
head: Buffer,
): void {
const server = guacServer as unknown as {
webSocketServer?: {
handleUpgrade: (
request: import("http").IncomingMessage,
socket: import("stream").Duplex,
head: Buffer,
done: (ws: unknown) => void,
) => void;
emit: (event: string, ...args: unknown[]) => void;
};
} | null;
const wss = server?.webSocketServer;
if (!wss) {
socket.destroy();
return;
}
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit("connection", ws, request);
});
}
/**
* Stops the guacamole-lite WebSocket server. Called from this plugin's
* deactivate().
+28 -10
View File
@@ -1,33 +1,51 @@
import type { PluginContext } from "@termix/plugin-sdk/backend";
import {
GUAC_WS_PORT,
getGuacSessionInfo,
handleGuacamoleUpgrade,
restartGuacServer,
startGuacamoleService,
stopGuacamoleService,
tokenService,
} from "./guacamole-server.js";
import { router } from "./routes.js";
import {
registerGuacamoleBridge,
unregisterGuacamoleBridge,
} from "../../../../src/backend/database/routes/guacamole-dispatch.js";
import { startRemoteDesktopService } from "./routes.js";
export async function activate(ctx: PluginContext) {
await startGuacamoleService();
registerGuacamoleBridge({
router,
startRemoteDesktopService(ctx.http.router());
// guacamole-lite owns its own WebSocketServer, so it takes the raw upgrade.
//
// Public because a Guacamole display authenticates with the encrypted,
// single-use connection token minted by POST /token, not with a session
// JWT. guacamole-lite decrypts and validates that token itself, and the
// route that mints one is authenticated normally.
ctx.ws.upgrade(
"/display",
(request, socket, head) =>
handleGuacamoleUpgrade(
request as Parameters<typeof handleGuacamoleUpgrade>[0],
socket as Parameters<typeof handleGuacamoleUpgrade>[1],
head as Buffer,
),
{ public: true },
);
// What collab, session-sharing and the admin guacd setting need from a
// running Guacamole server. Revoked automatically on deactivate, and
// hosts/guacamole-sessions.ts degrades safely while it is gone.
ctx.registry.provide("remote-desktop.sessions", {
restart: restartGuacServer,
createJoinToken: (guacamoleConnectionId: string, readOnly: boolean) =>
tokenService.createJoinToken(guacamoleConnectionId, readOnly),
getSessionInfo: getGuacSessionInfo,
});
ctx.log.info(`Remote Desktop listening on ${GUAC_WS_PORT ?? 30008}`);
ctx.log.info(
"Remote Desktop mounted at /plugin-api/remote-desktop and /plugin-ws/remote-desktop/display",
);
}
export async function deactivate() {
unregisterGuacamoleBridge();
await stopGuacamoleService();
}
+8 -5
View File
@@ -1,10 +1,9 @@
import { getErrorMessage } from "../../../../src/backend/utils/error-message.js";
import express from "express";
import express, { type Router } from "express";
import { GuacamoleTokenService } from "./token-service.js";
import { withRecordingSettings } from "./recording-settings.js";
import { withDriveSettings } from "./drive-settings.js";
import { guacLogger } from "../../../../src/backend/utils/logger.js";
import { AuthManager } from "../../../../src/backend/utils/auth-manager.js";
import { PermissionManager } from "../../../../src/backend/utils/permission-manager.js";
import {
resolveRecipientSharedHostAuthentication,
@@ -37,10 +36,10 @@ import { createMacosVncCompatibilityProxy } from "./macos-vnc-proxy.js";
const router = express.Router();
const tokenService = GuacamoleTokenService.getInstance();
const authManager = AuthManager.getInstance();
const DATA_DIR = process.env.DATA_DIR || "./db/data";
router.use(authManager.createAuthMiddleware());
// Core authenticates every /plugin-api route before it reaches here, so the
// router no longer applies its own middleware.
router.get("/connection/:connectId", (req: AuthenticatedRequest, res) => {
if (!req.userId)
@@ -816,7 +815,7 @@ router.get("/status", async (req, res) => {
status: isConnected ? "connected" : "disconnected",
},
websocket: {
port: 30008,
path: "/plugin-ws/remote-desktop/display",
status: "running",
},
});
@@ -829,3 +828,7 @@ router.get("/status", async (req, res) => {
});
export { router };
export function startRemoteDesktopService(mountOn: Router): void {
mountOn.use(router);
}