fix: resolve 35 memory leaks, resource leaks, and bugs across native host and extension
CI / Build & Test (push) Successful in 2m52s

CRITICAL:
- certificate-authority.js: declare `regenerated` variable in installRootCA Windows path to prevent ReferenceError crash

HIGH:
- virtual-hosts.js/service-tunnels.js: call hs.removeAllListeners() in catch blocks to prevent stale listeners on failed Holesail instances
- https-proxy.js: destroy rawSocket in TLS error handler to prevent file descriptor exhaustion
- message-router.js (native): move setEventEmitter() to module-level init instead of re-calling on every message
- ssh-manager.js: add error handler to WS server to prevent unhandled error crashes
- init.js: store setInterval ID and clear on beforeunload to prevent interval accumulation
- logs.js: store and remove chrome.runtime.onMessage listener on beforeunload; add duplicate-call guard
- events.js: move pending++ before async sendMessage call to fix SSH/RDP-only import showing "Nothing to import"
- ssh.js: store resizeTimer on activeSshSession and clear in disconnectSsh; fix auto-reconnect race with _sshConnecting lock
- native-messaging.js: track retry timer IDs in array and cancel all on disconnect
- rdp.js: reuse single offscreen canvas per session instead of allocating per bitmap

MEDIUM:
- virtual-hosts.js/service-tunnels.js: clear existing.reconnectTimer before replacing tunnel entries
- message-router.js (native): destroy pingTunnel socket on error path; clear 15s fallback timer via finally()
- startup.js: wrap setImmediate body in try/finally to always resolve proxiesReadyPromise
- https-proxy.js: fix pre-connect upstream error handler to avoid writing raw HTTP into piped TLS stream; move HOP_BY_HOP to module-level constant
- connect-proxy.js: destroy upstreamSocket on clientSocket close; track and destroy active sockets in stop()
- ssh-manager.js: call cancelPasswordWatch on WS disconnect during password collection
- rdp-manager.js: remove dead remotePort variable; add error handlers to both WS servers
- backup-manager.js: log cleanupStaging errors and non-zero exit codes
- rdp.js: null out ws callbacks before closing in disconnectRdp; disconnect MutationObserver on beforeunload
- proxy-ca.js: prune stale entries from validationResults Map in renderValidatorTable
- refresh.js: deduplicate in-flight pings per port via Set
- messaging.js: read chrome.runtime.lastError in sendToNative callback
- servers.js (dashboard): add null check for $('serverEditId') element

LOW:
- port-allocator.js: add dedup check before pushing to tunnelPortFreeList
- servers.js (native): add error listener to server-mode Holesail instances
- virtual-hosts.js: remove dead prevReconnectDelay variable
- tab-lifecycle.js: change swarmRefCount fallback from || 1 to || 0 to prevent premature swarm destroy
- ssh.js/rdp.js: disconnect MutationObservers on beforeunload
This commit is contained in:
Raven Scott
2026-03-01 00:10:20 -05:00
parent 849897f324
commit 0b31e7faa6
23 changed files with 205 additions and 84 deletions
+13 -5
View File
@@ -24,6 +24,11 @@ const BACKEND_TIMEOUT_MS = 30000;
// How long to wait for the first TLS bytes before giving up (ms)
const SNI_READ_TIMEOUT_MS = 5000;
const HOP_BY_HOP = new Set([
'transfer-encoding', 'connection', 'keep-alive', 'proxy-connection',
'proxy-authorization', 'proxy-authenticate', 'te', 'trailer', 'upgrade'
]);
const DEBUG = process.env.HOLESAIL_DEBUG === '1' || process.env.HOLESAIL_DEBUG === 'true';
function debugLog (...args) {
if (!DEBUG) return;
@@ -275,10 +280,6 @@ h1{color:#c0392b}code{background:#f4f4f4;padding:2px 6px;border-radius:3px;font-
// transparently. Forwarding Transfer-Encoding: chunked would cause
// ERR_INVALID_CHUNKED_ENCODING because the HTTP library already decodes
// the chunked body before emitting 'data' events.
const HOP_BY_HOP = new Set([
'transfer-encoding', 'connection', 'keep-alive', 'proxy-connection',
'proxy-authorization', 'proxy-authenticate', 'te', 'trailer', 'upgrade'
]);
const reqHeaders = {};
for (const k of Object.keys(req.headers)) {
@@ -366,9 +367,15 @@ function onUpgrade (req, socket, head) {
// Register error handler synchronously before the connect callback can fire,
// so an immediate ECONNREFUSED is never an unhandled error event.
const upstream = bareTcp.connect(targetPort, targetHost);
upstream.on('error', () => { try { socket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n'); socket.destroy(); } catch (_) {} });
function _preConnectError() {
try { socket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n'); socket.destroy(); } catch (_) {}
try { upstream.destroy(); } catch (_) {}
}
upstream.on('error', _preConnectError);
socket.on('error', () => { try { upstream.destroy(); } catch (_) {} });
upstream.on('connect', () => {
upstream.removeListener('error', _preConnectError);
upstream.on('error', () => { try { socket.destroy(); } catch (_) {} });
const headers = Object.entries(req.headers).map(([k, v]) => k + ': ' + v).join('\r\n');
const requestLine = (req.method || 'GET') + ' ' + (req.url || '/') + ' HTTP/1.1\r\n';
upstream.write(requestLine + headers + '\r\n\r\n');
@@ -451,6 +458,7 @@ function handleRawConnection (rawSocket) {
tlsSocket.on('error', (err) => {
debugLog('tls socket error:', err.message);
try { rawSocket.destroy(); } catch (_) {}
});
// Hand to bare-http1 for HTTP parsing — emits 'request' on the fakeServer