mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-15 04:43:36 +00:00
Fix status checks through jump hosts (#1045)
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import type { Client } from "ssh2";
|
||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
import {
|
import {
|
||||||
supportsMetrics,
|
supportsMetrics,
|
||||||
isTcpPingEnabled,
|
isTcpPingEnabled,
|
||||||
createConnectionLog,
|
createConnectionLog,
|
||||||
|
tcpPingThroughJumpHost,
|
||||||
} from "./host-metrics-helpers.js";
|
} from "./host-metrics-helpers.js";
|
||||||
|
|
||||||
describe("supportsMetrics", () => {
|
describe("supportsMetrics", () => {
|
||||||
@@ -66,3 +68,57 @@ describe("createConnectionLog", () => {
|
|||||||
expect("timestamp" in entry).toBe(false);
|
expect("timestamp" in entry).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("tcpPingThroughJumpHost", () => {
|
||||||
|
it("reports the final destination online when forwarding succeeds", async () => {
|
||||||
|
const stream = { destroy: vi.fn() };
|
||||||
|
const jumpClient = {
|
||||||
|
end: vi.fn(),
|
||||||
|
forwardOut: vi.fn((_src, _srcPort, host, port, callback) => {
|
||||||
|
expect(host).toBe("private.example");
|
||||||
|
expect(port).toBe(22);
|
||||||
|
callback(undefined, stream);
|
||||||
|
}),
|
||||||
|
} as unknown as Pick<Client, "forwardOut" | "end">;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
tcpPingThroughJumpHost(jumpClient, "private.example", 22),
|
||||||
|
).resolves.toBe(true);
|
||||||
|
expect(stream.destroy).toHaveBeenCalledOnce();
|
||||||
|
expect(jumpClient.end).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports the final destination offline when forwarding fails", async () => {
|
||||||
|
const jumpClient = {
|
||||||
|
end: vi.fn(),
|
||||||
|
forwardOut: vi.fn((_src, _srcPort, _host, _port, callback) => {
|
||||||
|
callback(new Error("Connection refused"));
|
||||||
|
}),
|
||||||
|
} as unknown as Pick<Client, "forwardOut" | "end">;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
tcpPingThroughJumpHost(jumpClient, "private.example", 22),
|
||||||
|
).resolves.toBe(false);
|
||||||
|
expect(jumpClient.end).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports the final destination offline when forwarding times out", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const jumpClient = {
|
||||||
|
end: vi.fn(),
|
||||||
|
forwardOut: vi.fn(),
|
||||||
|
} as unknown as Pick<Client, "forwardOut" | "end">;
|
||||||
|
|
||||||
|
const result = tcpPingThroughJumpHost(
|
||||||
|
jumpClient,
|
||||||
|
"private.example",
|
||||||
|
22,
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
await vi.advanceTimersByTimeAsync(5000);
|
||||||
|
|
||||||
|
await expect(result).resolves.toBe(false);
|
||||||
|
expect(jumpClient.end).toHaveBeenCalledOnce();
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { LogEntry, ConnectionStage } from "../../types/connection-log.js";
|
import type { LogEntry, ConnectionStage } from "../../types/connection-log.js";
|
||||||
|
import type { Client } from "ssh2";
|
||||||
|
|
||||||
export type StatsCapableHost = {
|
export type StatsCapableHost = {
|
||||||
connectionType?: string;
|
connectionType?: string;
|
||||||
@@ -21,6 +22,32 @@ export function isTcpPingEnabled(statsConfig: TcpPingStatsConfig): boolean {
|
|||||||
return statsConfig.statusCheckEnabled && !statsConfig.disableTcpPing;
|
return statsConfig.statusCheckEnabled && !statsConfig.disableTcpPing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function tcpPingThroughJumpHost(
|
||||||
|
jumpClient: Pick<Client, "forwardOut" | "end">,
|
||||||
|
host: string,
|
||||||
|
port: number,
|
||||||
|
timeoutMs = 5000,
|
||||||
|
): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let settled = false;
|
||||||
|
|
||||||
|
const finish = (result: boolean) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
jumpClient.end();
|
||||||
|
resolve(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => finish(false), timeoutMs);
|
||||||
|
|
||||||
|
jumpClient.forwardOut("127.0.0.1", 0, host, port, (error, stream) => {
|
||||||
|
stream?.destroy();
|
||||||
|
finish(!error && !!stream);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function createConnectionLog(
|
export function createConnectionLog(
|
||||||
type: "info" | "success" | "warning" | "error",
|
type: "info" | "success" | "warning" | "error",
|
||||||
stage: ConnectionStage,
|
stage: ConnectionStage,
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import {
|
|||||||
createConnectionLog,
|
createConnectionLog,
|
||||||
isTcpPingEnabled,
|
isTcpPingEnabled,
|
||||||
supportsMetrics,
|
supportsMetrics,
|
||||||
|
tcpPingThroughJumpHost,
|
||||||
} from "./host-metrics-helpers.js";
|
} from "./host-metrics-helpers.js";
|
||||||
import { applyAgentAuth } from "./terminal-auth-helpers.js";
|
import { applyAgentAuth } from "./terminal-auth-helpers.js";
|
||||||
import {
|
import {
|
||||||
@@ -361,24 +362,45 @@ class PollingManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let pingHost = refreshedHost.ip;
|
|
||||||
const ct = refreshedHost.connectionType || "ssh";
|
const ct = refreshedHost.connectionType || "ssh";
|
||||||
let pingPort: number;
|
let pingPort: number;
|
||||||
if (ct === "rdp") pingPort = refreshedHost.rdpPort ?? 3389;
|
if (ct === "rdp") pingPort = refreshedHost.rdpPort ?? 3389;
|
||||||
else if (ct === "vnc") pingPort = refreshedHost.vncPort ?? 5900;
|
else if (ct === "vnc") pingPort = refreshedHost.vncPort ?? 5900;
|
||||||
else if (ct === "telnet") pingPort = refreshedHost.telnetPort ?? 23;
|
else if (ct === "telnet") pingPort = refreshedHost.telnetPort ?? 23;
|
||||||
else pingPort = refreshedHost.port;
|
else pingPort = refreshedHost.port;
|
||||||
|
|
||||||
|
let isOnline: boolean;
|
||||||
if (refreshedHost.jumpHosts && refreshedHost.jumpHosts.length > 0) {
|
if (refreshedHost.jumpHosts && refreshedHost.jumpHosts.length > 0) {
|
||||||
const firstJump = await fetchHostById(
|
const proxyConfig: SOCKS5Config | null =
|
||||||
refreshedHost.jumpHosts[0].hostId,
|
refreshedHost.useSocks5 &&
|
||||||
|
(refreshedHost.socks5Host ||
|
||||||
|
(refreshedHost.socks5ProxyChain &&
|
||||||
|
refreshedHost.socks5ProxyChain.length > 0))
|
||||||
|
? {
|
||||||
|
useSocks5: true,
|
||||||
|
socks5Host: refreshedHost.socks5Host,
|
||||||
|
socks5Port: refreshedHost.socks5Port,
|
||||||
|
socks5Username: refreshedHost.socks5Username,
|
||||||
|
socks5Password: refreshedHost.socks5Password,
|
||||||
|
socks5ProxyChain: refreshedHost.socks5ProxyChain,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
const jumpClient = await createJumpHostChain(
|
||||||
|
refreshedHost.jumpHosts,
|
||||||
userId,
|
userId,
|
||||||
|
proxyConfig,
|
||||||
);
|
);
|
||||||
if (firstJump) {
|
isOnline = jumpClient
|
||||||
pingHost = firstJump.ip;
|
? await tcpPingThroughJumpHost(
|
||||||
pingPort = firstJump.port;
|
jumpClient,
|
||||||
|
refreshedHost.ip,
|
||||||
|
pingPort,
|
||||||
|
5000,
|
||||||
|
)
|
||||||
|
: false;
|
||||||
|
} else {
|
||||||
|
isOnline = await tcpPing(refreshedHost.ip, pingPort, 5000);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
const isOnline = await tcpPing(pingHost, pingPort, 5000);
|
|
||||||
const statusEntry: StatusEntry = {
|
const statusEntry: StatusEntry = {
|
||||||
status: isOnline ? "online" : "offline",
|
status: isOnline ? "online" : "offline",
|
||||||
lastChecked: new Date().toISOString(),
|
lastChecked: new Date().toISOString(),
|
||||||
|
|||||||
Reference in New Issue
Block a user