Fixed pasting permissions
This commit is contained in:
@@ -25,24 +25,10 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
|
|||||||
terminalContainer.style.height = `${parentHeight}px`;
|
terminalContainer.style.height = `${parentHeight}px`;
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (fitAddon.current && terminalInstance.current) {
|
fitAddon.current.fit();
|
||||||
fitAddon.current.fit();
|
if (socketRef.current && terminalInstance.current) {
|
||||||
|
const { cols, rows } = terminalInstance.current;
|
||||||
if (socketRef.current) {
|
socketRef.current.emit("resize", { cols, rows });
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -64,8 +50,6 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
scrollback: 1000,
|
scrollback: 1000,
|
||||||
ignoreBracketedPasteMode: true,
|
ignoreBracketedPasteMode: true,
|
||||||
fastScrollModifier: 'alt',
|
|
||||||
fastScrollSensitivity: 5,
|
|
||||||
letterSpacing: 0,
|
letterSpacing: 0,
|
||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
padding: 2,
|
padding: 2,
|
||||||
@@ -81,19 +65,26 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
|
|||||||
{
|
{
|
||||||
path: "/ssh.io/socket.io",
|
path: "/ssh.io/socket.io",
|
||||||
transports: ["websocket", "polling"],
|
transports: ["websocket", "polling"],
|
||||||
|
reconnection: true,
|
||||||
|
reconnectionAttempts: 5,
|
||||||
|
reconnectionDelay: 1000,
|
||||||
|
timeout: 20000,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
socketRef.current = socket;
|
socketRef.current = socket;
|
||||||
|
|
||||||
socket.on("connect_error", (error) => {
|
socket.on("connect_error", (error) => {
|
||||||
terminalInstance.current.write(`\r\n*** Socket connection error: ${error.message} ***\r\n`);
|
terminalInstance.current.write(`\r\n*** Socket connection error: ${error.message} ***\r\n`);
|
||||||
|
console.error("Socket connection error:", error);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("connect_timeout", () => {
|
socket.on("connect_timeout", () => {
|
||||||
terminalInstance.current.write(`\r\n*** Socket connection timeout ***\r\n`);
|
terminalInstance.current.write(`\r\n*** Socket connection timeout ***\r\n`);
|
||||||
|
console.error("Socket connection timeout");
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("error", (err) => {
|
socket.on("error", (err) => {
|
||||||
|
console.error("SSH connection error:", err);
|
||||||
const isAuthError = err.toLowerCase().includes("authentication") || err.toLowerCase().includes("auth");
|
const isAuthError = err.toLowerCase().includes("authentication") || err.toLowerCase().includes("auth");
|
||||||
if (isAuthError && !hostConfig.password?.trim() && !hostConfig.sshKey?.trim() && !authModalShown) {
|
if (isAuthError && !hostConfig.password?.trim() && !hostConfig.sshKey?.trim() && !authModalShown) {
|
||||||
authModalShown = true;
|
authModalShown = true;
|
||||||
@@ -103,23 +94,37 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on("connect", () => {
|
socket.on("connect", () => {
|
||||||
|
console.log("Socket connected, attempting SSH connection...");
|
||||||
|
|
||||||
fitAddon.current.fit();
|
fitAddon.current.fit();
|
||||||
resizeTerminal();
|
resizeTerminal();
|
||||||
const { cols, rows } = terminalInstance.current;
|
const { cols, rows } = terminalInstance.current;
|
||||||
|
|
||||||
|
// Check for authentication details
|
||||||
if (!hostConfig.password?.trim() && !hostConfig.sshKey?.trim()) {
|
if (!hostConfig.password?.trim() && !hostConfig.sshKey?.trim()) {
|
||||||
|
console.log("No authentication provided, showing modal");
|
||||||
setIsNoAuthHidden(false);
|
setIsNoAuthHidden(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure we have proper SSH config with both key field names for backward compatibility
|
||||||
const sshConfig = {
|
const sshConfig = {
|
||||||
ip: hostConfig.ip,
|
ip: hostConfig.ip,
|
||||||
user: hostConfig.user,
|
user: hostConfig.user,
|
||||||
port: Number(hostConfig.port) || 22,
|
port: Number(hostConfig.port) || 22,
|
||||||
password: hostConfig.password?.trim(),
|
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);
|
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.write(decoder.decode(new Uint8Array(data)));
|
||||||
});
|
});
|
||||||
|
|
||||||
terminalInstance.current.onData((data) => {
|
let isPasting = false;
|
||||||
if (data.length === 1) {
|
|
||||||
socketRef.current.emit("data", data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (socketRef.current) {
|
terminalInstance.current.onData((data) => {
|
||||||
|
if (socketRef.current && socketRef.current.connected) {
|
||||||
socketRef.current.emit("data", data);
|
socketRef.current.emit("data", data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const getClipboardText = async () => {
|
terminalInstance.current.attachCustomKeyEventHandler((event) => {
|
||||||
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) => {
|
|
||||||
if ((event.ctrlKey || event.metaKey) && event.key === "v") {
|
if ((event.ctrlKey || event.metaKey) && event.key === "v") {
|
||||||
|
if (isPasting) return false;
|
||||||
|
isPasting = true;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
// Prevent double paste execution
|
navigator.clipboard.readText().then((text) => {
|
||||||
if (pasteInProgress || !socketRef.current) return false;
|
text = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
||||||
|
|
||||||
pasteInProgress = true;
|
if (socketRef.current && socketRef.current.connected) {
|
||||||
|
const processedText = text.replace(/\n/g, "\r");
|
||||||
|
socketRef.current.emit("data", processedText);
|
||||||
|
|
||||||
try {
|
setTimeout(() => {
|
||||||
const text = await getClipboardText();
|
isPasting = false;
|
||||||
if (!text) {
|
}, 50);
|
||||||
terminalInstance.current.write("\r\nClipboard access denied or empty\r\n");
|
} else {
|
||||||
pasteInProgress = false;
|
isPasting = false;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
}).catch((err) => {
|
||||||
const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
console.error("Failed to read clipboard contents:", err);
|
||||||
|
isPasting = false;
|
||||||
for (let i = 0; i < lines.length; i++) {
|
});
|
||||||
const line = lines[i];
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to process clipboard contents:", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set timeout to reset paste lock
|
|
||||||
setTimeout(() => {
|
|
||||||
pasteInProgress = false;
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -273,43 +178,11 @@ export const NewTerminal = forwardRef(({ hostConfig, isVisible, setIsNoAuthHidde
|
|||||||
return true;
|
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 }) => {
|
terminalInstance.current.onKey(({ domEvent }) => {
|
||||||
if (domEvent.key === "c" && (domEvent.ctrlKey || domEvent.metaKey)) {
|
if (domEvent.key === "c" && (domEvent.ctrlKey || domEvent.metaKey)) {
|
||||||
const selection = terminalInstance.current.getSelection();
|
const selection = terminalInstance.current.getSelection();
|
||||||
if (selection) {
|
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(() => {
|
const pingInterval = setInterval(() => {
|
||||||
socketRef.current.emit("ping");
|
if (socketRef.current && socketRef.current.connected) {
|
||||||
|
socketRef.current.emit("ping");
|
||||||
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
socketRef.current.on("pong", () => {
|
socketRef.current.on("pong", () => {
|
||||||
@@ -397,6 +287,7 @@ NewTerminal.propTypes = {
|
|||||||
user: PropTypes.string.isRequired,
|
user: PropTypes.string.isRequired,
|
||||||
password: PropTypes.string,
|
password: PropTypes.string,
|
||||||
sshKey: PropTypes.string,
|
sshKey: PropTypes.string,
|
||||||
|
rsaKey: PropTypes.string,
|
||||||
port: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
port: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
isVisible: PropTypes.bool.isRequired,
|
isVisible: PropTypes.bool.isRequired,
|
||||||
|
|||||||
Reference in New Issue
Block a user