Fixed pasting permissions

This commit is contained in:
LukeGus
2025-03-23 01:52:24 -05:00
parent 71ffb74c28
commit b80e71015e

View File

@@ -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,