const { app, BrowserWindow, ipcMain, shell } = require('electron'); const path = require('path'); const fs = require('fs'); const os = require('os'); const axios = require('axios'); const https = require('https'); 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}` }; } }); // --- Self-Update Functionality --- // Compare semver versions: returns -1 if a < b, 0 if equal, 1 if a > b function compareVersions(a, b) { // Strip pre-release tags (e.g. "1.2.3-beta.1" → "1.2.3") const cleanA = a.replace(/-.*$/, ''); const cleanB = b.replace(/-.*$/, ''); const partsA = cleanA.split('.').map(Number); const partsB = cleanB.split('.').map(Number); for (let i = 0; i < 3; i++) { const numA = partsA[i] || 0; const numB = partsB[i] || 0; if (numA < numB) return -1; if (numA > numB) return 1; } return 0; } // Follow HTTPS redirects and return the final response (for GitHub asset downloads) function httpsGetFollowRedirects(url, callback, redirectCount = 0) { if (redirectCount >= 5) { return callback(null, new Error('Too many redirects')); } const parsed = new URL(url); if (parsed.protocol !== 'https:') { return callback(null, new Error('Only HTTPS URLs are allowed')); } https.get(url, { headers: { 'User-Agent': 'Alta-Proxy-Tool' } }, (res) => { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { const redirectUrl = new URL(res.headers.location, url).href; httpsGetFollowRedirects(redirectUrl, callback, redirectCount + 1); } else { callback(res); } }).on('error', (err) => { callback(null, err); }); } ipcMain.handle('get-current-version', async () => { return { success: true, version: app.getVersion() }; }); ipcMain.handle('check-for-updates', async () => { try { const response = await axios.get( 'https://api.github.com/repos/PageZ948/Alta-Proxy-Tool/releases/latest', { timeout: 10000, headers: { 'User-Agent': 'Alta-Proxy-Tool', 'Accept': 'application/vnd.github.v3+json' } } ); const release = response.data; const latestVersion = release.tag_name.replace(/^v/, ''); const currentVersion = app.getVersion(); const updateAvailable = compareVersions(currentVersion, latestVersion) < 0; // Find the portable .exe asset const exeAsset = release.assets.find(a => /AltaCameraProxy-.*-portable\.exe$/i.test(a.name)); const downloadUrl = exeAsset ? exeAsset.browser_download_url : null; return { success: true, updateAvailable, currentVersion, latestVersion, downloadUrl, releaseNotes: release.body || '', releaseName: release.name || `v${latestVersion}` }; } catch (error) { if (error.response && error.response.status === 404) { return { success: true, updateAvailable: false, currentVersion: app.getVersion(), message: 'No releases available yet' }; } if (error.response && error.response.status === 403) { return { success: false, message: 'GitHub API rate limit exceeded. Try again later.' }; } console.error('Check for updates error:', error.message); return { success: false, message: error.message || 'Failed to check for updates' }; } }); ipcMain.handle('download-and-install-update', async (event, { downloadUrl }) => { try { // Determine the path to the currently running executable const currentExePath = process.env.PORTABLE_EXECUTABLE_FILE || app.getPath('exe'); const currentDir = path.dirname(currentExePath); const currentExeName = path.basename(currentExePath); // Check write permission on the app directory try { fs.accessSync(currentDir, fs.constants.W_OK); } catch { return { success: false, message: 'No write permission to the application directory. Try running as administrator.' }; } const tempDir = os.tmpdir(); const tempExePath = path.join(tempDir, `AltaCameraProxy-update-${Date.now()}.exe`); // Download the file with progress reporting await new Promise((resolve, reject) => { httpsGetFollowRedirects(downloadUrl, (res, err) => { if (err) return reject(err); if (res.statusCode !== 200) { res.resume(); return reject(new Error(`Download failed with status ${res.statusCode}`)); } const totalSize = parseInt(res.headers['content-length'], 10) || 0; let downloadedSize = 0; const fileStream = fs.createWriteStream(tempExePath); res.on('data', (chunk) => { downloadedSize += chunk.length; if (totalSize > 0 && mainWindow && !mainWindow.isDestroyed()) { const percent = Math.round((downloadedSize / totalSize) * 100); mainWindow.webContents.send('update-download-progress', { percent, downloadedSize, totalSize }); } }); res.pipe(fileStream); fileStream.on('finish', () => { fileStream.close(); resolve(); }); fileStream.on('error', (err) => { fs.unlink(tempExePath, () => {}); reject(err); }); }); }); // Verify downloaded file size (sanity check: > 10MB for an Electron portable exe) const stats = fs.statSync(tempExePath); if (stats.size < 10 * 1024 * 1024) { fs.unlinkSync(tempExePath); return { success: false, message: 'Downloaded file is too small — update may be corrupt.' }; } // Create batch script to replace the exe after this process exits const batchPath = path.join(tempDir, `apt-update-${Date.now()}.bat`); const pid = process.pid; const batchContent = `@echo off\r\ntitle APT-Updater\r\necho Waiting for Alta Proxy Tool to close...\r\n:waitloop\r\ntasklist /fi "PID eq ${pid}" 2>nul | find "${pid}" >nul\r\nif not errorlevel 1 (\r\n timeout /t 1 /nobreak >nul\r\n goto waitloop\r\n)\r\necho Applying update...\r\ncopy /y "${tempExePath}" "${path.join(currentDir, currentExeName)}"\r\nif errorlevel 1 (\r\n echo Update failed! Could not copy new version.\r\n pause\r\n del "${tempExePath}" >nul 2>&1\r\n del "%~f0" >nul 2>&1\r\n exit /b 1\r\n)\r\necho Update complete. Launching new version...\r\nstart "" "${path.join(currentDir, currentExeName)}"\r\ndel "${tempExePath}" >nul 2>&1\r\ndel "%~f0" >nul 2>&1\r\n`; fs.writeFileSync(batchPath, batchContent); // Spawn the updater batch script detached let updater; try { updater = spawn('cmd', ['/c', batchPath], { detached: true, stdio: 'ignore', windowsHide: true }); updater.unref(); } catch (spawnError) { console.error('Failed to spawn updater:', spawnError); try { fs.unlinkSync(tempExePath); } catch {} try { fs.unlinkSync(batchPath); } catch {} return { success: false, message: 'Failed to start updater process.' }; } // Quit the app after a delay to let the IPC response return to renderer setTimeout(() => { app.quit(); }, 1500); return { success: true, message: 'Update is being installed. The app will restart shortly.' }; } catch (error) { console.error('Download and install update error:', error); return { success: false, message: error.message || 'Failed to download and install update' }; } });