mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 10:21:34 +00:00
route desktop guacd calls to the connected remote server (#1122)
resolveConnectionOrigin() pins RDP/VNC/Telnet to "remote" because the embedded desktop backend does not bundle guacd, and the Guacamole websocket already follows that. The status check and both token calls did not: they use the shared authApi, which in Electron is hard-coded to the embedded backend. So the desktop app asked the backend without guacd whether guacd was available, got "disconnected", and refused to connect — while the connected server it would actually have used reports it as connected and serves the same host fine from the web client. Send those three calls through a remote-origin instance in Electron, alongside the existing file-manager, tunnel and stats ones. Closes Termix-SSH/Support#1043
This commit is contained in:
+31
-12
@@ -1,4 +1,20 @@
|
||||
import { authApi, handleApiError } from "@/main-axios";
|
||||
import {
|
||||
authApi,
|
||||
getRemoteGuacamoleApi,
|
||||
handleApiError,
|
||||
isElectron,
|
||||
} from "@/main-axios";
|
||||
import type { AxiosInstance } from "axios";
|
||||
|
||||
/**
|
||||
* The embedded desktop backend does not bundle guacd, which is why
|
||||
* resolveConnectionOrigin() pins RDP/VNC/Telnet to "remote". These calls have to
|
||||
* follow: asking the embedded backend reports the guacd *it* cannot reach,
|
||||
* rather than the one on the connected server that serves the session.
|
||||
*/
|
||||
function guacamoleApi(): AxiosInstance {
|
||||
return isElectron() ? getRemoteGuacamoleApi() : authApi;
|
||||
}
|
||||
|
||||
export interface GuacamoleTokenRequest {
|
||||
protocol: "rdp" | "vnc" | "telnet";
|
||||
@@ -189,7 +205,7 @@ export async function getGuacamoleToken(
|
||||
try {
|
||||
const guacParams = toGuacamoleParams(request.guacamoleConfig);
|
||||
|
||||
const response = await authApi.post("/guacamole/token", {
|
||||
const response = await guacamoleApi().post("/guacamole/token", {
|
||||
type: request.protocol,
|
||||
hostname: request.hostname,
|
||||
port: request.port,
|
||||
@@ -212,15 +228,18 @@ export async function getGuacamoleTokenFromHost(
|
||||
promptedCredentials?: { username?: string; password?: string },
|
||||
): Promise<GuacamoleTokenResponse> {
|
||||
try {
|
||||
const response = await authApi.post(`/guacamole/connect-host/${hostId}`, {
|
||||
...(protocol ? { protocol } : {}),
|
||||
...(promptedCredentials?.username
|
||||
? { promptedUsername: promptedCredentials.username }
|
||||
: {}),
|
||||
...(promptedCredentials?.password
|
||||
? { promptedPassword: promptedCredentials.password }
|
||||
: {}),
|
||||
});
|
||||
const response = await guacamoleApi().post(
|
||||
`/guacamole/connect-host/${hostId}`,
|
||||
{
|
||||
...(protocol ? { protocol } : {}),
|
||||
...(promptedCredentials?.username
|
||||
? { promptedUsername: promptedCredentials.username }
|
||||
: {}),
|
||||
...(promptedCredentials?.password
|
||||
? { promptedPassword: promptedCredentials.password }
|
||||
: {}),
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "get guacamole token from host");
|
||||
@@ -230,6 +249,6 @@ export async function getGuacamoleTokenFromHost(
|
||||
export async function getGuacdStatus(): Promise<{
|
||||
guacd: { status: string };
|
||||
}> {
|
||||
const response = await authApi.get("/guacamole/status");
|
||||
const response = await guacamoleApi().get("/guacamole/status");
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -763,6 +763,7 @@ function createRemoteOriginApiInstance(path: string): AxiosInstance {
|
||||
let remoteFileManagerApi: AxiosInstance | null = null;
|
||||
let remoteTunnelApi: AxiosInstance | null = null;
|
||||
let remoteStatsApi: AxiosInstance | null = null;
|
||||
let remoteGuacamoleApi: AxiosInstance | null = null;
|
||||
|
||||
export function getRemoteFileManagerApi(): AxiosInstance {
|
||||
if (!remoteFileManagerApi) {
|
||||
@@ -785,6 +786,13 @@ export function getRemoteStatsApi(): AxiosInstance {
|
||||
return remoteStatsApi;
|
||||
}
|
||||
|
||||
export function getRemoteGuacamoleApi(): AxiosInstance {
|
||||
if (!remoteGuacamoleApi) {
|
||||
remoteGuacamoleApi = createRemoteOriginApiInstance("");
|
||||
}
|
||||
return remoteGuacamoleApi;
|
||||
}
|
||||
|
||||
// Maps a live SSH session (keyed by sessionId, which today is the host's
|
||||
// numeric id as a string -- see ensureSSHSessionForHost) to the resolved
|
||||
// origin it was connected through, so every subsequent file-manager call
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
|
||||
const authApiMock = vi.hoisted(() => ({
|
||||
get: vi.fn(async () => ({ data: { guacd: { status: "disconnected" } } })),
|
||||
post: vi.fn(async () => ({ data: { token: "local-token" } })),
|
||||
}));
|
||||
const remoteApiMock = vi.hoisted(() => ({
|
||||
get: vi.fn(async () => ({ data: { guacd: { status: "connected" } } })),
|
||||
post: vi.fn(async () => ({ data: { token: "remote-token" } })),
|
||||
}));
|
||||
const isElectronMock = vi.hoisted(() => vi.fn(() => false));
|
||||
|
||||
vi.mock("@/main-axios", () => ({
|
||||
authApi: authApiMock,
|
||||
getRemoteGuacamoleApi: () => remoteApiMock,
|
||||
isElectron: isElectronMock,
|
||||
handleApiError: (error: unknown) => error,
|
||||
}));
|
||||
|
||||
import {
|
||||
getGuacdStatus,
|
||||
getGuacamoleTokenFromHost,
|
||||
} from "../../api/guacamole-api";
|
||||
|
||||
beforeEach(() => {
|
||||
authApiMock.get.mockClear();
|
||||
authApiMock.post.mockClear();
|
||||
remoteApiMock.get.mockClear();
|
||||
remoteApiMock.post.mockClear();
|
||||
});
|
||||
|
||||
describe("guacamole API origin", () => {
|
||||
it("uses the shared instance in the browser", async () => {
|
||||
isElectronMock.mockReturnValue(false);
|
||||
|
||||
await getGuacdStatus();
|
||||
await getGuacamoleTokenFromHost(9, "vnc");
|
||||
|
||||
expect(authApiMock.get).toHaveBeenCalledWith("/guacamole/status");
|
||||
expect(authApiMock.post).toHaveBeenCalledOnce();
|
||||
expect(remoteApiMock.get).not.toHaveBeenCalled();
|
||||
expect(remoteApiMock.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses the connected remote server in the desktop app", async () => {
|
||||
isElectronMock.mockReturnValue(true);
|
||||
|
||||
// The embedded backend has no guacd, so asking it reports "disconnected"
|
||||
// even when the connected server can serve the session.
|
||||
const status = await getGuacdStatus();
|
||||
const token = await getGuacamoleTokenFromHost(9, "vnc");
|
||||
|
||||
expect(status.guacd.status).toBe("connected");
|
||||
expect(token.token).toBe("remote-token");
|
||||
expect(remoteApiMock.get).toHaveBeenCalledWith("/guacamole/status");
|
||||
expect(remoteApiMock.post).toHaveBeenCalledOnce();
|
||||
expect(authApiMock.get).not.toHaveBeenCalled();
|
||||
expect(authApiMock.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sends the connect-host payload unchanged to the remote server", async () => {
|
||||
isElectronMock.mockReturnValue(true);
|
||||
|
||||
await getGuacamoleTokenFromHost(9, "rdp", {
|
||||
username: "admin",
|
||||
password: "secret",
|
||||
});
|
||||
|
||||
expect(remoteApiMock.post).toHaveBeenCalledWith(
|
||||
"/guacamole/connect-host/9",
|
||||
{
|
||||
protocol: "rdp",
|
||||
promptedUsername: "admin",
|
||||
promptedPassword: "secret",
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user