Merge pull request #57 from LukeGus/dev-1.01

Dev 1.01
This commit was merged in pull request #57.
This commit is contained in:
Karmaa
2025-07-31 22:38:27 -05:00
committed by GitHub
4 changed files with 129 additions and 52 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ on:
jobs:
build:
runs-on: ubuntu-latest
runs-on: [self-hosted, linux]
steps:
- name: Checkout repository
uses: actions/checkout@v4
+19 -3
View File
@@ -20,10 +20,15 @@ export const SSHTerminal = forwardRef<any, SSHTerminalProps>(function SSHTermina
const webSocketRef = useRef<WebSocket | null>(null);
const resizeTimeout = useRef<NodeJS.Timeout | null>(null);
const wasDisconnectedBySSH = useRef(false);
const pingIntervalRef = useRef<NodeJS.Timeout | null>(null);
const [visible, setVisible] = useState(false);
useImperativeHandle(ref, () => ({
disconnect: () => {
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current);
pingIntervalRef.current = null;
}
webSocketRef.current?.close();
},
fit: () => {
@@ -97,6 +102,12 @@ export const SSHTerminal = forwardRef<any, SSHTerminalProps>(function SSHTermina
terminal.onData((data) => {
ws.send(JSON.stringify({type: 'input', data}));
});
pingIntervalRef.current = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({type: 'ping'}));
}
}, 30000);
});
ws.addEventListener('message', (event) => {
@@ -104,12 +115,13 @@ export const SSHTerminal = forwardRef<any, SSHTerminalProps>(function SSHTermina
const msg = JSON.parse(event.data);
if (msg.type === 'data') terminal.write(msg.data);
else if (msg.type === 'error') terminal.writeln(`\r\n[ERROR] ${msg.message}`);
else if (msg.type === 'connected') {}
else if (msg.type === 'disconnected') {
else if (msg.type === 'connected') {
} else if (msg.type === 'disconnected') {
wasDisconnectedBySSH.current = true;
terminal.writeln(`\r\n[${msg.message || 'Disconnected'}]`);
}
} catch (_) {}
} catch (_) {
}
});
ws.addEventListener('close', () => {
@@ -126,6 +138,10 @@ export const SSHTerminal = forwardRef<any, SSHTerminalProps>(function SSHTermina
return () => {
resizeObserver.disconnect();
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current);
pingIntervalRef.current = null;
}
webSocketRef.current?.close();
};
}, [xtermRef, terminal, hostConfig]);
+62 -6
View File
@@ -33,6 +33,7 @@ const logger = {
wss.on('connection', (ws: WebSocket) => {
let sshConn: Client | null = null;
let sshStream: ClientChannel | null = null;
let pingInterval: NodeJS.Timeout | null = null;
ws.on('close', () => {
cleanupSSH();
@@ -67,6 +68,10 @@ wss.on('connection', (ws: WebSocket) => {
if (sshStream) sshStream.write(data);
break;
case 'ping':
ws.send(JSON.stringify({type: 'pong'}));
break;
default:
logger.warn('Unknown message type: ' + type);
}
@@ -109,7 +114,16 @@ wss.on('connection', (ws: WebSocket) => {
sshConn = new Client();
const connectionTimeout = setTimeout(() => {
if (sshConn) {
logger.error('SSH connection timeout');
ws.send(JSON.stringify({type: 'error', message: 'SSH connection timeout'}));
cleanupSSH(connectionTimeout);
}
}, 15000);
sshConn.on('ready', () => {
clearTimeout(connectionTimeout);
const pseudoTtyOpts: PseudoTtyOptions = {
term: 'xterm-256color',
cols,
@@ -135,19 +149,32 @@ wss.on('connection', (ws: WebSocket) => {
});
stream.on('close', () => {
cleanupSSH();
cleanupSSH(connectionTimeout);
});
stream.on('error', (err: Error) => {
logger.error('SSH stream error: ' + err.message);
const isConnectionError = err.message.includes('ECONNRESET') ||
err.message.includes('EPIPE') ||
err.message.includes('ENOTCONN') ||
err.message.includes('ETIMEDOUT');
if (isConnectionError) {
ws.send(JSON.stringify({type: 'disconnected', message: 'Connection lost'}));
} else {
ws.send(JSON.stringify({type: 'error', message: 'SSH stream error: ' + err.message}));
}
});
setupPingInterval();
ws.send(JSON.stringify({type: 'connected', message: 'SSH connected'}));
});
});
sshConn.on('error', (err: Error) => {
clearTimeout(connectionTimeout);
logger.error('SSH connection error: ' + err.message);
let errorMessage = 'SSH error: ' + err.message;
@@ -163,23 +190,30 @@ wss.on('connection', (ws: WebSocket) => {
errorMessage = 'SSH error: Connection refused. The server may not be running or the port may be incorrect.';
} else if (err.message.includes('ETIMEDOUT')) {
errorMessage = 'SSH error: Connection timed out. Check your network connection and server availability.';
} else if (err.message.includes('ECONNRESET') || err.message.includes('EPIPE')) {
errorMessage = 'SSH error: Connection was reset. This may be due to network issues or server timeout.';
} else if (err.message.includes('authentication failed') || err.message.includes('Permission denied')) {
errorMessage = 'SSH error: Authentication failed. Please check your username and password/key.';
}
ws.send(JSON.stringify({type: 'error', message: errorMessage}));
cleanupSSH();
cleanupSSH(connectionTimeout);
});
sshConn.on('close', () => {
cleanupSSH();
clearTimeout(connectionTimeout);
cleanupSSH(connectionTimeout);
});
const connectConfig: any = {
host: ip,
port,
username,
keepaliveInterval: 5000,
keepaliveCountMax: 10,
keepaliveInterval: 30000,
keepaliveCountMax: 3,
readyTimeout: 10000,
tcpKeepAlive: true,
tcpKeepAliveInitialDelay: 30000,
algorithms: {
kex: [
@@ -242,7 +276,16 @@ wss.on('connection', (ws: WebSocket) => {
}
}
function cleanupSSH() {
function cleanupSSH(timeoutId?: NodeJS.Timeout) {
if (timeoutId) {
clearTimeout(timeoutId);
}
if (pingInterval) {
clearInterval(pingInterval);
pingInterval = null;
}
if (sshStream) {
try {
sshStream.end();
@@ -261,4 +304,17 @@ wss.on('connection', (ws: WebSocket) => {
sshConn = null;
}
}
function setupPingInterval() {
pingInterval = setInterval(() => {
if (sshConn && sshStream) {
try {
sshStream.write('\x00');
} catch (e: any) {
logger.error('SSH keepalive failed: ' + e.message);
cleanupSSH();
}
}
}, 60000);
}
});
+15 -10
View File
@@ -336,7 +336,6 @@ function handleDisconnect(tunnelName: string, tunnelConfig: TunnelConfig | null,
}
if (retryExhaustedTunnels.has(tunnelName)) {
broadcastTunnelStatus(tunnelName, {
connected: false,
@@ -571,6 +570,10 @@ function verifyTunnelConnection(tunnelName: string, tunnelConfig: TunnelConfig,
port: tunnelConfig.sourceSSHPort,
username: tunnelConfig.sourceUsername,
readyTimeout: 10000,
keepaliveInterval: 30000,
keepaliveCountMax: 3,
tcpKeepAlive: true,
tcpKeepAliveInitialDelay: 30000,
algorithms: {
kex: [
'diffie-hellman-group14-sha256',
@@ -692,7 +695,7 @@ function setupPingInterval(tunnelName: string, tunnelConfig: TunnelConfig): void
handleDisconnect(tunnelName, tunnelConfig, !manualDisconnects.has(tunnelName));
});
});
}, 30000);
}, 60000);
}
function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): void {
@@ -891,11 +894,9 @@ function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): void {
});
stream.stdout?.on("data", (data: Buffer) => {
// Ignore stdout data
});
stream.on("error", (err: Error) => {
// Ignore stream errors
});
stream.stderr.on("data", (data) => {
@@ -909,10 +910,11 @@ function connectSSHTunnel(tunnelConfig: TunnelConfig, retryAttempt = 0): void {
host: tunnelConfig.sourceIP,
port: tunnelConfig.sourceSSHPort,
username: tunnelConfig.sourceUsername,
keepaliveInterval: 5000,
keepaliveCountMax: 10,
keepaliveInterval: 30000,
keepaliveCountMax: 3,
readyTimeout: 10000,
tcpKeepAlive: true,
tcpKeepAliveInitialDelay: 30000,
algorithms: {
kex: [
'diffie-hellman-group14-sha256',
@@ -1025,10 +1027,11 @@ function killRemoteTunnelByMarker(tunnelConfig: TunnelConfig, tunnelName: string
host: tunnelConfig.sourceIP,
port: tunnelConfig.sourceSSHPort,
username: tunnelConfig.sourceUsername,
keepaliveInterval: 5000,
keepaliveCountMax: 10,
keepaliveInterval: 30000,
keepaliveCountMax: 3,
readyTimeout: 10000,
tcpKeepAlive: true,
tcpKeepAliveInitialDelay: 30000,
algorithms: {
kex: [
'diffie-hellman-group14-sha256',
@@ -1087,8 +1090,10 @@ function killRemoteTunnelByMarker(tunnelConfig: TunnelConfig, tunnelName: string
conn.end();
callback();
});
stream.on('data', () => {});
stream.stderr.on('data', () => {});
stream.on('data', () => {
});
stream.stderr.on('data', () => {
});
});
});
conn.on('error', (err) => {