diff --git a/docker/nginx.conf b/docker/nginx.conf index 1ffb8fa5..3ca07e3a 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -71,6 +71,16 @@ http { proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; + proxy_read_timeout 300s; + proxy_send_timeout 300s; + proxy_connect_timeout 75s; + + proxy_set_header Connection ""; + proxy_http_version 1.1; + + proxy_buffering off; + proxy_request_buffering off; + proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; diff --git a/src/backend/ssh/terminal.ts b/src/backend/ssh/terminal.ts index bf49f1e0..7b23cac0 100644 --- a/src/backend/ssh/terminal.ts +++ b/src/backend/ssh/terminal.ts @@ -4,6 +4,9 @@ import chalk from 'chalk'; const wss = new WebSocketServer({port: 8082}); + + + const sshIconSymbol = '🖥️'; const getTimeStamp = (): string => chalk.gray(`[${new Date().toLocaleTimeString()}]`); const formatMessage = (level: string, colorFn: chalk.Chalk, message: string): string => { @@ -30,16 +33,22 @@ 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(); }); ws.on('message', (msg: RawData) => { + + let parsed: any; try { parsed = JSON.parse(msg.toString()); @@ -132,34 +141,13 @@ wss.on('connection', (ws: WebSocket) => { sshConn.on('ready', () => { clearTimeout(connectionTimeout); - const pseudoTtyOpts: PseudoTtyOptions = { - term: 'xterm-256color', - cols, - rows, - modes: { - ECHO: 1, - ECHOCTL: 0, - ICANON: 1, - ISIG: 1, - ICRNL: 1, - IXON: 1, - IXOFF: 0, - ISTRIP: 0, - OPOST: 1, - ONLCR: 1, - OCRNL: 0, - ONOCR: 0, - ONLRET: 0, - CS7: 0, - CS8: 1, - PARENB: 0, - PARODD: 0, - TTY_OP_ISPEED: 38400, - TTY_OP_OSPEED: 38400, - } - }; - sshConn!.shell(pseudoTtyOpts, (err, stream) => { + + sshConn!.shell({ + rows: data.rows, + cols: data.cols, + term: 'xterm-256color' + } as PseudoTtyOptions, (err, stream) => { if (err) { logger.error('Shell error: ' + err.message); ws.send(JSON.stringify({type: 'error', message: 'Shell error: ' + err.message})); @@ -168,34 +156,18 @@ wss.on('connection', (ws: WebSocket) => { sshStream = stream; - stream.on('data', (chunk: Buffer) => { - let data: string; - try { - data = chunk.toString('utf8'); - } catch (e) { - data = chunk.toString('binary'); - } - - ws.send(JSON.stringify({type: 'data', data})); + stream.on('data', (data: Buffer) => { + ws.send(JSON.stringify({type: 'data', data: data.toString()})); }); stream.on('close', () => { - cleanupSSH(connectionTimeout); + + ws.send(JSON.stringify({type: 'disconnected', message: 'Connection lost'})); }); 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})); - } + ws.send(JSON.stringify({type: 'error', message: 'SSH stream error: ' + err.message})); }); setupPingInterval(); @@ -233,9 +205,12 @@ wss.on('connection', (ws: WebSocket) => { sshConn.on('close', () => { clearTimeout(connectionTimeout); + cleanupSSH(connectionTimeout); }); + + const connectConfig: any = { host: ip, port, @@ -245,6 +220,7 @@ wss.on('connection', (ws: WebSocket) => { readyTimeout: 10000, tcpKeepAlive: true, tcpKeepAliveInitialDelay: 30000, + env: { TERM: 'xterm-256color', LANG: 'en_US.UTF-8', @@ -374,4 +350,6 @@ wss.on('connection', (ws: WebSocket) => { } }, 60000); } + + }); diff --git a/src/ui/apps/Terminal/Terminal.tsx b/src/ui/apps/Terminal/Terminal.tsx index f1b899b6..62dc492c 100644 --- a/src/ui/apps/Terminal/Terminal.tsx +++ b/src/ui/apps/Terminal/Terminal.tsx @@ -26,6 +26,7 @@ export const Terminal = forwardRef(function SSHTerminal( const [visible, setVisible] = useState(false); const isVisibleRef = useRef(false); + const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null); const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null); const notifyTimerRef = useRef(null); @@ -115,6 +116,50 @@ export const Terminal = forwardRef(function SSHTerminal( return getCookie("rightClickCopyPaste") === "true" } + + + function setupWebSocketListeners(ws: WebSocket, cols: number, rows: number) { + ws.addEventListener('open', () => { + + ws.send(JSON.stringify({type: 'connectToHost', data: {cols, rows, hostConfig}})); + 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) => { + try { + 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') { + wasDisconnectedBySSH.current = true; + terminal.writeln(`\r\n[${msg.message || 'Disconnected'}]`); + } + } catch (error) { + } + }); + + ws.addEventListener('close', () => { + if (!wasDisconnectedBySSH.current) { + terminal.writeln('\r\n[Connection closed]'); + } + }); + + ws.addEventListener('error', () => { + terminal.writeln('\r\n[Connection error]'); + }); + } + async function writeTextToClipboard(text: string): Promise { try { if (navigator.clipboard && navigator.clipboard.writeText) { @@ -222,6 +267,9 @@ export const Terminal = forwardRef(function SSHTerminal( if (terminal) scheduleNotify(terminal.cols, terminal.rows); hardRefresh(); setVisible(true); + if (terminal && !splitScreen) { + terminal.focus(); + } }, 0); const cols = terminal.cols; @@ -234,38 +282,7 @@ export const Terminal = forwardRef(function SSHTerminal( webSocketRef.current = ws; wasDisconnectedBySSH.current = false; - ws.addEventListener('open', () => { - ws.send(JSON.stringify({type: 'connectToHost', data: {cols, rows, hostConfig}})); - 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) => { - try { - 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') { - wasDisconnectedBySSH.current = true; - terminal.writeln(`\r\n[${msg.message || 'Disconnected'}]`); - } - } catch (error) { - } - }); - - ws.addEventListener('close', () => { - if (!wasDisconnectedBySSH.current) terminal.writeln('\r\n[Connection closed]'); - }); - ws.addEventListener('error', () => { - terminal.writeln('\r\n[Connection error]'); - }); + setupWebSocketListeners(ws, cols, rows); }, 300); }); @@ -288,9 +305,18 @@ export const Terminal = forwardRef(function SSHTerminal( fitAddonRef.current?.fit(); if (terminal) scheduleNotify(terminal.cols, terminal.rows); hardRefresh(); + if (terminal && !splitScreen) { + terminal.focus(); + } }, 0); + + if (terminal && !splitScreen) { + setTimeout(() => { + terminal.focus(); + }, 100); + } } - }, [isVisible]); + }, [isVisible, splitScreen, terminal]); useEffect(() => { if (!fitAddonRef.current) return; @@ -298,12 +324,23 @@ export const Terminal = forwardRef(function SSHTerminal( fitAddonRef.current?.fit(); if (terminal) scheduleNotify(terminal.cols, terminal.rows); hardRefresh(); + if (terminal && !splitScreen && isVisible) { + terminal.focus(); + } }, 0); - }, [splitScreen]); + }, [splitScreen, isVisible, terminal]); return ( -
+
{ + if (terminal && !splitScreen) { + terminal.focus(); + } + }} + /> ); });