diff --git a/src/apps/ssh/Terminal.jsx b/src/apps/ssh/Terminal.jsx index d674f0d3..3011d35e 100644 --- a/src/apps/ssh/Terminal.jsx +++ b/src/apps/ssh/Terminal.jsx @@ -25,24 +25,10 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde terminalContainer.style.height = `${parentHeight}px`; requestAnimationFrame(() => { - if (fitAddon.current && terminalInstance.current) { - fitAddon.current.fit(); - - if (socketRef.current) { - let { cols, rows } = terminalInstance.current; - const originalCols = cols; - const originalRows = rows; - - cols += 1; - - try { - terminalInstance.current.resize(cols, rows); - socketRef.current.emit("resize", { cols, rows }); - } catch (e) { - terminalInstance.current.resize(originalCols, originalRows); - socketRef.current.emit("resize", { cols: originalCols, rows: originalRows }); - } - } + fitAddon.current.fit(); + if (socketRef.current && terminalInstance.current) { + const { cols, rows } = terminalInstance.current; + socketRef.current.emit("resize", { cols, rows }); } }); }; @@ -64,8 +50,6 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde fontSize: 14, scrollback: 1000, ignoreBracketedPasteMode: true, - fastScrollModifier: 'alt', - fastScrollSensitivity: 5, letterSpacing: 0, lineHeight: 1, padding: 2, @@ -81,19 +65,26 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde { path: "/ssh.io/socket.io", transports: ["websocket", "polling"], + reconnection: true, + reconnectionAttempts: 5, + reconnectionDelay: 1000, + timeout: 20000, } ); socketRef.current = socket; socket.on("connect_error", (error) => { terminalInstance.current.write(`\r\n*** Socket connection error: ${error.message} ***\r\n`); + console.error("Socket connection error:", error); }); socket.on("connect_timeout", () => { terminalInstance.current.write(`\r\n*** Socket connection timeout ***\r\n`); + console.error("Socket connection timeout"); }); socket.on("error", (err) => { + console.error("SSH connection error:", err); const isAuthError = err.toLowerCase().includes("authentication") || err.toLowerCase().includes("auth"); if (isAuthError && !hostConfig.password?.trim() && !hostConfig.sshKey?.trim() && !authModalShown) { authModalShown = true; @@ -103,23 +94,37 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde }); socket.on("connect", () => { + console.log("Socket connected, attempting SSH connection..."); + fitAddon.current.fit(); resizeTerminal(); const { cols, rows } = terminalInstance.current; + // Check for authentication details if (!hostConfig.password?.trim() && !hostConfig.sshKey?.trim()) { + console.log("No authentication provided, showing modal"); setIsNoAuthHidden(false); return; } + // Ensure we have proper SSH config with both key field names for backward compatibility const sshConfig = { ip: hostConfig.ip, user: hostConfig.user, port: Number(hostConfig.port) || 22, password: hostConfig.password?.trim(), - sshKey: hostConfig.sshKey?.trim() + sshKey: hostConfig.sshKey?.trim(), + rsaKey: hostConfig.sshKey?.trim() || hostConfig.rsaKey?.trim(), }; + console.log("Connecting to SSH with config:", { + ip: sshConfig.ip, + user: sshConfig.user, + port: sshConfig.port, + hasPassword: !!sshConfig.password, + hasKey: !!sshConfig.sshKey, + }); + socket.emit("connectToHost", cols, rows, sshConfig); }); @@ -134,138 +139,38 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde terminalInstance.current.write(decoder.decode(new Uint8Array(data))); }); - terminalInstance.current.onData((data) => { - if (data.length === 1) { - socketRef.current.emit("data", data); - return; - } + let isPasting = false; - if (socketRef.current) { + terminalInstance.current.onData((data) => { + if (socketRef.current && socketRef.current.connected) { socketRef.current.emit("data", data); } }); - const getClipboardText = async () => { - try { - // Modern Clipboard API - this will work in secure contexts (HTTPS or localhost) - if (navigator.clipboard && navigator.clipboard.readText) { - try { - return await navigator.clipboard.readText(); - } catch (clipboardErr) { - console.warn("Navigator clipboard API failed:", clipboardErr); - // Continue to fallback methods - } - } - - // Fallback method using document.execCommand - if (document.queryCommandSupported && document.queryCommandSupported('paste')) { - const textarea = document.createElement('textarea'); - textarea.style.position = 'fixed'; - textarea.style.opacity = '0'; - document.body.appendChild(textarea); - textarea.focus(); - - try { - document.execCommand('paste'); - const text = textarea.value; - document.body.removeChild(textarea); - if (text) return text; - } catch (execErr) { - document.body.removeChild(textarea); - console.warn("execCommand paste failed:", execErr); - // Continue to next fallback - } - } - - // Fallback UI prompt for non-secure contexts where clipboard API is restricted - if (!window.location.hostname.includes('localhost') && - !window.location.protocol.includes('https')) { - - // Display input prompt in the terminal itself - terminalInstance.current.write("\r\n\r\nPaste access denied. Please type or paste content here:\r\n"); - - // Use a terminal-based input method - return new Promise(resolve => { - let inputText = ''; - const dataHandler = terminalInstance.current.onData(data => { - // Check for enter key (carriage return) - if (data === '\r') { - terminalInstance.current.write('\r\n'); - dataHandler.dispose(); // Remove the handler - resolve(inputText); - return; - } - - // Handle backspace - if (data === '\x7f') { - if (inputText.length > 0) { - inputText = inputText.slice(0, -1); - terminalInstance.current.write('\b \b'); // Erase the character - } - return; - } - - // Normal character input - inputText += data; - terminalInstance.current.write(data); - }); - }); - } - - throw new Error('No clipboard access methods available'); - } catch (err) { - console.error("Failed to read clipboard contents:", err); - return null; - } - }; - - // Track if paste is in progress to prevent double paste - let pasteInProgress = false; - - terminalInstance.current.attachCustomKeyEventHandler(async (event) => { + terminalInstance.current.attachCustomKeyEventHandler((event) => { if ((event.ctrlKey || event.metaKey) && event.key === "v") { + if (isPasting) return false; + isPasting = true; + event.preventDefault(); - // Prevent double paste execution - if (pasteInProgress || !socketRef.current) return false; - - pasteInProgress = true; - - try { - const text = await getClipboardText(); - if (!text) { - terminalInstance.current.write("\r\nClipboard access denied or empty\r\n"); - pasteInProgress = false; - return false; - } + navigator.clipboard.readText().then((text) => { + text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); - const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n"); - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; + if (socketRef.current && socketRef.current.connected) { + const processedText = text.replace(/\n/g, "\r"); + socketRef.current.emit("data", processedText); - if (i === 0) { - socketRef.current.emit("data", line); - } - else if (i < lines.length - 1) { - socketRef.current.emit("data", "\r" + line); - } - else if (i > 0) { - const endsWithNewline = text.endsWith("\n") || text.endsWith("\r\n") || text.endsWith("\r"); - socketRef.current.emit("data", "\r" + line + (endsWithNewline ? "\r" : "")); - } - else if (lines.length === 1 && (text.endsWith("\n") || text.endsWith("\r\n") || text.endsWith("\r"))) { - socketRef.current.emit("data", line + "\r"); - } + setTimeout(() => { + isPasting = false; + }, 50); + } else { + isPasting = false; } - } catch (err) { - console.error("Failed to process clipboard contents:", err); - } - - // Set timeout to reset paste lock - setTimeout(() => { - pasteInProgress = false; - }, 100); + }).catch((err) => { + console.error("Failed to read clipboard contents:", err); + isPasting = false; + }); return false; } @@ -273,43 +178,11 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde return true; }); - const setClipboardText = (text) => { - try { - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(text); - return true; - } - - if (document.queryCommandSupported && document.queryCommandSupported('copy')) { - const textarea = document.createElement('textarea'); - textarea.value = text; - textarea.style.position = 'fixed'; - textarea.style.opacity = '0'; - document.body.appendChild(textarea); - textarea.select(); - - try { - document.execCommand('copy'); - document.body.removeChild(textarea); - return true; - } catch (e) { - document.body.removeChild(textarea); - return false; - } - } - - return false; - } catch (err) { - console.error("Failed to write to clipboard:", err); - return false; - } - }; - terminalInstance.current.onKey(({ domEvent }) => { if (domEvent.key === "c" && (domEvent.ctrlKey || domEvent.metaKey)) { const selection = terminalInstance.current.getSelection(); if (selection) { - setClipboardText(selection); + navigator.clipboard.writeText(selection); } } }); @@ -323,8 +196,25 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde } }); + socket.on("disconnect", (reason) => { + console.log("Socket disconnected:", reason); + terminalInstance.current.write(`\r\n*** Socket disconnected: ${reason} ***\r\n`); + }); + + socket.on("reconnect", (attemptNumber) => { + console.log("Socket reconnected after", attemptNumber, "attempts"); + terminalInstance.current.write(`\r\n*** Socket reconnected after ${attemptNumber} attempts ***\r\n`); + }); + + socket.on("reconnect_error", (error) => { + console.error("Socket reconnect error:", error); + terminalInstance.current.write(`\r\n*** Socket reconnect error: ${error.message} ***\r\n`); + }); + const pingInterval = setInterval(() => { - socketRef.current.emit("ping"); + if (socketRef.current && socketRef.current.connected) { + socketRef.current.emit("ping"); + } }, 5000); socketRef.current.on("pong", () => { @@ -397,6 +287,7 @@ NewTerminal.propTypes = { user: PropTypes.string.isRequired, password: PropTypes.string, sshKey: PropTypes.string, + rsaKey: PropTypes.string, port: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, }).isRequired, isVisible: PropTypes.bool.isRequired,