fix(main): SFTP data integrity, session lifecycle, security hardening

- upload: per-chunk buffer (ssh2 re-reads the overflow tail after the ACK;
  a reused buffer silently corrupted every file >= ~254KB)
- close the cached SFTP channel on eviction, attach an 'error' handler,
  close the download handle, time out execQuiet, fail partial deletes
- per-session StringDecoder for the ssh data plane (CJK mojibake), real
  exit codes, safe replay truncation, zmodem abort/counter/timer fixes
- sysinfo: idempotent poll end, error routing, per-poll watchdog, proc(5)
  CPU total; expand cd ~/$HOME/%USERPROFILE% paths; log sanitizer fixes
- security: will-navigate guard, central IPC sender check, scheme
  allowlist for openExternal, single-instance else branch, layout id and
  log-name whitelists, custom theme sanitizing, atomic JSON writes with
  EPERM retry in store.writeJson
- updater: per-attempt feed choice, quitAndInstall relaunch, dev guard,
  update-state getter
This commit is contained in:
Bill committed 2026-09-20 20:26:34 +08:00
1 parent 4c6a29ef28
commit e04f4f0ac1
16 files changed
+452 -98

No files matched your search

+19 -3
View File
@@ -28,6 +28,11 @@ export interface SshSessionHandle {
client: Client
/** open interactive shell channel (ClientChannel, a Duplex) */
stream: ClientChannel
/**
* Exit code reported for the session: the channel's 'exit' event when the
* remote sends one, 1 when the client errors out mid-session, else 0.
*/
exitCode: number
}
export interface SshServiceDeps {
@@ -80,6 +85,7 @@ export async function connectSsh(
let settled = false
let connectTimer: NodeJS.Timeout | undefined
let verifierErr: string | undefined
let sessionHandle: SshSessionHandle | undefined
let resolvePromise!: (handle: SshSessionHandle) => void
let rejectPromise!: (err: Error) => void
@@ -161,6 +167,9 @@ export async function connectSsh(
return
}
// Session already established: surface as a session exit and clean up.
// Record the failure code on the handle so the stream's later 'close'
// (pty.ts) reports the same exit code instead of a bogus 0.
if (sessionHandle) sessionHandle.exitCode = 1
try {
deps.broadcast(Ipc.PTY_EXIT, { id: sessionId, exitCode: 1 })
} catch {
@@ -201,7 +210,8 @@ export async function connectSsh(
} catch {
// store write failure must not break the session
}
resolvePromise({ id: sessionId, client: handshake, stream: shell })
sessionHandle = { id: sessionId, client: handshake, stream: shell, exitCode: 0 }
resolvePromise(sessionHandle)
}
)
})
@@ -215,8 +225,14 @@ export async function connectSsh(
hostVerifier
}
// Password auth
const password = secretOverride?.password ?? deps.connections.getSecret(conn, 'password')
// Password auth. A stored password is offered only when the bookmark is
// configured for password auth: connectionsStore keeps password_enc when a
// bookmark is switched to key/agent auth, and silently falling back to it
// would authenticate a weaker method than the user chose. An explicitly
// typed connect-time password (secretOverride) is always honoured.
const password =
secretOverride?.password ??
(conn.auth === 'password' ? deps.connections.getSecret(conn, 'password') : undefined)
if (password !== undefined) cfg.password = password
// Private key auth (keyPath takes precedence over stored keyContent)