mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
feat: serve plugin http and websockets from one mount instead of ports
This commit is contained in:
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().
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -285,11 +285,13 @@ export const GuacamoleDisplay = forwardRef<
|
||||
|
||||
let wsBase: string | null;
|
||||
if (isElectron()) {
|
||||
// includeJwt stays off: a display authenticates with the encrypted
|
||||
// connection token in the query, not a session.
|
||||
const target = await buildOriginWsUrl({
|
||||
origin,
|
||||
localPort: 30008,
|
||||
localPath: "/guacamole/websocket/",
|
||||
remotePath: "/guacamole/websocket/",
|
||||
localPort: 30001,
|
||||
localPath: "/plugin-ws/remote-desktop/display",
|
||||
remotePath: "/plugin-ws/remote-desktop/display",
|
||||
includeJwt: false,
|
||||
});
|
||||
if (!target) {
|
||||
@@ -299,7 +301,6 @@ export const GuacamoleDisplay = forwardRef<
|
||||
wsBase = target.url;
|
||||
} else {
|
||||
wsBase = buildGuacamoleWebSocketBaseUrl({
|
||||
isDev,
|
||||
isElectronApp: false,
|
||||
isEmbeddedApp: false,
|
||||
basePath: getBasePath(),
|
||||
|
||||
@@ -174,17 +174,20 @@ export async function getGuacamoleToken(
|
||||
try {
|
||||
const guacParams = toGuacamoleParams(request.guacamoleConfig);
|
||||
|
||||
const response = await guacamoleApi(origin).post("/guacamole/token", {
|
||||
type: request.protocol,
|
||||
hostname: request.hostname,
|
||||
port: request.port,
|
||||
username: request.username,
|
||||
password: request.password,
|
||||
domain: request.domain,
|
||||
security: request.security,
|
||||
"ignore-cert": request.ignoreCert,
|
||||
...guacParams,
|
||||
});
|
||||
const response = await guacamoleApi(origin).post(
|
||||
"/plugin-api/remote-desktop/token",
|
||||
{
|
||||
type: request.protocol,
|
||||
hostname: request.hostname,
|
||||
port: request.port,
|
||||
username: request.username,
|
||||
password: request.password,
|
||||
domain: request.domain,
|
||||
security: request.security,
|
||||
"ignore-cert": request.ignoreCert,
|
||||
...guacParams,
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "get guacamole token");
|
||||
@@ -214,7 +217,7 @@ export async function getGuacamoleTokenFromHost(
|
||||
}
|
||||
const targetHostId = remoteHostId ?? hostId;
|
||||
const response = await guacamoleApi(origin).post(
|
||||
`/guacamole/connect-host/${targetHostId}`,
|
||||
`/plugin-api/remote-desktop/connect-host/${targetHostId}`,
|
||||
{
|
||||
...(protocol ? { protocol } : {}),
|
||||
...(promptedCredentials?.username
|
||||
@@ -237,7 +240,9 @@ export async function getGuacamoleTokenFromHost(
|
||||
export async function getGuacdStatus(origin: ConnectionOrigin): Promise<{
|
||||
guacd: { status: string };
|
||||
}> {
|
||||
const response = await guacamoleApi(origin).get("/guacamole/status");
|
||||
const response = await guacamoleApi(origin).get(
|
||||
"/plugin-api/remote-desktop/status",
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -247,7 +252,7 @@ export async function getGuacamoleConnectionId(
|
||||
signal?: AbortSignal,
|
||||
): Promise<string | null> {
|
||||
const response = await guacamoleApi(origin).get(
|
||||
`/guacamole/connection/${encodeURIComponent(connectId)}`,
|
||||
`/plugin-api/remote-desktop/connection/${encodeURIComponent(connectId)}`,
|
||||
{ signal },
|
||||
);
|
||||
return response.data.guacamoleConnectionId ?? null;
|
||||
|
||||
@@ -1,31 +1,39 @@
|
||||
/**
|
||||
* Where the Guacamole display socket lives.
|
||||
*
|
||||
* One path in every deployment: the plugin serves it at
|
||||
* /plugin-ws/remote-desktop/display on the main backend, so there is no
|
||||
* guacamole port and no separate nginx block any more. Electron with a remote
|
||||
* server is the only case that targets a different host.
|
||||
*/
|
||||
export function buildGuacamoleWebSocketBaseUrl({
|
||||
isDev,
|
||||
isElectronApp,
|
||||
isEmbeddedApp,
|
||||
configuredServerUrl,
|
||||
basePath,
|
||||
location,
|
||||
}: {
|
||||
isDev: boolean;
|
||||
isDev?: boolean;
|
||||
isElectronApp: boolean;
|
||||
isEmbeddedApp: boolean;
|
||||
configuredServerUrl?: string;
|
||||
basePath: string;
|
||||
location: Pick<Location, "protocol" | "host">;
|
||||
}) {
|
||||
if (isDev) return "ws://localhost:30008";
|
||||
if (isElectronApp) {
|
||||
if (isEmbeddedApp || !configuredServerUrl) return "ws://127.0.0.1:30008";
|
||||
const route = "/plugin-ws/remote-desktop/display";
|
||||
|
||||
if (isElectronApp && !isEmbeddedApp && configuredServerUrl) {
|
||||
const wsProtocol = configuredServerUrl.startsWith("https://")
|
||||
? "wss://"
|
||||
: "ws://";
|
||||
const wsHost = configuredServerUrl
|
||||
.replace(/^https?:\/\//, "")
|
||||
.replace(/\/$/, "");
|
||||
return `${wsProtocol}${wsHost}/guacamole/websocket/`;
|
||||
return `${wsProtocol}${wsHost}${route}`;
|
||||
}
|
||||
|
||||
if (isElectronApp) return `ws://127.0.0.1:30001${route}`;
|
||||
|
||||
const wsProtocol = location.protocol === "https:" ? "wss" : "ws";
|
||||
return `${wsProtocol}://${location.host}${basePath}/guacamole/websocket/`;
|
||||
return `${wsProtocol}://${location.host}${basePath}${route}`;
|
||||
}
|
||||
Reference in new issue
Block a user