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:
1 parent
4c6a29ef28
commit
e04f4f0ac1
16 files changed
+452
-98
No files matched your search
+26
-7
@@ -37,10 +37,12 @@ export interface ZmodemDeps {
|
||||
broadcast(channel: string, ...args: unknown[]): void
|
||||
/**
|
||||
* Forward NON-ZMODEM octets back through the normal terminal data path
|
||||
* (utf8 + replay + session log + PTY_DATA). pty.ts injects this; the engine
|
||||
* only calls it when no transfer is active.
|
||||
* (utf8 decode + replay + session log + PTY_DATA). pty.ts injects this and
|
||||
* owns the decoding (per-session StringDecoder — a multi-byte char may be
|
||||
* split across TCP chunks); the engine only calls it when no transfer is
|
||||
* active.
|
||||
*/
|
||||
toTerminal(sessionId: string, text: string): void
|
||||
toTerminal(sessionId: string, data: Buffer): void
|
||||
/** Write bytes out to the ssh stream (zmodem frames + CAN abort sequence). */
|
||||
writeStream(sessionId: string, data: Buffer): void
|
||||
}
|
||||
@@ -129,7 +131,17 @@ function finalize(
|
||||
}
|
||||
engine.receiveStream = null
|
||||
}
|
||||
engine.session = null // the zsession already ended; drop the reference
|
||||
if (!ok && engine.session) {
|
||||
// Failure paths (stall watchdog, corrupt frame) must stop the peer too:
|
||||
// without an abort the remote rz/sz keeps transmitting frames that then
|
||||
// leak into the terminal and the session log.
|
||||
try {
|
||||
engine.session.abort()
|
||||
} catch {
|
||||
// session already ended
|
||||
}
|
||||
}
|
||||
engine.session = null
|
||||
engine.detection = null
|
||||
|
||||
if (!engine.progressEmitted) {
|
||||
@@ -300,7 +312,7 @@ export function attachZmodem(sessionId: string, deps: ZmodemDeps): void {
|
||||
// Analysis: also defensive against active http-parsing leftovers.
|
||||
if (engine.active) return // suppress binary terminal output during transfer
|
||||
try {
|
||||
deps.toTerminal(sessionId, Buffer.from(octets).toString('utf8'))
|
||||
deps.toTerminal(sessionId, Buffer.from(octets))
|
||||
} catch {
|
||||
// never crash the event loop
|
||||
}
|
||||
@@ -321,6 +333,8 @@ export function attachZmodem(sessionId: string, deps: ZmodemDeps): void {
|
||||
engine.dir = null
|
||||
engine.session = null
|
||||
engine.receiveStream = null
|
||||
engine.bytes = 0
|
||||
engine.totalBytes = 0
|
||||
engine.transferId = `zm-${sessionId}`
|
||||
emitProgress(engine, { file: '', bytes: 0, totalBytes: 0 })
|
||||
try {
|
||||
@@ -328,7 +342,6 @@ export function attachZmodem(sessionId: string, deps: ZmodemDeps): void {
|
||||
} catch {
|
||||
// never crash the event loop
|
||||
}
|
||||
touchActivity(engine)
|
||||
engine.offerTimer = setTimeout(() => {
|
||||
if (engine.detection && !engine.confirmed) {
|
||||
abortWithoutSession(engine, t('main.zmodem.offerTimedOut'))
|
||||
@@ -373,6 +386,12 @@ export function attachZmodem(sessionId: string, deps: ZmodemDeps): void {
|
||||
* the CAN abort sequence directly so the remote program exits.
|
||||
*/
|
||||
function abortWithoutSession(engine: Engine, message: string): void {
|
||||
try {
|
||||
engine.detection?.deny()
|
||||
} catch {
|
||||
// already retracted or confirmed
|
||||
}
|
||||
engine.detection = null
|
||||
try {
|
||||
engine.deps.writeStream(engine.id, ABORT_BUFFER)
|
||||
} catch {
|
||||
@@ -425,7 +444,7 @@ export function isZmodemActive(sessionId: string): boolean {
|
||||
*/
|
||||
export function respondZmodem(resp: ZmodemResponse): void {
|
||||
const engine = current(resp.id)
|
||||
if (!engine || engine.confirmed) return
|
||||
if (!engine || !engine.active || engine.confirmed) return
|
||||
|
||||
if (resp.cancelled) {
|
||||
abortWithoutSession(engine, t('main.zmodem.cancelled'))
|
||||
|
||||
Reference in new issue
Block a user