mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 18:31:33 +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 {
|
export interface GuacamoleTokenRequest {
|
||||||
protocol: "rdp" | "vnc" | "telnet";
|
protocol: "rdp" | "vnc" | "telnet";
|
||||||
@@ -189,7 +205,7 @@ export async function getGuacamoleToken(
|
|||||||
try {
|
try {
|
||||||
const guacParams = toGuacamoleParams(request.guacamoleConfig);
|
const guacParams = toGuacamoleParams(request.guacamoleConfig);
|
||||||
|
|
||||||
const response = await authApi.post("/guacamole/token", {
|
const response = await guacamoleApi().post("/guacamole/token", {
|
||||||
type: request.protocol,
|
type: request.protocol,
|
||||||
hostname: request.hostname,
|
hostname: request.hostname,
|
||||||
port: request.port,
|
port: request.port,
|
||||||
@@ -212,15 +228,18 @@ export async function getGuacamoleTokenFromHost(
|
|||||||
promptedCredentials?: { username?: string; password?: string },
|
promptedCredentials?: { username?: string; password?: string },
|
||||||
): Promise<GuacamoleTokenResponse> {
|
): Promise<GuacamoleTokenResponse> {
|
||||||
try {
|
try {
|
||||||
const response = await authApi.post(`/guacamole/connect-host/${hostId}`, {
|
const response = await guacamoleApi().post(
|
||||||
...(protocol ? { protocol } : {}),
|
`/guacamole/connect-host/${hostId}`,
|
||||||
...(promptedCredentials?.username
|
{
|
||||||
? { promptedUsername: promptedCredentials.username }
|
...(protocol ? { protocol } : {}),
|
||||||
: {}),
|
...(promptedCredentials?.username
|
||||||
...(promptedCredentials?.password
|
? { promptedUsername: promptedCredentials.username }
|
||||||
? { promptedPassword: promptedCredentials.password }
|
: {}),
|
||||||
: {}),
|
...(promptedCredentials?.password
|
||||||
});
|
? { promptedPassword: promptedCredentials.password }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw handleApiError(error, "get guacamole token from host");
|
throw handleApiError(error, "get guacamole token from host");
|
||||||
@@ -230,6 +249,6 @@ export async function getGuacamoleTokenFromHost(
|
|||||||
export async function getGuacdStatus(): Promise<{
|
export async function getGuacdStatus(): Promise<{
|
||||||
guacd: { status: string };
|
guacd: { status: string };
|
||||||
}> {
|
}> {
|
||||||
const response = await authApi.get("/guacamole/status");
|
const response = await guacamoleApi().get("/guacamole/status");
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -763,6 +763,7 @@ function createRemoteOriginApiInstance(path: string): AxiosInstance {
|
|||||||
let remoteFileManagerApi: AxiosInstance | null = null;
|
let remoteFileManagerApi: AxiosInstance | null = null;
|
||||||
let remoteTunnelApi: AxiosInstance | null = null;
|
let remoteTunnelApi: AxiosInstance | null = null;
|
||||||
let remoteStatsApi: AxiosInstance | null = null;
|
let remoteStatsApi: AxiosInstance | null = null;
|
||||||
|
let remoteGuacamoleApi: AxiosInstance | null = null;
|
||||||
|
|
||||||
export function getRemoteFileManagerApi(): AxiosInstance {
|
export function getRemoteFileManagerApi(): AxiosInstance {
|
||||||
if (!remoteFileManagerApi) {
|
if (!remoteFileManagerApi) {
|
||||||
@@ -785,6 +786,13 @@ export function getRemoteStatsApi(): AxiosInstance {
|
|||||||
return remoteStatsApi;
|
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
|
// Maps a live SSH session (keyed by sessionId, which today is the host's
|
||||||
// numeric id as a string -- see ensureSSHSessionForHost) to the resolved
|
// numeric id as a string -- see ensureSSHSessionForHost) to the resolved
|
||||||
// origin it was connected through, so every subsequent file-manager call
|
// 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