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
+55
-21
@@ -1,8 +1,8 @@
|
||||
import { app, BrowserWindow, globalShortcut, nativeImage, shell } from 'electron'
|
||||
import { existsSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { registerIpc } from './ipc'
|
||||
import { killAllPtys } from './pty'
|
||||
import { isTrustedRendererUrl, registerIpc } from './ipc'
|
||||
import { killAllPtys, killPtysByOwner } from './pty'
|
||||
import { applyStartupSystemSettings, loadSettings } from './settingsStore'
|
||||
import { initTray, markQuitting, onMainWindowClose, refreshTrayMenu } from './tray'
|
||||
import { configureAutoUpdater, registerUpdateIpc } from './updater'
|
||||
@@ -21,6 +21,21 @@ function showOrCreate(): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hand a link to the OS browser. Only the web schemes are allowed: a renderer
|
||||
* that asked to open `file:`/`ms-*:`/anything else must not reach the shell.
|
||||
*/
|
||||
function openExternal(url: string): void {
|
||||
try {
|
||||
const { protocol } = new URL(url)
|
||||
if (protocol === 'http:' || protocol === 'https:' || protocol === 'ftp:') {
|
||||
void shell.openExternal(url)
|
||||
}
|
||||
} catch {
|
||||
// unparsable target: nothing to open
|
||||
}
|
||||
}
|
||||
|
||||
// Dev runs get their own userData directory (settings, session snapshot,
|
||||
// command history, logs) *and* their own single-instance lock — both are keyed
|
||||
// on that path. Without this a dev instance fights the installed build: it
|
||||
@@ -32,11 +47,34 @@ if (!app.isPackaged) {
|
||||
|
||||
// Single instance: a second launch just surfaces the existing window (pulls
|
||||
// it out of the tray if hidden there) instead of starting another process.
|
||||
// The refused instance skips the whole startup path: `app.quit()` only asks
|
||||
// the app to exit, so running the `whenReady` init behind it left a second
|
||||
// process with IPC, a tray and an updater but no window.
|
||||
const gotSingleInstanceLock = app.requestSingleInstanceLock()
|
||||
if (!gotSingleInstanceLock) {
|
||||
app.quit()
|
||||
} else {
|
||||
app.on('second-instance', () => showOrCreate())
|
||||
|
||||
app.whenReady().then(() => {
|
||||
registerIpc()
|
||||
registerUpdateIpc()
|
||||
// Before the window exists: a renderer-triggered check must not run against
|
||||
// the updater's defaults (feed, proxy, autoDownload are set in here).
|
||||
configureAutoUpdater()
|
||||
// OS-level effects (login item, sleep blocker) must apply even if the
|
||||
// settings dialog is never opened this run.
|
||||
applyStartupSystemSettings(loadSettings())
|
||||
createWindow()
|
||||
initTray(showOrCreate)
|
||||
// Tray labels are resolved from the dictionary at build time, so the menu has
|
||||
// to be rebuilt whenever the interface language changes.
|
||||
onLanguageChange(() => refreshTrayMenu())
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function createWindow(): void {
|
||||
@@ -95,11 +133,25 @@ function createWindow(): void {
|
||||
}
|
||||
})
|
||||
|
||||
// A renderer that crashed or was destroyed without running its own cleanup
|
||||
// (normal close/reload kills its sessions via beforeunload first) leaves
|
||||
// orphaned PTY/SSH transports behind — and session logs that keep appending.
|
||||
const dropOwnedSessions = (): void => killPtysByOwner(win.webContents.id)
|
||||
win.webContents.on('render-process-gone', dropOwnedSessions)
|
||||
win.webContents.on('destroyed', dropOwnedSessions)
|
||||
|
||||
win.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url)
|
||||
openExternal(details.url)
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
// Dropping a local .html file on the window is a navigation like any other,
|
||||
// and the new document would inherit this window's privileged preload. Only
|
||||
// the app's own page may (re)load here.
|
||||
win.webContents.on('will-navigate', (event, url) => {
|
||||
if (!isTrustedRendererUrl(url)) event.preventDefault()
|
||||
})
|
||||
|
||||
const devUrl = process.env['ELECTRON_RENDERER_URL']
|
||||
if (devUrl) {
|
||||
win.loadURL(devUrl)
|
||||
@@ -115,24 +167,6 @@ process.on('uncaughtException', (err) => {
|
||||
console.error('[main] uncaughtException:', err)
|
||||
})
|
||||
|
||||
app.whenReady().then(() => {
|
||||
registerIpc()
|
||||
registerUpdateIpc()
|
||||
// OS-level effects (login item, sleep blocker) must apply even if the
|
||||
// settings dialog is never opened this run.
|
||||
applyStartupSystemSettings(loadSettings())
|
||||
createWindow()
|
||||
initTray(showOrCreate)
|
||||
// Tray labels are resolved from the dictionary at build time, so the menu has
|
||||
// to be rebuilt whenever the interface language changes.
|
||||
onLanguageChange(() => refreshTrayMenu())
|
||||
configureAutoUpdater()
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
})
|
||||
|
||||
app.on('before-quit', () => {
|
||||
// Let window 'close' events pass through so teardown completes.
|
||||
markQuitting()
|
||||
|
||||
Reference in new issue
Block a user