Ssh tunnel backup before forwardIn rewrite

This commit is contained in:
LukeGus
2025-07-27 14:21:15 -05:00
parent 5e88f8496e
commit 32945adcd9
11 changed files with 371 additions and 2643 deletions

View File

@@ -1,8 +1,7 @@
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";
import { getSSHHosts, getTunnelStatuses, connectTunnel, disconnectTunnel } from "@/apps/SSH/ssh-axios";
interface ConfigEditorProps {
onSelectView: (view: string) => void;
@@ -76,8 +75,7 @@ export function SSHTunnel({ onSelectView }: ConfigEditorProps): React.ReactEleme
// Poll backend for tunnel statuses
const fetchTunnelStatuses = useCallback(async () => {
try {
const res = await axios.get('http://localhost:8083/status');
const statusData = res.data || {};
const statusData = await getTunnelStatuses();
// Convert tunnel statuses to host statuses
const newHostStatuses: Record<number, HostStatus> = {};
@@ -95,37 +93,17 @@ export function SSHTunnel({ onSelectView }: ConfigEditorProps): React.ReactEleme
});
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';
}
// Just use the first tunnel's status for now - simplify
const firstTunnelStatus = hostTunnelStatuses[0];
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),
connectionState: firstTunnelStatus.status,
statusReason: firstTunnelStatus.reason,
statusErrorType: firstTunnelStatus.errorType,
statusRetryCount: firstTunnelStatus.retryCount,
statusMaxRetries: firstTunnelStatus.maxRetries,
statusNextRetryIn: firstTunnelStatus.nextRetryIn,
statusRetryExhausted: firstTunnelStatus.retryExhausted,
};
} else {
// Set default disconnected status
@@ -159,11 +137,7 @@ export function SSHTunnel({ onSelectView }: ConfigEditorProps): React.ReactEleme
return;
}
// Immediately set to CONNECTING for instant UI feedback
setHostStatuses(prev => ({
...prev,
[hostId]: { ...prev[hostId], connectionState: "connecting" }
}));
// Let the backend handle the status updates
try {
// For each tunnel connection, create a tunnel configuration
@@ -206,14 +180,10 @@ export function SSHTunnel({ onSelectView }: ConfigEditorProps): React.ReactEleme
isPinned: host.pin
};
await axios.post('http://localhost:8083/connect', tunnelConfig);
await connectTunnel(tunnelConfig);
}
} catch (err) {
// Reset status on error
setHostStatuses(prev => ({
...prev,
[hostId]: { ...prev[hostId], connectionState: "failed", statusReason: "Failed to connect" }
}));
// Let the backend handle error status updates
}
};
@@ -221,17 +191,13 @@ export function SSHTunnel({ onSelectView }: ConfigEditorProps): React.ReactEleme
const host = hosts.find(h => h.id === hostId);
if (!host) return;
// Immediately set to DISCONNECTING for instant UI feedback
setHostStatuses(prev => ({
...prev,
[hostId]: { ...prev[hostId], connectionState: "disconnecting" }
}));
// Let the backend handle the status updates
try {
// 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 });
await disconnectTunnel(tunnelName);
}
} catch (err) {
// Silent error handling

View File

@@ -13,6 +13,7 @@ const CONNECTION_STATES = {
FAILED: "failed",
UNSTABLE: "unstable",
RETRYING: "retrying",
WAITING: "waiting",
DISCONNECTING: "disconnecting"
};
@@ -77,6 +78,7 @@ export function SSHTunnelObject({
case "CONNECTING":
case "VERIFYING":
case "RETRYING":
case "WAITING":
return "bg-yellow-500";
case "FAILED":
return "bg-red-500";
@@ -88,27 +90,12 @@ export function SSHTunnelObject({
};
const getStatusText = (state: string) => {
const upperState = state.toUpperCase();
switch (upperState) {
case "CONNECTED":
return "Connected";
case "CONNECTING":
return "Connecting";
case "VERIFYING":
return "Verifying";
case "FAILED":
return "Failed";
case "UNSTABLE":
return "Unstable";
case "RETRYING":
return "Retrying";
default:
return "Disconnected";
}
// Just capitalize the first letter of the status from backend
return state.charAt(0).toUpperCase() + state.slice(1);
};
const isConnected = connectionState === "CONNECTED" || connectionState === "connected";
const isConnecting = ["CONNECTING", "VERIFYING", "RETRYING", "connecting", "verifying", "retrying"].includes(connectionState);
const isConnecting = ["CONNECTING", "VERIFYING", "RETRYING", "WAITING", "connecting", "verifying", "retrying", "waiting"].includes(connectionState);
const isDisconnecting = connectionState === "DISCONNECTING" || connectionState === "disconnecting";
return (
@@ -193,9 +180,9 @@ export function SSHTunnelObject({
)}
{/* Retry Info */}
{connectionState === "RETRYING" && statusRetryCount && statusMaxRetries && (
{(connectionState === "retrying" || connectionState === "waiting") && statusRetryCount && statusMaxRetries && (
<div className="mb-2 text-xs text-yellow-600 bg-yellow-500/10 rounded px-2 py-1 border border-yellow-500/20">
Retry {statusRetryCount}/{statusMaxRetries}
{connectionState === "waiting" ? "Waiting" : "Retry"} {statusRetryCount}/{statusMaxRetries}
{statusNextRetryIn && (
<span> Next retry in {statusNextRetryIn}s</span>
)}
@@ -213,7 +200,7 @@ export function SSHTunnelObject({
{isConnecting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Connecting...
{getStatusText(connectionState)}...
</>
) : isConnected ? (
"Connected"

View File

@@ -44,6 +44,43 @@ interface SSHHost {
updatedAt: string;
}
interface TunnelConfig {
name: string;
hostName: string;
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;
sourcePort: number;
endpointPort: number;
maxRetries: number;
retryInterval: number;
autoStart: boolean;
isPinned: boolean;
}
interface TunnelStatus {
status: string;
reason?: string;
errorType?: string;
retryCount?: number;
maxRetries?: number;
nextRetryIn?: number;
retryExhausted?: boolean;
}
// Determine the base URL based on environment
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
const baseURL = isLocalhost ? 'http://localhost:8081' : window.location.origin;
@@ -56,6 +93,13 @@ const api = axios.create({
},
});
// Create tunnel API instance
const tunnelApi = axios.create({
headers: {
'Content-Type': 'application/json',
},
});
function getCookie(name: string): string | undefined {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
@@ -71,6 +115,14 @@ api.interceptors.request.use((config) => {
return config;
});
tunnelApi.interceptors.request.use((config) => {
const token = getCookie('jwt'); // Adjust based on your token storage
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Get all SSH hosts
export async function getSSHHosts(): Promise<SSHHost[]> {
try {
@@ -119,22 +171,22 @@ export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
// Handle file upload for SSH key
if (hostData.authType === 'key' && hostData.key instanceof File) {
const formData = new FormData();
// Add the file
formData.append('key', hostData.key);
// Add all other data as JSON string
const dataWithoutFile = { ...submitData };
delete dataWithoutFile.key;
formData.append('data', JSON.stringify(dataWithoutFile));
// Submit with FormData
const response = await api.post('/ssh/host', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
} else {
// Submit with JSON
@@ -182,17 +234,17 @@ export async function updateSSHHost(hostId: number, hostData: SSHHostData): Prom
if (hostData.authType === 'key' && hostData.key instanceof File) {
const formData = new FormData();
formData.append('key', hostData.key);
const dataWithoutFile = { ...submitData };
delete dataWithoutFile.key;
formData.append('data', JSON.stringify(dataWithoutFile));
const response = await api.put(`/ssh/host/${hostId}`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
} else {
const response = await api.put(`/ssh/host/${hostId}`, submitData);
@@ -226,4 +278,45 @@ export async function getSSHHostById(hostId: number): Promise<SSHHost> {
}
}
export { api };
// Tunnel-related functions
// Get tunnel statuses
export async function getTunnelStatuses(): Promise<Record<string, TunnelStatus>> {
try {
// Determine the tunnel API URL based on environment
const tunnelUrl = isLocalhost ? 'http://localhost:8083/status' : `${baseURL}/ssh_tunnel/status`;
const response = await tunnelApi.get(tunnelUrl);
return response.data || {};
} catch (error) {
console.error('Error fetching tunnel statuses:', error);
throw error;
}
}
// Connect tunnel
export async function connectTunnel(tunnelConfig: TunnelConfig): Promise<any> {
try {
// Determine the tunnel API URL based on environment
const tunnelUrl = isLocalhost ? 'http://localhost:8083/connect' : `${baseURL}/ssh_tunnel/connect`;
const response = await tunnelApi.post(tunnelUrl, tunnelConfig);
return response.data;
} catch (error) {
console.error('Error connecting tunnel:', error);
throw error;
}
}
// Disconnect tunnel
export async function disconnectTunnel(tunnelName: string): Promise<any> {
try {
// Determine the tunnel API URL based on environment
const tunnelUrl = isLocalhost ? 'http://localhost:8083/disconnect' : `${baseURL}/ssh_tunnel/disconnect`;
const response = await tunnelApi.post(tunnelUrl, { tunnelName });
return response.data;
} catch (error) {
console.error('Error disconnecting tunnel:', error);
throw error;
}
}
export { api };