mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 18:31:33 +00:00
Fix synced client tunnel endpoints (#1302)
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
|||||||
} from "./ssh-primitives.js";
|
} from "./ssh-primitives.js";
|
||||||
import { sendC2SMessage, writeC2SRemoteChunk } from "./c2s-relay-utils.js";
|
import { sendC2SMessage, writeC2SRemoteChunk } from "./c2s-relay-utils.js";
|
||||||
import { getTunnelMode } from "./utils.js";
|
import { getTunnelMode } from "./utils.js";
|
||||||
|
import { createCurrentHostResolutionRepository } from "../../database/repositories/factory.js";
|
||||||
|
|
||||||
export type C2SOpenMessage = {
|
export type C2SOpenMessage = {
|
||||||
type: "open" | "test";
|
type: "open" | "test";
|
||||||
@@ -25,17 +26,36 @@ export type C2SOpenMessage = {
|
|||||||
const permissionManager = PermissionManager.getInstance();
|
const permissionManager = PermissionManager.getInstance();
|
||||||
let c2sRemoteStreamCounter = 0;
|
let c2sRemoteStreamCounter = 0;
|
||||||
|
|
||||||
|
export async function resolveC2SSourceHostId(
|
||||||
|
tunnelConfig: Partial<TunnelConfig>,
|
||||||
|
findHostIdBySyncId: (syncId: string) => Promise<number | null>,
|
||||||
|
): Promise<number> {
|
||||||
|
const sourceHostSyncId = tunnelConfig.sourceHostSyncId?.trim();
|
||||||
|
if (sourceHostSyncId) {
|
||||||
|
const remoteHostId = await findHostIdBySyncId(sourceHostSyncId);
|
||||||
|
if (!remoteHostId) {
|
||||||
|
throw new Error("Endpoint SSH host was not found on the remote server");
|
||||||
|
}
|
||||||
|
return remoteHostId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tunnelConfig.sourceHostId) {
|
||||||
|
throw new Error("Endpoint SSH host is required");
|
||||||
|
}
|
||||||
|
return tunnelConfig.sourceHostId;
|
||||||
|
}
|
||||||
|
|
||||||
async function resolveC2STunnelSource(
|
async function resolveC2STunnelSource(
|
||||||
tunnelConfig: Partial<TunnelConfig>,
|
tunnelConfig: Partial<TunnelConfig>,
|
||||||
userId: string,
|
userId: string,
|
||||||
): Promise<TunnelConfig> {
|
): Promise<TunnelConfig> {
|
||||||
if (!tunnelConfig.sourceHostId) {
|
const sourceHostId = await resolveC2SSourceHostId(tunnelConfig, (syncId) =>
|
||||||
throw new Error("Endpoint SSH host is required");
|
createCurrentHostResolutionRepository().findHostIdBySyncId(syncId),
|
||||||
}
|
);
|
||||||
|
|
||||||
const accessInfo = await permissionManager.canAccessHost(
|
const accessInfo = await permissionManager.canAccessHost(
|
||||||
userId,
|
userId,
|
||||||
tunnelConfig.sourceHostId,
|
sourceHostId,
|
||||||
"connect",
|
"connect",
|
||||||
);
|
);
|
||||||
if (!accessInfo.hasAccess) {
|
if (!accessInfo.hasAccess) {
|
||||||
@@ -43,13 +63,13 @@ async function resolveC2STunnelSource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { resolveHostById } = await import("../host-resolver.js");
|
const { resolveHostById } = await import("../host-resolver.js");
|
||||||
const resolvedHost = await resolveHostById(tunnelConfig.sourceHostId, userId);
|
const resolvedHost = await resolveHostById(sourceHostId, userId);
|
||||||
if (!resolvedHost) {
|
if (!resolvedHost) {
|
||||||
throw new Error("Endpoint SSH host not found");
|
throw new Error("Endpoint SSH host not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: tunnelConfig.name || `c2s:${tunnelConfig.sourceHostId}`,
|
name: tunnelConfig.name || `c2s:${sourceHostId}`,
|
||||||
scope: "c2s",
|
scope: "c2s",
|
||||||
mode: tunnelConfig.mode || "local",
|
mode: tunnelConfig.mode || "local",
|
||||||
tunnelType:
|
tunnelType:
|
||||||
@@ -57,7 +77,7 @@ async function resolveC2STunnelSource(
|
|||||||
(tunnelConfig.mode === "remote" ? "remote" : "local"),
|
(tunnelConfig.mode === "remote" ? "remote" : "local"),
|
||||||
bindHost: tunnelConfig.bindHost,
|
bindHost: tunnelConfig.bindHost,
|
||||||
targetHost: tunnelConfig.targetHost || "127.0.0.1",
|
targetHost: tunnelConfig.targetHost || "127.0.0.1",
|
||||||
sourceHostId: resolvedHost.id || tunnelConfig.sourceHostId,
|
sourceHostId: resolvedHost.id || sourceHostId,
|
||||||
tunnelIndex: tunnelConfig.tunnelIndex || 0,
|
tunnelIndex: tunnelConfig.tunnelIndex || 0,
|
||||||
requestingUserId: userId,
|
requestingUserId: userId,
|
||||||
hostName:
|
hostName:
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { resolveC2SSourceHostId } from "../../../hosts/tunnel/c2s-relay.js";
|
||||||
|
|
||||||
|
describe("resolveC2SSourceHostId", () => {
|
||||||
|
it("uses the remote row matching the stable sync id", async () => {
|
||||||
|
const findBySyncId = vi.fn().mockResolvedValue(42);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
resolveC2SSourceHostId(
|
||||||
|
{ sourceHostId: 7, sourceHostSyncId: "host-sync-id" },
|
||||||
|
findBySyncId,
|
||||||
|
),
|
||||||
|
).resolves.toBe(42);
|
||||||
|
expect(findBySyncId).toHaveBeenCalledWith("host-sync-id");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps legacy local ids when no sync id is available", async () => {
|
||||||
|
await expect(
|
||||||
|
resolveC2SSourceHostId({ sourceHostId: 7 }, vi.fn()),
|
||||||
|
).resolves.toBe(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not silently fall back to a mismatched id", async () => {
|
||||||
|
await expect(
|
||||||
|
resolveC2SSourceHostId(
|
||||||
|
{ sourceHostId: 7, sourceHostSyncId: "missing" },
|
||||||
|
vi.fn().mockResolvedValue(null),
|
||||||
|
),
|
||||||
|
).rejects.toThrow("not found on the remote server");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -482,6 +482,7 @@ export interface TunnelConnection {
|
|||||||
tunnelType?: "local" | "remote";
|
tunnelType?: "local" | "remote";
|
||||||
bindHost?: string;
|
bindHost?: string;
|
||||||
sourceHostId?: number;
|
sourceHostId?: number;
|
||||||
|
sourceHostSyncId?: string;
|
||||||
sourceHostName?: string;
|
sourceHostName?: string;
|
||||||
sourcePort: number;
|
sourcePort: number;
|
||||||
endpointPort: number;
|
endpointPort: number;
|
||||||
@@ -508,6 +509,7 @@ export interface TunnelConfig {
|
|||||||
targetHost?: string;
|
targetHost?: string;
|
||||||
|
|
||||||
sourceHostId: number;
|
sourceHostId: number;
|
||||||
|
sourceHostSyncId?: string;
|
||||||
tunnelIndex: number;
|
tunnelIndex: number;
|
||||||
|
|
||||||
requestingUserId?: string;
|
requestingUserId?: string;
|
||||||
|
|||||||
@@ -347,10 +347,20 @@ export function C2STunnelPresetManager(): React.ReactElement {
|
|||||||
getSSHHosts(),
|
getSSHHosts(),
|
||||||
]);
|
]);
|
||||||
setHosts(nextHosts);
|
setHosts(nextHosts);
|
||||||
|
const hostsById = new Map(nextHosts.map((host) => [host.id, host]));
|
||||||
const normalizedConfig = Array.isArray(config)
|
const normalizedConfig = Array.isArray(config)
|
||||||
? (config as TunnelConnection[])
|
? (config as TunnelConnection[])
|
||||||
.filter((tunnel) => tunnel.scope === "c2s")
|
.filter((tunnel) => tunnel.scope === "c2s")
|
||||||
.map(normalizeClientTunnel)
|
.map((tunnel) => {
|
||||||
|
const sourceHost = tunnel.sourceHostId
|
||||||
|
? hostsById.get(tunnel.sourceHostId)
|
||||||
|
: undefined;
|
||||||
|
return normalizeClientTunnel({
|
||||||
|
...tunnel,
|
||||||
|
sourceHostSyncId:
|
||||||
|
tunnel.sourceHostSyncId || sourceHost?.syncId || undefined,
|
||||||
|
});
|
||||||
|
})
|
||||||
: [];
|
: [];
|
||||||
setLocalConfig(normalizedConfig);
|
setLocalConfig(normalizedConfig);
|
||||||
setSavedLocalConfig(normalizedConfig);
|
setSavedLocalConfig(normalizedConfig);
|
||||||
@@ -473,6 +483,7 @@ export function C2STunnelPresetManager(): React.ReactElement {
|
|||||||
if (!host) return;
|
if (!host) return;
|
||||||
updateTunnel(index, {
|
updateTunnel(index, {
|
||||||
sourceHostId: host.id,
|
sourceHostId: host.id,
|
||||||
|
sourceHostSyncId: host.syncId || undefined,
|
||||||
sourceHostName: host.name,
|
sourceHostName: host.name,
|
||||||
endpointHost: host.name,
|
endpointHost: host.name,
|
||||||
endpointPort: 22,
|
endpointPort: 22,
|
||||||
|
|||||||
Reference in New Issue
Block a user