fix: File cleanup
This commit is contained in:
@@ -120,9 +120,7 @@ function cleanupSession(sessionId: string) {
|
||||
if (session) {
|
||||
try {
|
||||
session.client.end();
|
||||
} catch {
|
||||
// Ignore connection close errors
|
||||
}
|
||||
} catch {}
|
||||
clearTimeout(session.timeout);
|
||||
delete sshSessions[sessionId];
|
||||
}
|
||||
@@ -352,8 +350,6 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
||||
}
|
||||
config.password = resolvedCredentials.password;
|
||||
} else if (resolvedCredentials.authType === "none") {
|
||||
// Use authHandler to control authentication flow
|
||||
// This ensures we only try keyboard-interactive, not password auth
|
||||
config.authHandler = (
|
||||
methodsLeft: string[] | null,
|
||||
partialSuccess: boolean,
|
||||
@@ -409,7 +405,6 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
||||
scheduleSessionCleanup(sessionId);
|
||||
res.json({ status: "success", message: "SSH connection established" });
|
||||
|
||||
// Log activity to dashboard API
|
||||
if (hostId && userId) {
|
||||
(async () => {
|
||||
try {
|
||||
@@ -458,14 +453,6 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
||||
responseSent = true;
|
||||
|
||||
if (authMethodNotAvailable && resolvedCredentials.authType === "none") {
|
||||
fileLogger.info(
|
||||
"Keyboard-interactive not available, requesting credentials",
|
||||
{
|
||||
operation: "file_connect_auth_not_available",
|
||||
sessionId,
|
||||
hostId,
|
||||
},
|
||||
);
|
||||
res.status(200).json({
|
||||
status: "auth_required",
|
||||
message:
|
||||
@@ -557,51 +544,26 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
||||
prompt: prompts[totpPromptIndex].prompt,
|
||||
});
|
||||
} else {
|
||||
// Non-TOTP prompts (password, etc.)
|
||||
const hasStoredPassword =
|
||||
resolvedCredentials.password &&
|
||||
resolvedCredentials.authType !== "none";
|
||||
|
||||
// Check if this is a password prompt
|
||||
const passwordPromptIndex = prompts.findIndex((p) =>
|
||||
/password/i.test(p.prompt),
|
||||
);
|
||||
|
||||
// If no stored password (including authType "none"), prompt the user
|
||||
if (!hasStoredPassword && passwordPromptIndex !== -1) {
|
||||
if (responseSent) {
|
||||
// Connection is already being handled, don't send duplicate responses
|
||||
fileLogger.info(
|
||||
"Skipping duplicate password prompt - response already sent",
|
||||
{
|
||||
operation: "keyboard_interactive_skip",
|
||||
hostId,
|
||||
sessionId,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
responseSent = true;
|
||||
|
||||
if (pendingTOTPSessions[sessionId]) {
|
||||
// Session already waiting for TOTP, don't override
|
||||
fileLogger.info("Skipping password prompt - TOTP session pending", {
|
||||
operation: "keyboard_interactive_skip",
|
||||
hostId,
|
||||
sessionId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
keyboardInteractiveResponded = true;
|
||||
|
||||
fileLogger.info("Requesting password from user (authType: none)", {
|
||||
operation: "keyboard_interactive_password",
|
||||
hostId,
|
||||
sessionId,
|
||||
prompt: prompts[passwordPromptIndex].prompt,
|
||||
});
|
||||
|
||||
pendingTOTPSessions[sessionId] = {
|
||||
client,
|
||||
finish,
|
||||
@@ -627,7 +589,6 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-respond with stored credentials if available
|
||||
const responses = prompts.map((p) => {
|
||||
if (/password/i.test(p.prompt) && resolvedCredentials.password) {
|
||||
return resolvedCredentials.password;
|
||||
@@ -679,9 +640,7 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
|
||||
delete pendingTOTPSessions[sessionId];
|
||||
try {
|
||||
session.client.end();
|
||||
} catch {
|
||||
// Ignore errors when closing timed out session
|
||||
}
|
||||
} catch {}
|
||||
fileLogger.warn("TOTP session timeout before code submission", {
|
||||
operation: "file_totp_verify",
|
||||
sessionId,
|
||||
@@ -693,7 +652,6 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
|
||||
.json({ error: "TOTP session timeout. Please reconnect." });
|
||||
}
|
||||
|
||||
// Build responses for ALL prompts, just like in terminal.ts
|
||||
const responses = (session.prompts || []).map((p, index) => {
|
||||
if (index === session.totpPromptIndex) {
|
||||
return totpCode;
|
||||
@@ -704,22 +662,9 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
|
||||
return "";
|
||||
});
|
||||
|
||||
fileLogger.info("Full keyboard-interactive response for file manager", {
|
||||
operation: "file_totp_full_response",
|
||||
sessionId,
|
||||
userId,
|
||||
totalPrompts: session.prompts?.length || 0,
|
||||
responsesProvided: responses.filter((r) => r !== "").length,
|
||||
});
|
||||
|
||||
let responseSent = false;
|
||||
let responseTimeout: NodeJS.Timeout;
|
||||
|
||||
// Don't remove event listeners - just add our own 'once' handlers
|
||||
// The ssh2 library manages multiple listeners correctly
|
||||
// Removing them can cause the connection to become unstable
|
||||
|
||||
// CRITICAL: Attach event listeners BEFORE calling finish() to avoid race condition
|
||||
session.client.once("ready", () => {
|
||||
if (responseSent) return;
|
||||
responseSent = true;
|
||||
@@ -727,8 +672,6 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
|
||||
|
||||
delete pendingTOTPSessions[sessionId];
|
||||
|
||||
// Add a small delay to let SSH2 stabilize the connection after keyboard-interactive
|
||||
// This prevents "Not connected" errors when immediately trying to exec commands
|
||||
setTimeout(() => {
|
||||
sshSessions[sessionId] = {
|
||||
client: session.client,
|
||||
@@ -742,7 +685,6 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
|
||||
message: "TOTP verified, SSH connection established",
|
||||
});
|
||||
|
||||
// Log activity to dashboard API after connection is stable
|
||||
if (session.hostId && session.userId) {
|
||||
(async () => {
|
||||
try {
|
||||
@@ -789,7 +731,7 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, 200); // Give SSH2 connection 200ms to fully stabilize after keyboard-interactive
|
||||
}, 200);
|
||||
});
|
||||
|
||||
session.client.once("error", (err) => {
|
||||
@@ -822,7 +764,6 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
// Now that event listeners are attached, submit the TOTP response
|
||||
session.finish(responses);
|
||||
});
|
||||
|
||||
@@ -2493,15 +2434,6 @@ app.post("/ssh/file_manager/ssh/executeFile", async (req, res) => {
|
||||
: code;
|
||||
const cleanOutput = output.replace(/EXIT_CODE:\d+$/, "").trim();
|
||||
|
||||
fileLogger.info("File execution completed", {
|
||||
operation: "execute_file",
|
||||
sessionId,
|
||||
filePath,
|
||||
exitCode: actualExitCode,
|
||||
outputLength: cleanOutput.length,
|
||||
errorLength: errorOutput.length,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
exitCode: actualExitCode,
|
||||
|
||||
@@ -112,8 +112,6 @@ class SSHConnectionPool {
|
||||
);
|
||||
|
||||
if (totpPrompt) {
|
||||
// Record TOTP failure as permanent - never retry
|
||||
// The recordFailure method will log this once
|
||||
authFailureTracker.recordFailure(host.id, "TOTP", true);
|
||||
client.end();
|
||||
reject(
|
||||
@@ -158,9 +156,7 @@ class SSHConnectionPool {
|
||||
if (!conn.inUse && now - conn.lastUsed > maxAge) {
|
||||
try {
|
||||
conn.client.end();
|
||||
} catch {
|
||||
// Ignore errors when closing stale connections
|
||||
}
|
||||
} catch {}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -180,9 +176,7 @@ class SSHConnectionPool {
|
||||
for (const conn of connections) {
|
||||
try {
|
||||
conn.client.end();
|
||||
} catch {
|
||||
// Ignore errors when closing connections during cleanup
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
this.connections.clear();
|
||||
@@ -220,9 +214,7 @@ class RequestQueue {
|
||||
if (request) {
|
||||
try {
|
||||
await request();
|
||||
} catch {
|
||||
// Ignore errors from queued requests
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,13 +264,13 @@ interface AuthFailureRecord {
|
||||
count: number;
|
||||
lastFailure: number;
|
||||
reason: "TOTP" | "AUTH" | "TIMEOUT";
|
||||
permanent: boolean; // If true, don't retry at all
|
||||
permanent: boolean;
|
||||
}
|
||||
|
||||
class AuthFailureTracker {
|
||||
private failures = new Map<number, AuthFailureRecord>();
|
||||
private maxRetries = 3;
|
||||
private backoffBase = 60000; // 1 minute base backoff
|
||||
private backoffBase = 60000;
|
||||
|
||||
recordFailure(
|
||||
hostId: number,
|
||||
@@ -305,17 +297,14 @@ class AuthFailureTracker {
|
||||
const record = this.failures.get(hostId);
|
||||
if (!record) return false;
|
||||
|
||||
// Always skip TOTP hosts
|
||||
if (record.reason === "TOTP" || record.permanent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Skip if we've exceeded max retries
|
||||
if (record.count >= this.maxRetries) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Calculate exponential backoff
|
||||
const backoffTime = this.backoffBase * Math.pow(2, record.count - 1);
|
||||
const timeSinceFailure = Date.now() - record.lastFailure;
|
||||
|
||||
@@ -351,11 +340,9 @@ class AuthFailureTracker {
|
||||
|
||||
reset(hostId: number): void {
|
||||
this.failures.delete(hostId);
|
||||
// Don't log reset - it's not important
|
||||
}
|
||||
|
||||
cleanup(): void {
|
||||
// Clean up old failures (older than 1 hour)
|
||||
const maxAge = 60 * 60 * 1000;
|
||||
const now = Date.now();
|
||||
|
||||
@@ -459,7 +446,6 @@ class PollingManager {
|
||||
const statsConfig = this.parseStatsConfig(host.statsConfig);
|
||||
const existingConfig = this.pollingConfigs.get(host.id);
|
||||
|
||||
// Clear existing timers if they exist
|
||||
if (existingConfig) {
|
||||
if (existingConfig.statusTimer) {
|
||||
clearInterval(existingConfig.statusTimer);
|
||||
@@ -474,35 +460,27 @@ class PollingManager {
|
||||
statsConfig,
|
||||
};
|
||||
|
||||
// Start status polling if enabled
|
||||
if (statsConfig.statusCheckEnabled) {
|
||||
const intervalMs = statsConfig.statusCheckInterval * 1000;
|
||||
|
||||
// Poll immediately (don't await - let it run in background)
|
||||
this.pollHostStatus(host);
|
||||
|
||||
// Then set up interval to poll periodically
|
||||
config.statusTimer = setInterval(() => {
|
||||
this.pollHostStatus(host);
|
||||
}, intervalMs);
|
||||
} else {
|
||||
// Remove status if monitoring is disabled
|
||||
this.statusStore.delete(host.id);
|
||||
}
|
||||
|
||||
// Start metrics polling if enabled
|
||||
if (statsConfig.metricsEnabled) {
|
||||
const intervalMs = statsConfig.metricsInterval * 1000;
|
||||
|
||||
// Poll immediately (don't await - let it run in background)
|
||||
this.pollHostMetrics(host);
|
||||
|
||||
// Then set up interval to poll periodically
|
||||
config.metricsTimer = setInterval(() => {
|
||||
this.pollHostMetrics(host);
|
||||
}, intervalMs);
|
||||
} else {
|
||||
// Remove metrics if monitoring is disabled
|
||||
this.metricsStore.delete(host.id);
|
||||
}
|
||||
|
||||
@@ -576,12 +554,10 @@ class PollingManager {
|
||||
}
|
||||
|
||||
async refreshHostPolling(userId: string): Promise<void> {
|
||||
// Stop all current polling
|
||||
for (const hostId of this.pollingConfigs.keys()) {
|
||||
this.stopPollingForHost(hostId);
|
||||
}
|
||||
|
||||
// Reinitialize
|
||||
await this.initializePolling(userId);
|
||||
}
|
||||
|
||||
@@ -1019,10 +995,8 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
|
||||
os: string | null;
|
||||
};
|
||||
}> {
|
||||
// Check if we should skip this host due to auth failures
|
||||
if (authFailureTracker.shouldSkip(host.id)) {
|
||||
const reason = authFailureTracker.getSkipReason(host.id);
|
||||
// Don't log - just skip silently to avoid spam
|
||||
throw new Error(reason || "Authentication failed");
|
||||
}
|
||||
|
||||
@@ -1166,7 +1140,6 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
|
||||
availableHuman = null;
|
||||
}
|
||||
|
||||
// Collect network interfaces
|
||||
const interfaces: Array<{
|
||||
name: string;
|
||||
ip: string;
|
||||
@@ -1225,7 +1198,6 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// Collect uptime
|
||||
let uptimeSeconds: number | null = null;
|
||||
let uptimeFormatted: string | null = null;
|
||||
try {
|
||||
@@ -1242,7 +1214,6 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// Collect process information
|
||||
let totalProcesses: number | null = null;
|
||||
let runningProcesses: number | null = null;
|
||||
const topProcesses: Array<{
|
||||
@@ -1285,7 +1256,6 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
|
||||
runningProcesses = Number(runningCount.stdout.trim());
|
||||
} catch (e) {}
|
||||
|
||||
// Collect system information
|
||||
let hostname: string | null = null;
|
||||
let kernel: string | null = null;
|
||||
let os: string | null = null;
|
||||
@@ -1338,25 +1308,20 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
|
||||
return result;
|
||||
});
|
||||
} catch (error) {
|
||||
// Record authentication failures for backoff
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes("TOTP authentication required")) {
|
||||
// TOTP failures are already recorded in keyboard-interactive handler
|
||||
throw error;
|
||||
} else if (
|
||||
error.message.includes("No password available") ||
|
||||
error.message.includes("Unsupported authentication type") ||
|
||||
error.message.includes("No SSH key available")
|
||||
) {
|
||||
// Configuration errors - permanent failures, don't retry
|
||||
// recordFailure will log once when first detected
|
||||
authFailureTracker.recordFailure(host.id, "AUTH", true);
|
||||
} else if (
|
||||
error.message.includes("authentication") ||
|
||||
error.message.includes("Permission denied") ||
|
||||
error.message.includes("All configured authentication methods failed")
|
||||
) {
|
||||
// recordFailure will log once when first detected
|
||||
authFailureTracker.recordFailure(host.id, "AUTH");
|
||||
} else if (
|
||||
error.message.includes("timeout") ||
|
||||
@@ -1384,9 +1349,7 @@ function tcpPing(
|
||||
settled = true;
|
||||
try {
|
||||
socket.destroy();
|
||||
} catch {
|
||||
// Ignore errors when destroying socket
|
||||
}
|
||||
} catch {}
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
@@ -1409,7 +1372,6 @@ app.get("/status", async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize polling if no hosts are being polled yet
|
||||
const statuses = pollingManager.getAllStatuses();
|
||||
if (statuses.size === 0) {
|
||||
await pollingManager.initializePolling(userId);
|
||||
@@ -1433,7 +1395,6 @@ app.get("/status/:id", validateHostId, async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize polling if no hosts are being polled yet
|
||||
const statuses = pollingManager.getAllStatuses();
|
||||
if (statuses.size === 0) {
|
||||
await pollingManager.initializePolling(userId);
|
||||
@@ -1520,7 +1481,6 @@ app.listen(PORT, async () => {
|
||||
});
|
||||
}
|
||||
|
||||
// Cleanup old auth failures every 10 minutes
|
||||
setInterval(
|
||||
() => {
|
||||
authFailureTracker.cleanup();
|
||||
|
||||
@@ -333,15 +333,9 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
}
|
||||
|
||||
case "password_response": {
|
||||
const passwordData = data as TOTPResponseData; // Same structure
|
||||
const passwordData = data as TOTPResponseData;
|
||||
if (keyboardInteractiveFinish && passwordData?.code) {
|
||||
const password = passwordData.code;
|
||||
sshLogger.info("Password received from user", {
|
||||
operation: "password_response",
|
||||
userId,
|
||||
passwordLength: password.length,
|
||||
});
|
||||
|
||||
keyboardInteractiveFinish([password]);
|
||||
keyboardInteractiveFinish = null;
|
||||
} else {
|
||||
@@ -374,7 +368,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
keyPassword?: string;
|
||||
};
|
||||
|
||||
// Update the host config with provided credentials
|
||||
if (credentialsData.password) {
|
||||
credentialsData.hostConfig.password = credentialsData.password;
|
||||
credentialsData.hostConfig.authType = "password";
|
||||
@@ -384,10 +377,8 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
credentialsData.hostConfig.authType = "key";
|
||||
}
|
||||
|
||||
// Cleanup existing connection if any
|
||||
cleanupSSH();
|
||||
|
||||
// Reconnect with new credentials
|
||||
const reconnectData: ConnectToHostData = {
|
||||
cols: credentialsData.cols,
|
||||
rows: credentialsData.rows,
|
||||
@@ -555,8 +546,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
sshConn.on("ready", () => {
|
||||
clearTimeout(connectionTimeout);
|
||||
|
||||
// Immediately try to create shell - don't delay as it can cause connection to be cleaned up
|
||||
// The connection is already ready at this point
|
||||
if (!sshConn) {
|
||||
sshLogger.warn(
|
||||
"SSH connection was cleaned up before shell could be created",
|
||||
@@ -666,11 +655,9 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
JSON.stringify({ type: "connected", message: "SSH connected" }),
|
||||
);
|
||||
|
||||
// Log activity to dashboard API
|
||||
if (id && hostConfig.userId) {
|
||||
(async () => {
|
||||
try {
|
||||
// Fetch host name from database
|
||||
const hosts = await SimpleDBOps.select(
|
||||
getDb()
|
||||
.select()
|
||||
@@ -790,8 +777,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
prompts: Array<{ prompt: string; echo: boolean }>,
|
||||
finish: (responses: string[]) => void,
|
||||
) => {
|
||||
// Notify frontend that keyboard-interactive is available (e.g., for Warpgate OIDC)
|
||||
// This allows the terminal to be displayed immediately so user can see auth prompts
|
||||
if (resolvedCredentials.authType === "none") {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
@@ -846,37 +831,19 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
resolvedCredentials.password &&
|
||||
resolvedCredentials.authType !== "none";
|
||||
|
||||
// Check if this is a password prompt
|
||||
const passwordPromptIndex = prompts.findIndex((p) =>
|
||||
/password/i.test(p.prompt),
|
||||
);
|
||||
|
||||
// If no stored password (including authType "none"), prompt the user
|
||||
if (!hasStoredPassword && passwordPromptIndex !== -1) {
|
||||
// Don't block duplicate password prompts - some servers (like Warpgate) may ask multiple times
|
||||
if (keyboardInteractiveResponded && totpPromptSent) {
|
||||
// Only block if we already sent a TOTP prompt
|
||||
sshLogger.info(
|
||||
"Skipping duplicate password prompt after TOTP sent",
|
||||
{
|
||||
operation: "keyboard_interactive_skip",
|
||||
hostId: id,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
keyboardInteractiveResponded = true;
|
||||
|
||||
sshLogger.info("Requesting password from user (authType: none)", {
|
||||
operation: "keyboard_interactive_password",
|
||||
hostId: id,
|
||||
prompt: prompts[passwordPromptIndex].prompt,
|
||||
});
|
||||
|
||||
keyboardInteractiveFinish = (userResponses: string[]) => {
|
||||
const userInput = (userResponses[0] || "").trim();
|
||||
|
||||
// Build responses for all prompts
|
||||
const responses = prompts.map((p, index) => {
|
||||
if (index === passwordPromptIndex) {
|
||||
return userInput;
|
||||
@@ -884,16 +851,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
return "";
|
||||
});
|
||||
|
||||
sshLogger.info(
|
||||
"User-provided password being sent to SSH server",
|
||||
{
|
||||
operation: "interactive_password_verification",
|
||||
hostId: id,
|
||||
passwordLength: userInput.length,
|
||||
totalPrompts: prompts.length,
|
||||
},
|
||||
);
|
||||
|
||||
finish(responses);
|
||||
};
|
||||
|
||||
@@ -906,8 +863,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-respond with stored credentials if available
|
||||
// Allow multiple responses - the server might ask multiple times during auth flow
|
||||
const responses = prompts.map((p) => {
|
||||
if (/password/i.test(p.prompt) && resolvedCredentials.password) {
|
||||
return resolvedCredentials.password;
|
||||
@@ -991,28 +946,15 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
};
|
||||
|
||||
if (resolvedCredentials.authType === "none") {
|
||||
// For "none" auth type, allow natural SSH negotiation
|
||||
// The authHandler will try keyboard-interactive if available, otherwise notify frontend
|
||||
// This allows for Warpgate OIDC and other interactive auth scenarios
|
||||
connectConfig.authHandler = (
|
||||
methodsLeft: string[] | null,
|
||||
partialSuccess: boolean,
|
||||
callback: (nextMethod: string | false) => void,
|
||||
) => {
|
||||
if (methodsLeft && methodsLeft.length > 0) {
|
||||
// Prefer keyboard-interactive if available
|
||||
if (methodsLeft.includes("keyboard-interactive")) {
|
||||
callback("keyboard-interactive");
|
||||
} else {
|
||||
// No keyboard-interactive available - notify frontend to show auth dialog
|
||||
sshLogger.info(
|
||||
"Server does not support keyboard-interactive auth for 'none' auth type",
|
||||
{
|
||||
operation: "ssh_auth_handler_no_keyboard",
|
||||
hostId: id,
|
||||
methodsLeft,
|
||||
},
|
||||
);
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "auth_method_not_available",
|
||||
@@ -1024,11 +966,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
callback(false);
|
||||
}
|
||||
} else {
|
||||
// No methods left or empty - try to proceed without auth
|
||||
sshLogger.info("No auth methods available, proceeding without auth", {
|
||||
operation: "ssh_auth_no_methods",
|
||||
hostId: id,
|
||||
});
|
||||
callback(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -217,9 +217,7 @@ function cleanupTunnelResources(
|
||||
if (verification?.timeout) clearTimeout(verification.timeout);
|
||||
try {
|
||||
verification?.conn.end();
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
} catch {}
|
||||
tunnelVerifications.delete(tunnelName);
|
||||
}
|
||||
|
||||
@@ -284,9 +282,7 @@ function handleDisconnect(
|
||||
const verification = tunnelVerifications.get(tunnelName);
|
||||
if (verification?.timeout) clearTimeout(verification.timeout);
|
||||
verification?.conn.end();
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
} catch {}
|
||||
tunnelVerifications.delete(tunnelName);
|
||||
}
|
||||
|
||||
@@ -642,9 +638,7 @@ async function connectSSHTunnel(
|
||||
|
||||
try {
|
||||
conn.end();
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
} catch {}
|
||||
|
||||
activeTunnels.delete(tunnelName);
|
||||
|
||||
@@ -784,9 +778,7 @@ async function connectSSHTunnel(
|
||||
const verification = tunnelVerifications.get(tunnelName);
|
||||
if (verification?.timeout) clearTimeout(verification.timeout);
|
||||
verification?.conn.end();
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
} catch {}
|
||||
tunnelVerifications.delete(tunnelName);
|
||||
}
|
||||
|
||||
@@ -837,13 +829,9 @@ async function connectSSHTunnel(
|
||||
}
|
||||
});
|
||||
|
||||
stream.stdout?.on("data", () => {
|
||||
// Silently consume stdout data
|
||||
});
|
||||
stream.stdout?.on("data", () => {});
|
||||
|
||||
stream.on("error", () => {
|
||||
// Silently consume stream errors
|
||||
});
|
||||
stream.on("error", () => {});
|
||||
|
||||
stream.stderr.on("data", (data) => {
|
||||
const errorMsg = data.toString().trim();
|
||||
@@ -1222,9 +1210,7 @@ async function killRemoteTunnelByMarker(
|
||||
executeNextKillCommand();
|
||||
});
|
||||
|
||||
stream.on("data", () => {
|
||||
// Silently consume stream data
|
||||
});
|
||||
stream.on("data", () => {});
|
||||
|
||||
stream.stderr.on("data", (data) => {
|
||||
const output = data.toString().trim();
|
||||
|
||||
Reference in New Issue
Block a user