fix: Sidebar resize issues and issues with TOTP interfering with password auth

This commit is contained in:
LukeGus
2025-11-02 15:44:25 -06:00
parent 9a697a7c10
commit 855a2b5a64
19 changed files with 338 additions and 218 deletions

View File

@@ -180,6 +180,10 @@ async function initializeCompleteDatabase(): Promise<void> {
tunnel_connections TEXT,
enable_file_manager INTEGER NOT NULL DEFAULT 1,
default_path TEXT,
autostart_password TEXT,
autostart_key TEXT,
autostart_key_password TEXT,
force_keyboard_interactive TEXT,
stats_config TEXT,
terminal_config TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -417,7 +421,10 @@ const migrateSchema = () => {
"updated_at",
"TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
);
addColumnIfNotExists("ssh_data", "force_keyboard_interactive", "TEXT");
addColumnIfNotExists("ssh_data", "autostart_password", "TEXT");
addColumnIfNotExists("ssh_data", "autostart_key", "TEXT");
addColumnIfNotExists("ssh_data", "autostart_key_password", "TEXT");
addColumnIfNotExists(
"ssh_data",
"credential_id",

View File

@@ -60,6 +60,7 @@ export const sshData = sqliteTable("ssh_data", {
tags: text("tags"),
pin: integer("pin", { mode: "boolean" }).notNull().default(false),
authType: text("auth_type").notNull(),
forceKeyboardInteractive: text("force_keyboard_interactive"),
password: text("password"),
key: text("key", { length: 8192 }),

View File

@@ -236,6 +236,7 @@ router.post(
tunnelConnections,
statsConfig,
terminalConfig,
forceKeyboardInteractive,
} = hostData;
if (
!isNonEmptyString(userId) ||
@@ -273,6 +274,7 @@ router.post(
defaultPath: defaultPath || null,
statsConfig: statsConfig ? JSON.stringify(statsConfig) : null,
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
};
if (effectiveAuthType === "password") {
@@ -424,6 +426,7 @@ router.put(
tunnelConnections,
statsConfig,
terminalConfig,
forceKeyboardInteractive,
} = hostData;
if (
!isNonEmptyString(userId) ||
@@ -462,6 +465,7 @@ router.put(
defaultPath: defaultPath || null,
statsConfig: statsConfig ? JSON.stringify(statsConfig) : null,
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
};
if (effectiveAuthType === "password") {
@@ -611,6 +615,7 @@ router.get("/db/host", authenticateJWT, async (req: Request, res: Response) => {
terminalConfig: row.terminalConfig
? JSON.parse(row.terminalConfig as string)
: undefined,
forceKeyboardInteractive: row.forceKeyboardInteractive === "true",
};
return (await resolveHostCredentials(baseHost)) || baseHost;
@@ -681,6 +686,7 @@ router.get(
terminalConfig: host.terminalConfig
? JSON.parse(host.terminalConfig)
: undefined,
forceKeyboardInteractive: host.forceKeyboardInteractive === "true",
};
res.json((await resolveHostCredentials(result)) || result);

View File

@@ -173,6 +173,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
authType,
credentialId,
userProvidedPassword,
forceKeyboardInteractive,
} = req.body;
const userId = (req as AuthenticatedRequest).userId;
@@ -257,39 +258,66 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
const config: Record<string, unknown> = {
host: ip,
port: port || 22,
port,
username,
tryKeyboard: true,
readyTimeout: 60000,
keepaliveInterval: 30000,
keepaliveCountMax: 3,
readyTimeout: 60000,
tcpKeepAlive: true,
tcpKeepAliveInitialDelay: 30000,
env: {
TERM: "xterm-256color",
LANG: "en_US.UTF-8",
LC_ALL: "en_US.UTF-8",
LC_CTYPE: "en_US.UTF-8",
LC_MESSAGES: "en_US.UTF-8",
LC_MONETARY: "en_US.UTF-8",
LC_NUMERIC: "en_US.UTF-8",
LC_TIME: "en_US.UTF-8",
LC_COLLATE: "en_US.UTF-8",
COLORTERM: "truecolor",
},
algorithms: {
kex: [
"curve25519-sha256",
"curve25519-sha256@libssh.org",
"ecdh-sha2-nistp521",
"ecdh-sha2-nistp384",
"ecdh-sha2-nistp256",
"diffie-hellman-group-exchange-sha256",
"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",
"diffie-hellman-group1-sha1",
],
serverHostKey: [
"ssh-ed25519",
"ecdsa-sha2-nistp521",
"ecdsa-sha2-nistp384",
"ecdsa-sha2-nistp256",
"rsa-sha2-512",
"rsa-sha2-256",
"ssh-rsa",
"ssh-dss",
],
cipher: [
"aes128-ctr",
"aes192-ctr",
"aes256-ctr",
"aes128-gcm@openssh.com",
"chacha20-poly1305@openssh.com",
"aes256-gcm@openssh.com",
"aes128-cbc",
"aes192-cbc",
"aes128-gcm@openssh.com",
"aes256-ctr",
"aes192-ctr",
"aes128-ctr",
"aes256-cbc",
"aes192-cbc",
"aes128-cbc",
"3des-cbc",
],
hmac: [
"hmac-sha2-256-etm@openssh.com",
"hmac-sha2-512-etm@openssh.com",
"hmac-sha2-256",
"hmac-sha2-256-etm@openssh.com",
"hmac-sha2-512",
"hmac-sha2-256",
"hmac-sha1",
"hmac-md5",
],
@@ -335,7 +363,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
.json({ error: "Password required for password authentication" });
}
if (userProvidedPassword) {
if (!forceKeyboardInteractive) {
config.password = resolvedCredentials.password;
}
} else if (resolvedCredentials.authType === "none") {
@@ -413,27 +441,6 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
});
client.on("error", (err) => {
if (
(err.message.includes("All configured authentication methods failed") ||
err.message.includes("No authentication methods remaining")) &&
resolvedCredentials.authType === "password" &&
!config.password &&
resolvedCredentials.password &&
!userProvidedPassword
) {
fileLogger.info(
"Retrying password auth with password method for file manager",
{
operation: "file_connect_retry",
sessionId,
hostId,
},
);
config.password = resolvedCredentials.password;
client.connect(config);
return;
}
if (responseSent) return;
responseSent = true;
fileLogger.error("SSH connection failed for file manager", {
@@ -613,7 +620,6 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
return "";
});
keyboardInteractiveResponded = true;
finish(responses);
}
},

View File

@@ -788,10 +788,26 @@ function addLegacyCredentials(
function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig {
const base: ConnectConfig = {
host: host.ip,
port: host.port || 22,
username: host.username || "root",
port: host.port,
username: host.username,
tryKeyboard: true,
readyTimeout: 10_000,
keepaliveInterval: 30000,
keepaliveCountMax: 3,
readyTimeout: 60000,
tcpKeepAlive: true,
tcpKeepAliveInitialDelay: 30000,
env: {
TERM: "xterm-256color",
LANG: "en_US.UTF-8",
LC_ALL: "en_US.UTF-8",
LC_CTYPE: "en_US.UTF-8",
LC_MESSAGES: "en_US.UTF-8",
LC_MONETARY: "en_US.UTF-8",
LC_NUMERIC: "en_US.UTF-8",
LC_TIME: "en_US.UTF-8",
LC_COLLATE: "en_US.UTF-8",
COLORTERM: "truecolor",
},
algorithms: {
kex: [
"curve25519-sha256",

View File

@@ -30,6 +30,7 @@ interface ConnectToHostData {
authType?: string;
credentialId?: number;
userId?: string;
forceKeyboardInteractive?: boolean;
};
initialPath?: string;
executeCommand?: string;
@@ -149,6 +150,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
let pingInterval: NodeJS.Timeout | null = null;
let keyboardInteractiveFinish: ((responses: string[]) => void) | null = null;
let totpPromptSent = false;
let isKeyboardInteractive = false;
let keyboardInteractiveResponded = false;
ws.on("close", () => {
@@ -362,10 +364,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
}
});
async function handleConnectToHost(
data: ConnectToHostData,
retryWithPassword = false,
) {
async function handleConnectToHost(data: ConnectToHostData) {
const { hostConfig, initialPath, executeCommand } = data;
const {
id,
@@ -661,22 +660,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
sshConn.on("error", (err: Error) => {
clearTimeout(connectionTimeout);
if (
(err.message.includes("All configured authentication methods failed") ||
err.message.includes("No authentication methods remaining")) &&
resolvedCredentials.authType === "password" &&
!retryWithPassword &&
!(hostConfig as any).userProvidedPassword
) {
sshLogger.info("Retrying password auth with password method", {
operation: "ssh_connect_retry",
hostId: id,
});
cleanupSSH();
handleConnectToHost(data, true);
return;
}
if (
(authMethodNotAvailable && resolvedCredentials.authType === "none") ||
(resolvedCredentials.authType === "none" &&
@@ -756,6 +739,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
prompts: Array<{ prompt: string; echo: boolean }>,
finish: (responses: string[]) => void,
) => {
isKeyboardInteractive = true;
const promptTexts = prompts.map((p) => p.prompt);
const totpPromptIndex = prompts.findIndex((p) =>
/verification code|verification_code|token|otp|2fa|authenticator|google.*auth/i.test(
@@ -840,7 +824,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
return "";
});
keyboardInteractiveResponded = true;
finish(responses);
}
},
@@ -931,7 +914,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
return;
}
if ((hostConfig as any).userProvidedPassword || retryWithPassword) {
if (!hostConfig.forceKeyboardInteractive) {
connectConfig.password = resolvedCredentials.password;
}
} else if (
@@ -1033,6 +1016,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
}
totpPromptSent = false;
isKeyboardInteractive = false;
keyboardInteractiveResponded = false;
keyboardInteractiveFinish = null;
}

View File

@@ -895,11 +895,24 @@ async function connectSSHTunnel(
host: tunnelConfig.sourceIP,
port: tunnelConfig.sourceSSHPort,
username: tunnelConfig.sourceUsername,
tryKeyboard: true,
keepaliveInterval: 30000,
keepaliveCountMax: 3,
readyTimeout: 60000,
tcpKeepAlive: true,
tcpKeepAliveInitialDelay: 15000,
tcpKeepAliveInitialDelay: 30000,
env: {
TERM: "xterm-256color",
LANG: "en_US.UTF-8",
LC_ALL: "en_US.UTF-8",
LC_CTYPE: "en_US.UTF-8",
LC_MESSAGES: "en_US.UTF-8",
LC_MONETARY: "en_US.UTF-8",
LC_NUMERIC: "en_US.UTF-8",
LC_TIME: "en_US.UTF-8",
LC_COLLATE: "en_US.UTF-8",
COLORTERM: "truecolor",
},
algorithms: {
kex: [
"curve25519-sha256",