Fix credentials not sending right and terminals/file manager not connecting

This commit is contained in:
LukeGus
2025-09-28 21:26:42 -05:00
parent f59faaf887
commit 58b28cab50
5 changed files with 724 additions and 387 deletions

View File

@@ -1124,36 +1124,98 @@ async function deploySSHKeyToHost(
connectionTimeout = setTimeout(() => {
conn.destroy();
resolve({ success: false, error: "Connection timeout" });
}, 30000);
}, 120000);
conn.on("ready", async () => {
clearTimeout(connectionTimeout);
authLogger.info("SSH connection established for key deployment", {
host: hostConfig.ip,
username: hostConfig.username,
authType: hostConfig.authType,
});
try {
authLogger.info("Ensuring .ssh directory exists", { host: hostConfig.ip });
await new Promise<void>((resolveCmd, rejectCmd) => {
conn.exec("mkdir -p ~/.ssh && chmod 700 ~/.ssh", (err, stream) => {
if (err) return rejectCmd(err);
const cmdTimeout = setTimeout(() => {
rejectCmd(new Error("mkdir command timeout"));
}, 10000); // Reduced to 10 seconds
// Use a more robust command that handles existing directories
conn.exec("test -d ~/.ssh || mkdir -p ~/.ssh; chmod 700 ~/.ssh", (err, stream) => {
if (err) {
clearTimeout(cmdTimeout);
authLogger.error("mkdir command error", { host: hostConfig.ip, error: err.message });
return rejectCmd(err);
}
stream.on("close", (code) => {
clearTimeout(cmdTimeout);
authLogger.info("mkdir command completed", { host: hostConfig.ip, code });
if (code === 0) {
resolveCmd();
} else {
rejectCmd(new Error(`mkdir command failed with code ${code}`));
}
});
stream.on("data", (data) => {
authLogger.info("mkdir command output", { host: hostConfig.ip, output: data.toString() });
});
});
});
const keyExists = await new Promise<boolean>(
(resolveCheck, rejectCheck) => {
const keyPattern = publicKey.split(" ")[1];
const checkTimeout = setTimeout(() => {
rejectCheck(new Error("Key check timeout"));
}, 5000); // Reduced to 5 seconds
// Parse public key - handle both JSON and plain text formats
let actualPublicKey = publicKey;
try {
// Try to parse as JSON first
const parsed = JSON.parse(publicKey);
if (parsed.data) {
actualPublicKey = parsed.data;
authLogger.info("Parsed public key from JSON format", { host: hostConfig.ip });
}
} catch (e) {
// Not JSON, use as-is
authLogger.info("Using public key as plain text", { host: hostConfig.ip });
}
// Validate public key format
const keyParts = actualPublicKey.trim().split(" ");
if (keyParts.length < 2) {
clearTimeout(checkTimeout);
authLogger.error("Invalid public key format", { host: hostConfig.ip, publicKey: actualPublicKey.substring(0, 50) + "..." });
return rejectCheck(new Error("Invalid public key format - must contain at least 2 parts"));
}
const keyPattern = keyParts[1];
authLogger.info("Checking for existing key", { host: hostConfig.ip, keyPattern: keyPattern.substring(0, 20) + "..." });
// Use a simpler approach - just check if the file exists and has content
conn.exec(
`grep -q "${keyPattern}" ~/.ssh/authorized_keys 2>/dev/null`,
`if [ -f ~/.ssh/authorized_keys ]; then grep -F "${keyPattern}" ~/.ssh/authorized_keys >/dev/null 2>&1; echo $?; else echo 1; fi`,
(err, stream) => {
if (err) return rejectCheck(err);
if (err) {
clearTimeout(checkTimeout);
authLogger.error("Key check error", { host: hostConfig.ip, error: err.message });
return rejectCheck(err);
}
let output = '';
stream.on('data', (data) => {
output += data.toString();
});
stream.on("close", (code) => {
resolveCheck(code === 0);
clearTimeout(checkTimeout);
const exists = output.trim() === '0';
authLogger.info("Key check completed", { host: hostConfig.ip, code, output: output.trim(), exists });
resolveCheck(exists);
});
},
);
@@ -1166,14 +1228,40 @@ async function deploySSHKeyToHost(
return;
}
authLogger.info("Adding SSH key to authorized_keys", { host: hostConfig.ip });
await new Promise<void>((resolveAdd, rejectAdd) => {
const escapedKey = publicKey.replace(/'/g, "'\\''");
const addTimeout = setTimeout(() => {
rejectAdd(new Error("Key add timeout"));
}, 10000); // Reduced to 10 seconds
// Parse public key - handle both JSON and plain text formats
let actualPublicKey = publicKey;
try {
// Try to parse as JSON first
const parsed = JSON.parse(publicKey);
if (parsed.data) {
actualPublicKey = parsed.data;
}
} catch (e) {
// Not JSON, use as-is
}
// Use printf instead of echo for more reliable key addition
const escapedKey = actualPublicKey.replace(/\\/g, '\\\\').replace(/'/g, "'\\''");
authLogger.info("Adding key to authorized_keys", { host: hostConfig.ip, keyLength: actualPublicKey.length });
conn.exec(
`echo '${escapedKey}' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`,
`printf '%s\\n' '${escapedKey}' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`,
(err, stream) => {
if (err) return rejectAdd(err);
if (err) {
clearTimeout(addTimeout);
authLogger.error("Key add error", { host: hostConfig.ip, error: err.message });
return rejectAdd(err);
}
stream.on("close", (code) => {
clearTimeout(addTimeout);
authLogger.info("Key add completed", { host: hostConfig.ip, code });
if (code === 0) {
resolveAdd();
} else {
@@ -1182,20 +1270,61 @@ async function deploySSHKeyToHost(
);
}
});
stream.on("data", (data) => {
authLogger.info("Key add output", { host: hostConfig.ip, output: data.toString() });
});
},
);
});
authLogger.info("Verifying key deployment", { host: hostConfig.ip });
const verifySuccess = await new Promise<boolean>(
(resolveVerify, rejectVerify) => {
const keyPattern = publicKey.split(" ")[1];
const verifyTimeout = setTimeout(() => {
rejectVerify(new Error("Key verification timeout"));
}, 5000); // Reduced to 5 seconds
// Parse public key - handle both JSON and plain text formats
let actualPublicKey = publicKey;
try {
// Try to parse as JSON first
const parsed = JSON.parse(publicKey);
if (parsed.data) {
actualPublicKey = parsed.data;
}
} catch (e) {
// Not JSON, use as-is
}
// Use the same key pattern extraction as above
const keyParts = actualPublicKey.trim().split(" ");
if (keyParts.length < 2) {
clearTimeout(verifyTimeout);
authLogger.error("Invalid public key format for verification", { host: hostConfig.ip, publicKey: actualPublicKey.substring(0, 50) + "..." });
return rejectVerify(new Error("Invalid public key format - must contain at least 2 parts"));
}
const keyPattern = keyParts[1];
conn.exec(
`grep -q "${keyPattern}" ~/.ssh/authorized_keys`,
`grep -F "${keyPattern}" ~/.ssh/authorized_keys >/dev/null 2>&1; echo $?`,
(err, stream) => {
if (err) return rejectVerify(err);
if (err) {
clearTimeout(verifyTimeout);
authLogger.error("Key verification error", { host: hostConfig.ip, error: err.message });
return rejectVerify(err);
}
let output = '';
stream.on('data', (data) => {
output += data.toString();
});
stream.on("close", (code) => {
resolveVerify(code === 0);
clearTimeout(verifyTimeout);
const verified = output.trim() === '0';
authLogger.info("Key verification completed", { host: hostConfig.ip, code, output: output.trim(), verified });
resolveVerify(verified);
});
},
);
@@ -1223,7 +1352,32 @@ async function deploySSHKeyToHost(
conn.on("error", (err) => {
clearTimeout(connectionTimeout);
resolve({ success: false, error: err.message });
let errorMessage = err.message;
// Log detailed error information for debugging
authLogger.error("SSH connection failed during key deployment", {
host: hostConfig.ip,
username: hostConfig.username,
authType: hostConfig.authType,
hasPassword: !!hostConfig.password,
hasPrivateKey: !!hostConfig.privateKey,
error: err.message,
errorCode: (err as any).code,
});
if (err.message.includes("All configured authentication methods failed")) {
errorMessage = "Authentication failed. Please check your credentials and ensure the SSH service is running.";
} else if (err.message.includes("ENOTFOUND") || err.message.includes("ENOENT")) {
errorMessage = "Could not resolve hostname or connect to server.";
} else if (err.message.includes("ECONNREFUSED")) {
errorMessage = "Connection refused. The server may not be running or the port may be incorrect.";
} else if (err.message.includes("ETIMEDOUT")) {
errorMessage = "Connection timed out. Check your network connection and server availability.";
} else if (err.message.includes("authentication failed") || err.message.includes("Permission denied")) {
errorMessage = "Authentication failed. Please check your username and password/key.";
}
resolve({ success: false, error: errorMessage });
});
try {
@@ -1231,26 +1385,101 @@ async function deploySSHKeyToHost(
host: hostConfig.ip,
port: hostConfig.port || 22,
username: hostConfig.username,
readyTimeout: 60000,
keepaliveInterval: 30000,
keepaliveCountMax: 3,
tcpKeepAlive: true,
tcpKeepAliveInitialDelay: 30000,
algorithms: {
kex: [
"diffie-hellman-group14-sha256",
"diffie-hellman-group14-sha1",
"diffie-hellman-group1-sha1",
"diffie-hellman-group-exchange-sha256",
"diffie-hellman-group-exchange-sha1",
"ecdh-sha2-nistp256",
"ecdh-sha2-nistp384",
"ecdh-sha2-nistp521",
],
cipher: [
"aes128-ctr",
"aes192-ctr",
"aes256-ctr",
"aes128-gcm@openssh.com",
"aes256-gcm@openssh.com",
"aes128-cbc",
"aes192-cbc",
"aes256-cbc",
"3des-cbc",
],
hmac: [
"hmac-sha2-256-etm@openssh.com",
"hmac-sha2-512-etm@openssh.com",
"hmac-sha2-256",
"hmac-sha2-512",
"hmac-sha1",
"hmac-md5",
],
compress: ["none", "zlib@openssh.com", "zlib"],
},
};
if (hostConfig.authType === "password" && hostConfig.password) {
connectionConfig.password = hostConfig.password;
} else if (hostConfig.authType === "key" && hostConfig.privateKey) {
connectionConfig.privateKey = hostConfig.privateKey;
if (hostConfig.keyPassword) {
connectionConfig.passphrase = hostConfig.keyPassword;
try {
if (
!hostConfig.privateKey.includes("-----BEGIN") ||
!hostConfig.privateKey.includes("-----END")
) {
throw new Error("Invalid private key format");
}
const cleanKey = hostConfig.privateKey
.trim()
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n");
connectionConfig.privateKey = Buffer.from(cleanKey, "utf8");
if (hostConfig.keyPassword) {
connectionConfig.passphrase = hostConfig.keyPassword;
}
} catch (keyError) {
clearTimeout(connectionTimeout);
resolve({
success: false,
error: `Invalid SSH key format: ${keyError instanceof Error ? keyError.message : "Unknown error"}`,
});
return;
}
} else {
clearTimeout(connectionTimeout);
resolve({
success: false,
error: "Invalid authentication configuration",
error: `Invalid authentication configuration. Auth type: ${hostConfig.authType}, has password: ${!!hostConfig.password}, has key: ${!!hostConfig.privateKey}`,
});
return;
}
// Log connection attempt
authLogger.info("Attempting SSH connection for key deployment", {
host: connectionConfig.host,
port: connectionConfig.port,
username: connectionConfig.username,
authType: hostConfig.authType,
hasPassword: !!connectionConfig.password,
hasPrivateKey: !!connectionConfig.privateKey,
hasPassphrase: !!connectionConfig.passphrase,
});
conn.connect(connectionConfig);
} catch (error) {
clearTimeout(connectionTimeout);
authLogger.error("Failed to initiate SSH connection", {
host: hostConfig.ip,
error: error instanceof Error ? error.message : "Unknown error",
});
resolve({
success: false,
error: error instanceof Error ? error.message : "Connection failed",
@@ -1276,11 +1505,24 @@ router.post(
}
try {
const credential = await db
.select()
.from(sshCredentials)
.where(eq(sshCredentials.id, credentialId))
.limit(1);
const userId = (req as any).userId;
if (!userId) {
return res.status(401).json({
success: false,
error: "Authentication required",
});
}
const { SimpleDBOps } = await import("../../utils/simple-db-ops.js");
const credential = await SimpleDBOps.select(
db
.select()
.from(sshCredentials)
.where(eq(sshCredentials.id, credentialId))
.limit(1),
"ssh_credentials",
userId,
);
if (!credential || credential.length === 0) {
return res.status(404).json({
@@ -1304,12 +1546,15 @@ router.post(
error: "Public key is required for deployment",
});
}
const targetHost = await db
.select()
.from(sshData)
.where(eq(sshData.id, targetHostId))
.limit(1);
const targetHost = await SimpleDBOps.select(
db
.select()
.from(sshData)
.where(eq(sshData.id, targetHostId))
.limit(1),
"ssh_data",
userId,
);
if (!targetHost || targetHost.length === 0) {
return res.status(404).json({
@@ -1330,29 +1575,84 @@ router.post(
keyPassword: hostData.keyPassword,
};
authLogger.info("Host configuration for SSH key deployment", {
hostId: targetHostId,
ip: hostConfig.ip,
port: hostConfig.port,
username: hostConfig.username,
authType: hostConfig.authType,
hasPassword: !!hostConfig.password,
hasPrivateKey: !!hostConfig.privateKey,
hasKeyPassword: !!hostConfig.keyPassword,
passwordLength: hostConfig.password ? hostConfig.password.length : 0,
});
if (hostData.authType === "credential" && hostData.credentialId) {
const hostCredential = await db
.select()
.from(sshCredentials)
.where(eq(sshCredentials.id, hostData.credentialId))
.limit(1);
if (hostCredential && hostCredential.length > 0) {
const cred = hostCredential[0];
hostConfig.authType = cred.authType;
hostConfig.username = cred.username;
if (cred.authType === "password") {
hostConfig.password = cred.password;
} else if (cred.authType === "key") {
hostConfig.privateKey = cred.privateKey || cred.key;
hostConfig.keyPassword = cred.keyPassword;
}
} else {
const userId = (req as any).userId;
if (!userId) {
authLogger.error("Missing userId for credential resolution", {
hostId: targetHostId,
credentialId: hostData.credentialId,
});
return res.status(400).json({
success: false,
error: "Host credential not found",
error: "Authentication required for credential resolution",
});
}
try {
const { SimpleDBOps } = await import("../../utils/simple-db-ops.js");
const hostCredential = await SimpleDBOps.select(
db
.select()
.from(sshCredentials)
.where(eq(sshCredentials.id, hostData.credentialId))
.limit(1),
"ssh_credentials",
userId,
);
if (hostCredential && hostCredential.length > 0) {
const cred = hostCredential[0];
hostConfig.authType = cred.authType;
hostConfig.username = cred.username;
if (cred.authType === "password") {
hostConfig.password = cred.password;
} else if (cred.authType === "key") {
hostConfig.privateKey = cred.privateKey || cred.key;
hostConfig.keyPassword = cred.keyPassword;
}
authLogger.info("Resolved host credentials for SSH key deployment", {
hostId: targetHostId,
credentialId: hostData.credentialId,
authType: hostConfig.authType,
username: hostConfig.username,
hasPassword: !!hostConfig.password,
hasPrivateKey: !!hostConfig.privateKey,
hasKeyPassword: !!hostConfig.keyPassword,
});
} else {
authLogger.error("Host credential not found", {
hostId: targetHostId,
credentialId: hostData.credentialId,
});
return res.status(400).json({
success: false,
error: "Host credential not found",
});
}
} catch (error) {
authLogger.error("Failed to resolve host credentials", {
hostId: targetHostId,
credentialId: hostData.credentialId,
error: error instanceof Error ? error.message : "Unknown error",
});
return res.status(500).json({
success: false,
error: "Failed to resolve host credentials",
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -152,7 +152,7 @@ class AuthManager {
getSecureCookieOptions(req: any, maxAge: number = 24 * 60 * 60 * 1000) {
return {
httpOnly: true,
httpOnly: false,
secure: req.secure || req.headers["x-forwarded-proto"] === "https",
sameSite: "strict" as const,
maxAge: maxAge,

View File

@@ -105,12 +105,12 @@ export function CredentialsManager({
if (showDeployDialog) {
setDropdownOpen(false);
setHostSearchQuery("");
setSelectedHostId("");
setTimeout(() => {
const activeElement = document.activeElement as HTMLElement;
if (activeElement && activeElement.blur) {
activeElement.blur();
if (document.activeElement && (document.activeElement as HTMLElement).blur) {
(document.activeElement as HTMLElement).blur();
}
}, 100);
}, 50);
}
}, [showDeployDialog]);
@@ -908,14 +908,13 @@ export function CredentialsManager({
value={hostSearchQuery}
onChange={(e) => {
setHostSearchQuery(e.target.value);
if (e.target.value.trim() !== "") {
setDropdownOpen(true);
} else {
setDropdownOpen(false);
}
}}
onClick={() => {
setDropdownOpen(true);
}}
className="w-full"
autoFocus={false}
tabIndex={0}
/>
{dropdownOpen && (
<div className="absolute top-full left-0 z-50 mt-1 w-full bg-card border border-border rounded-lg shadow-lg max-h-60 overflow-y-auto">

View File

@@ -493,7 +493,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
}
useEffect(() => {
if (!terminal || !xtermRef.current || !hostConfig) return;
if (!terminal || !xtermRef.current) return;
terminal.options = {
cursorBlink: true,
@@ -598,7 +598,35 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
resizeObserver.observe(xtermRef.current);
setVisible(true);
setIsConnecting(true); // Show connecting state immediately
return () => {
isUnmountingRef.current = true;
shouldNotReconnectRef.current = true;
isReconnectingRef.current = false;
setIsConnecting(false);
resizeObserver.disconnect();
element?.removeEventListener("contextmenu", handleContextMenu);
element?.removeEventListener("keydown", handleMacKeyboard, true);
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
if (reconnectTimeoutRef.current)
clearTimeout(reconnectTimeoutRef.current);
if (connectionTimeoutRef.current)
clearTimeout(connectionTimeoutRef.current);
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current);
pingIntervalRef.current = null;
}
webSocketRef.current?.close();
};
}, [xtermRef, terminal]);
useEffect(() => {
if (!terminal || !hostConfig || !visible) return;
if (isConnected || isConnecting) return;
setIsConnecting(true);
const readyFonts =
(document as any).fonts?.ready instanceof Promise
@@ -607,7 +635,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
readyFonts.then(() => {
setTimeout(() => {
fitAddon.fit();
fitAddonRef.current?.fit();
if (terminal) scheduleNotify(terminal.cols, terminal.rows);
hardRefresh();
@@ -630,28 +658,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
connectToHost(cols, rows);
}, 200);
});
return () => {
isUnmountingRef.current = true;
shouldNotReconnectRef.current = true;
isReconnectingRef.current = false;
setIsConnecting(false);
resizeObserver.disconnect();
element?.removeEventListener("contextmenu", handleContextMenu);
element?.removeEventListener("keydown", handleMacKeyboard, true);
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
if (reconnectTimeoutRef.current)
clearTimeout(reconnectTimeoutRef.current);
if (connectionTimeoutRef.current)
clearTimeout(connectionTimeoutRef.current);
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current);
pingIntervalRef.current = null;
}
webSocketRef.current?.close();
};
}, [xtermRef, terminal, hostConfig]);
}, [terminal, hostConfig, visible, isConnected, isConnecting, splitScreen]);
useEffect(() => {
if (isVisible && fitAddonRef.current) {
@@ -686,7 +693,6 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
return (
<div className="h-full w-full m-1 relative">
{/* Terminal */}
<div
ref={xtermRef}
className={`h-full w-full transition-opacity duration-200 ${visible && isVisible && !isConnecting ? "opacity-100" : "opacity-0"} overflow-hidden`}
@@ -697,7 +703,6 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
}}
/>
{/* Connecting State */}
{isConnecting && (
<div className="absolute inset-0 flex items-center justify-center bg-dark-bg">
<div className="flex items-center gap-3">