fix: resolve backend build errors, preserve sudo file read buffers, and format code (#798)

* fix: resolve backend TypeScript build errors and preserve sudo file read buffers

* style: format code

* fix: ssh2 ESM import failure

---------

Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
TomyJan
2026-05-21 02:18:42 +08:00
committed by GitHub
parent 21f24a8386
commit 3375733789
14 changed files with 176 additions and 94 deletions
+5 -6
View File
@@ -139,10 +139,7 @@ async function resolveJumpHost(
): Promise<JumpHostConfig | null> {
try {
const hostResults = await SimpleDBOps.select(
getDb()
.select()
.from(hosts)
.where(eq(hosts.id, hostId)),
getDb().select().from(hosts).where(eq(hosts.id, hostId)),
"ssh_data",
userId,
);
@@ -160,8 +157,10 @@ async function resolveJumpHost(
const { SharedCredentialManager } =
await import("../utils/shared-credential-manager.js");
const sharedCredManager = SharedCredentialManager.getInstance();
const sharedCred =
await sharedCredManager.getSharedCredentialForUser(hostId, userId);
const sharedCred = await sharedCredManager.getSharedCredentialForUser(
hostId,
userId,
);
if (sharedCred) {
return {
...host,
+36 -11
View File
@@ -454,21 +454,33 @@ function execWithSudo(
command: string,
sudoPassword: string,
): Promise<{ stdout: string; stderr: string; code: number }> {
return execWithSudoBuffer(session, command, sudoPassword).then((result) => ({
stdout: result.stdout.toString("utf8"),
stderr: result.stderr,
code: result.code,
}));
}
function execWithSudoBuffer(
session: SSHSession,
command: string,
sudoPassword: string,
): Promise<{ stdout: Buffer; stderr: string; code: number }> {
return new Promise((resolve) => {
const escapedPassword = sudoPassword.replace(/'/g, "'\"'\"'");
const sudoCommand = `echo '${escapedPassword}' | sudo -S ${command} 2>&1`;
execChannel(session, sudoCommand, (err, stream) => {
if (err) {
resolve({ stdout: "", stderr: err.message, code: 1 });
resolve({ stdout: Buffer.alloc(0), stderr: err.message, code: 1 });
return;
}
let stdout = "";
const stdoutChunks: Buffer[] = [];
let stderr = "";
stream.on("data", (chunk: Buffer) => {
stdout += chunk.toString();
stdoutChunks.push(chunk);
});
stream.stderr.on("data", (chunk: Buffer) => {
@@ -476,12 +488,22 @@ function execWithSudo(
});
stream.on("close", (code: number) => {
stdout = stdout.replace(/\[sudo\] password for .+?:\s*/g, "");
let stdout = Buffer.concat(stdoutChunks);
const sudoPromptMatch = stdout
.toString("utf8", 0, Math.min(stdout.length, 256))
.match(/^\[sudo\] password for .+?:\s*/);
if (sudoPromptMatch) {
stdout = stdout.subarray(Buffer.byteLength(sudoPromptMatch[0]));
}
resolve({ stdout, stderr, code: code || 0 });
});
stream.on("error", (streamErr: Error) => {
resolve({ stdout, stderr: streamErr.message, code: 1 });
resolve({
stdout: Buffer.concat(stdoutChunks),
stderr: streamErr.message,
code: 1,
});
});
});
});
@@ -3098,22 +3120,25 @@ app.get("/ssh/file_manager/ssh/readFile", (req, res) => {
.includes("permission denied");
if (isPermissionDenied && sshConn.sudoPassword) {
execWithSudo(
execWithSudoBuffer(
sshConn,
`cat '${escapedPath}'`,
sshConn.sudoPassword,
)
.then(({ stdout, stderr, code: sudoCode }) => {
if (sudoCode !== 0) {
.then((result) => {
if (result.code !== 0) {
return res.status(403).json({
error: `Permission denied: ${stderr}`,
error: `Permission denied: ${result.stderr || result.stdout.toString("utf8")}`,
needsSudo: true,
});
}
const sudoData = Buffer.from(stdout, "utf8");
const sudoData = result.stdout;
const isBinary = detectBinary(sudoData);
res.json({
content: isBinary ? sudoData.toString("base64") : stdout,
content: isBinary
? sudoData.toString("base64")
: sudoData.toString("utf8"),
isBinary,
size: sudoData.length,
});
+9 -6
View File
@@ -83,13 +83,12 @@ export async function resolveHostById(
.select()
.from(hostAccess)
.where(
and(
eq(hostAccess.hostId, hostId),
eq(hostAccess.userId, userId),
),
and(eq(hostAccess.hostId, hostId), eq(hostAccess.userId, userId)),
)
.limit(1);
const overrideCredId = accessRecords[0]?.overrideCredentialId as number | null;
const overrideCredId = accessRecords[0]?.overrideCredentialId as
| number
| null;
if (overrideCredId) {
const userCreds = await SimpleDBOps.select(
db
@@ -113,7 +112,11 @@ export async function resolveHostById(
if (!host.overrideCredentialUsername) {
host.username = cred.username;
}
host.authType = cred.key ? "key" : cred.password ? "password" : "none";
host.authType = cred.key
? "key"
: cred.password
? "password"
: "none";
return host as unknown as SSHHost;
}
}
+5 -6
View File
@@ -75,10 +75,7 @@ async function resolveJumpHost(
): Promise<JumpHostConfig | null> {
try {
const hostResults = await SimpleDBOps.select(
getDb()
.select()
.from(hosts)
.where(eq(hosts.id, hostId)),
getDb().select().from(hosts).where(eq(hosts.id, hostId)),
"ssh_data",
userId,
);
@@ -96,8 +93,10 @@ async function resolveJumpHost(
const { SharedCredentialManager } =
await import("../utils/shared-credential-manager.js");
const sharedCredManager = SharedCredentialManager.getInstance();
const sharedCred =
await sharedCredManager.getSharedCredentialForUser(hostId, userId);
const sharedCred = await sharedCredManager.getSharedCredentialForUser(
hostId,
userId,
);
if (sharedCred) {
return {
...host,
+14 -15
View File
@@ -1,15 +1,14 @@
import { WebSocketServer, WebSocket, type RawData } from "ws";
import ssh2pkg from "ssh2";
const { Client, BaseAgent, utils: ssh2Utils } = ssh2pkg;
import type {
Client as ClientType,
ClientChannel,
PseudoTtyOptions,
ParsedKey,
SignCallback,
SigningRequestOptions,
IdentityCallback,
import ssh2Pkg, {
type Client as SSHClientType,
type ClientChannel,
type PseudoTtyOptions,
type ParsedKey,
type SignCallback,
type SigningRequestOptions,
type IdentityCallback,
} from "ssh2";
const { Client, BaseAgent, utils: ssh2Utils } = ssh2Pkg;
import net from "net";
import dgram from "dgram";
import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js";
@@ -273,13 +272,13 @@ async function createJumpHostChain(
jumpHosts: Array<{ hostId: number }>,
userId: string,
socks5Config?: SOCKS5Config | null,
): Promise<ClientType | null> {
): Promise<SSHClientType | null> {
if (!jumpHosts || jumpHosts.length === 0) {
return null;
}
let currentClient: ClientType | null = null;
const clients: ClientType[] = [];
let currentClient: SSHClientType | null = null;
const clients: SSHClientType[] = [];
try {
const jumpHostConfigs = await Promise.all(
@@ -560,9 +559,9 @@ wss.on("connection", async (ws: WebSocket, req) => {
});
let currentSessionId: string | null = null;
let sshConn: ClientType | null = null;
let sshConn: SSHClientType | null = null;
let sshStream: ClientChannel | null = null;
let lastJumpClient: ClientType | null = null;
let lastJumpClient: SSHClientType | null = null;
let keyboardInteractiveFinish: ((responses: string[]) => void) | null = null;
let totpPromptSent = false;
let totpTimeout: NodeJS.Timeout | null = null;
+4 -1
View File
@@ -124,7 +124,10 @@ export async function waitForTmuxSession(
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
await execCommand(conn, `tmux has-session -t ${shellEscape(sessionName)} 2>/dev/null`);
await execCommand(
conn,
`tmux has-session -t ${shellEscape(sessionName)} 2>/dev/null`,
);
return sessionName;
} catch {
// session not ready yet