diff --git a/src/apps/SSH/Config Editor/ConfigEditorSidebar.tsx b/src/apps/SSH/Config Editor/ConfigEditorSidebar.tsx
index 8ccc5b71..0cdaac43 100644
--- a/src/apps/SSH/Config Editor/ConfigEditorSidebar.tsx
+++ b/src/apps/SSH/Config Editor/ConfigEditorSidebar.tsx
@@ -471,7 +471,9 @@ const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
filteredSshByFolder[folder] = hosts.filter(conn => {
const q = debouncedSearch.trim().toLowerCase();
if (!q) return true;
- return (conn.name || '').toLowerCase().includes(q) || (conn.ip || '').toLowerCase().includes(q);
+ return (conn.name || '').toLowerCase().includes(q) || (conn.ip || '').toLowerCase().includes(q) ||
+ (conn.username || '').toLowerCase().includes(q) || (conn.folder || '').toLowerCase().includes(q) ||
+ (conn.tags || '').toLowerCase().includes(q);
});
});
@@ -926,7 +928,7 @@ const ConfigEditorSidebar = forwardRef(function ConfigEditorSidebar(
setSearch(e.target.value)}
- placeholder="Search hosts..."
+ placeholder="Search hosts by name, username, IP, folder, tags..."
className="w-full h-8 text-sm bg-background border border-border rounded"
autoComplete="off"
/>
diff --git a/src/apps/SSH/Terminal/SSHSidebar.tsx b/src/apps/SSH/Terminal/SSHSidebar.tsx
index 173bb686..356f0813 100644
--- a/src/apps/SSH/Terminal/SSHSidebar.tsx
+++ b/src/apps/SSH/Terminal/SSHSidebar.tsx
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import {
CornerDownLeft,
- Hammer
+ Hammer, Pin
} from "lucide-react"
import {
@@ -131,10 +131,16 @@ export function SSHSidebar({ onSelectView, onHostConnect, allTabs, runCommandOnT
if (!debouncedSearch.trim()) return hosts;
const q = debouncedSearch.trim().toLowerCase();
return hosts.filter(h => {
- const name = (h.name || "").toLowerCase();
- const ip = (h.ip || "").toLowerCase();
- const tags = Array.isArray(h.tags) ? h.tags : [];
- return name.includes(q) || ip.includes(q) || tags.some((tag: string) => tag.toLowerCase().includes(q));
+ const searchableText = [
+ h.name || '',
+ h.username,
+ h.ip,
+ h.folder || '',
+ ...(h.tags || []),
+ h.authType,
+ h.defaultPath || ''
+ ].join(' ').toLowerCase();
+ return searchableText.includes(q);
});
}, [hosts, debouncedSearch]);
@@ -214,7 +220,7 @@ export function SSHSidebar({ onSelectView, onHostConnect, allTabs, runCommandOnT
setSearch(e.target.value)}
- placeholder="Search hosts..."
+ placeholder="Search hosts by name, username, IP, folder, tags..."
className="w-full h-8 text-sm bg-background border border-border rounded"
autoComplete="off"
/>
@@ -343,7 +349,9 @@ const HostMenuItem = React.memo(function HostMenuItem({ host, onHostConnect }: {
onClick={() => onHostConnect(host)}
>
- {host.pin &&
★ }
+ {host.pin &&
+
+ }
{host.name || host.ip}
diff --git a/src/apps/SSH/Tunnel/SSHTunnel.tsx b/src/apps/SSH/Tunnel/SSHTunnel.tsx
index 1a54c7f1..325839d7 100644
--- a/src/apps/SSH/Tunnel/SSHTunnel.tsx
+++ b/src/apps/SSH/Tunnel/SSHTunnel.tsx
@@ -1,90 +1,75 @@
import React, { useState, useEffect, useCallback } from "react";
import { SSHTunnelSidebar } from "@/apps/SSH/Tunnel/SSHTunnelSidebar.tsx";
import { SSHTunnelViewer } from "@/apps/SSH/Tunnel/SSHTunnelViewer.tsx";
+import { getSSHHosts } from "@/apps/SSH/ssh-axios";
import axios from "axios";
interface ConfigEditorProps {
onSelectView: (view: string) => void;
}
-interface SSHTunnel {
- id: number;
- name: string;
- folder: string;
+interface TunnelConnection {
sourcePort: number;
endpointPort: number;
- sourceIP: string;
- sourceSSHPort: number;
- sourceUsername: string;
- sourcePassword: string;
- sourceAuthMethod: string;
- sourceSSHKey: string;
- sourceKeyPassword: string;
- sourceKeyType: string;
- endpointIP: string;
- endpointSSHPort: number;
- endpointUsername: string;
- endpointPassword: string;
- endpointAuthMethod: string;
- endpointSSHKey: string;
- endpointKeyPassword: string;
- endpointKeyType: string;
+ endpointHost: string;
maxRetries: number;
retryInterval: number;
- connectionState: string;
autoStart: boolean;
- isPinned: boolean;
+}
+
+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;
+ enableConfigEditor: boolean;
+ defaultPath: string;
+ tunnelConnections: TunnelConnection[];
+ createdAt: string;
+ updatedAt: string;
+}
+
+interface HostStatus {
+ connectionState?: string;
+ statusReason?: string;
+ statusErrorType?: string;
+ statusRetryCount?: number;
+ statusMaxRetries?: number;
+ statusNextRetryIn?: number;
+ statusRetryExhausted?: boolean;
+}
+
+interface TunnelStatus {
+ status: string;
+ reason?: string;
+ errorType?: string;
+ retryCount?: number;
+ maxRetries?: number;
+ nextRetryIn?: number;
+ retryExhausted?: boolean;
}
export function SSHTunnel({ onSelectView }: ConfigEditorProps): React.ReactElement {
- const [tunnels, setTunnels] = useState([]);
- const [tunnelsLoading, setTunnelsLoading] = useState(false);
- const [tunnelsError, setTunnelsError] = useState(null);
- const [tunnelStatusMap, setTunnelStatusMap] = useState>({});
- const sidebarRef = React.useRef(null);
+ const [hosts, setHosts] = useState([]);
+ const [hostStatuses, setHostStatuses] = useState>({});
- const fetchTunnels = useCallback(async () => {
- setTunnelsLoading(true);
- setTunnelsError(null);
+ const fetchHosts = useCallback(async () => {
try {
- const jwt = document.cookie.split('; ').find(row => row.startsWith('jwt='))?.split('=')[1];
- const res = await axios.get(
- (window.location.hostname === 'localhost' ? 'http://localhost:8081' : '') + '/ssh_tunnel/tunnel',
- { headers: { Authorization: `Bearer ${jwt}` } }
- );
- const tunnelData = res.data || [];
- setTunnels(tunnelData.map((tunnel: any) => ({
- id: tunnel.id,
- name: tunnel.name,
- folder: tunnel.folder || '',
- sourcePort: tunnel.sourcePort,
- endpointPort: tunnel.endpointPort,
- sourceIP: tunnel.sourceIP,
- sourceSSHPort: tunnel.sourceSSHPort,
- sourceUsername: tunnel.sourceUsername || '',
- sourcePassword: tunnel.sourcePassword || '',
- sourceAuthMethod: tunnel.sourceAuthMethod || 'password',
- sourceSSHKey: tunnel.sourceSSHKey || '',
- sourceKeyPassword: tunnel.sourceKeyPassword || '',
- sourceKeyType: tunnel.sourceKeyType || '',
- endpointIP: tunnel.endpointIP,
- endpointSSHPort: tunnel.endpointSSHPort,
- endpointUsername: tunnel.endpointUsername || '',
- endpointPassword: tunnel.endpointPassword || '',
- endpointAuthMethod: tunnel.endpointAuthMethod || 'password',
- endpointSSHKey: tunnel.endpointSSHKey || '',
- endpointKeyPassword: tunnel.endpointKeyPassword || '',
- endpointKeyType: tunnel.endpointKeyType || '',
- maxRetries: tunnel.maxRetries || 3,
- retryInterval: tunnel.retryInterval || 5000,
- connectionState: tunnel.connectionState || 'DISCONNECTED',
- autoStart: tunnel.autoStart || false,
- isPinned: tunnel.isPinned || false
- })));
- } catch (err: any) {
- setTunnelsError('Failed to load tunnels');
- } finally {
- setTunnelsLoading(false);
+ const hostsData = await getSSHHosts();
+ setHosts(hostsData);
+ } catch (err) {
+ // Silent error handling
}
}, []);
@@ -92,17 +77,75 @@ export function SSHTunnel({ onSelectView }: ConfigEditorProps): React.ReactEleme
const fetchTunnelStatuses = useCallback(async () => {
try {
const res = await axios.get('http://localhost:8083/status');
- setTunnelStatusMap(res.data || {});
+ const statusData = res.data || {};
+
+ // Convert tunnel statuses to host statuses
+ const newHostStatuses: Record = {};
+
+ hosts.forEach(host => {
+ // Find all tunnel statuses for this host
+ const hostName = host.name || `${host.username}@${host.ip}`;
+ const hostTunnelStatuses: TunnelStatus[] = [];
+
+ // Look for tunnel statuses that start with this host name
+ Object.entries(statusData).forEach(([tunnelName, status]) => {
+ if (tunnelName.startsWith(hostName + '_')) {
+ hostTunnelStatuses.push(status as any);
+ }
+ });
+
+ if (hostTunnelStatuses.length > 0) {
+ // Determine overall host status based on tunnel statuses
+ const connectedTunnels = hostTunnelStatuses.filter(s => s.status === 'connected');
+ const failedTunnels = hostTunnelStatuses.filter(s => s.status === 'failed');
+ const connectingTunnels = hostTunnelStatuses.filter(s =>
+ ['connecting', 'verifying', 'retrying'].includes(s.status)
+ );
+
+ let overallStatus: string;
+ let statusReason: string | undefined;
+
+ if (connectingTunnels.length > 0) {
+ overallStatus = 'connecting';
+ } else if (failedTunnels.length === hostTunnelStatuses.length) {
+ overallStatus = 'failed';
+ statusReason = failedTunnels[0]?.reason;
+ } else if (connectedTunnels.length === hostTunnelStatuses.length) {
+ overallStatus = 'connected';
+ } else if (connectedTunnels.length > 0) {
+ overallStatus = 'connected';
+ } else {
+ overallStatus = 'disconnected';
+ }
+
+ newHostStatuses[host.id] = {
+ connectionState: overallStatus,
+ statusReason,
+ statusErrorType: failedTunnels[0]?.errorType,
+ statusRetryCount: connectingTunnels.find(s => s.status === 'retrying')?.retryCount,
+ statusMaxRetries: connectingTunnels.find(s => s.status === 'retrying')?.maxRetries,
+ statusNextRetryIn: connectingTunnels.find(s => s.status === 'retrying')?.nextRetryIn,
+ statusRetryExhausted: failedTunnels.some(s => s.retryExhausted),
+ };
+ } else {
+ // Set default disconnected status
+ newHostStatuses[host.id] = {
+ connectionState: 'disconnected'
+ };
+ }
+ });
+
+ setHostStatuses(newHostStatuses);
} catch (err) {
- // Optionally handle error
+ // Silent error handling
}
- }, []);
+ }, [hosts]);
useEffect(() => {
- fetchTunnels();
- const interval = setInterval(fetchTunnels, 10000);
+ fetchHosts();
+ const interval = setInterval(fetchHosts, 10000);
return () => clearInterval(interval);
- }, [fetchTunnels]);
+ }, [fetchHosts]);
useEffect(() => {
fetchTunnelStatuses();
@@ -110,94 +153,88 @@ export function SSHTunnel({ onSelectView }: ConfigEditorProps): React.ReactEleme
return () => clearInterval(interval);
}, [fetchTunnelStatuses]);
- // Merge backend status into tunnels
- const tunnelsWithStatus = tunnels.map(tunnel => {
- const status = tunnelStatusMap[tunnel.name] || {};
- return {
- ...tunnel,
- connectionState: status.status ? status.status.toUpperCase() : tunnel.connectionState,
- statusReason: status.reason || '',
- statusErrorType: status.errorType || '',
- statusManualDisconnect: status.manualDisconnect || false,
- statusRetryCount: status.retryCount,
- statusMaxRetries: status.maxRetries,
- statusNextRetryIn: status.nextRetryIn,
- statusRetryExhausted: status.retryExhausted,
- };
- });
+ const handleConnect = async (hostId: number) => {
+ const host = hosts.find(h => h.id === hostId);
+ if (!host || !host.tunnelConnections || host.tunnelConnections.length === 0) {
+ return;
+ }
- const handleConnect = async (tunnelId: string) => {
// Immediately set to CONNECTING for instant UI feedback
- setTunnels(prev => prev.map(t =>
- t.id.toString() === tunnelId
- ? { ...t, connectionState: "CONNECTING" }
- : t
- ));
- const tunnel = tunnels.find(t => t.id.toString() === tunnelId);
- if (!tunnel) return;
+ setHostStatuses(prev => ({
+ ...prev,
+ [hostId]: { ...prev[hostId], connectionState: "connecting" }
+ }));
+
try {
- await axios.post('http://localhost:8083/connect', {
- ...tunnel,
- name: tunnel.name
- });
- // No need to update state here; polling will update real status
+ // For each tunnel connection, create a tunnel configuration
+ for (const tunnelConnection of host.tunnelConnections) {
+ // Find the endpoint host configuration
+ const endpointHost = hosts.find(h =>
+ h.name === tunnelConnection.endpointHost ||
+ `${h.username}@${h.ip}` === tunnelConnection.endpointHost
+ );
+
+ if (!endpointHost) {
+ continue;
+ }
+
+ // Create tunnel configuration
+ const tunnelConfig = {
+ name: `${host.name || `${host.username}@${host.ip}`}_${tunnelConnection.sourcePort}_${tunnelConnection.endpointPort}`,
+ hostName: host.name || `${host.username}@${host.ip}`,
+ sourceIP: host.ip,
+ sourceSSHPort: host.port,
+ sourceUsername: host.username,
+ sourcePassword: host.password,
+ sourceAuthMethod: host.authType,
+ sourceSSHKey: host.key,
+ sourceKeyPassword: host.keyPassword,
+ sourceKeyType: host.keyType,
+ endpointIP: endpointHost.ip,
+ endpointSSHPort: endpointHost.port,
+ endpointUsername: endpointHost.username,
+ endpointPassword: endpointHost.password,
+ endpointAuthMethod: endpointHost.authType,
+ endpointSSHKey: endpointHost.key,
+ endpointKeyPassword: endpointHost.keyPassword,
+ endpointKeyType: endpointHost.keyType,
+ sourcePort: tunnelConnection.sourcePort,
+ endpointPort: tunnelConnection.endpointPort,
+ maxRetries: tunnelConnection.maxRetries,
+ retryInterval: tunnelConnection.retryInterval * 1000, // Convert to milliseconds
+ autoStart: tunnelConnection.autoStart,
+ isPinned: host.pin
+ };
+
+ await axios.post('http://localhost:8083/connect', tunnelConfig);
+ }
} catch (err) {
- // Optionally handle error
+ // Reset status on error
+ setHostStatuses(prev => ({
+ ...prev,
+ [hostId]: { ...prev[hostId], connectionState: "failed", statusReason: "Failed to connect" }
+ }));
}
};
- const handleDisconnect = async (tunnelId: string) => {
+ const handleDisconnect = async (hostId: number) => {
+ const host = hosts.find(h => h.id === hostId);
+ if (!host) return;
+
// Immediately set to DISCONNECTING for instant UI feedback
- setTunnels(prev => prev.map(t =>
- t.id.toString() === tunnelId
- ? { ...t, connectionState: "DISCONNECTING" }
- : t
- ));
- const tunnel = tunnels.find(t => t.id.toString() === tunnelId);
- if (!tunnel) return;
+ setHostStatuses(prev => ({
+ ...prev,
+ [hostId]: { ...prev[hostId], connectionState: "disconnecting" }
+ }));
+
try {
- await axios.post('http://localhost:8083/disconnect', {
- tunnelName: tunnel.name
- });
- // No need to update state here; polling will update real status
+ // Disconnect all tunnels for this host
+ for (const tunnelConnection of host.tunnelConnections) {
+ const tunnelName = `${host.name || `${host.username}@${host.ip}`}_${tunnelConnection.sourcePort}_${tunnelConnection.endpointPort}`;
+ await axios.post('http://localhost:8083/disconnect', { tunnelName });
+ }
} catch (err) {
- // Optionally handle error
- }
- };
-
- const handleDeleteTunnel = async (tunnelId: string) => {
- try {
- const jwt = document.cookie.split('; ').find(row => row.startsWith('jwt='))?.split('=')[1];
- await axios.delete(
- (window.location.hostname === 'localhost' ? 'http://localhost:8081' : '') + `/ssh_tunnel/tunnel/${tunnelId}`,
- { headers: { Authorization: `Bearer ${jwt}` } }
- );
- fetchTunnels();
- } catch (err: any) {
- console.error('Failed to delete tunnel:', err);
- }
- };
-
- const handleEditTunnel = async (tunnelId: string, data: any) => {
- try {
- const jwt = document.cookie.split('; ').find(row => row.startsWith('jwt='))?.split('=')[1];
- await axios.put(
- (window.location.hostname === 'localhost' ? 'http://localhost:8081' : '') + `/ssh_tunnel/tunnel/${tunnelId}`,
- data,
- { headers: { Authorization: `Bearer ${jwt}` } }
- );
- fetchTunnels();
- } catch (err: any) {
- console.error('Failed to edit tunnel:', err);
- }
- };
-
- const handleEditTunnelClick = (tunnelId: string) => {
- // Find the tunnel data and pass it to the sidebar
- const tunnel = tunnels.find(t => t.id.toString() === tunnelId);
- if (tunnel && sidebarRef.current) {
- // Call the sidebar's openEditSheet function
- sidebarRef.current.openEditSheet(tunnel);
+ // Silent error handling
}
};
@@ -205,19 +242,15 @@ export function SSHTunnel({ onSelectView }: ConfigEditorProps): React.ReactEleme
diff --git a/src/apps/SSH/Tunnel/SSHTunnelObject.tsx b/src/apps/SSH/Tunnel/SSHTunnelObject.tsx
index d11c4170..1308a2bf 100644
--- a/src/apps/SSH/Tunnel/SSHTunnelObject.tsx
+++ b/src/apps/SSH/Tunnel/SSHTunnelObject.tsx
@@ -2,7 +2,8 @@ import React from "react";
import { Button } from "@/components/ui/button.tsx";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card.tsx";
import { Separator } from "@/components/ui/separator.tsx";
-import { Loader2, Edit, Trash2 } from "lucide-react";
+import { Loader2, Pin, Terminal, Network, FileEdit, Tag } from "lucide-react";
+import { Badge } from "@/components/ui/badge.tsx";
const CONNECTION_STATES = {
DISCONNECTED: "disconnected",
@@ -15,27 +16,62 @@ const CONNECTION_STATES = {
DISCONNECTING: "disconnecting"
};
+interface TunnelConnection {
+ sourcePort: number;
+ endpointPort: number;
+ endpointHost: string;
+ maxRetries: number;
+ retryInterval: number;
+ autoStart: boolean;
+}
+
+interface SSHHost {
+ id: number;
+ name: string;
+ ip: string;
+ port: number;
+ username: string;
+ folder: string;
+ tags: string[];
+ pin: boolean;
+ authType: string;
+ enableTerminal: boolean;
+ enableTunnel: boolean;
+ enableConfigEditor: boolean;
+ defaultPath: string;
+ tunnelConnections: TunnelConnection[];
+ createdAt: string;
+ updatedAt: string;
+}
+
interface SSHTunnelObjectProps {
- hostConfig: any;
- onConnect?: () => void;
- onDisconnect?: () => void;
- onDelete?: () => void;
- onEdit?: () => void;
- connectionState?: keyof typeof CONNECTION_STATES;
- isPinned?: boolean;
+ host: SSHHost;
+ onConnect?: (hostId: number) => void;
+ onDisconnect?: (hostId: number) => void;
+ connectionState?: keyof typeof CONNECTION_STATES | string;
+ statusReason?: string;
+ statusErrorType?: string;
+ statusRetryCount?: number;
+ statusMaxRetries?: number;
+ statusNextRetryIn?: number;
+ statusRetryExhausted?: boolean;
}
export function SSHTunnelObject({
- hostConfig = {},
+ host,
onConnect,
onDisconnect,
- onDelete,
- onEdit,
connectionState = "DISCONNECTED",
- isPinned = false
+ statusReason,
+ statusErrorType,
+ statusRetryCount,
+ statusMaxRetries,
+ statusNextRetryIn,
+ statusRetryExhausted
}: SSHTunnelObjectProps): React.ReactElement {
- const getStatusColor = (state: keyof typeof CONNECTION_STATES) => {
- switch (state) {
+ const getStatusColor = (state: string) => {
+ const upperState = state.toUpperCase();
+ switch (upperState) {
case "CONNECTED":
return "bg-green-500";
case "CONNECTING":
@@ -51,8 +87,9 @@ export function SSHTunnelObject({
}
};
- const getStatusText = (state: keyof typeof CONNECTION_STATES) => {
- switch (state) {
+ const getStatusText = (state: string) => {
+ const upperState = state.toUpperCase();
+ switch (upperState) {
case "CONNECTED":
return "Connected";
case "CONNECTING":
@@ -70,43 +107,26 @@ export function SSHTunnelObject({
}
};
-
-
- const isConnected = connectionState === "CONNECTED";
- const isConnecting = ["CONNECTING", "VERIFYING", "RETRYING"].includes(connectionState);
- const isDisconnecting = connectionState === "DISCONNECTING";
+ const isConnected = connectionState === "CONNECTED" || connectionState === "connected";
+ const isConnecting = ["CONNECTING", "VERIFYING", "RETRYING", "connecting", "verifying", "retrying"].includes(connectionState);
+ const isDisconnecting = connectionState === "DISCONNECTING" || connectionState === "disconnecting";
return (
- {/* Hover overlay buttons */}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {isPinned && ★ }
- {hostConfig.name || "My SSH Tunnel"}
-
+
+ {/* Host Header */}
+
+
+ {host.pin &&
}
+
+
+ {host.name || `${host.username}@${host.ip}`}
+
+
+ {host.ip}:{host.port} • {host.username}
+
+
-
@@ -115,28 +135,53 @@ export function SSHTunnelObject({
-
-
-
- Source:
-
- {hostConfig.source || "localhost:22"}
-
-
-
-
Endpoint:
-
- {hostConfig.endpoint || "test:224"}
-
+ {/* Tags */}
+ {host.tags && host.tags.length > 0 && (
+
+ {host.tags.slice(0, 3).map((tag, index) => (
+
+
+ {tag}
+
+ ))}
+ {host.tags.length > 3 && (
+
+ +{host.tags.length - 3}
+
+ )}
+ )}
+
+
+
+ {/* Tunnel Connections */}
+
+
Tunnel Connections
+ {host.tunnelConnections && host.tunnelConnections.length > 0 ? (
+
+ {host.tunnelConnections.map((tunnel, index) => (
+
+
+ Port {tunnel.sourcePort} → {tunnel.endpointHost}:{tunnel.endpointPort}
+ {tunnel.autoStart && (
+
+ Auto
+
+ )}
+
+
+ ))}
+
+ ) : (
+
No tunnel connections configured
+ )}
-
{/* Error/Status Reason */}
- {((connectionState === "FAILED" || connectionState === "UNSTABLE") && hostConfig.statusReason) && (
+ {((connectionState === "FAILED" || connectionState === "UNSTABLE") && statusReason) && (
- {hostConfig.statusReason}
- {typeof hostConfig.statusReason === 'string' && hostConfig.statusReason.includes('Max retries exhausted') && (
+ {statusReason}
+ {statusReason && statusReason.includes('Max retries exhausted') && (
<>
@@ -146,9 +191,21 @@ export function SSHTunnelObject({
)}
)}
-
+
+ {/* Retry Info */}
+ {connectionState === "RETRYING" && statusRetryCount && statusMaxRetries && (
+
+ Retry {statusRetryCount}/{statusMaxRetries}
+ {statusNextRetryIn && (
+ • Next retry in {statusNextRetryIn}s
+ )}
+
+ )}
+
+ {/* Action Buttons */}
+
onConnect?.(host.id)}
disabled={isConnected || isConnecting || isDisconnecting}
className="flex-1"
variant={isConnected ? "secondary" : "default"}
@@ -165,7 +222,7 @@ export function SSHTunnelObject({
)}
onDisconnect?.(host.id)}
disabled={!isConnected || isDisconnecting || isConnecting}
variant="outline"
className="flex-1"
diff --git a/src/apps/SSH/Tunnel/SSHTunnelSidebar.tsx b/src/apps/SSH/Tunnel/SSHTunnelSidebar.tsx
index 189c3e44..a0b4ad32 100644
--- a/src/apps/SSH/Tunnel/SSHTunnelSidebar.tsx
+++ b/src/apps/SSH/Tunnel/SSHTunnelSidebar.tsx
@@ -1,12 +1,8 @@
-import React, { useState } from 'react';
-import { useForm, Controller } from "react-hook-form";
+import React from 'react';
import {
CornerDownLeft,
- Plus,
- ArrowRight,
- AlertTriangle,
- Info
+ Settings
} from "lucide-react"
import {
@@ -26,451 +22,12 @@ import {
import {
Separator,
} from "@/components/ui/separator.tsx"
-import {
- Sheet,
- SheetClose,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
- SheetTrigger
-} from "@/components/ui/sheet.tsx";
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form.tsx";
-import { Input } from "@/components/ui/input.tsx";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs.tsx";
-import { Switch } from "@/components/ui/switch.tsx";
-import axios from "axios";
-import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert.tsx";
interface SidebarProps {
onSelectView: (view: string) => void;
- onTunnelAdded?: () => void;
- onEditTunnel?: (tunnelId: string, data: any) => void;
}
-interface AddTunnelFormData {
- tunnelName: string;
- folder: string;
- sourcePort: number;
- endpointPort: number;
- sourceIP: string;
- sourceSSHPort: number;
- sourceUsername: string;
- sourcePassword: string;
- sourceAuthMethod: string;
- sourceSSHKeyFile: File | null;
- sourceSSHKeyContent?: string;
- sourceKeyPassword?: string;
- sourceKeyType?: string;
- endpointIP: string;
- endpointSSHPort: number;
- endpointUsername: string;
- endpointPassword: string;
- endpointAuthMethod: string;
- endpointSSHKeyFile: File | null;
- endpointSSHKeyContent?: string;
- endpointKeyPassword?: string;
- endpointKeyType?: string;
- maxRetries: number;
- retryInterval: number;
- autoStart: boolean;
- isPinned: boolean;
-}
-
-export const SSHTunnelSidebar = React.forwardRef<{ openEditSheet: (tunnel: any) => void }, SidebarProps>(
- ({ onSelectView, onTunnelAdded, onEditTunnel }, ref) => {
- const addTunnelForm = useForm({
- defaultValues: {
- tunnelName: 'My SSH Tunnel',
- folder: '',
- sourcePort: 22,
- endpointPort: 224,
- sourceIP: 'localhost',
- sourceSSHPort: 22,
- sourceUsername: 'test',
- sourcePassword: '',
- sourceAuthMethod: 'password',
- sourceSSHKeyFile: null,
- endpointIP: 'test',
- endpointSSHPort: 22,
- endpointUsername: 'test',
- endpointPassword: '',
- endpointAuthMethod: 'password',
- endpointSSHKeyFile: null,
- maxRetries: 3,
- retryInterval: 5000,
- autoStart: false,
- isPinned: false
- }
- });
-
- const editTunnelForm = useForm({
- defaultValues: {
- tunnelName: '',
- folder: '',
- sourcePort: 22,
- endpointPort: 224,
- sourceIP: '',
- sourceSSHPort: 22,
- sourceUsername: '',
- sourcePassword: '',
- sourceAuthMethod: 'password',
- sourceSSHKeyFile: null,
- endpointIP: '',
- endpointSSHPort: 22,
- endpointUsername: '',
- endpointPassword: '',
- endpointAuthMethod: 'password',
- endpointSSHKeyFile: null,
- maxRetries: 3,
- retryInterval: 5000,
- autoStart: false,
- isPinned: false
- }
- });
-
- const [submitting, setSubmitting] = useState(false);
- const [submitError, setSubmitError] = useState(null);
- const [sheetOpen, setSheetOpen] = useState(false);
- const [editSheetOpen, setEditSheetOpen] = useState(false);
- const [editTunnelData, setEditTunnelData] = useState(null);
- const [folders, setFolders] = useState([]);
- const [foldersLoading, setFoldersLoading] = useState(false);
- const [foldersError, setFoldersError] = useState(null);
-
- React.useEffect(() => {
- if (!sheetOpen) {
- setSubmitError(null);
- }
- }, [sheetOpen]);
-
- React.useEffect(() => {
- if (!editSheetOpen) {
- setEditFolderDropdownOpen(false);
- }
- }, [editSheetOpen]);
-
- React.useEffect(() => {
- async function fetchFolders() {
- setFoldersLoading(true);
- setFoldersError(null);
- try {
- const jwt = document.cookie.split('; ').find(row => row.startsWith('jwt='))?.split('=')[1];
- const res = await axios.get(
- (window.location.hostname === 'localhost' ? 'http://localhost:8081' : '') + '/ssh_tunnel/folders',
- { headers: { Authorization: `Bearer ${jwt}` } }
- );
- setFolders(res.data || []);
- } catch (err: any) {
- setFoldersError('Failed to load folders');
- } finally {
- setFoldersLoading(false);
- }
- }
- fetchFolders();
- }, []);
-
- const onAddTunnelSubmit = async (data: AddTunnelFormData) => {
- setSubmitting(true);
- setSubmitError(null);
- try {
- let sourceSSHKeyContent = data.sourceSSHKeyContent;
- if (data.sourceSSHKeyFile instanceof File) {
- sourceSSHKeyContent = await data.sourceSSHKeyFile.text();
- }
-
- let endpointSSHKeyContent = data.endpointSSHKeyContent;
- if (data.endpointSSHKeyFile instanceof File) {
- endpointSSHKeyContent = await data.endpointSSHKeyFile.text();
- }
-
- const jwt = document.cookie.split('; ').find(row => row.startsWith('jwt='))?.split('=')[1];
- await axios.post(
- (window.location.hostname === 'localhost' ? 'http://localhost:8081' : '') + '/ssh_tunnel/tunnel',
- {
- name: data.tunnelName,
- folder: data.folder,
- sourcePort: data.sourcePort,
- endpointPort: data.endpointPort,
- sourceIP: data.sourceIP,
- sourceSSHPort: data.sourceSSHPort,
- sourceUsername: data.sourceUsername,
- sourcePassword: data.sourcePassword,
- sourceAuthMethod: data.sourceAuthMethod,
- sourceSSHKey: sourceSSHKeyContent,
- sourceKeyPassword: data.sourceKeyPassword,
- sourceKeyType: data.sourceKeyType === 'auto' ? '' : data.sourceKeyType,
- endpointIP: data.endpointIP,
- endpointSSHPort: data.endpointSSHPort,
- endpointUsername: data.endpointUsername,
- endpointPassword: data.endpointPassword,
- endpointAuthMethod: data.endpointAuthMethod,
- endpointSSHKey: endpointSSHKeyContent,
- endpointKeyPassword: data.endpointKeyPassword,
- endpointKeyType: data.endpointKeyType === 'auto' ? '' : data.endpointKeyType,
- maxRetries: data.maxRetries,
- retryInterval: data.retryInterval,
- autoStart: data.autoStart,
- isPinned: data.isPinned
- },
- { headers: { Authorization: `Bearer ${jwt}` } }
- );
- setSheetOpen(false);
- addTunnelForm.reset();
- if (data.folder && !folders.includes(data.folder)) {
- setFolders(prev => [...prev, data.folder]);
- }
- onTunnelAdded?.();
- } catch (err: any) {
- setSubmitError(err?.response?.data?.error || 'Failed to create SSH tunnel');
- } finally {
- setSubmitting(false);
- }
- };
-
- const onEditTunnelSubmit = async (data: AddTunnelFormData) => {
- setSubmitting(true);
- setSubmitError(null);
- try {
- let sourceSSHKeyContent = data.sourceSSHKeyContent;
- if (data.sourceSSHKeyFile instanceof File) {
- sourceSSHKeyContent = await data.sourceSSHKeyFile.text();
- }
-
- let endpointSSHKeyContent = data.endpointSSHKeyContent;
- if (data.endpointSSHKeyFile instanceof File) {
- endpointSSHKeyContent = await data.endpointSSHKeyFile.text();
- }
-
- const jwt = document.cookie.split('; ').find(row => row.startsWith('jwt='))?.split('=')[1];
- if (!editTunnelData?.id) {
- throw new Error('No tunnel ID found for editing');
- }
- await axios.put(
- (window.location.hostname === 'localhost' ? 'http://localhost:8081' : '') + `/ssh_tunnel/tunnel/${editTunnelData.id}`,
- {
- name: data.tunnelName,
- folder: data.folder,
- sourcePort: data.sourcePort,
- endpointPort: data.endpointPort,
- sourceIP: data.sourceIP,
- sourceSSHPort: data.sourceSSHPort,
- sourceUsername: data.sourceUsername,
- sourcePassword: data.sourcePassword,
- sourceAuthMethod: data.sourceAuthMethod,
- sourceSSHKey: sourceSSHKeyContent,
- sourceKeyPassword: data.sourceKeyPassword,
- sourceKeyType: data.sourceKeyType === 'auto' ? '' : data.sourceKeyType,
- endpointIP: data.endpointIP,
- endpointSSHPort: data.endpointSSHPort,
- endpointUsername: data.endpointUsername,
- endpointPassword: data.endpointPassword,
- endpointAuthMethod: data.endpointAuthMethod,
- endpointSSHKey: endpointSSHKeyContent,
- endpointKeyPassword: data.endpointKeyPassword,
- endpointKeyType: data.endpointKeyType === 'auto' ? '' : data.endpointKeyType,
- maxRetries: data.maxRetries,
- retryInterval: data.retryInterval,
- autoStart: data.autoStart,
- isPinned: data.isPinned
- },
- { headers: { Authorization: `Bearer ${jwt}` } }
- );
- setEditSheetOpen(false);
- editTunnelForm.reset();
- onTunnelAdded?.();
- } catch (err: any) {
- setSubmitError(err?.response?.data?.error || 'Failed to update SSH tunnel');
- } finally {
- setSubmitting(false);
- }
- };
-
- const sourcePort = addTunnelForm.watch('sourcePort');
- const endpointPort = addTunnelForm.watch('endpointPort');
- const folderValue = addTunnelForm.watch('folder');
- const filteredFolders = React.useMemo(() => {
- if (!folderValue) return folders;
- return folders.filter(f => f.toLowerCase().includes(folderValue.toLowerCase()));
- }, [folderValue, folders]);
-
- // Key type options
- const keyTypeOptions = [
- { value: 'auto', label: 'Auto-detect' },
- { value: 'ssh-rsa', label: 'RSA' },
- { value: 'ssh-ed25519', label: 'ED25519' },
- { value: 'ecdsa-sha2-nistp256', label: 'ECDSA NIST P-256' },
- { value: 'ecdsa-sha2-nistp384', label: 'ECDSA NIST P-384' },
- { value: 'ecdsa-sha2-nistp521', label: 'ECDSA NIST P-521' },
- { value: 'ssh-dss', label: 'DSA' },
- { value: 'ssh-rsa-sha2-256', label: 'RSA SHA2-256' },
- { value: 'ssh-rsa-sha2-512', label: 'RSA SHA2-512' },
- ];
-
- // Key type dropdown state and refs for source
- const [sourceKeyTypeDropdownOpen, setSourceKeyTypeDropdownOpen] = useState(false);
- const sourceKeyTypeDropdownRef = React.useRef(null);
- const sourceKeyTypeButtonRef = React.useRef(null);
-
- // Key type dropdown state and refs for endpoint
- const [endpointKeyTypeDropdownOpen, setEndpointKeyTypeDropdownOpen] = useState(false);
- const endpointKeyTypeDropdownRef = React.useRef(null);
- const endpointKeyTypeButtonRef = React.useRef(null);
- const [folderDropdownOpen, setFolderDropdownOpen] = useState(false);
- const folderInputRef = React.useRef(null);
- const folderDropdownRef = React.useRef(null);
- const [editSourceKeyTypeDropdownOpen, setEditSourceKeyTypeDropdownOpen] = useState(false);
- const [editEndpointKeyTypeDropdownOpen, setEditEndpointKeyTypeDropdownOpen] = useState(false);
- const [editFolderDropdownOpen, setEditFolderDropdownOpen] = useState(false);
- const editFolderInputRef = React.useRef(null);
- const editFolderDropdownRef = React.useRef(null);
-
- // Close dropdown on outside click (source)
- React.useEffect(() => {
- function handleClickOutside(event: MouseEvent) {
- if (
- sourceKeyTypeDropdownRef.current &&
- !sourceKeyTypeDropdownRef.current.contains(event.target as Node) &&
- sourceKeyTypeButtonRef.current &&
- !sourceKeyTypeButtonRef.current.contains(event.target as Node)
- ) {
- setSourceKeyTypeDropdownOpen(false);
- }
- }
- if (sourceKeyTypeDropdownOpen) {
- document.addEventListener('mousedown', handleClickOutside);
- } else {
- document.removeEventListener('mousedown', handleClickOutside);
- }
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
- };
- }, [sourceKeyTypeDropdownOpen]);
-
- // Close dropdown on outside click (endpoint)
- React.useEffect(() => {
- function handleClickOutside(event: MouseEvent) {
- if (
- endpointKeyTypeDropdownRef.current &&
- !endpointKeyTypeDropdownRef.current.contains(event.target as Node) &&
- endpointKeyTypeButtonRef.current &&
- !endpointKeyTypeButtonRef.current.contains(event.target as Node)
- ) {
- setEditEndpointKeyTypeDropdownOpen(false);
- }
- }
- if (endpointKeyTypeDropdownOpen) {
- document.addEventListener('mousedown', handleClickOutside);
- } else {
- document.removeEventListener('mousedown', handleClickOutside);
- }
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
- };
- }, [endpointKeyTypeDropdownOpen]);
-
- // Close dropdown on outside click (folder)
- React.useEffect(() => {
- function handleClickOutside(event: MouseEvent) {
- if (
- folderDropdownRef.current &&
- !folderDropdownRef.current.contains(event.target as Node) &&
- folderInputRef.current &&
- !folderInputRef.current.contains(event.target as Node)
- ) {
- setFolderDropdownOpen(false);
- }
- }
- if (folderDropdownOpen) {
- document.addEventListener('mousedown', handleClickOutside);
- } else {
- document.removeEventListener('mousedown', handleClickOutside);
- }
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
- };
- }, [folderDropdownOpen]);
-
- // Close dropdown on outside click (edit folder)
- React.useEffect(() => {
- function handleClickOutside(event: MouseEvent) {
- if (
- editFolderDropdownRef.current &&
- !editFolderDropdownRef.current.contains(event.target as Node) &&
- editFolderInputRef.current &&
- !editFolderInputRef.current.contains(event.target as Node)
- ) {
- setEditFolderDropdownOpen(false);
- }
- }
- if (editFolderDropdownOpen) {
- document.addEventListener('mousedown', handleClickOutside);
- } else {
- document.removeEventListener('mousedown', handleClickOutside);
- }
- return () => {
- document.removeEventListener('mousedown', handleClickOutside);
- };
- }, [editFolderDropdownOpen]);
-
- const handleFolderClick = (folder: string) => {
- addTunnelForm.setValue('folder', folder);
- setFolderDropdownOpen(false);
- };
-
- // Expose the openEditSheet function through the ref
- const openEditSheet = React.useCallback((tunnel: any) => {
- setEditTunnelData(tunnel);
- setEditSheetOpen(true);
- }, []);
-
- // Expose the function through the ref
- React.useImperativeHandle(ref, () => ({
- openEditSheet
- }), [openEditSheet]);
-
- // Populate edit form when editTunnelData changes
- React.useEffect(() => {
- if (editTunnelData) {
- editTunnelForm.reset({
- tunnelName: editTunnelData.name || '',
- folder: editTunnelData.folder || '',
- sourcePort: editTunnelData.sourcePort || 22,
- endpointPort: editTunnelData.endpointPort || 22,
- sourceIP: editTunnelData.sourceIP || '',
- sourceSSHPort: editTunnelData.sourceSSHPort || 22,
- sourceUsername: editTunnelData.sourceUsername || '',
- sourcePassword: editTunnelData.sourcePassword || '',
- sourceAuthMethod: editTunnelData.sourceAuthMethod || 'password',
- sourceSSHKeyFile: null,
- sourceSSHKeyContent: editTunnelData.sourceSSHKey || '',
- sourceKeyPassword: editTunnelData.sourceKeyPassword || '',
- sourceKeyType: editTunnelData.sourceKeyType || '',
- endpointIP: editTunnelData.endpointIP || '',
- endpointSSHPort: editTunnelData.endpointSSHPort || 22,
- endpointUsername: editTunnelData.endpointUsername || '',
- endpointPassword: editTunnelData.endpointPassword || '',
- endpointAuthMethod: editTunnelData.endpointAuthMethod || 'password',
- endpointSSHKeyFile: null,
- endpointSSHKeyContent: editTunnelData.endpointSSHKey || '',
- endpointKeyPassword: editTunnelData.endpointKeyPassword || '',
- endpointKeyType: editTunnelData.endpointKeyType || '',
- maxRetries: editTunnelData.maxRetries || 3,
- retryInterval: editTunnelData.retryInterval || 5000,
- autoStart: editTunnelData.autoStart || false,
- isPinned: editTunnelData.isPinned || false
- });
- }
- }, [editTunnelData, editTunnelForm]);
-
+export function SSHTunnelSidebar({ onSelectView }: SidebarProps): React.ReactElement {
return (
@@ -483,1282 +40,19 @@ export const SSHTunnelSidebar = React.forwardRef<{ openEditSheet: (tunnel: any)
- {/* Sidebar Items */}
onSelectView("homepage")} variant="outline">
-
+
Return
-
- { if (!submitting) setSheetOpen(open); }}>
-
- setSheetOpen(true)}
- disabled={submitting}
- >
-
- Add Tunnel
-
-
-
-
- Add SSH Tunnel
-
- Create a new SSH tunnel connection.
-
-
-
-
- {submitError && (
-
{submitError}
- )}
-
-
-
-
-
-
-
-
- {submitting ? 'Creating...' : 'Create Tunnel'}
-
-
-
- Close
-
-
-
-
-
-
-
- {/* Edit Tunnel Sheet */}
- {
- if (!open) {
- setTimeout(() => {
- setEditTunnelData(null);
- editTunnelForm.reset();
- }, 100);
- }
- setEditSheetOpen(open);
- }}>
-
-
- Edit SSH Tunnel
-
- Modify the SSH tunnel configuration.
-
-
-
- {submitError && (
-
{submitError}
- )}
-
-
-
-
-
-
-
- {submitting ? 'Updating...' : 'Update Tunnel'}
-
-
-
- Close
-
-
-
-
- );
-});
\ No newline at end of file
+ )
+}
\ No newline at end of file
diff --git a/src/apps/SSH/Tunnel/SSHTunnelViewer.tsx b/src/apps/SSH/Tunnel/SSHTunnelViewer.tsx
index 372dc94f..0f7516f6 100644
--- a/src/apps/SSH/Tunnel/SSHTunnelViewer.tsx
+++ b/src/apps/SSH/Tunnel/SSHTunnelViewer.tsx
@@ -2,81 +2,120 @@ import React from "react";
import { SSHTunnelObject } from "./SSHTunnelObject.tsx";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion.tsx";
import { Separator } from "@/components/ui/separator.tsx";
+import { Input } from "@/components/ui/input.tsx";
+import { Search } from "lucide-react";
+
+interface SSHHost {
+ id: number;
+ name: string;
+ ip: string;
+ port: number;
+ username: string;
+ folder: string;
+ tags: string[];
+ pin: boolean;
+ authType: string;
+ enableTerminal: boolean;
+ enableTunnel: boolean;
+ enableConfigEditor: boolean;
+ defaultPath: string;
+ tunnelConnections: any[];
+ createdAt: string;
+ updatedAt: string;
+}
+
+interface HostStatus {
+ connectionState?: string;
+ statusReason?: string;
+ statusErrorType?: string;
+ statusRetryCount?: number;
+ statusMaxRetries?: number;
+ statusNextRetryIn?: number;
+ statusRetryExhausted?: boolean;
+}
interface SSHTunnelViewerProps {
- tunnels: Array<{
- id: number;
- name: string;
- folder: string;
- sourcePort: number;
- endpointPort: number;
- sourceIP: string;
- sourceSSHPort: number;
- sourceUsername: string;
- sourcePassword: string;
- sourceAuthMethod: string;
- sourceSSHKey: string;
- sourceKeyPassword: string;
- sourceKeyType: string;
- endpointIP: string;
- endpointSSHPort: number;
- endpointUsername: string;
- endpointPassword: string;
- endpointAuthMethod: string;
- endpointSSHKey: string;
- endpointKeyPassword: string;
- endpointKeyType: string;
- maxRetries: number;
- retryInterval: number;
- connectionState?: string;
- autoStart: boolean;
- isPinned: boolean;
- }>;
- onConnect?: (tunnelId: string) => void;
- onDisconnect?: (tunnelId: string) => void;
- onDeleteTunnel?: (tunnelId: string) => void;
- onEditTunnel?: (tunnelId: string) => void;
+ hosts: SSHHost[];
+ hostStatuses?: Record;
+ onConnect?: (hostId: number) => void;
+ onDisconnect?: (hostId: number) => void;
}
export function SSHTunnelViewer({
- tunnels = [],
- onConnect,
- onDisconnect,
- onDeleteTunnel,
- onEditTunnel
+ hosts = [],
+ hostStatuses = {},
+ onConnect,
+ onDisconnect
}: SSHTunnelViewerProps): React.ReactElement {
- const handleConnect = (tunnelId: string) => {
- onConnect?.(tunnelId);
+ const [searchQuery, setSearchQuery] = React.useState("");
+ const [debouncedSearch, setDebouncedSearch] = React.useState("");
+
+ // Debounce search
+ React.useEffect(() => {
+ const handler = setTimeout(() => setDebouncedSearch(searchQuery), 200);
+ return () => clearTimeout(handler);
+ }, [searchQuery]);
+
+ const handleConnect = (hostId: number) => {
+ onConnect?.(hostId);
};
- const handleDisconnect = (tunnelId: string) => {
- onDisconnect?.(tunnelId);
+ const handleDisconnect = (hostId: number) => {
+ onDisconnect?.(hostId);
};
- // Group tunnels by folder and sort
- const tunnelsByFolder = React.useMemo(() => {
- const map: Record = {};
- tunnels.forEach(tunnel => {
- const folder = tunnel.folder && tunnel.folder.trim() ? tunnel.folder : 'No Folder';
+ // Filter hosts by search query
+ const filteredHosts = React.useMemo(() => {
+ if (!debouncedSearch.trim()) return hosts;
+
+ const query = debouncedSearch.trim().toLowerCase();
+ return hosts.filter(host => {
+ const searchableText = [
+ host.name || '',
+ host.username,
+ host.ip,
+ host.folder || '',
+ ...(host.tags || []),
+ host.authType,
+ host.defaultPath || ''
+ ].join(' ').toLowerCase();
+ return searchableText.includes(query);
+ });
+ }, [hosts, debouncedSearch]);
+
+ // Filter hosts to only show those with enableTunnel: true and tunnelConnections
+ const tunnelHosts = React.useMemo(() => {
+ return filteredHosts.filter(host =>
+ host.enableTunnel &&
+ host.tunnelConnections &&
+ host.tunnelConnections.length > 0
+ );
+ }, [filteredHosts]);
+
+ // Group hosts by folder and sort
+ const hostsByFolder = React.useMemo(() => {
+ const map: Record = {};
+ tunnelHosts.forEach(host => {
+ const folder = host.folder && host.folder.trim() ? host.folder : 'Uncategorized';
if (!map[folder]) map[folder] = [];
- map[folder].push(tunnel);
+ map[folder].push(host);
});
return map;
- }, [tunnels]);
+ }, [tunnelHosts]);
const sortedFolders = React.useMemo(() => {
- const folders = Object.keys(tunnelsByFolder);
+ const folders = Object.keys(hostsByFolder);
folders.sort((a, b) => {
- if (a === 'No Folder') return -1;
- if (b === 'No Folder') return 1;
+ if (a === 'Uncategorized') return -1;
+ if (b === 'Uncategorized') return 1;
return a.localeCompare(b);
});
return folders;
- }, [tunnelsByFolder]);
+ }, [hostsByFolder]);
- const getSortedTunnels = (arr: typeof tunnels) => {
- const pinned = arr.filter(t => t.isPinned).sort((a, b) => (a.name || '').localeCompare(b.name || ''));
- const rest = arr.filter(t => !t.isPinned).sort((a, b) => (a.name || '').localeCompare(b.name || ''));
+ const getSortedHosts = (arr: SSHHost[]) => {
+ const pinned = arr.filter(h => h.pin).sort((a, b) => (a.name || '').localeCompare(b.name || ''));
+ const rest = arr.filter(h => !h.pin).sort((a, b) => (a.name || '').localeCompare(b.name || ''));
return [...pinned, ...rest];
};
@@ -93,14 +132,28 @@ export function SSHTunnelViewer({
+ {/* Search Bar */}
+
+
+ setSearchQuery(e.target.value)}
+ className="pl-10"
+ />
+
+
{/* Accordion Layout */}
- {tunnels.length === 0 ? (
+ {tunnelHosts.length === 0 ? (
No SSH Tunnels
- Create your first SSH tunnel to get started. Use the sidebar to add a new tunnel configuration.
+ {searchQuery.trim() ?
+ "No hosts match your search criteria." :
+ "Create your first SSH tunnel to get started. Use the SSH Manager to add hosts with tunnel connections."
+ }
) : (
@@ -112,16 +165,19 @@ export function SSHTunnelViewer({
- {getSortedTunnels(tunnelsByFolder[folder]).map((tunnel, tunnelIndex) => (
-
+ {getSortedHosts(hostsByFolder[folder]).map((host, hostIndex) => (
+
handleConnect(tunnel.id.toString())}
- onDisconnect={() => handleDisconnect(tunnel.id.toString())}
- onDelete={() => onDeleteTunnel?.(tunnel.id.toString())}
- onEdit={() => onEditTunnel?.(tunnel.id.toString())}
+ host={host}
+ connectionState={hostStatuses[host.id]?.connectionState as any}
+ statusReason={hostStatuses[host.id]?.statusReason}
+ statusErrorType={hostStatuses[host.id]?.statusErrorType}
+ statusRetryCount={hostStatuses[host.id]?.statusRetryCount}
+ statusMaxRetries={hostStatuses[host.id]?.statusMaxRetries}
+ statusNextRetryIn={hostStatuses[host.id]?.statusNextRetryIn}
+ statusRetryExhausted={hostStatuses[host.id]?.statusRetryExhausted}
+ onConnect={() => handleConnect(host.id)}
+ onDisconnect={() => handleDisconnect(host.id)}
/>
))}
diff --git a/src/backend/ssh_tunnel/ssh_tunnel.ts b/src/backend/ssh_tunnel/ssh_tunnel.ts
index 96f607cb..ff8aa756 100644
--- a/src/backend/ssh_tunnel/ssh_tunnel.ts
+++ b/src/backend/ssh_tunnel/ssh_tunnel.ts
@@ -46,21 +46,55 @@ const logger = {
}
};
-// State management
-const activeTunnels = new Map
();
-const retryCounters = new Map();
-const connectionStatus = new Map();
-const tunnelVerifications = new Map();
-const manualDisconnects = new Set();
-const verificationTimers = new Map();
-const activeRetryTimers = new Map();
-const retryExhaustedTunnels = new Set();
-const remoteClosureEvents = new Map();
-const hostConfigs = new Map();
+// State management for host-based tunnels
+const activeTunnels = new Map(); // tunnelName -> Client
+const retryCounters = new Map(); // tunnelName -> retryCount
+const connectionStatus = new Map(); // tunnelName -> status
+const tunnelVerifications = new Map(); // tunnelName -> verification
+const manualDisconnects = new Set(); // tunnelNames
+const verificationTimers = new Map(); // timer keys -> timeout
+const activeRetryTimers = new Map(); // tunnelName -> retry timer
+const retryExhaustedTunnels = new Set(); // tunnelNames
+const remoteClosureEvents = new Map(); // tunnelName -> count
+const hostConfigs = new Map(); // hostName -> hostConfig
+const tunnelConfigs = new Map(); // tunnelName -> tunnelConfig
// Types
-interface HostConfig {
+interface TunnelConnection {
+ sourcePort: number;
+ endpointPort: number;
+ endpointHost: string;
+ maxRetries: number;
+ retryInterval: number;
+ autoStart: boolean;
+}
+
+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;
+ enableConfigEditor: boolean;
+ defaultPath: string;
+ tunnelConnections: TunnelConnection[];
+ createdAt: string;
+ updatedAt: string;
+}
+
+interface TunnelConfig {
+ name: string;
+ hostName: string;
sourceIP: string;
sourceSSHPort: number;
sourceUsername: string;
@@ -85,6 +119,11 @@ interface HostConfig {
isPinned: boolean;
}
+interface HostConfig {
+ host: SSHHost;
+ tunnels: TunnelConfig[];
+}
+
interface TunnelStatus {
connected: boolean;
status: ConnectionState;
@@ -135,8 +174,6 @@ function broadcastTunnelStatus(tunnelName: string, status: TunnelStatus): void {
status.reason = "Max retries exhausted";
}
- // In Express, we'll use a different approach for broadcasting
- // For now, we'll store the status and provide endpoints to fetch it
connectionStatus.set(tunnelName, status);
}
@@ -244,7 +281,7 @@ function resetRetryState(tunnelName: string): void {
});
}
-function handleDisconnect(tunnelName: string, hostConfig: HostConfig | null, shouldRetry = true, isRemoteClosure = false): void {
+function handleDisconnect(tunnelName: string, tunnelConfig: TunnelConfig | null, shouldRetry = true, isRemoteClosure = false): void {
if (tunnelVerifications.has(tunnelName)) {
try {
const verification = tunnelVerifications.get(tunnelName);
@@ -299,9 +336,9 @@ function handleDisconnect(tunnelName: string, hostConfig: HostConfig | null, sho
return;
}
- if (shouldRetry && hostConfig) {
- const maxRetries = hostConfig.maxRetries || 3;
- const retryInterval = hostConfig.retryInterval || 5000;
+ if (shouldRetry && tunnelConfig) {
+ const maxRetries = tunnelConfig.maxRetries || 3;
+ const retryInterval = tunnelConfig.retryInterval || 5000;
if (isRemoteClosure) {
const currentCount = remoteClosureEvents.get(tunnelName) || 0;
@@ -351,7 +388,7 @@ function handleDisconnect(tunnelName: string, hostConfig: HostConfig | null, sho
if (!manualDisconnects.has(tunnelName)) {
activeTunnels.delete(tunnelName);
- connectSSHTunnel(hostConfig, retryCount);
+ connectSSHTunnel(tunnelConfig, retryCount);
}
}, retryInterval);
@@ -368,7 +405,7 @@ function handleDisconnect(tunnelName: string, hostConfig: HostConfig | null, sho
}
// Tunnel verification function
-function verifyTunnelConnection(tunnelName: string, hostConfig: HostConfig, isPeriodic = false): void {
+function verifyTunnelConnection(tunnelName: string, tunnelConfig: TunnelConfig, isPeriodic = false): void {
if (manualDisconnects.has(tunnelName) || !activeTunnels.has(tunnelName)) {
return;
}
@@ -411,7 +448,7 @@ function verifyTunnelConnection(tunnelName: string, hostConfig: HostConfig, isPe
});
if (!isPeriodic) {
- setupPingInterval(tunnelName, hostConfig);
+ setupPingInterval(tunnelName, tunnelConfig);
}
} else {
logger.error(`Verification failed for '${tunnelName}': ${failureReason}`);
@@ -425,12 +462,12 @@ function verifyTunnelConnection(tunnelName: string, hostConfig: HostConfig, isPe
}
activeTunnels.delete(tunnelName);
- handleDisconnect(tunnelName, hostConfig, !manualDisconnects.has(tunnelName));
+ handleDisconnect(tunnelName, tunnelConfig, !manualDisconnects.has(tunnelName));
}
}
function attemptVerification() {
- const testCmd = `nc -z localhost ${hostConfig.sourcePort}`;
+ const testCmd = `nc -z localhost ${tunnelConfig.sourcePort}`;
verificationConn.exec(testCmd, (err, stream) => {
if (err) {
@@ -447,7 +484,7 @@ function verifyTunnelConnection(tunnelName: string, hostConfig: HostConfig, isPe
if (code === 0 && code !== undefined) {
cleanupVerification(true);
} else {
- cleanupVerification(false, `Port ${hostConfig.sourcePort} is not accessible`);
+ cleanupVerification(false, `Port ${tunnelConfig.sourcePort} is not accessible`);
}
});
@@ -472,9 +509,9 @@ function verifyTunnelConnection(tunnelName: string, hostConfig: HostConfig, isPe
});
const connOptions: any = {
- host: hostConfig.sourceIP,
- port: hostConfig.sourceSSHPort,
- username: hostConfig.sourceUsername,
+ host: tunnelConfig.sourceIP,
+ port: tunnelConfig.sourceSSHPort,
+ username: tunnelConfig.sourceUsername,
readyTimeout: 10000,
algorithms: {
kex: [
@@ -512,19 +549,19 @@ function verifyTunnelConnection(tunnelName: string, hostConfig: HostConfig, isPe
}
};
- if (hostConfig.sourceAuthMethod === "key" && hostConfig.sourceSSHKey) {
- connOptions.privateKey = hostConfig.sourceSSHKey;
- if (hostConfig.sourceKeyPassword) {
- connOptions.passphrase = hostConfig.sourceKeyPassword;
+ if (tunnelConfig.sourceAuthMethod === "key" && tunnelConfig.sourceSSHKey) {
+ connOptions.privateKey = tunnelConfig.sourceSSHKey;
+ if (tunnelConfig.sourceKeyPassword) {
+ connOptions.passphrase = tunnelConfig.sourceKeyPassword;
}
} else {
- connOptions.password = hostConfig.sourcePassword;
+ connOptions.password = tunnelConfig.sourcePassword;
}
verificationConn.connect(connOptions);
}
-function setupPingInterval(tunnelName: string, hostConfig: HostConfig): void {
+function setupPingInterval(tunnelName: string, tunnelConfig: TunnelConfig): void {
const pingInterval = setInterval(() => {
if (!activeTunnels.has(tunnelName) || manualDisconnects.has(tunnelName)) {
clearInterval(pingInterval);
@@ -550,7 +587,7 @@ function setupPingInterval(tunnelName: string, hostConfig: HostConfig): void {
}
activeTunnels.delete(tunnelName);
- handleDisconnect(tunnelName, hostConfig, !manualDisconnects.has(tunnelName));
+ handleDisconnect(tunnelName, tunnelConfig, !manualDisconnects.has(tunnelName));
return;
}
@@ -567,7 +604,7 @@ function setupPingInterval(tunnelName: string, hostConfig: HostConfig): void {
}
activeTunnels.delete(tunnelName);
- handleDisconnect(tunnelName, hostConfig, !manualDisconnects.has(tunnelName));
+ handleDisconnect(tunnelName, tunnelConfig, !manualDisconnects.has(tunnelName));
}
});
@@ -583,15 +620,15 @@ function setupPingInterval(tunnelName: string, hostConfig: HostConfig): void {
}
activeTunnels.delete(tunnelName);
- handleDisconnect(tunnelName, hostConfig, !manualDisconnects.has(tunnelName));
+ handleDisconnect(tunnelName, tunnelConfig, !manualDisconnects.has(tunnelName));
});
});
}, 30000); // Ping every 30 seconds
}
// Main SSH tunnel connection function
-function connectSSHTunnel(hostConfig: HostConfig, retryAttempt = 0): void {
- const tunnelName = hostConfig.name;
+function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): void {
+ const tunnelName = tunnelConfig.name;
if (manualDisconnects.has(tunnelName)) {
return;
@@ -614,7 +651,7 @@ function connectSSHTunnel(hostConfig: HostConfig, retryAttempt = 0): void {
isRemoteRetry: !!isRetryAfterRemoteClosure
});
- if (!hostConfig || !hostConfig.sourceIP || !hostConfig.sourceUsername || !hostConfig.sourceSSHPort) {
+ if (!tunnelConfig || !tunnelConfig.sourceIP || !tunnelConfig.sourceUsername || !tunnelConfig.sourceSSHPort) {
logger.error(`Invalid connection details for '${tunnelName}'`);
broadcastTunnelStatus(tunnelName, {
connected: false,
@@ -639,7 +676,7 @@ function connectSSHTunnel(hostConfig: HostConfig, retryAttempt = 0): void {
activeTunnels.delete(tunnelName);
if (!activeRetryTimers.has(tunnelName)) {
- handleDisconnect(tunnelName, hostConfig, !manualDisconnects.has(tunnelName));
+ handleDisconnect(tunnelName, tunnelConfig, !manualDisconnects.has(tunnelName));
}
}
}, 15000);
@@ -679,7 +716,7 @@ function connectSSHTunnel(hostConfig: HostConfig, retryAttempt = 0): void {
manualDisconnects.has(tunnelName)
);
- handleDisconnect(tunnelName, hostConfig, !shouldNotRetry, isRemoteHostClosure);
+ handleDisconnect(tunnelName, tunnelConfig, !shouldNotRetry, isRemoteHostClosure);
});
conn.on("close", () => {
@@ -699,7 +736,7 @@ function connectSSHTunnel(hostConfig: HostConfig, retryAttempt = 0): void {
}
if (!activeRetryTimers.has(tunnelName)) {
- handleDisconnect(tunnelName, hostConfig, !manualDisconnects.has(tunnelName));
+ handleDisconnect(tunnelName, tunnelConfig, !manualDisconnects.has(tunnelName));
}
}
});
@@ -713,10 +750,10 @@ function connectSSHTunnel(hostConfig: HostConfig, retryAttempt = 0): void {
}
let tunnelCmd: string;
- if (hostConfig.endpointAuthMethod === "key" && hostConfig.endpointSSHKey) {
- tunnelCmd = `ssh -T -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes -R ${hostConfig.endpointPort}:localhost:${hostConfig.sourcePort} ${hostConfig.endpointUsername}@${hostConfig.endpointIP}`;
+ if (tunnelConfig.endpointAuthMethod === "key" && tunnelConfig.endpointSSHKey) {
+ tunnelCmd = `ssh -T -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes -R ${tunnelConfig.endpointPort}:localhost:${tunnelConfig.sourcePort} ${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}`;
} else {
- tunnelCmd = `sshpass -p '${hostConfig.endpointPassword || ''}' ssh -T -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes -R ${hostConfig.endpointPort}:localhost:${hostConfig.sourcePort} ${hostConfig.endpointUsername}@${hostConfig.endpointIP}`;
+ tunnelCmd = `sshpass -p '${tunnelConfig.endpointPassword || ''}' ssh -T -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes -R ${tunnelConfig.endpointPort}:localhost:${tunnelConfig.sourcePort} ${tunnelConfig.endpointUsername}@${tunnelConfig.endpointIP}`;
}
conn.exec(tunnelCmd, (err, stream) => {
@@ -732,7 +769,7 @@ function connectSSHTunnel(hostConfig: HostConfig, retryAttempt = 0): void {
errorType === ERROR_TYPES.PORT ||
errorType === ERROR_TYPES.PERMISSION;
- handleDisconnect(tunnelName, hostConfig, !shouldNotRetry);
+ handleDisconnect(tunnelName, tunnelConfig, !shouldNotRetry);
return;
}
@@ -740,7 +777,7 @@ function connectSSHTunnel(hostConfig: HostConfig, retryAttempt = 0): void {
setTimeout(() => {
if (!manualDisconnects.has(tunnelName) && activeTunnels.has(tunnelName)) {
- verifyTunnelConnection(tunnelName, hostConfig, false);
+ verifyTunnelConnection(tunnelName, tunnelConfig, false);
}
}, 2000);
@@ -783,11 +820,11 @@ function connectSSHTunnel(hostConfig: HostConfig, retryAttempt = 0): void {
}
if (!activeRetryTimers.has(tunnelName) && !retryExhaustedTunnels.has(tunnelName)) {
- handleDisconnect(tunnelName, hostConfig, !manualDisconnects.has(tunnelName), isLikelyRemoteClosure);
+ handleDisconnect(tunnelName, tunnelConfig, !manualDisconnects.has(tunnelName), isLikelyRemoteClosure);
} else if (retryExhaustedTunnels.has(tunnelName) && isLikelyRemoteClosure) {
retryExhaustedTunnels.delete(tunnelName);
retryCounters.delete(tunnelName);
- handleDisconnect(tunnelName, hostConfig, true, true);
+ handleDisconnect(tunnelName, tunnelConfig, true, true);
}
});
@@ -835,16 +872,16 @@ function connectSSHTunnel(hostConfig: HostConfig, retryAttempt = 0): void {
errorType === ERROR_TYPES.PERMISSION
);
- handleDisconnect(tunnelName, hostConfig, !shouldNotRetry, isRemoteHostClosure);
+ handleDisconnect(tunnelName, tunnelConfig, !shouldNotRetry, isRemoteHostClosure);
}
});
});
});
const connOptions: any = {
- host: hostConfig.sourceIP,
- port: hostConfig.sourceSSHPort,
- username: hostConfig.sourceUsername,
+ host: tunnelConfig.sourceIP,
+ port: tunnelConfig.sourceSSHPort,
+ username: tunnelConfig.sourceUsername,
keepaliveInterval: 5000,
keepaliveCountMax: 10,
readyTimeout: 10000,
@@ -885,13 +922,13 @@ function connectSSHTunnel(hostConfig: HostConfig, retryAttempt = 0): void {
}
};
- if (hostConfig.sourceAuthMethod === "key" && hostConfig.sourceSSHKey) {
- connOptions.privateKey = hostConfig.sourceSSHKey;
- if (hostConfig.sourceKeyPassword) {
- connOptions.passphrase = hostConfig.sourceKeyPassword;
+ if (tunnelConfig.sourceAuthMethod === "key" && tunnelConfig.sourceSSHKey) {
+ connOptions.privateKey = tunnelConfig.sourceSSHKey;
+ if (tunnelConfig.sourceKeyPassword) {
+ connOptions.passphrase = tunnelConfig.sourceKeyPassword;
}
} else {
- connOptions.password = hostConfig.sourcePassword;
+ connOptions.password = tunnelConfig.sourcePassword;
}
conn.connect(connOptions);
@@ -914,24 +951,24 @@ app.get('/status/:tunnelName', (req, res) => {
});
app.post('/connect', (req, res) => {
- const hostConfig: HostConfig = req.body;
+ const tunnelConfig: TunnelConfig = req.body;
- if (!hostConfig || !hostConfig.name) {
+ if (!tunnelConfig || !tunnelConfig.name) {
return res.status(400).json({ error: 'Invalid tunnel configuration' });
}
- const tunnelName = hostConfig.name;
+ const tunnelName = tunnelConfig.name;
// Reset retry state for new connection
manualDisconnects.delete(tunnelName);
retryCounters.delete(tunnelName);
retryExhaustedTunnels.delete(tunnelName);
- // Store host config
- hostConfigs.set(tunnelName, hostConfig);
+ // Store tunnel config
+ tunnelConfigs.set(tunnelName, tunnelConfig);
// Start connection
- connectSSHTunnel(hostConfig, 0);
+ connectSSHTunnel(tunnelConfig, 0);
res.json({ message: 'Connection request received', tunnelName });
});
@@ -958,8 +995,8 @@ app.post('/disconnect', (req, res) => {
manualDisconnect: true
});
- const hostConfig = hostConfigs.get(tunnelName) || null;
- handleDisconnect(tunnelName, hostConfig, false);
+ const tunnelConfig = tunnelConfigs.get(tunnelName) || null;
+ handleDisconnect(tunnelName, tunnelConfig, false);
// Clear manual disconnect flag after a delay
setTimeout(() => {
@@ -972,55 +1009,80 @@ app.post('/disconnect', (req, res) => {
// Auto-start functionality
async function initializeAutoStartTunnels(): Promise {
try {
- // Fetch auto-start tunnels from database
- const response = await axios.get('http://localhost:8081/ssh_tunnel/tunnel?allAutoStart=1', {
+ // Fetch hosts with auto-start tunnel connections
+ const response = await axios.get('http://localhost:8081/ssh/host', {
headers: {
'Content-Type': 'application/json',
'X-Internal-Request': '1'
}
});
- const tunnels = response.data || [];
- const autoStartTunnels = tunnels.filter((tunnel: any) => tunnel.autoStart);
+ const hosts: SSHHost[] = response.data || [];
+ const autoStartTunnels: TunnelConfig[] = [];
+
+ // Process each host and extract auto-start tunnel connections
+ for (const host of hosts) {
+ if (host.enableTunnel && host.tunnelConnections) {
+ for (const tunnelConnection of host.tunnelConnections) {
+ if (tunnelConnection.autoStart) {
+ // Find the endpoint host
+ const endpointHost = hosts.find(h =>
+ h.name === tunnelConnection.endpointHost ||
+ `${h.username}@${h.ip}` === tunnelConnection.endpointHost
+ );
+
+ if (endpointHost) {
+ const tunnelConfig: TunnelConfig = {
+ name: `${host.name || `${host.username}@${host.ip}`}_${tunnelConnection.sourcePort}_${tunnelConnection.endpointPort}`,
+ hostName: host.name || `${host.username}@${host.ip}`,
+ sourceIP: host.ip,
+ sourceSSHPort: host.port,
+ sourceUsername: host.username,
+ sourcePassword: host.password,
+ sourceAuthMethod: host.authType,
+ sourceSSHKey: host.key,
+ sourceKeyPassword: host.keyPassword,
+ sourceKeyType: host.keyType,
+ endpointIP: endpointHost.ip,
+ endpointSSHPort: endpointHost.port,
+ endpointUsername: endpointHost.username,
+ endpointPassword: endpointHost.password,
+ endpointAuthMethod: endpointHost.authType,
+ endpointSSHKey: endpointHost.key,
+ endpointKeyPassword: endpointHost.keyPassword,
+ endpointKeyType: endpointHost.keyType,
+ sourcePort: tunnelConnection.sourcePort,
+ endpointPort: tunnelConnection.endpointPort,
+ maxRetries: tunnelConnection.maxRetries,
+ retryInterval: tunnelConnection.retryInterval * 1000, // Convert to milliseconds
+ autoStart: tunnelConnection.autoStart,
+ isPinned: host.pin
+ };
+
+ autoStartTunnels.push(tunnelConfig);
+ }
+ }
+ }
+ }
+ }
logger.info(`Found ${autoStartTunnels.length} auto-start tunnels`);
- for (const tunnel of autoStartTunnels) {
- const hostConfig: HostConfig = {
- name: tunnel.name,
- sourceIP: tunnel.sourceIP,
- sourceSSHPort: tunnel.sourceSSHPort,
- sourceUsername: tunnel.sourceUsername,
- sourcePassword: tunnel.sourcePassword,
- sourceAuthMethod: tunnel.sourceAuthMethod,
- sourceSSHKey: tunnel.sourceSSHKey,
- sourceKeyPassword: tunnel.sourceKeyPassword,
- sourceKeyType: tunnel.sourceKeyType,
- endpointIP: tunnel.endpointIP,
- endpointSSHPort: tunnel.endpointSSHPort,
- endpointUsername: tunnel.endpointUsername,
- endpointPassword: tunnel.endpointPassword,
- endpointAuthMethod: tunnel.endpointAuthMethod,
- endpointSSHKey: tunnel.endpointSSHKey,
- endpointKeyPassword: tunnel.endpointKeyPassword,
- endpointKeyType: tunnel.endpointKeyType,
- sourcePort: tunnel.sourcePort,
- endpointPort: tunnel.endpointPort,
- maxRetries: tunnel.maxRetries || 3,
- retryInterval: tunnel.retryInterval || 5000,
- autoStart: tunnel.autoStart,
- isPinned: tunnel.isPinned || false
- };
-
- hostConfigs.set(tunnel.name, hostConfig);
+ // Start each auto-start tunnel
+ for (const tunnelConfig of autoStartTunnels) {
+ tunnelConfigs.set(tunnelConfig.name, tunnelConfig);
- // Start the tunnel
+ // Start the tunnel with a delay to avoid overwhelming the system
setTimeout(() => {
- connectSSHTunnel(hostConfig, 0);
- }, 1000); // Stagger startup to avoid overwhelming the system
+ connectSSHTunnel(tunnelConfig, 0);
+ }, 1000);
+ }
+ } catch (error: any) {
+ if (error.response?.status === 401) {
+ logger.warn('Authentication required for auto-start tunnels. Skipping auto-start initialization.');
+ } else {
+ logger.error('Failed to initialize auto-start tunnels:', error.message);
}
- } catch (error) {
- logger.error('Failed to initialize auto-start tunnels:', error);
}
}
@@ -1035,29 +1097,29 @@ app.get('/health', (req, res) => {
// Get all tunnel configurations
app.get('/tunnels', (req, res) => {
- const tunnels = Array.from(hostConfigs.values());
+ const tunnels = Array.from(tunnelConfigs.values());
res.json(tunnels);
});
// Update tunnel configuration
app.put('/tunnel/:name', (req, res) => {
const { name } = req.params;
- const hostConfig: HostConfig = req.body;
+ const tunnelConfig: TunnelConfig = req.body;
- if (!hostConfig || !hostConfig.name) {
+ if (!tunnelConfig || !tunnelConfig.name) {
return res.status(400).json({ error: 'Invalid tunnel configuration' });
}
- hostConfigs.set(name, hostConfig);
+ tunnelConfigs.set(name, tunnelConfig);
// If tunnel is currently connected, disconnect and reconnect with new config
if (activeTunnels.has(name)) {
manualDisconnects.add(name);
- handleDisconnect(name, hostConfig, false);
+ handleDisconnect(name, tunnelConfig, false);
setTimeout(() => {
manualDisconnects.delete(name);
- connectSSHTunnel(hostConfig, 0);
+ connectSSHTunnel(tunnelConfig, 0);
}, 2000);
}
@@ -1071,12 +1133,12 @@ app.delete('/tunnel/:name', (req, res) => {
// Disconnect if active
if (activeTunnels.has(name)) {
manualDisconnects.add(name);
- const hostConfig = hostConfigs.get(name) || null;
- handleDisconnect(name, hostConfig, false);
+ const tunnelConfig = tunnelConfigs.get(name) || null;
+ handleDisconnect(name, tunnelConfig, false);
}
// Remove from configurations
- hostConfigs.delete(name);
+ tunnelConfigs.delete(name);
res.json({ message: 'Tunnel deleted', name });
});
@@ -1084,7 +1146,6 @@ app.delete('/tunnel/:name', (req, res) => {
// Start the server
const PORT = 8083;
app.listen(PORT, () => {
- // Initialize auto-start tunnels after a short delay
setTimeout(() => {
initializeAutoStartTunnels();
}, 2000);