const { app, BrowserWindow, ipcMain, shell } = require('electron'); const path = require('path'); const fs = require('fs'); const os = require('os'); const axios = require('axios'); const { spawn } = require('child_process'); const http = require('http'); let mainWindow; let activeProxyProcesses = new Map(); // Track active camera proxy processes let cookieServer = null; const COOKIE_SERVER_PORT = 18247; const COOKIE_SERVER_TOKEN = 'apt-local-bridge-token'; // Sanitize strings before embedding in batch files to prevent command injection function sanitizeBatchInput(input) { if (typeof input !== 'string') return ''; // Remove characters that have special meaning in batch/cmd: & | < > ^ % " ` ! return input.replace(/[&|<>^%"`!]/g, ''); } function startCookieServer() { cookieServer = http.createServer((req, res) => { // CORS headers — only allow Chrome extension origins const origin = req.headers.origin || ''; if (origin.startsWith('chrome-extension://')) { res.setHeader('Access-Control-Allow-Origin', origin); } res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-APT-Token'); // Handle preflight if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } // Only accept POST /cookie if (req.method !== 'POST' || req.url !== '/cookie') { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: 'Not found' })); return; } // Verify shared token if (req.headers['x-apt-token'] !== COOKIE_SERVER_TOKEN) { res.writeHead(403, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: 'Forbidden' })); return; } // Read body with 64KB size limit let body = ''; let bodySize = 0; const MAX_BODY_SIZE = 65536; req.on('data', (chunk) => { bodySize += chunk.length; if (bodySize > MAX_BODY_SIZE) { res.writeHead(413, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: 'Payload too large' })); req.destroy(); return; } body += chunk; }); req.on('end', () => { try { const data = JSON.parse(body); const { deploymentUrl, cookieValue } = data; if (!deploymentUrl || !cookieValue) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: 'Missing deploymentUrl or cookieValue' })); return; } // Validate types and lengths if (typeof deploymentUrl !== 'string' || typeof cookieValue !== 'string' || deploymentUrl.length > 512 || cookieValue.length > 4096) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: 'Invalid parameter types or lengths' })); return; } // Validate deployment URL is an Alta domain try { const parsed = new URL(deploymentUrl); const isAltaDomain = parsed.hostname.endsWith('.avasecurity.com') || parsed.hostname.endsWith('.avigilon.com'); if (!isAltaDomain || parsed.protocol !== 'https:') { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: 'Invalid deployment URL domain' })); return; } } catch { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: 'Invalid deployment URL' })); return; } if (mainWindow && !mainWindow.isDestroyed()) { const cookies = ['va=' + cookieValue]; mainWindow.webContents.send('extension-cookie-received', { deploymentUrl: deploymentUrl.replace(/\/$/, ''), cookies, cookieValue }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, message: 'Cookie received' })); } else { res.writeHead(503, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: 'Application window not available' })); } } catch (e) { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, message: 'Invalid JSON' })); } }); }); cookieServer.listen(COOKIE_SERVER_PORT, '127.0.0.1', () => { console.log(`Cookie server listening on http://127.0.0.1:${COOKIE_SERVER_PORT}`); }); cookieServer.on('error', (err) => { if (err.code === 'EADDRINUSE') { console.error(`Cookie server error: Port ${COOKIE_SERVER_PORT} is already in use`); } else { console.error('Cookie server error:', err.message); } }); } function createWindow() { mainWindow = new BrowserWindow({ width: 1400, height: 900, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') }, icon: path.join(__dirname, 'assets', 'icon.png'), // Optional icon title: 'Alta Video Camera Proxy with API' }); mainWindow.loadFile('index.html'); // Open DevTools in development if (process.argv.includes('--dev')) { mainWindow.webContents.openDevTools(); } } app.whenReady().then(() => { createWindow(); startCookieServer(); }); app.on('before-quit', () => { if (cookieServer) { cookieServer.close(); } }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); // IPC handlers for API communication ipcMain.handle('api-get-devices', async (event, { deploymentUrl, cookies }) => { try { const devicesUrl = `${deploymentUrl}/api/v1/devices`; // Create axios instance with cookies const axiosInstance = axios.create({ timeout: 10000, headers: { 'Cookie': cookies ? cookies.join('; ') : '' } }); const response = await axiosInstance.get(devicesUrl); return { success: true, devices: response.data, message: `Found ${response.data.length} devices` }; } catch (error) { console.error('Get devices error:', error); return { success: false, message: error.response?.data?.message || error.message || 'Failed to get devices' }; } }); ipcMain.handle('api-get-auth-info', async (event, { deploymentUrl, cookies }) => { try { const authUrl = `${deploymentUrl}/api/v1/auth`; const axiosInstance = axios.create({ timeout: 10000, headers: { 'Cookie': cookies ? cookies.join('; ') : '' } }); const response = await axiosInstance.get(authUrl); return { success: true, authInfo: response.data }; } catch (error) { console.error('Get auth info error:', error); return { success: false, message: error.response?.data?.message || error.message || 'Failed to get auth info' }; } }); // Cookie-based camera proxy functionality ipcMain.handle('camera-proxy-cookie-launch', async (event, { deploymentUrl, cookieKey, deviceUuid }) => { try { // Path to the cookie-based camera proxy executable const proxyExePath = path.join(__dirname, 'aware-cam-proxy.exe'); // Check if the executable exists if (!fs.existsSync(proxyExePath)) { return { success: false, message: 'Cookie-based camera proxy executable not found. Please ensure aware-cam-proxy.exe is in the application directory.' }; } // Extract the domain from the deployment URL let domain = deploymentUrl; if (domain.startsWith('https://')) { domain = domain.substring(8); } else if (domain.startsWith('http://')) { domain = domain.substring(7); } // Remove trailing path segments domain = domain.split('/')[0]; // Sanitize all inputs before embedding in batch file const safeDomain = sanitizeBatchInput(domain); const safeDeviceUuid = sanitizeBatchInput(deviceUuid); const safeCookieKey = sanitizeBatchInput(cookieKey); if (!safeDomain || !safeDeviceUuid || !safeCookieKey) { return { success: false, message: 'Invalid characters detected in connection parameters.' }; } // Create a batch file to launch the cookie-based camera proxy const truncatedKey = safeCookieKey.length > 20 ? safeCookieKey.substring(0, 20) + '...' : safeCookieKey; const batchContent = `@echo off title APT-Proxy-${safeDeviceUuid} echo Launching Alta Video Camera Proxy (Cookie Method)... echo Domain: ${safeDomain} echo Device UUID: ${safeDeviceUuid} echo Cookie Key: ${truncatedKey} echo. "${proxyExePath}" -a "${safeDomain}" -d "${safeDeviceUuid}" -k "${safeCookieKey}" echo. echo Cookie-based camera proxy has finished. Press any key to close this window. pause >nul`; const tempDir = os.tmpdir(); const batchPath = path.join(tempDir, `cookie-proxy-${Date.now()}.bat`); // Write the batch file fs.writeFileSync(batchPath, batchContent); console.log('Launching cookie-based camera proxy via batch file:', batchPath); console.log('Command will be: aware-cam-proxy.exe -a', safeDomain, '-d', safeDeviceUuid, '-k [REDACTED]'); // Launch the batch file in a new command prompt window with unique title const windowTitle = `APT-Proxy-${safeDeviceUuid}`; const cmdProcess = spawn('cmd', ['/c', 'start', `"${windowTitle}"`, 'cmd', '/c', batchPath], { detached: true, stdio: 'ignore' }); // Store the process information for later termination const processInfo = { process: cmdProcess, batchPath: batchPath, deviceUuid: safeDeviceUuid, startTime: Date.now(), cookieKey: truncatedKey, domain: safeDomain, windowTitle: windowTitle, // Store window title for targeted cleanup type: 'cookie' // Mark as cookie-based proxy }; activeProxyProcesses.set(cmdProcess.pid, processInfo); // Clean up the batch file after a delay setTimeout(() => { try { if (fs.existsSync(batchPath)) { fs.unlinkSync(batchPath); } } catch (error) { console.log('Could not clean up cookie proxy batch file:', error.message); } }, 60000); // Clean up after 1 minute cmdProcess.unref(); // Allow the parent process to exit independently // Clean up process tracking when it exits cmdProcess.on('exit', () => { activeProxyProcesses.delete(cmdProcess.pid); }); return { success: true, message: `Cookie-based camera proxy launched for ${deviceUuid}!`, processId: cmdProcess.pid, deviceUuid: deviceUuid, type: 'cookie' }; } catch (error) { console.error('Failed to launch cookie-based camera proxy:', error); return { success: false, message: `Failed to launch cookie-based camera proxy: ${error.message}` }; } }); // Stop camera proxy functionality ipcMain.handle('camera-proxy-stop', async (event, { processId }) => { try { console.log('Attempting to stop camera proxy processes...'); return new Promise((resolve) => { // Kill all aware-cam-proxy.exe processes by name const killProxy = spawn('taskkill', ['/f', '/im', 'aware-cam-proxy.exe'], { stdio: ['ignore', 'pipe', 'pipe'] }); let proxyOutput = ''; let proxyError = ''; killProxy.stdout.on('data', (data) => { proxyOutput += data.toString(); }); killProxy.stderr.on('data', (data) => { proxyError += data.toString(); }); killProxy.on('close', (code) => { // Clean up our process tracking activeProxyProcesses.clear(); if (code === 0 || proxyOutput.includes('SUCCESS')) { console.log('Camera proxy processes terminated successfully'); resolve({ success: true, message: 'Camera proxy processes stopped successfully' }); } else if (proxyError.includes('not found') || proxyError.includes('No tasks')) { console.log('No camera proxy processes were running'); resolve({ success: true, message: 'No camera proxy processes were running' }); } else { resolve({ success: true, message: 'Attempted to stop all camera proxy processes' }); } }); killProxy.on('error', (error) => { console.error('Error with taskkill by name:', error); activeProxyProcesses.clear(); resolve({ success: false, message: `Failed to stop camera proxy: ${error.message}` }); }); }); } catch (error) { console.error('Failed to stop camera proxy:', error); return { success: false, message: `Failed to stop camera proxy: ${error.message}` }; } });