'use strict'; const nodeFs = require('node:fs'); const nodePath = require('node:path'); const { spawn: nodeSpawn } = require('node:child_process'); const HELPER_FILENAME = 'aware-cam-proxy.exe'; const MAX_USERNAME_LENGTH = 254; const MAX_HOST_LENGTH = 253; const DEVICE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; const DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/; const ALTA_SUFFIXES = ['.avasecurity.com', '.avigilon.com']; class ProxyLaunchError extends Error { constructor(message, code) { super(message); this.name = 'ProxyLaunchError'; this.code = code; } } function validateDeploymentHost(value) { if (typeof value !== 'string' || value.length === 0 || value.length > MAX_HOST_LENGTH) { throw new ProxyLaunchError('Deployment host is invalid.', 'INVALID_DEPLOYMENT_HOST'); } const host = value.toLowerCase(); const labels = host.split('.'); const hasAltaSuffix = ALTA_SUFFIXES.some((suffix) => host.endsWith(suffix)); if (!hasAltaSuffix || labels.some((label) => !DNS_LABEL_PATTERN.test(label))) { throw new ProxyLaunchError('Deployment host is invalid.', 'INVALID_DEPLOYMENT_HOST'); } return host; } function validateDeviceId(value) { if (typeof value !== 'string' || value.length !== 36 || !DEVICE_ID_PATTERN.test(value)) { throw new ProxyLaunchError('Device identifier is invalid.', 'INVALID_DEVICE_ID'); } return value.toLowerCase(); } function validateUsername(value) { if (typeof value !== 'string' || value.length === 0 || value.length > MAX_USERNAME_LENGTH || value.trim().length === 0) { throw new ProxyLaunchError('Alta username is invalid.', 'INVALID_USERNAME'); } if (/[\u0000-\u001f\u007f-\u009f]/.test(value)) { throw new ProxyLaunchError('Alta username must not contain control characters.', 'INVALID_USERNAME'); } return value; } function safeProcessError(error) { const source = error && typeof error.message === 'string' ? error.message : 'Unknown process error'; return source.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ').slice(0, 512); } function safeMetadata(entry, status = entry.status) { return { processId: entry.processId, deviceId: entry.deviceId, startedAt: entry.startedAt, status }; } function createProxyManager({ appDirectory, fs = nodeFs, spawn = nodeSpawn, platform = process.platform, now = Date.now } = {}) { if (platform !== 'win32') { throw new ProxyLaunchError('Proxy helper is supported only on the Windows platform.', 'UNSUPPORTED_PLATFORM'); } if (typeof appDirectory !== 'string' || !nodePath.win32.isAbsolute(appDirectory)) { throw new ProxyLaunchError('Approved application directory must be an absolute path.', 'INVALID_APP_DIRECTORY'); } if (typeof fs.existsSync !== 'function' || typeof spawn !== 'function' || typeof now !== 'function') { throw new TypeError('Invalid proxy manager dependency.'); } const helperPath = nodePath.win32.join(appDirectory, HELPER_FILENAME); const trackedChildren = new Map(); function removeIfOwned(processId, child) { const current = trackedChildren.get(processId); if (current && current.child === child) trackedChildren.delete(processId); } function launchProxy(request) { if (!request || typeof request !== 'object' || Array.isArray(request)) { throw new ProxyLaunchError('Proxy launch request is invalid.', 'INVALID_REQUEST'); } const deploymentHost = validateDeploymentHost(request.deploymentHost); const deviceId = validateDeviceId(request.deviceId); const username = validateUsername(request.username); if (!fs.existsSync(helperPath)) { throw new ProxyLaunchError('Proxy helper was not found in the approved application directory.', 'HELPER_NOT_FOUND'); } let child; try { child = spawn( helperPath, ['-a', deploymentHost, '-u', username, '-d', deviceId], { shell: false, detached: true, stdio: 'inherit', windowsHide: false } ); } catch (error) { throw new ProxyLaunchError( `Failed to launch proxy helper: ${safeProcessError(error)}`, 'SPAWN_FAILED' ); } if (!child || !Number.isSafeInteger(child.pid) || child.pid <= 0 || typeof child.kill !== 'function') { throw new ProxyLaunchError('Proxy helper did not return a valid child process.', 'INVALID_CHILD_PROCESS'); } if (trackedChildren.has(child.pid)) { try { child.kill('SIGTERM'); } catch { // The new child is deliberately not tracked when its PID collides. } throw new ProxyLaunchError('Proxy helper returned a process identifier already in use.', 'DUPLICATE_PROCESS_ID'); } const entry = { child, processId: child.pid, deviceId, startedAt: now(), status: 'running' }; trackedChildren.set(entry.processId, entry); if (typeof child.once === 'function') { child.once('exit', () => removeIfOwned(entry.processId, child)); child.once('error', () => removeIfOwned(entry.processId, child)); } return { success: true, ...safeMetadata(entry) }; } function stopProxy(processId) { if (!Number.isSafeInteger(processId) || processId <= 0) { throw new ProxyLaunchError('Process identifier is invalid.', 'INVALID_PROCESS_ID'); } const entry = trackedChildren.get(processId); if (!entry) return { success: false, processId, status: 'not-tracked' }; try { const requested = entry.child.kill('SIGTERM'); if (!requested) { removeIfOwned(processId, entry.child); return { success: true, processId, deviceId: entry.deviceId, status: 'already-exited' }; } removeIfOwned(processId, entry.child); return { success: true, processId, deviceId: entry.deviceId, status: 'stop-requested' }; } catch (error) { const permissionDenied = error && (error.code === 'EPERM' || error.code === 'EACCES'); return { success: false, processId, deviceId: entry.deviceId, status: permissionDenied ? 'permission-denied' : 'stop-failed', message: permissionDenied ? 'Unable to stop the tracked proxy process: permission denied.' : 'Unable to stop the tracked proxy process.' }; } } function listTrackedProxies() { return Array.from(trackedChildren.values(), (entry) => safeMetadata(entry)); } return Object.freeze({ launchProxy, stopProxy, listTrackedProxies }); } module.exports = { HELPER_FILENAME, MAX_USERNAME_LENGTH, ProxyLaunchError, createProxyManager, validateDeploymentHost, validateDeviceId, validateUsername };