SOCKS5 support #452
4
package-lock.json
generated
4
package-lock.json
generated
@@ -85,6 +85,7 @@
|
|||||||
"react-xtermjs": "^1.0.10",
|
"react-xtermjs": "^1.0.10",
|
||||||
"recharts": "^3.2.1",
|
"recharts": "^3.2.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
|
"socks": "^2.8.7",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"speakeasy": "^2.0.0",
|
"speakeasy": "^2.0.0",
|
||||||
"ssh2": "^1.16.0",
|
"ssh2": "^1.16.0",
|
||||||
@@ -10624,7 +10625,6 @@
|
|||||||
"version": "10.0.1",
|
"version": "10.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
||||||
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
|
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
@@ -15614,7 +15614,6 @@
|
|||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||||
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6.0.0",
|
"node": ">= 6.0.0",
|
||||||
@@ -15625,7 +15624,6 @@
|
|||||||
"version": "2.8.7",
|
"version": "2.8.7",
|
||||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
|
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
|
||||||
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
|
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ip-address": "^10.0.1",
|
"ip-address": "^10.0.1",
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"react-xtermjs": "^1.0.10",
|
"react-xtermjs": "^1.0.10",
|
||||||
"recharts": "^3.2.1",
|
"recharts": "^3.2.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
|
"socks": "^2.8.7",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"speakeasy": "^2.0.0",
|
"speakeasy": "^2.0.0",
|
||||||
"ssh2": "^1.16.0",
|
"ssh2": "^1.16.0",
|
||||||
|
|||||||
@@ -210,6 +210,11 @@ async function initializeCompleteDatabase(): Promise<void> {
|
|||||||
stats_config TEXT,
|
stats_config TEXT,
|
||||||
docker_config TEXT,
|
docker_config TEXT,
|
||||||
terminal_config TEXT,
|
terminal_config TEXT,
|
||||||
|
use_socks5 INTEGER,
|
||||||
|
socks5_host TEXT,
|
||||||
|
socks5_port INTEGER,
|
||||||
|
socks5_username TEXT,
|
||||||
|
socks5_password TEXT,
|
||||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||||
@@ -570,6 +575,14 @@ const migrateSchema = () => {
|
|||||||
);
|
);
|
||||||
addColumnIfNotExists("ssh_data", "docker_config", "TEXT");
|
addColumnIfNotExists("ssh_data", "docker_config", "TEXT");
|
||||||
|
|
||||||
|
// SOCKS5 Proxy columns
|
||||||
|
addColumnIfNotExists("ssh_data", "use_socks5", "INTEGER");
|
||||||
|
addColumnIfNotExists("ssh_data", "socks5_host", "TEXT");
|
||||||
|
addColumnIfNotExists("ssh_data", "socks5_port", "INTEGER");
|
||||||
|
addColumnIfNotExists("ssh_data", "socks5_username", "TEXT");
|
||||||
|
addColumnIfNotExists("ssh_data", "socks5_password", "TEXT");
|
||||||
|
addColumnIfNotExists("ssh_data", "socks5_proxy_chain", "TEXT");
|
||||||
|
|
||||||
addColumnIfNotExists("ssh_credentials", "private_key", "TEXT");
|
addColumnIfNotExists("ssh_credentials", "private_key", "TEXT");
|
||||||
addColumnIfNotExists("ssh_credentials", "public_key", "TEXT");
|
addColumnIfNotExists("ssh_credentials", "public_key", "TEXT");
|
||||||
addColumnIfNotExists("ssh_credentials", "detected_key_type", "TEXT");
|
addColumnIfNotExists("ssh_credentials", "detected_key_type", "TEXT");
|
||||||
|
|||||||
@@ -93,6 +93,14 @@ export const sshData = sqliteTable("ssh_data", {
|
|||||||
statsConfig: text("stats_config"),
|
statsConfig: text("stats_config"),
|
||||||
terminalConfig: text("terminal_config"),
|
terminalConfig: text("terminal_config"),
|
||||||
quickActions: text("quick_actions"),
|
quickActions: text("quick_actions"),
|
||||||
|
|
||||||
|
useSocks5: integer("use_socks5", { mode: "boolean" }),
|
||||||
|
socks5Host: text("socks5_host"),
|
||||||
|
socks5Port: integer("socks5_port"),
|
||||||
|
socks5Username: text("socks5_username"),
|
||||||
|
socks5Password: text("socks5_password"),
|
||||||
|
socks5ProxyChain: text("socks5_proxy_chain"), // JSON array for proxy chains
|
||||||
|
|
||||||
createdAt: text("created_at")
|
createdAt: text("created_at")
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(sql`CURRENT_TIMESTAMP`),
|
.default(sql`CURRENT_TIMESTAMP`),
|
||||||
|
|||||||
@@ -245,7 +245,20 @@ router.post(
|
|||||||
statsConfig,
|
statsConfig,
|
||||||
terminalConfig,
|
terminalConfig,
|
||||||
forceKeyboardInteractive,
|
forceKeyboardInteractive,
|
||||||
|
useSocks5,
|
||||||
|
socks5Host,
|
||||||
|
socks5Port,
|
||||||
|
socks5Username,
|
||||||
|
socks5Password,
|
||||||
|
socks5ProxyChain,
|
||||||
} = hostData;
|
} = hostData;
|
||||||
|
|
||||||
|
console.log("POST /db/ssh - Received SOCKS5 data:", {
|
||||||
|
useSocks5,
|
||||||
|
socks5Host,
|
||||||
|
socks5ProxyChain,
|
||||||
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isNonEmptyString(userId) ||
|
!isNonEmptyString(userId) ||
|
||||||
!isNonEmptyString(ip) ||
|
!isNonEmptyString(ip) ||
|
||||||
@@ -288,6 +301,12 @@ router.post(
|
|||||||
statsConfig: statsConfig ? JSON.stringify(statsConfig) : null,
|
statsConfig: statsConfig ? JSON.stringify(statsConfig) : null,
|
||||||
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
|
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
|
||||||
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
|
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
|
||||||
|
useSocks5: useSocks5 ? 1 : 0,
|
||||||
|
socks5Host: socks5Host || null,
|
||||||
|
socks5Port: socks5Port || null,
|
||||||
|
socks5Username: socks5Username || null,
|
||||||
|
socks5Password: socks5Password || null,
|
||||||
|
socks5ProxyChain: socks5ProxyChain ? JSON.stringify(socks5ProxyChain) : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (effectiveAuthType === "password") {
|
if (effectiveAuthType === "password") {
|
||||||
@@ -470,6 +489,12 @@ router.put(
|
|||||||
statsConfig,
|
statsConfig,
|
||||||
terminalConfig,
|
terminalConfig,
|
||||||
forceKeyboardInteractive,
|
forceKeyboardInteractive,
|
||||||
|
useSocks5,
|
||||||
|
socks5Host,
|
||||||
|
socks5Port,
|
||||||
|
socks5Username,
|
||||||
|
socks5Password,
|
||||||
|
socks5ProxyChain,
|
||||||
} = hostData;
|
} = hostData;
|
||||||
if (
|
if (
|
||||||
!isNonEmptyString(userId) ||
|
!isNonEmptyString(userId) ||
|
||||||
@@ -514,6 +539,12 @@ router.put(
|
|||||||
statsConfig: statsConfig ? JSON.stringify(statsConfig) : null,
|
statsConfig: statsConfig ? JSON.stringify(statsConfig) : null,
|
||||||
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
|
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
|
||||||
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
|
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
|
||||||
|
useSocks5: useSocks5 ? 1 : 0,
|
||||||
|
socks5Host: socks5Host || null,
|
||||||
|
socks5Port: socks5Port || null,
|
||||||
|
socks5Username: socks5Username || null,
|
||||||
|
socks5Password: socks5Password || null,
|
||||||
|
socks5ProxyChain: socks5ProxyChain ? JSON.stringify(socks5ProxyChain) : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (effectiveAuthType === "password") {
|
if (effectiveAuthType === "password") {
|
||||||
@@ -792,6 +823,9 @@ router.get(
|
|||||||
? JSON.parse(row.terminalConfig as string)
|
? JSON.parse(row.terminalConfig as string)
|
||||||
: undefined,
|
: undefined,
|
||||||
forceKeyboardInteractive: row.forceKeyboardInteractive === "true",
|
forceKeyboardInteractive: row.forceKeyboardInteractive === "true",
|
||||||
|
socks5ProxyChain: row.socks5ProxyChain
|
||||||
|
? JSON.parse(row.socks5ProxyChain as string)
|
||||||
|
: [],
|
||||||
|
|
||||||
// Add shared access metadata
|
// Add shared access metadata
|
||||||
isShared: !!row.isShared,
|
isShared: !!row.isShared,
|
||||||
@@ -872,6 +906,9 @@ router.get(
|
|||||||
? JSON.parse(host.terminalConfig)
|
? JSON.parse(host.terminalConfig)
|
||||||
: undefined,
|
: undefined,
|
||||||
forceKeyboardInteractive: host.forceKeyboardInteractive === "true",
|
forceKeyboardInteractive: host.forceKeyboardInteractive === "true",
|
||||||
|
socks5ProxyChain: host.socks5ProxyChain
|
||||||
|
? JSON.parse(host.socks5ProxyChain)
|
||||||
|
: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
res.json((await resolveHostCredentials(result)) || result);
|
res.json((await resolveHostCredentials(result)) || result);
|
||||||
@@ -943,6 +980,9 @@ router.get(
|
|||||||
tunnelConnections: resolvedHost.tunnelConnections
|
tunnelConnections: resolvedHost.tunnelConnections
|
||||||
? JSON.parse(resolvedHost.tunnelConnections as string)
|
? JSON.parse(resolvedHost.tunnelConnections as string)
|
||||||
: [],
|
: [],
|
||||||
|
socks5ProxyChain: resolvedHost.socks5ProxyChain
|
||||||
|
? JSON.parse(resolvedHost.socks5ProxyChain as string)
|
||||||
|
: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
sshLogger.success("Host exported with decrypted credentials", {
|
sshLogger.success("Host exported with decrypted credentials", {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { fileLogger, sshLogger } from "../utils/logger.js";
|
|||||||
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
||||||
import { AuthManager } from "../utils/auth-manager.js";
|
import { AuthManager } from "../utils/auth-manager.js";
|
||||||
import type { AuthenticatedRequest } from "../../types/index.js";
|
import type { AuthenticatedRequest } from "../../types/index.js";
|
||||||
|
import { createSocks5Connection } from "../utils/socks5-helper.js";
|
||||||
|
|
||||||
function isExecutableFile(permissions: string, fileName: string): boolean {
|
function isExecutableFile(permissions: string, fileName: string): boolean {
|
||||||
const hasExecutePermission =
|
const hasExecutePermission =
|
||||||
@@ -356,6 +357,12 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|||||||
userProvidedPassword,
|
userProvidedPassword,
|
||||||
forceKeyboardInteractive,
|
forceKeyboardInteractive,
|
||||||
jumpHosts,
|
jumpHosts,
|
||||||
|
useSocks5,
|
||||||
|
socks5Host,
|
||||||
|
socks5Port,
|
||||||
|
socks5Username,
|
||||||
|
socks5Password,
|
||||||
|
socks5ProxyChain,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const userId = (req as AuthenticatedRequest).userId;
|
const userId = (req as AuthenticatedRequest).userId;
|
||||||
@@ -808,6 +815,83 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
fileLogger.info("SFTP connection request received", {
|
||||||
|
operation: "sftp_connect_request",
|
||||||
|
sessionId,
|
||||||
|
hostId,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
useSocks5,
|
||||||
|
socks5Host,
|
||||||
|
socks5Port,
|
||||||
|
hasSocks5ProxyChain: !!(socks5ProxyChain && (socks5ProxyChain as any).length > 0),
|
||||||
|
proxyChainLength: socks5ProxyChain ? (socks5ProxyChain as any).length : 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if SOCKS5 proxy is enabled (either single proxy or chain)
|
||||||
|
if (useSocks5 && (socks5Host || (socks5ProxyChain && (socks5ProxyChain as any).length > 0))) {
|
||||||
|
fileLogger.info("SOCKS5 enabled for SFTP, creating connection", {
|
||||||
|
operation: "sftp_socks5_enabled",
|
||||||
|
sessionId,
|
||||||
|
socks5Host,
|
||||||
|
socks5Port,
|
||||||
|
hasChain: !!(socks5ProxyChain && (socks5ProxyChain as any).length > 0),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const socks5Socket = await createSocks5Connection(
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
{
|
||||||
|
useSocks5,
|
||||||
|
socks5Host,
|
||||||
|
socks5Port,
|
||||||
|
socks5Username,
|
||||||
|
socks5Password,
|
||||||
|
socks5ProxyChain: socks5ProxyChain as any,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (socks5Socket) {
|
||||||
|
fileLogger.info("SOCKS5 socket created for SFTP", {
|
||||||
|
operation: "sftp_socks5_socket_ready",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
config.sock = socks5Socket;
|
||||||
|
client.connect(config);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
fileLogger.error("SOCKS5 socket is null for SFTP", undefined, {
|
||||||
|
operation: "sftp_socks5_socket_null",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (socks5Error) {
|
||||||
|
fileLogger.error("SOCKS5 connection failed", socks5Error, {
|
||||||
|
operation: "socks5_connect",
|
||||||
|
sessionId,
|
||||||
|
hostId,
|
||||||
|
proxyHost: socks5Host,
|
||||||
|
proxyPort: socks5Port || 1080,
|
||||||
|
});
|
||||||
|
return res.status(500).json({
|
||||||
|
error:
|
||||||
|
"SOCKS5 proxy connection failed: " +
|
||||||
|
(socks5Error instanceof Error
|
||||||
|
? socks5Error.message
|
||||||
|
: "Unknown error"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fileLogger.info("SOCKS5 NOT enabled for SFTP connection", {
|
||||||
|
operation: "sftp_no_socks5",
|
||||||
|
sessionId,
|
||||||
|
useSocks5,
|
||||||
|
socks5Host,
|
||||||
|
hasChain: !!(socks5ProxyChain && (socks5ProxyChain as any).length > 0),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (jumpHosts && jumpHosts.length > 0 && userId) {
|
if (jumpHosts && jumpHosts.length > 0 && userId) {
|
||||||
try {
|
try {
|
||||||
const jumpClient = await createJumpHostChain(jumpHosts, userId);
|
const jumpClient = await createJumpHostChain(jumpHosts, userId);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { eq, and } from "drizzle-orm";
|
|||||||
import { statsLogger, sshLogger } from "../utils/logger.js";
|
import { statsLogger, sshLogger } from "../utils/logger.js";
|
||||||
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
||||||
import { AuthManager } from "../utils/auth-manager.js";
|
import { AuthManager } from "../utils/auth-manager.js";
|
||||||
import type { AuthenticatedRequest } from "../../types/index.js";
|
import type { AuthenticatedRequest, ProxyNode } from "../../types/index.js";
|
||||||
import { collectCpuMetrics } from "./widgets/cpu-collector.js";
|
import { collectCpuMetrics } from "./widgets/cpu-collector.js";
|
||||||
import { collectMemoryMetrics } from "./widgets/memory-collector.js";
|
import { collectMemoryMetrics } from "./widgets/memory-collector.js";
|
||||||
import { collectDiskMetrics } from "./widgets/disk-collector.js";
|
import { collectDiskMetrics } from "./widgets/disk-collector.js";
|
||||||
@@ -18,6 +18,7 @@ import { collectUptimeMetrics } from "./widgets/uptime-collector.js";
|
|||||||
import { collectProcessesMetrics } from "./widgets/processes-collector.js";
|
import { collectProcessesMetrics } from "./widgets/processes-collector.js";
|
||||||
import { collectSystemMetrics } from "./widgets/system-collector.js";
|
import { collectSystemMetrics } from "./widgets/system-collector.js";
|
||||||
import { collectLoginStats } from "./widgets/login-stats-collector.js";
|
import { collectLoginStats } from "./widgets/login-stats-collector.js";
|
||||||
|
import { createSocks5Connection } from "../utils/socks5-helper.js";
|
||||||
|
|
||||||
async function resolveJumpHost(
|
async function resolveJumpHost(
|
||||||
hostId: number,
|
hostId: number,
|
||||||
@@ -209,21 +210,44 @@ class SSHConnectionPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getHostKey(host: SSHHostWithCredentials): string {
|
private getHostKey(host: SSHHostWithCredentials): string {
|
||||||
return `${host.ip}:${host.port}:${host.username}`;
|
// Include SOCKS5 settings in the key to ensure separate connection pools
|
||||||
|
// for direct connections vs SOCKS5 connections
|
||||||
|
const socks5Key = host.useSocks5
|
||||||
|
? `:socks5:${host.socks5Host}:${host.socks5Port}:${JSON.stringify(host.socks5ProxyChain || [])}`
|
||||||
|
: "";
|
||||||
|
return `${host.ip}:${host.port}:${host.username}${socks5Key}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getConnection(host: SSHHostWithCredentials): Promise<Client> {
|
async getConnection(host: SSHHostWithCredentials): Promise<Client> {
|
||||||
const hostKey = this.getHostKey(host);
|
const hostKey = this.getHostKey(host);
|
||||||
const connections = this.connections.get(hostKey) || [];
|
const connections = this.connections.get(hostKey) || [];
|
||||||
|
|
||||||
|
statsLogger.info("Getting connection from pool", {
|
||||||
|
operation: "get_connection_from_pool",
|
||||||
|
hostKey: hostKey,
|
||||||
|
availableConnections: connections.length,
|
||||||
|
useSocks5: host.useSocks5,
|
||||||
|
socks5Host: host.socks5Host,
|
||||||
|
hasSocks5ProxyChain: !!(host.socks5ProxyChain && host.socks5ProxyChain.length > 0),
|
||||||
|
hostId: host.id,
|
||||||
|
});
|
||||||
|
|
||||||
const available = connections.find((conn) => !conn.inUse);
|
const available = connections.find((conn) => !conn.inUse);
|
||||||
if (available) {
|
if (available) {
|
||||||
|
statsLogger.info("Reusing existing connection from pool", {
|
||||||
|
operation: "reuse_connection",
|
||||||
|
hostKey,
|
||||||
|
});
|
||||||
available.inUse = true;
|
available.inUse = true;
|
||||||
available.lastUsed = Date.now();
|
available.lastUsed = Date.now();
|
||||||
return available.client;
|
return available.client;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connections.length < this.maxConnectionsPerHost) {
|
if (connections.length < this.maxConnectionsPerHost) {
|
||||||
|
statsLogger.info("Creating new connection for pool", {
|
||||||
|
operation: "create_new_connection",
|
||||||
|
hostKey,
|
||||||
|
});
|
||||||
const client = await this.createConnection(host);
|
const client = await this.createConnection(host);
|
||||||
const pooled: PooledConnection = {
|
const pooled: PooledConnection = {
|
||||||
client,
|
client,
|
||||||
@@ -311,6 +335,68 @@ class SSHConnectionPool {
|
|||||||
try {
|
try {
|
||||||
const config = buildSshConfig(host);
|
const config = buildSshConfig(host);
|
||||||
|
|
||||||
|
// Check if SOCKS5 proxy is enabled (either single proxy or chain)
|
||||||
|
if (
|
||||||
|
host.useSocks5 &&
|
||||||
|
(host.socks5Host || (host.socks5ProxyChain && host.socks5ProxyChain.length > 0))
|
||||||
|
) {
|
||||||
|
statsLogger.info("Using SOCKS5 proxy for connection", {
|
||||||
|
operation: "socks5_enabled",
|
||||||
|
hostIp: host.ip,
|
||||||
|
hostPort: host.port,
|
||||||
|
socks5Host: host.socks5Host,
|
||||||
|
socks5Port: host.socks5Port,
|
||||||
|
hasChain: !!(host.socks5ProxyChain && host.socks5ProxyChain.length > 0),
|
||||||
|
chainLength: host.socks5ProxyChain?.length || 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const socks5Socket = await createSocks5Connection(
|
||||||
|
host.ip,
|
||||||
|
host.port,
|
||||||
|
{
|
||||||
|
useSocks5: host.useSocks5,
|
||||||
|
socks5Host: host.socks5Host,
|
||||||
|
socks5Port: host.socks5Port,
|
||||||
|
socks5Username: host.socks5Username,
|
||||||
|
socks5Password: host.socks5Password,
|
||||||
|
socks5ProxyChain: host.socks5ProxyChain,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (socks5Socket) {
|
||||||
|
statsLogger.info("SOCKS5 socket created successfully", {
|
||||||
|
operation: "socks5_socket_ready",
|
||||||
|
hostIp: host.ip,
|
||||||
|
});
|
||||||
|
config.sock = socks5Socket;
|
||||||
|
client.connect(config);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
statsLogger.error("SOCKS5 socket is null", undefined, {
|
||||||
|
operation: "socks5_socket_null",
|
||||||
|
hostIp: host.ip,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (socks5Error) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
statsLogger.error("SOCKS5 connection error", socks5Error, {
|
||||||
|
operation: "socks5_connection_error",
|
||||||
|
hostIp: host.ip,
|
||||||
|
errorMessage: socks5Error instanceof Error ? socks5Error.message : "Unknown",
|
||||||
|
});
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
"SOCKS5 proxy connection failed: " +
|
||||||
|
(socks5Error instanceof Error
|
||||||
|
? socks5Error.message
|
||||||
|
: "Unknown error"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (host.jumpHosts && host.jumpHosts.length > 0 && host.userId) {
|
if (host.jumpHosts && host.jumpHosts.length > 0 && host.userId) {
|
||||||
const jumpClient = await createJumpHostChain(
|
const jumpClient = await createJumpHostChain(
|
||||||
host.jumpHosts,
|
host.jumpHosts,
|
||||||
@@ -364,6 +450,29 @@ class SSHConnectionPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearHostConnections(host: SSHHostWithCredentials): void {
|
||||||
|
const hostKey = this.getHostKey(host);
|
||||||
|
const connections = this.connections.get(hostKey) || [];
|
||||||
|
|
||||||
|
statsLogger.info("Clearing all connections for host", {
|
||||||
|
operation: "clear_host_connections",
|
||||||
|
hostKey,
|
||||||
|
connectionCount: connections.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const conn of connections) {
|
||||||
|
try {
|
||||||
|
conn.client.end();
|
||||||
|
} catch (error) {
|
||||||
|
statsLogger.error("Error closing connection during cleanup", error, {
|
||||||
|
operation: "clear_connection_error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.connections.delete(hostKey);
|
||||||
|
}
|
||||||
|
|
||||||
private cleanup(): void {
|
private cleanup(): void {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const maxAge = 10 * 60 * 1000;
|
const maxAge = 10 * 60 * 1000;
|
||||||
@@ -387,6 +496,27 @@ class SSHConnectionPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearAllConnections(): void {
|
||||||
|
statsLogger.info("Clearing ALL connections from pool", {
|
||||||
|
operation: "clear_all_connections",
|
||||||
|
totalHosts: this.connections.size,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const [hostKey, connections] of this.connections.entries()) {
|
||||||
|
for (const conn of connections) {
|
||||||
|
try {
|
||||||
|
conn.client.end();
|
||||||
|
} catch (error) {
|
||||||
|
statsLogger.error("Error closing connection during full cleanup", error, {
|
||||||
|
operation: "clear_all_error",
|
||||||
|
hostKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.connections.clear();
|
||||||
|
}
|
||||||
|
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
clearInterval(this.cleanupInterval);
|
clearInterval(this.cleanupInterval);
|
||||||
for (const connections of this.connections.values()) {
|
for (const connections of this.connections.values()) {
|
||||||
@@ -604,6 +734,14 @@ interface SSHHostWithCredentials {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
|
||||||
|
// SOCKS5 Proxy configuration
|
||||||
|
useSocks5?: boolean;
|
||||||
|
socks5Host?: string;
|
||||||
|
socks5Port?: number;
|
||||||
|
socks5Username?: string;
|
||||||
|
socks5Password?: string;
|
||||||
|
socks5ProxyChain?: ProxyNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatusEntry = {
|
type StatusEntry = {
|
||||||
@@ -742,33 +880,51 @@ class PollingManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async pollHostStatus(host: SSHHostWithCredentials): Promise<void> {
|
private async pollHostStatus(host: SSHHostWithCredentials): Promise<void> {
|
||||||
|
// Refresh host data from database to get latest settings
|
||||||
|
const refreshedHost = await fetchHostById(host.id, host.userId);
|
||||||
|
if (!refreshedHost) {
|
||||||
|
statsLogger.warn("Host not found during status polling", {
|
||||||
|
operation: "poll_host_status",
|
||||||
|
hostId: host.id,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isOnline = await tcpPing(host.ip, host.port, 5000);
|
const isOnline = await tcpPing(refreshedHost.ip, refreshedHost.port, 5000);
|
||||||
const statusEntry: StatusEntry = {
|
const statusEntry: StatusEntry = {
|
||||||
status: isOnline ? "online" : "offline",
|
status: isOnline ? "online" : "offline",
|
||||||
lastChecked: new Date().toISOString(),
|
lastChecked: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
this.statusStore.set(host.id, statusEntry);
|
this.statusStore.set(refreshedHost.id, statusEntry);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const statusEntry: StatusEntry = {
|
const statusEntry: StatusEntry = {
|
||||||
status: "offline",
|
status: "offline",
|
||||||
lastChecked: new Date().toISOString(),
|
lastChecked: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
this.statusStore.set(host.id, statusEntry);
|
this.statusStore.set(refreshedHost.id, statusEntry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async pollHostMetrics(host: SSHHostWithCredentials): Promise<void> {
|
private async pollHostMetrics(host: SSHHostWithCredentials): Promise<void> {
|
||||||
const config = this.pollingConfigs.get(host.id);
|
// Refresh host data from database to get latest SOCKS5 and other settings
|
||||||
|
const refreshedHost = await fetchHostById(host.id, host.userId);
|
||||||
|
if (!refreshedHost) {
|
||||||
|
statsLogger.warn("Host not found during metrics polling", {
|
||||||
|
operation: "poll_host_metrics",
|
||||||
|
hostId: host.id,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = this.pollingConfigs.get(refreshedHost.id);
|
||||||
if (!config || !config.statsConfig.metricsEnabled) {
|
if (!config || !config.statsConfig.metricsEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentHost = config.host;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const metrics = await collectMetrics(currentHost);
|
const metrics = await collectMetrics(refreshedHost);
|
||||||
this.metricsStore.set(currentHost.id, {
|
this.metricsStore.set(refreshedHost.id, {
|
||||||
data: metrics,
|
data: metrics,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
@@ -776,12 +932,12 @@ class PollingManager {
|
|||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error ? error.message : String(error);
|
error instanceof Error ? error.message : String(error);
|
||||||
|
|
||||||
const latestConfig = this.pollingConfigs.get(currentHost.id);
|
const latestConfig = this.pollingConfigs.get(refreshedHost.id);
|
||||||
if (latestConfig && latestConfig.statsConfig.metricsEnabled) {
|
if (latestConfig && latestConfig.statsConfig.metricsEnabled) {
|
||||||
statsLogger.warn("Failed to collect metrics for host", {
|
statsLogger.warn("Failed to collect metrics for host", {
|
||||||
operation: "metrics_poll_failed",
|
operation: "metrics_poll_failed",
|
||||||
hostId: currentHost.id,
|
hostId: refreshedHost.id,
|
||||||
hostName: currentHost.name,
|
hostName: refreshedHost.name,
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1007,6 +1163,15 @@ async function resolveHostCredentials(
|
|||||||
createdAt: host.createdAt,
|
createdAt: host.createdAt,
|
||||||
updatedAt: host.updatedAt,
|
updatedAt: host.updatedAt,
|
||||||
userId: host.userId,
|
userId: host.userId,
|
||||||
|
// SOCKS5 proxy settings
|
||||||
|
useSocks5: !!host.useSocks5,
|
||||||
|
socks5Host: host.socks5Host || undefined,
|
||||||
|
socks5Port: host.socks5Port || undefined,
|
||||||
|
socks5Username: host.socks5Username || undefined,
|
||||||
|
socks5Password: host.socks5Password || undefined,
|
||||||
|
socks5ProxyChain: host.socks5ProxyChain
|
||||||
|
? JSON.parse(host.socks5ProxyChain as string)
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (host.credentialId) {
|
if (host.credentialId) {
|
||||||
@@ -1057,6 +1222,16 @@ async function resolveHostCredentials(
|
|||||||
addLegacyCredentials(baseHost, host);
|
addLegacyCredentials(baseHost, host);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
statsLogger.info("Resolved host credentials with SOCKS5 settings", {
|
||||||
|
operation: "resolve_host",
|
||||||
|
hostId: host.id as number,
|
||||||
|
useSocks5: baseHost.useSocks5,
|
||||||
|
socks5Host: baseHost.socks5Host,
|
||||||
|
socks5Port: baseHost.socks5Port,
|
||||||
|
hasSocks5ProxyChain: !!(baseHost.socks5ProxyChain && (baseHost.socks5ProxyChain as any[]).length > 0),
|
||||||
|
proxyChainLength: baseHost.socks5ProxyChain ? (baseHost.socks5ProxyChain as any[]).length : 0,
|
||||||
|
});
|
||||||
|
|
||||||
return baseHost as unknown as SSHHostWithCredentials;
|
return baseHost as unknown as SSHHostWithCredentials;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
statsLogger.error(
|
statsLogger.error(
|
||||||
@@ -1194,6 +1369,7 @@ async function withSshConnection<T>(
|
|||||||
fn: (client: Client) => Promise<T>,
|
fn: (client: Client) => Promise<T>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const client = await connectionPool.getConnection(host);
|
const client = await connectionPool.getConnection(host);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await fn(client);
|
const result = await fn(client);
|
||||||
return result;
|
return result;
|
||||||
@@ -1402,6 +1578,20 @@ app.get("/status/:id", validateHostId, async (req, res) => {
|
|||||||
res.json(statusEntry);
|
res.json(statusEntry);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post("/clear-connections", async (req, res) => {
|
||||||
|
const userId = (req as AuthenticatedRequest).userId;
|
||||||
|
|
||||||
|
if (!SimpleDBOps.isUserDataUnlocked(userId)) {
|
||||||
|
return res.status(401).json({
|
||||||
|
error: "Session expired - please log in again",
|
||||||
|
code: "SESSION_EXPIRED",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionPool.clearAllConnections();
|
||||||
|
res.json({ message: "All SSH connections cleared" });
|
||||||
|
});
|
||||||
|
|
||||||
app.post("/refresh", async (req, res) => {
|
app.post("/refresh", async (req, res) => {
|
||||||
const userId = (req as AuthenticatedRequest).userId;
|
const userId = (req as AuthenticatedRequest).userId;
|
||||||
|
|
||||||
@@ -1412,6 +1602,9 @@ app.post("/refresh", async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear all connections to ensure fresh connections with updated settings
|
||||||
|
connectionPool.clearAllConnections();
|
||||||
|
|
||||||
await pollingManager.refreshHostPolling(userId);
|
await pollingManager.refreshHostPolling(userId);
|
||||||
res.json({ message: "Polling refreshed" });
|
res.json({ message: "Polling refreshed" });
|
||||||
});
|
});
|
||||||
@@ -1434,6 +1627,9 @@ app.post("/host-updated", async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const host = await fetchHostById(hostId, userId);
|
const host = await fetchHostById(hostId, userId);
|
||||||
if (host) {
|
if (host) {
|
||||||
|
// Clear existing connections for this host to ensure new settings (like SOCKS5) are used
|
||||||
|
connectionPool.clearHostConnections(host);
|
||||||
|
|
||||||
await pollingManager.startPollingForHost(host);
|
await pollingManager.startPollingForHost(host);
|
||||||
res.json({ message: "Host polling started" });
|
res.json({ message: "Host polling started" });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { sshLogger } from "../utils/logger.js";
|
|||||||
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
||||||
import { AuthManager } from "../utils/auth-manager.js";
|
import { AuthManager } from "../utils/auth-manager.js";
|
||||||
import { UserCrypto } from "../utils/user-crypto.js";
|
import { UserCrypto } from "../utils/user-crypto.js";
|
||||||
|
import { createSocks5Connection } from "../utils/socks5-helper.js";
|
||||||
|
|
||||||
interface ConnectToHostData {
|
interface ConnectToHostData {
|
||||||
cols: number;
|
cols: number;
|
||||||
@@ -32,6 +33,12 @@ interface ConnectToHostData {
|
|||||||
userId?: string;
|
userId?: string;
|
||||||
forceKeyboardInteractive?: boolean;
|
forceKeyboardInteractive?: boolean;
|
||||||
jumpHosts?: Array<{ hostId: number }>;
|
jumpHosts?: Array<{ hostId: number }>;
|
||||||
|
useSocks5?: boolean;
|
||||||
|
socks5Host?: string;
|
||||||
|
socks5Port?: number;
|
||||||
|
socks5Username?: string;
|
||||||
|
socks5Password?: string;
|
||||||
|
socks5ProxyChain?: unknown;
|
||||||
};
|
};
|
||||||
initialPath?: string;
|
initialPath?: string;
|
||||||
executeCommand?: string;
|
executeCommand?: string;
|
||||||
@@ -1180,6 +1187,53 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if SOCKS5 proxy is enabled (either single proxy or chain)
|
||||||
|
if (
|
||||||
|
hostConfig.useSocks5 &&
|
||||||
|
(hostConfig.socks5Host ||
|
||||||
|
(hostConfig.socks5ProxyChain && (hostConfig.socks5ProxyChain as any).length > 0))
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const socks5Socket = await createSocks5Connection(
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
{
|
||||||
|
useSocks5: hostConfig.useSocks5,
|
||||||
|
socks5Host: hostConfig.socks5Host,
|
||||||
|
socks5Port: hostConfig.socks5Port,
|
||||||
|
socks5Username: hostConfig.socks5Username,
|
||||||
|
socks5Password: hostConfig.socks5Password,
|
||||||
|
socks5ProxyChain: hostConfig.socks5ProxyChain as any,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (socks5Socket) {
|
||||||
|
connectConfig.sock = socks5Socket;
|
||||||
|
sshConn.connect(connectConfig);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (socks5Error) {
|
||||||
|
sshLogger.error("SOCKS5 connection failed", socks5Error, {
|
||||||
|
operation: "socks5_connect",
|
||||||
|
hostId: id,
|
||||||
|
proxyHost: hostConfig.socks5Host,
|
||||||
|
proxyPort: hostConfig.socks5Port || 1080,
|
||||||
|
});
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "error",
|
||||||
|
message:
|
||||||
|
"SOCKS5 proxy connection failed: " +
|
||||||
|
(socks5Error instanceof Error
|
||||||
|
? socks5Error.message
|
||||||
|
: "Unknown error"),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
cleanupSSH(connectionTimeout);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hostConfig.jumpHosts &&
|
hostConfig.jumpHosts &&
|
||||||
hostConfig.jumpHosts.length > 0 &&
|
hostConfig.jumpHosts.length > 0 &&
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { tunnelLogger, sshLogger } from "../utils/logger.js";
|
|||||||
import { SystemCrypto } from "../utils/system-crypto.js";
|
import { SystemCrypto } from "../utils/system-crypto.js";
|
||||||
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
||||||
import { DataCrypto } from "../utils/data-crypto.js";
|
import { DataCrypto } from "../utils/data-crypto.js";
|
||||||
|
import { createSocks5Connection } from "../utils/socks5-helper.js";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(
|
app.use(
|
||||||
@@ -1016,6 +1017,51 @@ async function connectSSHTunnel(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if SOCKS5 proxy is enabled (either single proxy or chain)
|
||||||
|
if (
|
||||||
|
tunnelConfig.useSocks5 &&
|
||||||
|
(tunnelConfig.socks5Host ||
|
||||||
|
(tunnelConfig.socks5ProxyChain && tunnelConfig.socks5ProxyChain.length > 0))
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const socks5Socket = await createSocks5Connection(
|
||||||
|
tunnelConfig.sourceIP,
|
||||||
|
tunnelConfig.sourceSSHPort,
|
||||||
|
{
|
||||||
|
useSocks5: tunnelConfig.useSocks5,
|
||||||
|
socks5Host: tunnelConfig.socks5Host,
|
||||||
|
socks5Port: tunnelConfig.socks5Port,
|
||||||
|
socks5Username: tunnelConfig.socks5Username,
|
||||||
|
socks5Password: tunnelConfig.socks5Password,
|
||||||
|
socks5ProxyChain: tunnelConfig.socks5ProxyChain,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (socks5Socket) {
|
||||||
|
connOptions.sock = socks5Socket;
|
||||||
|
conn.connect(connOptions);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (socks5Error) {
|
||||||
|
tunnelLogger.error("SOCKS5 connection failed for tunnel", socks5Error, {
|
||||||
|
operation: "socks5_connect",
|
||||||
|
tunnelName,
|
||||||
|
proxyHost: tunnelConfig.socks5Host,
|
||||||
|
proxyPort: tunnelConfig.socks5Port || 1080,
|
||||||
|
});
|
||||||
|
broadcastTunnelStatus(tunnelName, {
|
||||||
|
connected: false,
|
||||||
|
status: CONNECTION_STATES.FAILED,
|
||||||
|
reason:
|
||||||
|
"SOCKS5 proxy connection failed: " +
|
||||||
|
(socks5Error instanceof Error
|
||||||
|
? socks5Error.message
|
||||||
|
: "Unknown error"),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
conn.connect(connOptions);
|
conn.connect(connOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1248,7 +1294,57 @@ async function killRemoteTunnelByMarker(
|
|||||||
callback(err);
|
callback(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if SOCKS5 proxy is enabled (either single proxy or chain)
|
||||||
|
if (
|
||||||
|
tunnelConfig.useSocks5 &&
|
||||||
|
(tunnelConfig.socks5Host ||
|
||||||
|
(tunnelConfig.socks5ProxyChain && tunnelConfig.socks5ProxyChain.length > 0))
|
||||||
|
) {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const socks5Socket = await createSocks5Connection(
|
||||||
|
tunnelConfig.sourceIP,
|
||||||
|
tunnelConfig.sourceSSHPort,
|
||||||
|
{
|
||||||
|
useSocks5: tunnelConfig.useSocks5,
|
||||||
|
socks5Host: tunnelConfig.socks5Host,
|
||||||
|
socks5Port: tunnelConfig.socks5Port,
|
||||||
|
socks5Username: tunnelConfig.socks5Username,
|
||||||
|
socks5Password: tunnelConfig.socks5Password,
|
||||||
|
socks5ProxyChain: tunnelConfig.socks5ProxyChain,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (socks5Socket) {
|
||||||
|
connOptions.sock = socks5Socket;
|
||||||
conn.connect(connOptions);
|
conn.connect(connOptions);
|
||||||
|
} else {
|
||||||
|
callback(new Error("Failed to create SOCKS5 connection"));
|
||||||
|
}
|
||||||
|
} catch (socks5Error) {
|
||||||
|
tunnelLogger.error(
|
||||||
|
"SOCKS5 connection failed for killing tunnel",
|
||||||
|
socks5Error,
|
||||||
|
{
|
||||||
|
operation: "socks5_connect_kill",
|
||||||
|
tunnelName,
|
||||||
|
proxyHost: tunnelConfig.socks5Host,
|
||||||
|
proxyPort: tunnelConfig.socks5Port || 1080,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
callback(
|
||||||
|
new Error(
|
||||||
|
"SOCKS5 proxy connection failed: " +
|
||||||
|
(socks5Error instanceof Error
|
||||||
|
? socks5Error.message
|
||||||
|
: "Unknown error"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
} else {
|
||||||
|
conn.connect(connOptions);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get("/ssh/tunnel/status", (req, res) => {
|
app.get("/ssh/tunnel/status", (req, res) => {
|
||||||
@@ -1453,6 +1549,11 @@ async function initializeAutoStartTunnels(): Promise<void> {
|
|||||||
retryInterval: tunnelConnection.retryInterval * 1000,
|
retryInterval: tunnelConnection.retryInterval * 1000,
|
||||||
autoStart: tunnelConnection.autoStart,
|
autoStart: tunnelConnection.autoStart,
|
||||||
isPinned: host.pin,
|
isPinned: host.pin,
|
||||||
|
useSocks5: host.useSocks5,
|
||||||
|
socks5Host: host.socks5Host,
|
||||||
|
socks5Port: host.socks5Port,
|
||||||
|
socks5Username: host.socks5Username,
|
||||||
|
socks5Password: host.socks5Password,
|
||||||
};
|
};
|
||||||
|
|
||||||
autoStartTunnels.push(tunnelConfig);
|
autoStartTunnels.push(tunnelConfig);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { getDb, DatabaseSaveTrigger } from "../database/db/index.js";
|
|||||||
import { DataCrypto } from "./data-crypto.js";
|
import { DataCrypto } from "./data-crypto.js";
|
||||||
import type { SQLiteTable } from "drizzle-orm/sqlite-core";
|
import type { SQLiteTable } from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
type TableName = "users" | "ssh_data" | "ssh_credentials" | "recent_activity";
|
type TableName = "users" | "ssh_data" | "ssh_credentials" | "recent_activity" | "socks5_proxy_presets";
|
||||||
|
|
||||||
class SimpleDBOps {
|
class SimpleDBOps {
|
||||||
static async insert<T extends Record<string, unknown>>(
|
static async insert<T extends Record<string, unknown>>(
|
||||||
|
|||||||
160
src/backend/utils/socks5-helper.ts
Normal file
160
src/backend/utils/socks5-helper.ts
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import { SocksClient } from "socks";
|
||||||
|
import type { SocksClientOptions } from "socks";
|
||||||
|
import net from "net";
|
||||||
|
import { sshLogger } from "./logger.js";
|
||||||
|
import type { ProxyNode } from "../../types/index.js";
|
||||||
|
|
||||||
|
export interface SOCKS5Config {
|
||||||
|
useSocks5?: boolean;
|
||||||
|
socks5Host?: string;
|
||||||
|
socks5Port?: number;
|
||||||
|
socks5Username?: string;
|
||||||
|
socks5Password?: string;
|
||||||
|
socks5ProxyChain?: ProxyNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a SOCKS5 connection through a single proxy or a chain of proxies
|
||||||
|
* @param targetHost - Target SSH server hostname/IP
|
||||||
|
* @param targetPort - Target SSH server port
|
||||||
|
* @param socks5Config - SOCKS5 proxy configuration
|
||||||
|
* @returns Promise with connected socket or null if SOCKS5 is not enabled
|
||||||
|
*/
|
||||||
|
export async function createSocks5Connection(
|
||||||
|
targetHost: string,
|
||||||
|
targetPort: number,
|
||||||
|
socks5Config: SOCKS5Config,
|
||||||
|
): Promise<net.Socket | null> {
|
||||||
|
// If SOCKS5 is not enabled, return null
|
||||||
|
if (!socks5Config.useSocks5) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If proxy chain is provided, use chain connection
|
||||||
|
if (socks5Config.socks5ProxyChain && socks5Config.socks5ProxyChain.length > 0) {
|
||||||
|
return createProxyChainConnection(targetHost, targetPort, socks5Config.socks5ProxyChain);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If single proxy is configured, use single proxy connection
|
||||||
|
if (socks5Config.socks5Host) {
|
||||||
|
return createSingleProxyConnection(targetHost, targetPort, socks5Config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No proxy configured
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a connection through a single SOCKS proxy
|
||||||
|
*/
|
||||||
|
async function createSingleProxyConnection(
|
||||||
|
targetHost: string,
|
||||||
|
targetPort: number,
|
||||||
|
socks5Config: SOCKS5Config,
|
||||||
|
): Promise<net.Socket> {
|
||||||
|
const socksOptions: SocksClientOptions = {
|
||||||
|
proxy: {
|
||||||
|
host: socks5Config.socks5Host!,
|
||||||
|
port: socks5Config.socks5Port || 1080,
|
||||||
|
type: 5,
|
||||||
|
userId: socks5Config.socks5Username,
|
||||||
|
password: socks5Config.socks5Password,
|
||||||
|
},
|
||||||
|
command: "connect",
|
||||||
|
destination: {
|
||||||
|
host: targetHost,
|
||||||
|
port: targetPort,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
sshLogger.info("Creating SOCKS5 connection", {
|
||||||
|
operation: "socks5_connect",
|
||||||
|
proxyHost: socks5Config.socks5Host,
|
||||||
|
proxyPort: socks5Config.socks5Port || 1080,
|
||||||
|
targetHost,
|
||||||
|
targetPort,
|
||||||
|
hasAuth: !!(socks5Config.socks5Username && socks5Config.socks5Password),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const info = await SocksClient.createConnection(socksOptions);
|
||||||
|
|
||||||
|
sshLogger.info("SOCKS5 connection established", {
|
||||||
|
operation: "socks5_connected",
|
||||||
|
targetHost,
|
||||||
|
targetPort,
|
||||||
|
});
|
||||||
|
|
||||||
|
return info.socket;
|
||||||
|
} catch (error) {
|
||||||
|
sshLogger.error("SOCKS5 connection failed", error, {
|
||||||
|
operation: "socks5_connect_failed",
|
||||||
|
proxyHost: socks5Config.socks5Host,
|
||||||
|
proxyPort: socks5Config.socks5Port || 1080,
|
||||||
|
targetHost,
|
||||||
|
targetPort,
|
||||||
|
errorMessage: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a connection through a chain of SOCKS proxies
|
||||||
|
* Each proxy in the chain connects through the previous one
|
||||||
|
*/
|
||||||
|
async function createProxyChainConnection(
|
||||||
|
targetHost: string,
|
||||||
|
targetPort: number,
|
||||||
|
proxyChain: ProxyNode[],
|
||||||
|
): Promise<net.Socket> {
|
||||||
|
if (proxyChain.length === 0) {
|
||||||
|
throw new Error("Proxy chain is empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
const chainPath = proxyChain.map((p) => `${p.host}:${p.port}`).join(" → ");
|
||||||
|
sshLogger.info(`Creating SOCKS proxy chain: ${chainPath} → ${targetHost}:${targetPort}`, {
|
||||||
|
operation: "socks5_chain_connect",
|
||||||
|
chainLength: proxyChain.length,
|
||||||
|
targetHost,
|
||||||
|
targetPort,
|
||||||
|
proxies: proxyChain.map((p) => `${p.host}:${p.port}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const info = await SocksClient.createConnectionChain({
|
||||||
|
proxies: proxyChain.map((p) => ({
|
||||||
|
host: p.host,
|
||||||
|
port: p.port,
|
||||||
|
type: p.type,
|
||||||
|
userId: p.username,
|
||||||
|
password: p.password,
|
||||||
|
timeout: 10000, // 10-second timeout for each hop
|
||||||
|
})),
|
||||||
|
command: "connect",
|
||||||
|
destination: {
|
||||||
|
host: targetHost,
|
||||||
|
port: targetPort,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
sshLogger.info(`✓ Proxy chain established: ${chainPath} → ${targetHost}:${targetPort}`, {
|
||||||
|
operation: "socks5_chain_connected",
|
||||||
|
chainLength: proxyChain.length,
|
||||||
|
targetHost,
|
||||||
|
targetPort,
|
||||||
|
fullPath: `${chainPath} → ${targetHost}:${targetPort}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return info.socket;
|
||||||
|
} catch (error) {
|
||||||
|
sshLogger.error("SOCKS proxy chain connection failed", error, {
|
||||||
|
operation: "socks5_chain_connect_failed",
|
||||||
|
chainLength: proxyChain.length,
|
||||||
|
targetHost,
|
||||||
|
targetPort,
|
||||||
|
errorMessage: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -640,6 +640,7 @@
|
|||||||
"failedToLoadHosts": "Failed to load hosts",
|
"failedToLoadHosts": "Failed to load hosts",
|
||||||
"retry": "Retry",
|
"retry": "Retry",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
|
"optional": "Optional",
|
||||||
"hostsCount": "{{count}} hosts",
|
"hostsCount": "{{count}} hosts",
|
||||||
"importJson": "Import JSON",
|
"importJson": "Import JSON",
|
||||||
"importing": "Importing...",
|
"importing": "Importing...",
|
||||||
@@ -889,6 +890,47 @@
|
|||||||
"searchServers": "Search servers...",
|
"searchServers": "Search servers...",
|
||||||
"noServerFound": "No server found",
|
"noServerFound": "No server found",
|
||||||
"jumpHostsOrder": "Connections will be made in order: Jump Host 1 → Jump Host 2 → ... → Target Server",
|
"jumpHostsOrder": "Connections will be made in order: Jump Host 1 → Jump Host 2 → ... → Target Server",
|
||||||
|
"socks5Proxy": "SOCKS5 Proxy",
|
||||||
|
"socks5Description": "Configure SOCKS5 proxy for SSH connection. All traffic will be routed through the specified proxy server.",
|
||||||
|
"enableSocks5": "Enable SOCKS5 Proxy",
|
||||||
|
"enableSocks5Description": "Use SOCKS5 proxy for this SSH connection",
|
||||||
|
"socks5Host": "Proxy Host",
|
||||||
|
"socks5Port": "Proxy Port",
|
||||||
|
"socks5Username": "Proxy Username",
|
||||||
|
"socks5Password": "Proxy Password",
|
||||||
|
"socks5UsernameOptional": "Optional: leave empty if proxy doesn't require authentication",
|
||||||
|
"socks5PasswordOptional": "Optional: leave empty if proxy doesn't require authentication",
|
||||||
|
"socks5ProxyChain": "Proxy Chain",
|
||||||
|
"socks5ProxyChainDescription": "Configure a chain of SOCKS proxies. Each proxy in the chain will connect through the previous one.",
|
||||||
|
"socks5ProxyMode": "Proxy Mode",
|
||||||
|
"socks5UseSingleProxy": "Use Single Proxy",
|
||||||
|
"socks5UseProxyChain": "Use Proxy Chain",
|
||||||
|
"socks5UsePreset": "Use Saved Preset",
|
||||||
|
"socks5SelectPreset": "Select Preset",
|
||||||
|
"socks5ManagePresets": "Manage Presets",
|
||||||
|
"socks5ProxyNode": "Proxy {{number}}",
|
||||||
|
"socks5AddProxy": "Add Proxy to Chain",
|
||||||
|
"socks5RemoveProxy": "Remove Proxy",
|
||||||
|
"socks5ProxyType": "Proxy Type",
|
||||||
|
"socks5SaveAsPreset": "Save as Preset",
|
||||||
|
"socks5SavePresetTitle": "Save Proxy Chain as Preset",
|
||||||
|
"socks5SavePresetDescription": "Save the current proxy chain configuration as a reusable preset",
|
||||||
|
"socks5PresetName": "Preset Name",
|
||||||
|
"socks5PresetDescription": "Description (optional)",
|
||||||
|
"socks5PresetCreated": "Proxy chain preset created",
|
||||||
|
"socks5PresetUpdated": "Proxy chain preset updated",
|
||||||
|
"socks5PresetDeleted": "Proxy chain preset deleted",
|
||||||
|
"socks5PresetSaved": "Preset \"{{name}}\" saved successfully",
|
||||||
|
"socks5PresetSaveError": "Failed to save preset",
|
||||||
|
"socks5PresetNameRequired": "Preset name is required",
|
||||||
|
"socks5EmptyChainError": "Cannot save an empty proxy chain",
|
||||||
|
"socks5ProxyChainEmpty": "Add at least one proxy to the chain",
|
||||||
|
"socks5HostDescription": "Hostname or IP address of the SOCKS proxy server",
|
||||||
|
"socks5PortDescription": "Port number of the SOCKS proxy server (default: 1080)",
|
||||||
|
"addProxyNode": "Add Proxy Node",
|
||||||
|
"noProxyNodes": "No proxy nodes configured. Click 'Add Proxy Node' to add one.",
|
||||||
|
"proxyNode": "Proxy Node",
|
||||||
|
"proxyType": "Proxy Type",
|
||||||
"quickActions": "Quick Actions",
|
"quickActions": "Quick Actions",
|
||||||
"quickActionsDescription": "Quick actions allow you to create custom buttons that execute SSH snippets on this server. These buttons will appear at the top of the Server Stats page for quick access.",
|
"quickActionsDescription": "Quick actions allow you to create custom buttons that execute SSH snippets on this server. These buttons will appear at the top of the Server Stats page for quick access.",
|
||||||
"quickActionsList": "Quick Actions List",
|
"quickActionsList": "Quick Actions List",
|
||||||
@@ -1600,7 +1642,12 @@
|
|||||||
"folderName": "Enter folder name",
|
"folderName": "Enter folder name",
|
||||||
"fullPath": "Enter full path to item",
|
"fullPath": "Enter full path to item",
|
||||||
"currentPath": "Enter current path to item",
|
"currentPath": "Enter current path to item",
|
||||||
"newName": "Enter new name"
|
"newName": "Enter new name",
|
||||||
|
"socks5Host": "127.0.0.1",
|
||||||
|
"socks5Username": "proxy username",
|
||||||
|
"socks5Password": "proxy password",
|
||||||
|
"socks5PresetName": "e.g., Work VPN Chain",
|
||||||
|
"socks5PresetDescription": "e.g., Proxy chain for accessing work servers"
|
||||||
},
|
},
|
||||||
"leftSidebar": {
|
"leftSidebar": {
|
||||||
"failedToLoadHosts": "Failed to load hosts",
|
"failedToLoadHosts": "Failed to load hosts",
|
||||||
|
|||||||
@@ -874,6 +874,48 @@
|
|||||||
"searchServers": "Поиск серверов...",
|
"searchServers": "Поиск серверов...",
|
||||||
"noServerFound": "Сервер не найден",
|
"noServerFound": "Сервер не найден",
|
||||||
"jumpHostsOrder": "Подключения будут выполнены в порядке: Промежуточный хост 1 → Промежуточный хост 2 → ... → Целевой сервер",
|
"jumpHostsOrder": "Подключения будут выполнены в порядке: Промежуточный хост 1 → Промежуточный хост 2 → ... → Целевой сервер",
|
||||||
|
"socks5Proxy": "SOCKS5 Прокси",
|
||||||
|
"socks5Description": "Настройте SOCKS5 прокси для SSH подключения. Весь трафик будет направлен через указанный прокси-сервер.",
|
||||||
|
"enableSocks5": "Включить SOCKS5 Прокси",
|
||||||
|
"enableSocks5Description": "Использовать SOCKS5 прокси для этого SSH подключения",
|
||||||
|
"socks5Host": "Хост прокси",
|
||||||
|
"socks5Port": "Порт прокси",
|
||||||
|
"socks5Username": "Имя пользователя прокси",
|
||||||
|
"socks5Password": "Пароль прокси",
|
||||||
|
"socks5UsernameOptional": "Необязательно: оставьте пустым, если прокси не требует аутентификации",
|
||||||
|
"socks5PasswordOptional": "Необязательно: оставьте пустым, если прокси не требует аутентификации",
|
||||||
|
"socks5ProxyChain": "Цепочка Прокси",
|
||||||
|
"socks5ProxyChainDescription": "Настройте цепочку SOCKS прокси. Каждый прокси в цепочке будет подключаться через предыдущий.",
|
||||||
|
"socks5ProxyMode": "Режим Прокси",
|
||||||
|
"socks5UseSingleProxy": "Использовать Один Прокси",
|
||||||
|
"socks5UseProxyChain": "Использовать Цепочку Прокси",
|
||||||
|
"socks5UsePreset": "Использовать Сохраненный Пресет",
|
||||||
|
"socks5SelectPreset": "Выбрать Пресет",
|
||||||
|
"socks5ManagePresets": "Управление Пресетами",
|
||||||
|
"socks5ProxyNode": "Прокси {{number}}",
|
||||||
|
"socks5AddProxy": "Добавить Прокси в Цепочку",
|
||||||
|
"socks5RemoveProxy": "Удалить Прокси",
|
||||||
|
"socks5ProxyType": "Тип Прокси",
|
||||||
|
"socks5SaveAsPreset": "Сохранить как Пресет",
|
||||||
|
"socks5SavePresetTitle": "Сохранить Цепочку Прокси как Пресет",
|
||||||
|
"socks5SavePresetDescription": "Сохраните текущую конфигурацию цепочки прокси как переиспользуемый пресет",
|
||||||
|
"socks5PresetName": "Название Пресета",
|
||||||
|
"socks5PresetDescription": "Описание (необязательно)",
|
||||||
|
"socks5PresetCreated": "Пресет цепочки прокси создан",
|
||||||
|
"socks5PresetUpdated": "Пресет цепочки прокси обновлен",
|
||||||
|
"socks5PresetDeleted": "Пресет цепочки прокси удален",
|
||||||
|
"socks5PresetSaved": "Пресет \"{{name}}\" успешно сохранен",
|
||||||
|
"socks5PresetSaveError": "Не удалось сохранить пресет",
|
||||||
|
"socks5PresetNameRequired": "Необходимо указать название пресета",
|
||||||
|
"socks5EmptyChainError": "Невозможно сохранить пустую цепочку прокси",
|
||||||
|
"socks5ProxyChainEmpty": "Добавьте хотя бы один прокси в цепочку",
|
||||||
|
"socks5HostDescription": "Имя хоста или IP-адрес SOCKS прокси сервера",
|
||||||
|
"socks5PortDescription": "Номер порта SOCKS прокси сервера (по умолчанию: 1080)",
|
||||||
|
"addProxyNode": "Добавить узел прокси",
|
||||||
|
"noProxyNodes": "Узлы прокси не настроены. Нажмите 'Добавить узел прокси' чтобы добавить.",
|
||||||
|
"proxyNode": "Узел прокси",
|
||||||
|
"proxyType": "Тип прокси",
|
||||||
|
"advancedAuthSettings": "Расширенные настройки аутентификации"
|
||||||
"advancedAuthSettings": "Расширенные настройки аутентификации",
|
"advancedAuthSettings": "Расширенные настройки аутентификации",
|
||||||
"addQuickAction": "Добавить Quick Action",
|
"addQuickAction": "Добавить Quick Action",
|
||||||
"allHostsInFolderDeleted": "{{count}} хостов успешно удалены из папки \"{{folder}}\"",
|
"allHostsInFolderDeleted": "{{count}} хостов успешно удалены из папки \"{{folder}}\"",
|
||||||
@@ -1594,7 +1636,12 @@
|
|||||||
"folderName": "Введите имя папки",
|
"folderName": "Введите имя папки",
|
||||||
"fullPath": "Введите полный путь к элементу",
|
"fullPath": "Введите полный путь к элементу",
|
||||||
"currentPath": "Введите текущий путь к элементу",
|
"currentPath": "Введите текущий путь к элементу",
|
||||||
"newName": "Введите новое имя"
|
"newName": "Введите новое имя",
|
||||||
|
"socks5Host": "127.0.0.1",
|
||||||
|
"socks5Username": "имя пользователя прокси",
|
||||||
|
"socks5Password": "пароль прокси",
|
||||||
|
"socks5PresetName": "например, Рабочая VPN Цепочка",
|
||||||
|
"socks5PresetDescription": "например, Цепочка прокси для доступа к рабочим серверам"
|
||||||
},
|
},
|
||||||
"leftSidebar": {
|
"leftSidebar": {
|
||||||
"failedToLoadHosts": "Не удалось загрузить хосты",
|
"failedToLoadHosts": "Не удалось загрузить хосты",
|
||||||
|
|||||||
@@ -45,8 +45,16 @@ export interface SSHHost {
|
|||||||
tunnelConnections: TunnelConnection[];
|
tunnelConnections: TunnelConnection[];
|
||||||
jumpHosts?: JumpHost[];
|
jumpHosts?: JumpHost[];
|
||||||
quickActions?: QuickAction[];
|
quickActions?: QuickAction[];
|
||||||
statsConfig?: string;
|
statsConfig?: string | Record<string, unknown>;
|
||||||
terminalConfig?: TerminalConfig;
|
terminalConfig?: TerminalConfig;
|
||||||
|
|
||||||
|
useSocks5?: boolean;
|
||||||
|
socks5Host?: string;
|
||||||
|
socks5Port?: number;
|
||||||
|
socks5Username?: string;
|
||||||
|
socks5Password?: string;
|
||||||
|
socks5ProxyChain?: ProxyNode[];
|
||||||
|
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|
||||||
@@ -65,6 +73,14 @@ export interface QuickActionData {
|
|||||||
snippetId: number;
|
snippetId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProxyNode {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
type: 4 | 5; // SOCKS4 or SOCKS5
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SSHHostData {
|
export interface SSHHostData {
|
||||||
name?: string;
|
name?: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
@@ -91,6 +107,14 @@ export interface SSHHostData {
|
|||||||
quickActions?: QuickActionData[];
|
quickActions?: QuickActionData[];
|
||||||
statsConfig?: string | Record<string, unknown>;
|
statsConfig?: string | Record<string, unknown>;
|
||||||
terminalConfig?: TerminalConfig;
|
terminalConfig?: TerminalConfig;
|
||||||
|
|
||||||
|
// SOCKS5 Proxy configuration
|
||||||
|
useSocks5?: boolean;
|
||||||
|
socks5Host?: string;
|
||||||
|
socks5Port?: number;
|
||||||
|
socks5Username?: string;
|
||||||
|
socks5Password?: string;
|
||||||
|
socks5ProxyChain?: ProxyNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SSHFolder {
|
export interface SSHFolder {
|
||||||
@@ -211,6 +235,14 @@ export interface TunnelConfig {
|
|||||||
retryInterval: number;
|
retryInterval: number;
|
||||||
autoStart: boolean;
|
autoStart: boolean;
|
||||||
isPinned: boolean;
|
isPinned: boolean;
|
||||||
|
|
||||||
|
// SOCKS5 Proxy configuration
|
||||||
|
useSocks5?: boolean;
|
||||||
|
socks5Host?: string;
|
||||||
|
socks5Port?: number;
|
||||||
|
socks5Username?: string;
|
||||||
|
socks5Password?: string;
|
||||||
|
socks5ProxyChain?: ProxyNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TunnelStatus {
|
export interface TunnelStatus {
|
||||||
|
|||||||
@@ -336,6 +336,12 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
|||||||
credentialId: currentHost.credentialId,
|
credentialId: currentHost.credentialId,
|
||||||
userId: currentHost.userId,
|
userId: currentHost.userId,
|
||||||
forceKeyboardInteractive: currentHost.forceKeyboardInteractive,
|
forceKeyboardInteractive: currentHost.forceKeyboardInteractive,
|
||||||
|
useSocks5: currentHost.useSocks5,
|
||||||
|
socks5Host: currentHost.socks5Host,
|
||||||
|
socks5Port: currentHost.socks5Port,
|
||||||
|
socks5Username: currentHost.socks5Username,
|
||||||
|
socks5Password: currentHost.socks5Password,
|
||||||
|
socks5ProxyChain: currentHost.socks5ProxyChain,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.requires_totp) {
|
if (result?.requires_totp) {
|
||||||
@@ -760,14 +766,20 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
|||||||
if (!status.connected) {
|
if (!status.connected) {
|
||||||
const result = await connectSSH(currentSessionId, {
|
const result = await connectSSH(currentSessionId, {
|
||||||
hostId: currentHost.id,
|
hostId: currentHost.id,
|
||||||
host: currentHost.ip,
|
ip: currentHost.ip,
|
||||||
port: currentHost.port,
|
port: currentHost.port,
|
||||||
username: currentHost.username,
|
username: currentHost.username,
|
||||||
authType: currentHost.authType,
|
authType: currentHost.authType,
|
||||||
password: currentHost.password,
|
password: currentHost.password,
|
||||||
key: currentHost.key,
|
sshKey: currentHost.key,
|
||||||
keyPassword: currentHost.keyPassword,
|
keyPassword: currentHost.keyPassword,
|
||||||
credentialId: currentHost.credentialId,
|
credentialId: currentHost.credentialId,
|
||||||
|
useSocks5: currentHost.useSocks5,
|
||||||
|
socks5Host: currentHost.socks5Host,
|
||||||
|
socks5Port: currentHost.socks5Port,
|
||||||
|
socks5Username: currentHost.socks5Username,
|
||||||
|
socks5Password: currentHost.socks5Password,
|
||||||
|
socks5ProxyChain: currentHost.socks5ProxyChain,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
@@ -1313,6 +1325,12 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
|||||||
authType: currentHost.authType,
|
authType: currentHost.authType,
|
||||||
credentialId: currentHost.credentialId,
|
credentialId: currentHost.credentialId,
|
||||||
userId: currentHost.userId,
|
userId: currentHost.userId,
|
||||||
|
useSocks5: currentHost.useSocks5,
|
||||||
|
socks5Host: currentHost.socks5Host,
|
||||||
|
socks5Port: currentHost.socks5Port,
|
||||||
|
socks5Username: currentHost.socks5Username,
|
||||||
|
socks5Password: currentHost.socks5Password,
|
||||||
|
socks5ProxyChain: currentHost.socks5ProxyChain,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1464,7 +1482,12 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
|||||||
authType: credentials.password ? "password" : "key",
|
authType: credentials.password ? "password" : "key",
|
||||||
credentialId: currentHost.credentialId,
|
credentialId: currentHost.credentialId,
|
||||||
userId: currentHost.userId,
|
userId: currentHost.userId,
|
||||||
userProvidedPassword: true,
|
useSocks5: currentHost.useSocks5,
|
||||||
|
socks5Host: currentHost.socks5Host,
|
||||||
|
socks5Port: currentHost.socks5Port,
|
||||||
|
socks5Username: currentHost.socks5Username,
|
||||||
|
socks5Password: currentHost.socks5Password,
|
||||||
|
socks5ProxyChain: currentHost.socks5ProxyChain,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.requires_totp) {
|
if (result?.requires_totp) {
|
||||||
|
|||||||
@@ -70,6 +70,14 @@ import {
|
|||||||
AccordionItem,
|
AccordionItem,
|
||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
} from "@/components/ui/accordion.tsx";
|
} from "@/components/ui/accordion.tsx";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog.tsx";
|
||||||
import {
|
import {
|
||||||
TERMINAL_THEMES,
|
TERMINAL_THEMES,
|
||||||
TERMINAL_FONTS,
|
TERMINAL_FONTS,
|
||||||
@@ -79,8 +87,8 @@ import {
|
|||||||
DEFAULT_TERMINAL_CONFIG,
|
DEFAULT_TERMINAL_CONFIG,
|
||||||
} from "@/constants/terminal-themes";
|
} from "@/constants/terminal-themes";
|
||||||
import { TerminalPreview } from "@/ui/desktop/apps/terminal/TerminalPreview.tsx";
|
import { TerminalPreview } from "@/ui/desktop/apps/terminal/TerminalPreview.tsx";
|
||||||
import type { TerminalConfig } from "@/types";
|
import type { TerminalConfig, SSHHost, Credential } from "@/types";
|
||||||
import { Plus, X, Check, ChevronsUpDown } from "lucide-react";
|
import { Plus, X, Check, ChevronsUpDown, Save } from "lucide-react";
|
||||||
|
|
||||||
interface JumpHostItemProps {
|
interface JumpHostItemProps {
|
||||||
jumpHost: { hostId: number };
|
jumpHost: { hostId: number };
|
||||||
@@ -278,46 +286,6 @@ function QuickActionItem({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SSHHost {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
ip: string;
|
|
||||||
port: number;
|
|
||||||
username: string;
|
|
||||||
folder: string;
|
|
||||||
tags: string[];
|
|
||||||
pin: boolean;
|
|
||||||
authType: string;
|
|
||||||
password?: string;
|
|
||||||
key?: string;
|
|
||||||
keyPassword?: string;
|
|
||||||
keyType?: string;
|
|
||||||
enableTerminal: boolean;
|
|
||||||
enableTunnel: boolean;
|
|
||||||
enableFileManager: boolean;
|
|
||||||
defaultPath: string;
|
|
||||||
tunnelConnections: Array<{
|
|
||||||
sourcePort: number;
|
|
||||||
endpointPort: number;
|
|
||||||
endpointHost: string;
|
|
||||||
maxRetries: number;
|
|
||||||
retryInterval: number;
|
|
||||||
autoStart: boolean;
|
|
||||||
}>;
|
|
||||||
jumpHosts?: Array<{
|
|
||||||
hostId: number;
|
|
||||||
}>;
|
|
||||||
quickActions?: Array<{
|
|
||||||
name: string;
|
|
||||||
snippetId: number;
|
|
||||||
}>;
|
|
||||||
statsConfig?: StatsConfig;
|
|
||||||
terminalConfig?: TerminalConfig;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
credentialId?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SSHManagerHostEditorProps {
|
interface SSHManagerHostEditorProps {
|
||||||
editingHost?: SSHHost | null;
|
editingHost?: SSHHost | null;
|
||||||
onFormSubmit?: (updatedHost?: SSHHost) => void;
|
onFormSubmit?: (updatedHost?: SSHHost) => void;
|
||||||
@@ -331,12 +299,11 @@ export function HostManagerEditor({
|
|||||||
const [folders, setFolders] = useState<string[]>([]);
|
const [folders, setFolders] = useState<string[]>([]);
|
||||||
const [sshConfigurations, setSshConfigurations] = useState<string[]>([]);
|
const [sshConfigurations, setSshConfigurations] = useState<string[]>([]);
|
||||||
const [hosts, setHosts] = useState<SSHHost[]>([]);
|
const [hosts, setHosts] = useState<SSHHost[]>([]);
|
||||||
const [credentials, setCredentials] = useState<
|
const [credentials, setCredentials] = useState<Credential[]>([]);
|
||||||
Array<{ id: number; username: string; authType: string }>
|
|
||||||
>([]);
|
|
||||||
const [snippets, setSnippets] = useState<
|
const [snippets, setSnippets] = useState<
|
||||||
Array<{ id: number; name: string; content: string }>
|
Array<{ id: number; name: string; content: string }>
|
||||||
>([]);
|
>([]);
|
||||||
|
const [proxyMode, setProxyMode] = useState<"single" | "chain">("single");
|
||||||
|
|
||||||
const [authTab, setAuthTab] = useState<
|
const [authTab, setAuthTab] = useState<
|
||||||
"password" | "key" | "credential" | "none"
|
"password" | "key" | "credential" | "none"
|
||||||
@@ -370,7 +337,7 @@ export function HostManagerEditor({
|
|||||||
getSnippets(),
|
getSnippets(),
|
||||||
]);
|
]);
|
||||||
setHosts(hostsData);
|
setHosts(hostsData);
|
||||||
setCredentials(credentialsData);
|
setCredentials(credentialsData as Credential[]);
|
||||||
setSnippets(Array.isArray(snippetsData) ? snippetsData : []);
|
setSnippets(Array.isArray(snippetsData) ? snippetsData : []);
|
||||||
|
|
||||||
const uniqueFolders = [
|
const uniqueFolders = [
|
||||||
@@ -567,6 +534,22 @@ export function HostManagerEditor({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.default([]),
|
.default([]),
|
||||||
|
useSocks5: z.boolean().optional(),
|
||||||
|
socks5Host: z.string().optional(),
|
||||||
|
socks5Port: z.coerce.number().min(1).max(65535).optional(),
|
||||||
|
socks5Username: z.string().optional(),
|
||||||
|
socks5Password: z.string().optional(),
|
||||||
|
socks5ProxyChain: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
host: z.string().min(1),
|
||||||
|
port: z.number().min(1).max(65535),
|
||||||
|
type: z.union([z.literal(4), z.literal(5)]),
|
||||||
|
username: z.string().optional(),
|
||||||
|
password: z.string().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
enableDocker: z.boolean().default(false),
|
enableDocker: z.boolean().default(false),
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
@@ -604,11 +587,7 @@ export function HostManagerEditor({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (data.authType === "credential") {
|
} else if (data.authType === "credential") {
|
||||||
if (
|
if (!data.credentialId) {
|
||||||
!data.credentialId ||
|
|
||||||
(typeof data.credentialId === "string" &&
|
|
||||||
data.credentialId.trim() === "")
|
|
||||||
) {
|
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
message: t("hosts.credentialRequired"),
|
message: t("hosts.credentialRequired"),
|
||||||
@@ -660,6 +639,12 @@ export function HostManagerEditor({
|
|||||||
statsConfig: DEFAULT_STATS_CONFIG,
|
statsConfig: DEFAULT_STATS_CONFIG,
|
||||||
terminalConfig: DEFAULT_TERMINAL_CONFIG,
|
terminalConfig: DEFAULT_TERMINAL_CONFIG,
|
||||||
forceKeyboardInteractive: false,
|
forceKeyboardInteractive: false,
|
||||||
|
useSocks5: false,
|
||||||
|
socks5Host: "",
|
||||||
|
socks5Port: 1080,
|
||||||
|
socks5Username: "",
|
||||||
|
socks5Password: "",
|
||||||
|
socks5ProxyChain: [],
|
||||||
enableDocker: false,
|
enableDocker: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -677,7 +662,7 @@ export function HostManagerEditor({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [authTab, credentials, form.getValues, form.setValue]);
|
}, [authTab, credentials, form]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editingHost) {
|
if (editingHost) {
|
||||||
@@ -701,13 +686,13 @@ export function HostManagerEditor({
|
|||||||
: "none";
|
: "none";
|
||||||
setAuthTab(defaultAuthType);
|
setAuthTab(defaultAuthType);
|
||||||
|
|
||||||
let parsedStatsConfig = DEFAULT_STATS_CONFIG;
|
let parsedStatsConfig: StatsConfig = DEFAULT_STATS_CONFIG;
|
||||||
try {
|
try {
|
||||||
if (cleanedHost.statsConfig) {
|
if (cleanedHost.statsConfig) {
|
||||||
parsedStatsConfig =
|
parsedStatsConfig =
|
||||||
typeof cleanedHost.statsConfig === "string"
|
typeof cleanedHost.statsConfig === "string"
|
||||||
? JSON.parse(cleanedHost.statsConfig)
|
? JSON.parse(cleanedHost.statsConfig)
|
||||||
: cleanedHost.statsConfig;
|
: (cleanedHost.statsConfig as StatsConfig);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to parse statsConfig:", error);
|
console.error("Failed to parse statsConfig:", error);
|
||||||
@@ -715,7 +700,7 @@ export function HostManagerEditor({
|
|||||||
|
|
||||||
parsedStatsConfig = { ...DEFAULT_STATS_CONFIG, ...parsedStatsConfig };
|
parsedStatsConfig = { ...DEFAULT_STATS_CONFIG, ...parsedStatsConfig };
|
||||||
|
|
||||||
const formData = {
|
const formData: Partial<FormData> = {
|
||||||
name: cleanedHost.name || "",
|
name: cleanedHost.name || "",
|
||||||
ip: cleanedHost.ip || "",
|
ip: cleanedHost.ip || "",
|
||||||
port: cleanedHost.port || 22,
|
port: cleanedHost.port || 22,
|
||||||
@@ -724,7 +709,7 @@ export function HostManagerEditor({
|
|||||||
tags: Array.isArray(cleanedHost.tags) ? cleanedHost.tags : [],
|
tags: Array.isArray(cleanedHost.tags) ? cleanedHost.tags : [],
|
||||||
pin: Boolean(cleanedHost.pin),
|
pin: Boolean(cleanedHost.pin),
|
||||||
authType: defaultAuthType as "password" | "key" | "credential" | "none",
|
authType: defaultAuthType as "password" | "key" | "credential" | "none",
|
||||||
credentialId: null,
|
credentialId: cleanedHost.credentialId,
|
||||||
overrideCredentialUsername: Boolean(
|
overrideCredentialUsername: Boolean(
|
||||||
cleanedHost.overrideCredentialUsername,
|
cleanedHost.overrideCredentialUsername,
|
||||||
),
|
),
|
||||||
@@ -756,9 +741,27 @@ export function HostManagerEditor({
|
|||||||
: [],
|
: [],
|
||||||
},
|
},
|
||||||
forceKeyboardInteractive: Boolean(cleanedHost.forceKeyboardInteractive),
|
forceKeyboardInteractive: Boolean(cleanedHost.forceKeyboardInteractive),
|
||||||
|
useSocks5: Boolean(cleanedHost.useSocks5),
|
||||||
|
socks5Host: cleanedHost.socks5Host || "",
|
||||||
|
socks5Port: cleanedHost.socks5Port || 1080,
|
||||||
|
socks5Username: cleanedHost.socks5Username || "",
|
||||||
|
socks5Password: cleanedHost.socks5Password || "",
|
||||||
|
socks5ProxyChain: Array.isArray(cleanedHost.socks5ProxyChain)
|
||||||
|
? cleanedHost.socks5ProxyChain
|
||||||
|
: [],
|
||||||
enableDocker: Boolean(cleanedHost.enableDocker),
|
enableDocker: Boolean(cleanedHost.enableDocker),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Determine proxy mode based on existing data
|
||||||
|
if (
|
||||||
|
Array.isArray(cleanedHost.socks5ProxyChain) &&
|
||||||
|
cleanedHost.socks5ProxyChain.length > 0
|
||||||
|
) {
|
||||||
|
setProxyMode("chain");
|
||||||
|
} else {
|
||||||
|
setProxyMode("single");
|
||||||
|
}
|
||||||
|
|
||||||
if (defaultAuthType === "password") {
|
if (defaultAuthType === "password") {
|
||||||
formData.password = cleanedHost.password || "";
|
formData.password = cleanedHost.password || "";
|
||||||
} else if (defaultAuthType === "key") {
|
} else if (defaultAuthType === "key") {
|
||||||
@@ -776,14 +779,13 @@ export function HostManagerEditor({
|
|||||||
| "ssh-rsa-sha2-256"
|
| "ssh-rsa-sha2-256"
|
||||||
| "ssh-rsa-sha2-512") || "auto";
|
| "ssh-rsa-sha2-512") || "auto";
|
||||||
} else if (defaultAuthType === "credential") {
|
} else if (defaultAuthType === "credential") {
|
||||||
formData.credentialId =
|
formData.credentialId = cleanedHost.credentialId;
|
||||||
cleanedHost.credentialId || "existing_credential";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
form.reset(formData);
|
form.reset(formData as FormData);
|
||||||
} else {
|
} else {
|
||||||
setAuthTab("password");
|
setAuthTab("password");
|
||||||
const defaultFormData = {
|
const defaultFormData: Partial<FormData> = {
|
||||||
name: "",
|
name: "",
|
||||||
ip: "",
|
ip: "",
|
||||||
port: 22,
|
port: 22,
|
||||||
@@ -811,9 +813,9 @@ export function HostManagerEditor({
|
|||||||
enableDocker: false,
|
enableDocker: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
form.reset(defaultFormData);
|
form.reset(defaultFormData as FormData);
|
||||||
}
|
}
|
||||||
}, [editingHost?.id]);
|
}, [editingHost, form]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const focusTimer = setTimeout(() => {
|
const focusTimer = setTimeout(() => {
|
||||||
@@ -826,6 +828,8 @@ export function HostManagerEditor({
|
|||||||
}, [editingHost]);
|
}, [editingHost]);
|
||||||
|
|
||||||
const onSubmit = async (data: FormData) => {
|
const onSubmit = async (data: FormData) => {
|
||||||
|
await form.trigger();
|
||||||
|
console.log("onSubmit called with data:", data);
|
||||||
try {
|
try {
|
||||||
isSubmittingRef.current = true;
|
isSubmittingRef.current = true;
|
||||||
setFormError(null);
|
setFormError(null);
|
||||||
@@ -855,66 +859,45 @@ export function HostManagerEditor({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitData: Record<string, unknown> = {
|
const submitData: Partial<SSHHost> = {
|
||||||
name: data.name,
|
...data,
|
||||||
ip: data.ip,
|
|
||||||
port: data.port,
|
|
||||||
username: data.username,
|
|
||||||
folder: data.folder || "",
|
|
||||||
tags: data.tags || [],
|
|
||||||
pin: Boolean(data.pin),
|
|
||||||
authType: data.authType,
|
|
||||||
overrideCredentialUsername: Boolean(data.overrideCredentialUsername),
|
|
||||||
enableTerminal: Boolean(data.enableTerminal),
|
|
||||||
enableDocker: Boolean(data.enableDocker),
|
|
||||||
enableTunnel: Boolean(data.enableTunnel),
|
|
||||||
enableFileManager: Boolean(data.enableFileManager),
|
|
||||||
defaultPath: data.defaultPath || "/",
|
|
||||||
tunnelConnections: data.tunnelConnections || [],
|
|
||||||
jumpHosts: data.jumpHosts || [],
|
|
||||||
quickActions: data.quickActions || [],
|
|
||||||
statsConfig: data.statsConfig || DEFAULT_STATS_CONFIG,
|
|
||||||
terminalConfig: data.terminalConfig || DEFAULT_TERMINAL_CONFIG,
|
|
||||||
forceKeyboardInteractive: Boolean(data.forceKeyboardInteractive),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
submitData.credentialId = null;
|
if (proxyMode === "single") {
|
||||||
submitData.password = null;
|
submitData.socks5ProxyChain = [];
|
||||||
submitData.key = null;
|
} else if (proxyMode === "chain") {
|
||||||
submitData.keyPassword = null;
|
submitData.socks5Host = "";
|
||||||
submitData.keyType = null;
|
submitData.socks5Port = 1080;
|
||||||
|
submitData.socks5Username = "";
|
||||||
if (data.authType === "credential") {
|
submitData.socks5Password = "";
|
||||||
if (
|
|
||||||
data.credentialId === "existing_credential" &&
|
|
||||||
editingHost &&
|
|
||||||
editingHost.id
|
|
||||||
) {
|
|
||||||
delete submitData.credentialId;
|
|
||||||
} else {
|
|
||||||
submitData.credentialId = data.credentialId;
|
|
||||||
}
|
}
|
||||||
} else if (data.authType === "password") {
|
|
||||||
submitData.password = data.password;
|
if (data.authType !== "credential") {
|
||||||
} else if (data.authType === "key") {
|
submitData.credentialId = undefined;
|
||||||
|
}
|
||||||
|
if (data.authType !== "password") {
|
||||||
|
submitData.password = undefined;
|
||||||
|
}
|
||||||
|
if (data.authType !== "key") {
|
||||||
|
submitData.key = undefined;
|
||||||
|
submitData.keyPassword = undefined;
|
||||||
|
submitData.keyType = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.authType === "key") {
|
||||||
if (data.key instanceof File) {
|
if (data.key instanceof File) {
|
||||||
const keyContent = await data.key.text();
|
submitData.key = await data.key.text();
|
||||||
submitData.key = keyContent;
|
|
||||||
} else if (data.key === "existing_key") {
|
} else if (data.key === "existing_key") {
|
||||||
delete submitData.key;
|
delete submitData.key;
|
||||||
} else {
|
|
||||||
submitData.key = data.key;
|
|
||||||
}
|
}
|
||||||
submitData.keyPassword = data.keyPassword;
|
|
||||||
submitData.keyType = data.keyType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let savedHost;
|
let savedHost;
|
||||||
if (editingHost && editingHost.id) {
|
if (editingHost && editingHost.id) {
|
||||||
savedHost = await updateSSHHost(editingHost.id, submitData);
|
savedHost = await updateSSHHost(editingHost.id, submitData as any);
|
||||||
toast.success(t("hosts.hostUpdatedSuccessfully", { name: data.name }));
|
toast.success(t("hosts.hostUpdatedSuccessfully", { name: data.name }));
|
||||||
} else {
|
} else {
|
||||||
savedHost = await createSSHHost(submitData);
|
savedHost = await createSSHHost(submitData as any);
|
||||||
toast.success(t("hosts.hostAddedSuccessfully", { name: data.name }));
|
toast.success(t("hosts.hostAddedSuccessfully", { name: data.name }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -959,7 +942,9 @@ export function HostManagerEditor({
|
|||||||
notifyHostCreatedOrUpdated(savedHost.id);
|
notifyHostCreatedOrUpdated(savedHost.id);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(t("hosts.failedToSaveHost"));
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
toast.error(t("hosts.failedToSaveHost") + ": " + errorMessage);
|
||||||
console.error("Failed to save host:", error);
|
console.error("Failed to save host:", error);
|
||||||
} finally {
|
} finally {
|
||||||
isSubmittingRef.current = false;
|
isSubmittingRef.current = false;
|
||||||
@@ -1499,7 +1484,8 @@ export function HostManagerEditor({
|
|||||||
<span
|
<span
|
||||||
className="truncate"
|
className="truncate"
|
||||||
title={
|
title={
|
||||||
field.value?.name || t("hosts.upload")
|
(field.value as File)?.name ||
|
||||||
|
t("hosts.upload")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{field.value === "existing_key"
|
{field.value === "existing_key"
|
||||||
@@ -1507,7 +1493,7 @@ export function HostManagerEditor({
|
|||||||
: field.value
|
: field.value
|
||||||
? editingHost
|
? editingHost
|
||||||
? t("hosts.updateKey")
|
? t("hosts.updateKey")
|
||||||
: field.value.name
|
: (field.value as File).name
|
||||||
: t("hosts.upload")}
|
: t("hosts.upload")}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1546,8 +1532,6 @@ export function HostManagerEditor({
|
|||||||
dropCursor: false,
|
dropCursor: false,
|
||||||
allowMultipleSelections: false,
|
allowMultipleSelections: false,
|
||||||
highlightSelectionMatches: false,
|
highlightSelectionMatches: false,
|
||||||
searchKeymap: false,
|
|
||||||
scrollPastEnd: false,
|
|
||||||
}}
|
}}
|
||||||
extensions={[
|
extensions={[
|
||||||
EditorView.theme({
|
EditorView.theme({
|
||||||
@@ -1796,159 +1780,405 @@ export function HostManagerEditor({
|
|||||||
/>
|
/>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
</Accordion>
|
|
||||||
</TabsContent>
|
<AccordionItem value="socks5">
|
||||||
<TabsContent value="terminal" className="space-y-1">
|
<AccordionTrigger>
|
||||||
|
{t("hosts.socks5Proxy")}
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="space-y-4 pt-4">
|
||||||
|
<Alert>
|
||||||
|
<AlertDescription>
|
||||||
|
{t("hosts.socks5Description")}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="enableTerminal"
|
name="useSocks5"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
|
||||||
<FormLabel>{t("hosts.enableTerminal")}</FormLabel>
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>{t("hosts.enableSocks5")}</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{t("hosts.enableSocks5Description")}
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{form.watch("useSocks5") && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<FormLabel>
|
||||||
|
{t("hosts.socks5ProxyMode")}
|
||||||
|
</FormLabel>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={
|
||||||
|
proxyMode === "single"
|
||||||
|
? "default"
|
||||||
|
: "outline"
|
||||||
|
}
|
||||||
|
onClick={() => setProxyMode("single")}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{t("hosts.socks5UseSingleProxy")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={
|
||||||
|
proxyMode === "chain"
|
||||||
|
? "default"
|
||||||
|
: "outline"
|
||||||
|
}
|
||||||
|
onClick={() => setProxyMode("chain")}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{t("hosts.socks5UseProxyChain")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{proxyMode === "single" && (
|
||||||
|
<div className="space-y-4 p-4 border rounded-lg">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="socks5Host"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("hosts.socks5Host")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="proxy.example.com"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t("hosts.enableTerminalDesc")}
|
{t("hosts.socks5HostDescription")}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Alert className="mt-4 mb-4">
|
|
||||||
<AlertDescription>
|
<FormField
|
||||||
{t("hosts.terminalCustomizationNotice")}
|
control={form.control}
|
||||||
</AlertDescription>
|
name="socks5Port"
|
||||||
</Alert>
|
render={({ field }) => (
|
||||||
<h1 className="text-xl font-semibold mt-7">
|
<FormItem>
|
||||||
{t("hosts.terminalCustomization")}
|
<FormLabel>
|
||||||
</h1>
|
{t("hosts.socks5Port")}
|
||||||
<Accordion type="multiple" className="w-full">
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="1080"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(
|
||||||
|
parseInt(e.target.value) || 1080,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t("hosts.socks5PortDescription")}
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="socks5Username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("hosts.socks5Username")} (
|
||||||
|
{t("hosts.optional")})
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder={t("hosts.username")}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="socks5Password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("hosts.socks5Password")} (
|
||||||
|
{t("hosts.optional")})
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<PasswordInput
|
||||||
|
placeholder={t("hosts.password")}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{proxyMode === "chain" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<FormLabel>
|
||||||
|
{t("hosts.socks5ProxyChain")}
|
||||||
|
</FormLabel>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
const currentChain =
|
||||||
|
form.watch("socks5ProxyChain") || [];
|
||||||
|
form.setValue("socks5ProxyChain", [
|
||||||
|
...currentChain,
|
||||||
|
{
|
||||||
|
host: "",
|
||||||
|
port: 1080,
|
||||||
|
type: 5 as 4 | 5,
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
{t("hosts.addProxyNode")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(form.watch("socks5ProxyChain") || [])
|
||||||
|
.length === 0 && (
|
||||||
|
<div className="text-sm text-muted-foreground text-center p-4 border rounded-lg border-dashed">
|
||||||
|
{t("hosts.noProxyNodes")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(form.watch("socks5ProxyChain") || []).map(
|
||||||
|
(node: any, index: number) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="p-4 border rounded-lg space-y-3 relative"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{t("hosts.proxyNode")} {index + 1}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => {
|
||||||
|
const currentChain =
|
||||||
|
form.watch("socks5ProxyChain") ||
|
||||||
|
[];
|
||||||
|
form.setValue(
|
||||||
|
"socks5ProxyChain",
|
||||||
|
currentChain.filter(
|
||||||
|
(_: any, i: number) =>
|
||||||
|
i !== index,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<FormLabel>
|
||||||
|
{t("hosts.socks5Host")}
|
||||||
|
</FormLabel>
|
||||||
|
<Input
|
||||||
|
placeholder="proxy.example.com"
|
||||||
|
value={node.host}
|
||||||
|
onChange={(e) => {
|
||||||
|
const currentChain =
|
||||||
|
form.watch(
|
||||||
|
"socks5ProxyChain",
|
||||||
|
) || [];
|
||||||
|
const newChain = [
|
||||||
|
...currentChain,
|
||||||
|
];
|
||||||
|
newChain[index] = {
|
||||||
|
...newChain[index],
|
||||||
|
host: e.target.value,
|
||||||
|
};
|
||||||
|
form.setValue(
|
||||||
|
"socks5ProxyChain",
|
||||||
|
newChain,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<FormLabel>
|
||||||
|
{t("hosts.socks5Port")}
|
||||||
|
</FormLabel>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="1080"
|
||||||
|
value={node.port}
|
||||||
|
onChange={(e) => {
|
||||||
|
const currentChain =
|
||||||
|
form.watch(
|
||||||
|
"socks5ProxyChain",
|
||||||
|
) || [];
|
||||||
|
const newChain = [
|
||||||
|
...currentChain,
|
||||||
|
];
|
||||||
|
newChain[index] = {
|
||||||
|
...newChain[index],
|
||||||
|
port:
|
||||||
|
parseInt(e.target.value) ||
|
||||||
|
1080,
|
||||||
|
};
|
||||||
|
form.setValue(
|
||||||
|
"socks5ProxyChain",
|
||||||
|
newChain,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<FormLabel>
|
||||||
|
{t("hosts.proxyType")}
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
value={String(node.type)}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const currentChain =
|
||||||
|
form.watch("socks5ProxyChain") ||
|
||||||
|
[];
|
||||||
|
const newChain = [...currentChain];
|
||||||
|
newChain[index] = {
|
||||||
|
...newChain[index],
|
||||||
|
type: parseInt(value) as 4 | 5,
|
||||||
|
};
|
||||||
|
form.setValue(
|
||||||
|
"socks5ProxyChain",
|
||||||
|
newChain,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="4">
|
||||||
|
SOCKS4
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="5">
|
||||||
|
SOCKS5
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<FormLabel>
|
||||||
|
{t("hosts.socks5Username")} (
|
||||||
|
{t("hosts.optional")})
|
||||||
|
</FormLabel>
|
||||||
|
<Input
|
||||||
|
placeholder={t("hosts.username")}
|
||||||
|
value={node.username || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const currentChain =
|
||||||
|
form.watch(
|
||||||
|
"socks5ProxyChain",
|
||||||
|
) || [];
|
||||||
|
const newChain = [
|
||||||
|
...currentChain,
|
||||||
|
];
|
||||||
|
newChain[index] = {
|
||||||
|
...newChain[index],
|
||||||
|
username: e.target.value,
|
||||||
|
};
|
||||||
|
form.setValue(
|
||||||
|
"socks5ProxyChain",
|
||||||
|
newChain,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<FormLabel>
|
||||||
|
{t("hosts.socks5Password")} (
|
||||||
|
{t("hosts.optional")})
|
||||||
|
</FormLabel>
|
||||||
|
<PasswordInput
|
||||||
|
placeholder={t("hosts.password")}
|
||||||
|
value={node.password || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const currentChain =
|
||||||
|
form.watch(
|
||||||
|
"socks5ProxyChain",
|
||||||
|
) || [];
|
||||||
|
const newChain = [
|
||||||
|
...currentChain,
|
||||||
|
];
|
||||||
|
newChain[index] = {
|
||||||
|
...newChain[index],
|
||||||
|
password: e.target.value,
|
||||||
|
};
|
||||||
|
form.setValue(
|
||||||
|
"socks5ProxyChain",
|
||||||
|
newChain,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="terminal">
|
||||||
|
<Accordion
|
||||||
|
type="multiple"
|
||||||
|
className="w-full"
|
||||||
|
defaultValue={["appearance", "behavior", "advanced"]}
|
||||||
|
>
|
||||||
<AccordionItem value="appearance">
|
<AccordionItem value="appearance">
|
||||||
<AccordionTrigger>
|
<AccordionTrigger>
|
||||||
{t("hosts.appearance")}
|
{t("hosts.appearance")}
|
||||||
</AccordionTrigger>
|
</AccordionTrigger>
|
||||||
<AccordionContent className="space-y-4 pt-4">
|
<AccordionContent className="space-y-4 pt-4">
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium">
|
|
||||||
{t("hosts.themePreview")}
|
|
||||||
</label>
|
|
||||||
<TerminalPreview
|
|
||||||
theme={form.watch("terminalConfig.theme")}
|
|
||||||
fontSize={form.watch("terminalConfig.fontSize")}
|
|
||||||
fontFamily={form.watch("terminalConfig.fontFamily")}
|
|
||||||
cursorStyle={form.watch(
|
|
||||||
"terminalConfig.cursorStyle",
|
|
||||||
)}
|
|
||||||
cursorBlink={form.watch(
|
|
||||||
"terminalConfig.cursorBlink",
|
|
||||||
)}
|
|
||||||
letterSpacing={form.watch(
|
|
||||||
"terminalConfig.letterSpacing",
|
|
||||||
)}
|
|
||||||
lineHeight={form.watch("terminalConfig.lineHeight")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="terminalConfig.theme"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t("hosts.theme")}</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
value={field.value}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue
|
|
||||||
placeholder={t("hosts.selectTheme")}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
{Object.entries(TERMINAL_THEMES).map(
|
|
||||||
([key, theme]) => (
|
|
||||||
<SelectItem key={key} value={key}>
|
|
||||||
{theme.name}
|
|
||||||
</SelectItem>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormDescription>
|
|
||||||
{t("hosts.chooseColorTheme")}
|
|
||||||
</FormDescription>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="terminalConfig.fontFamily"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t("hosts.fontFamily")}</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
value={field.value}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue
|
|
||||||
placeholder={t("hosts.selectFont")}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
{TERMINAL_FONTS.map((font) => (
|
|
||||||
<SelectItem
|
|
||||||
key={font.value}
|
|
||||||
value={font.value}
|
|
||||||
>
|
|
||||||
{font.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormDescription>
|
|
||||||
{t("hosts.selectFontDesc")}
|
|
||||||
</FormDescription>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="terminalConfig.fontSize"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t("hosts.fontSizeValue", {
|
|
||||||
value: field.value,
|
|
||||||
})}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Slider
|
|
||||||
min={8}
|
|
||||||
max={24}
|
|
||||||
step={1}
|
|
||||||
value={[field.value]}
|
|
||||||
onValueChange={([value]) =>
|
|
||||||
field.onChange(value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t("hosts.adjustFontSize")}
|
|
||||||
</FormDescription>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="terminalConfig.letterSpacing"
|
name="terminalConfig.letterSpacing"
|
||||||
|
|||||||
@@ -183,6 +183,12 @@ export function Tunnel({ filterHostKey }: SSHTunnelProps): React.ReactElement {
|
|||||||
retryInterval: tunnel.retryInterval * 1000,
|
retryInterval: tunnel.retryInterval * 1000,
|
||||||
autoStart: tunnel.autoStart,
|
autoStart: tunnel.autoStart,
|
||||||
isPinned: host.pin,
|
isPinned: host.pin,
|
||||||
|
useSocks5: host.useSocks5,
|
||||||
|
socks5Host: host.socks5Host,
|
||||||
|
socks5Port: host.socks5Port,
|
||||||
|
socks5Username: host.socks5Username,
|
||||||
|
socks5Password: host.socks5Password,
|
||||||
|
socks5ProxyChain: host.socks5ProxyChain,
|
||||||
};
|
};
|
||||||
|
|
||||||
await connectTunnel(tunnelConfig);
|
await connectTunnel(tunnelConfig);
|
||||||
|
|||||||
@@ -931,6 +931,12 @@ export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
|
|||||||
: null,
|
: null,
|
||||||
terminalConfig: hostData.terminalConfig || null,
|
terminalConfig: hostData.terminalConfig || null,
|
||||||
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
|
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
|
||||||
|
useSocks5: Boolean(hostData.useSocks5),
|
||||||
|
socks5Host: hostData.socks5Host || null,
|
||||||
|
socks5Port: hostData.socks5Port || null,
|
||||||
|
socks5Username: hostData.socks5Username || null,
|
||||||
|
socks5Password: hostData.socks5Password || null,
|
||||||
|
socks5ProxyChain: hostData.socks5ProxyChain || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!submitData.enableTunnel) {
|
if (!submitData.enableTunnel) {
|
||||||
@@ -1003,6 +1009,12 @@ export async function updateSSHHost(
|
|||||||
: null,
|
: null,
|
||||||
terminalConfig: hostData.terminalConfig || null,
|
terminalConfig: hostData.terminalConfig || null,
|
||||||
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
|
forceKeyboardInteractive: Boolean(hostData.forceKeyboardInteractive),
|
||||||
|
useSocks5: Boolean(hostData.useSocks5),
|
||||||
|
socks5Host: hostData.socks5Host || null,
|
||||||
|
socks5Port: hostData.socks5Port || null,
|
||||||
|
socks5Username: hostData.socks5Username || null,
|
||||||
|
socks5Password: hostData.socks5Password || null,
|
||||||
|
socks5ProxyChain: hostData.socks5ProxyChain || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!submitData.enableTunnel) {
|
if (!submitData.enableTunnel) {
|
||||||
@@ -1314,6 +1326,12 @@ export async function connectSSH(
|
|||||||
credentialId?: number;
|
credentialId?: number;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
forceKeyboardInteractive?: boolean;
|
forceKeyboardInteractive?: boolean;
|
||||||
|
useSocks5?: boolean;
|
||||||
|
socks5Host?: string;
|
||||||
|
socks5Port?: number;
|
||||||
|
socks5Username?: string;
|
||||||
|
socks5Password?: string;
|
||||||
|
socks5ProxyChain?: unknown;
|
||||||
},
|
},
|
||||||
): Promise<Record<string, unknown>> {
|
): Promise<Record<string, unknown>> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user