const { app, BrowserWindow, ipcMain, shell, clipboard } = require('electron'); const path = require('path'); const fs = require('fs'); const os = require('os'); const axios = require('axios'); const CryptoJS = require('crypto-js'); const { spawn } = require('child_process'); const crypto = require('crypto'); let mainWindow; let activeProxyProcesses = new Map(); // Track active camera proxy processes // 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, ''); } // Profile management const PROFILES_FILE = path.join(os.homedir(), '.alta-api-profiles.json'); // Derive encryption key from machine-specific identifiers so the profiles file // is only decryptable on this machine. Falls back to a static key if derivation fails. function deriveEncryptionKey() { try { const machineFactors = [ os.hostname(), os.homedir(), os.userInfo().username ].join('|'); return crypto.createHash('sha256').update('alta-proxy-' + machineFactors).digest('hex'); } catch { return 'alta-api-client-key-2024-fallback'; } } const ENCRYPTION_KEY = deriveEncryptionKey(); // Helper functions for profile management function getProfilesFilePath() { return PROFILES_FILE; } function encryptPassword(password) { return CryptoJS.AES.encrypt(password, ENCRYPTION_KEY).toString(); } function decryptPassword(encryptedPassword) { try { const bytes = CryptoJS.AES.decrypt(encryptedPassword, ENCRYPTION_KEY); return bytes.toString(CryptoJS.enc.Utf8); } catch (error) { console.error('Failed to decrypt password:', error); return ''; } } // Legacy key for migrating existing profiles const LEGACY_ENCRYPTION_KEY = 'alta-api-client-key-2024'; function decryptPasswordLegacy(encryptedPassword) { try { const bytes = CryptoJS.AES.decrypt(encryptedPassword, LEGACY_ENCRYPTION_KEY); const result = bytes.toString(CryptoJS.enc.Utf8); return result || null; } catch { return null; } } // Migrate profiles from legacy encryption key to machine-derived key function migrateProfilesIfNeeded(profiles) { let migrated = false; for (const profile of profiles) { if (!profile.password) continue; // Try decrypting with current key first const currentDecrypt = decryptPassword(profile.password); if (currentDecrypt) continue; // Try legacy key const legacyDecrypt = decryptPasswordLegacy(profile.password); if (legacyDecrypt) { profile.password = encryptPassword(legacyDecrypt); migrated = true; } } if (migrated) { saveProfiles(profiles); console.log('Migrated profiles to new encryption key'); } return profiles; } function loadProfiles() { try { if (fs.existsSync(PROFILES_FILE)) { const data = fs.readFileSync(PROFILES_FILE, 'utf8'); const profiles = JSON.parse(data); if (!Array.isArray(profiles)) return []; return migrateProfilesIfNeeded(profiles); } } catch (error) { console.error('Failed to load profiles:', error); } return []; } function saveProfiles(profiles) { try { fs.writeFileSync(PROFILES_FILE, JSON.stringify(profiles, null, 2)); return true; } catch (error) { console.error('Failed to save profiles:', error); return false; } } 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); 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-login', async (event, { deploymentUrl, username, password }) => { try { const loginUrl = `${deploymentUrl}/api/v1/dologin`; const response = await axios.post(loginUrl, { username: username, password: password }, { timeout: 10000, withCredentials: true }); // Store cookies for subsequent requests const cookies = response.headers['set-cookie']; return { success: true, cookies: cookies, message: 'Login successful' }; } catch (error) { console.error('Login error:', error); return { success: false, message: error.response?.data?.message || error.message || 'Login failed' }; } }); 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' }; } }); // Profile management IPC handlers ipcMain.handle('profiles-load', async () => { try { const profiles = loadProfiles(); // Return profiles without passwords for security const safeProfiles = profiles.map(profile => ({ id: profile.id, name: profile.name, deploymentUrl: profile.deploymentUrl, username: profile.username })); return { success: true, profiles: safeProfiles }; } catch (error) { console.error('Failed to load profiles:', error); return { success: false, message: 'Failed to load profiles' }; } }); ipcMain.handle('profiles-save', async (event, { name, deploymentUrl, username, password }) => { try { const profiles = loadProfiles(); const newProfile = { id: Date.now().toString(), name: name, deploymentUrl: deploymentUrl, username: username, password: encryptPassword(password), createdAt: new Date().toISOString() }; profiles.push(newProfile); const saved = saveProfiles(profiles); if (saved) { return { success: true, message: 'Profile saved successfully', profile: { id: newProfile.id, name: newProfile.name, deploymentUrl: newProfile.deploymentUrl, username: newProfile.username } }; } else { return { success: false, message: 'Failed to save profile' }; } } catch (error) { console.error('Failed to save profile:', error); return { success: false, message: 'Failed to save profile' }; } }); ipcMain.handle('profiles-get', async (event, { profileId }) => { try { const profiles = loadProfiles(); const profile = profiles.find(p => p.id === profileId); if (profile) { return { success: true, profile: { id: profile.id, name: profile.name, deploymentUrl: profile.deploymentUrl, username: profile.username, password: decryptPassword(profile.password) } }; } else { return { success: false, message: 'Profile not found' }; } } catch (error) { console.error('Failed to get profile:', error); return { success: false, message: 'Failed to get profile' }; } }); ipcMain.handle('profiles-delete', async (event, { profileId }) => { try { const profiles = loadProfiles(); const filteredProfiles = profiles.filter(p => p.id !== profileId); if (filteredProfiles.length < profiles.length) { const saved = saveProfiles(filteredProfiles); if (saved) { return { success: true, message: 'Profile deleted successfully' }; } else { return { success: false, message: 'Failed to save changes' }; } } else { return { success: false, message: 'Profile not found' }; } } catch (error) { console.error('Failed to delete profile:', error); return { success: false, message: 'Failed to delete profile' }; } }); ipcMain.handle('profiles-update', async (event, { profileId, name, deploymentUrl, username, password }) => { try { const profiles = loadProfiles(); const profileIndex = profiles.findIndex(p => p.id === profileId); if (profileIndex !== -1) { profiles[profileIndex] = { ...profiles[profileIndex], name: name, deploymentUrl: deploymentUrl, username: username, password: password ? encryptPassword(password) : profiles[profileIndex].password, updatedAt: new Date().toISOString() }; const saved = saveProfiles(profiles); if (saved) { return { success: true, message: 'Profile updated successfully', profile: { id: profiles[profileIndex].id, name: profiles[profileIndex].name, deploymentUrl: profiles[profileIndex].deploymentUrl, username: profiles[profileIndex].username } }; } else { return { success: false, message: 'Failed to save changes' }; } } else { return { success: false, message: 'Profile not found' }; } } catch (error) { console.error('Failed to update profile:', error); return { success: false, message: 'Failed to update profile' }; } }); // Camera Proxy functionality ipcMain.handle('camera-proxy-launch', async (event, { deploymentUrl, username, password, deviceUuid }) => { try { // Path to the camera proxy executable const proxyExePath = path.join(__dirname, 'aware-cam-proxy-win.exe'); // Check if the executable exists if (!fs.existsSync(proxyExePath)) { return { success: false, message: 'Camera proxy executable not found. Please ensure aware-cam-proxy-win.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 safeUsername = sanitizeBatchInput(username); const safeDeviceUuid = sanitizeBatchInput(deviceUuid); if (!safeDomain || !safeUsername || !safeDeviceUuid) { return { success: false, message: 'Invalid characters detected in connection parameters. Please check your profile settings.' }; } // Copy password to clipboard for easy pasting, then clear after 30 seconds clipboard.writeText(password); setTimeout(() => { try { if (clipboard.readText() === password) { clipboard.clear(); } } catch { /* ignore clipboard errors */ } }, 30000); // Create a batch file to launch the camera proxy with proper console const batchContent = `@echo off echo Launching Alta Video Camera Proxy... echo Domain: ${safeDomain} echo Username: ${safeUsername} echo Device UUID: ${safeDeviceUuid} echo. echo *** PASSWORD HAS BEEN COPIED TO CLIPBOARD *** echo When prompted for password, simply press Ctrl+V to paste and then Enter. echo. "${proxyExePath}" -a "${safeDomain}" -u "${safeUsername}" -d "${safeDeviceUuid}" echo. echo Camera proxy has finished. Press any key to close this window. pause >nul`; const tempDir = os.tmpdir(); const batchPath = path.join(tempDir, `camera-proxy-${Date.now()}.bat`); // Write the batch file fs.writeFileSync(batchPath, batchContent); console.log('Launching camera proxy via batch file:', batchPath); console.log('Command will be: aware-cam-proxy-win.exe -a', safeDomain, '-u', safeUsername, '-d', safeDeviceUuid); // Launch the batch file in a new command prompt window const cmdProcess = spawn('cmd', ['/c', 'start', 'cmd', '/k', batchPath], { detached: true, stdio: 'ignore' }); // Store the process information for later termination const processInfo = { process: cmdProcess, batchPath: batchPath, deviceUuid: safeDeviceUuid, startTime: Date.now(), username: safeUsername, domain: safeDomain }; 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 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: `Camera proxy launched for ${deviceUuid}! Password copied to clipboard - press Ctrl+V to paste when prompted.`, processId: cmdProcess.pid, deviceUuid: deviceUuid }; } catch (error) { console.error('Failed to launch camera proxy:', error); return { success: false, message: `Failed to launch camera proxy: ${error.message}` }; } }); // 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 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 const cmdProcess = spawn('cmd', ['/c', 'start', 'cmd', '/k', 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, 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 and close terminal windows...'); return new Promise((resolve) => { let processesKilled = 0; let totalAttempts = 0; // Step 1: Kill all aware-cam-proxy-win.exe processes by name const killProxy = spawn('taskkill', ['/f', '/im', 'aware-cam-proxy-win.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) => { totalAttempts++; if (code === 0 || proxyOutput.includes('SUCCESS')) { processesKilled++; console.log('Camera proxy processes terminated successfully'); } // Step 2: Kill command prompt windows containing our batch file or camera proxy const killCmdWindows = spawn('powershell', [ '-Command', `Get-Process | Where-Object {$_.ProcessName -eq "cmd" -and $_.MainWindowTitle -like "*camera-proxy*" -or $_.MainWindowTitle -like "*aware-cam-proxy*" -or $_.MainWindowTitle -like "*Command Prompt*"} | Stop-Process -Force` ], { stdio: 'ignore' }); killCmdWindows.on('close', () => { totalAttempts++; // Step 3: More aggressive approach - kill all cmd processes that might be related const killAllCmd = spawn('taskkill', ['/f', '/im', 'cmd.exe'], { stdio: ['ignore', 'pipe', 'pipe'] }); let cmdOutput = ''; killAllCmd.stdout.on('data', (data) => { cmdOutput += data.toString(); }); killAllCmd.on('close', (cmdCode) => { totalAttempts++; if (cmdCode === 0 || cmdOutput.includes('SUCCESS')) { processesKilled++; console.log('Command prompt windows closed successfully'); } // Step 4: Final cleanup - use wmic as fallback const wmicKill = spawn('wmic', [ 'process', 'where', 'name="cmd.exe" or name="aware-cam-proxy-win.exe"', 'delete' ], { stdio: 'ignore' }); wmicKill.on('close', (wmicCode) => { totalAttempts++; if (wmicCode === 0) { processesKilled++; } // Clean up our process tracking activeProxyProcesses.clear(); // Determine final result if (processesKilled > 0) { resolve({ success: true, message: 'Camera proxy processes and terminal windows closed successfully' }); } else if (proxyError.includes('not found') || proxyError.includes('No tasks')) { resolve({ success: true, message: 'No camera proxy processes were running' }); } else { resolve({ success: true, message: 'Attempted to close all camera proxy processes and windows' }); } }); wmicKill.on('error', () => { // Even if wmic fails, we might have succeeded with other methods activeProxyProcesses.clear(); resolve({ success: processesKilled > 0, message: processesKilled > 0 ? 'Camera proxy processes terminated, some terminal windows may remain open' : 'Unable to terminate camera proxy processes. Please close terminal windows manually.' }); }); }); }); }); killProxy.on('error', (error) => { console.error('Error with taskkill by name:', error); 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}` }; } }); // Check if camera proxy executable exists ipcMain.handle('camera-proxy-check', async () => { try { const proxyExePath = path.join(__dirname, 'aware-cam-proxy-win.exe'); const exists = fs.existsSync(proxyExePath); return { success: true, exists: exists, path: proxyExePath }; } catch (error) { return { success: false, exists: false, message: error.message }; } }); // Get version of camera proxy ipcMain.handle('camera-proxy-version', async () => { try { const proxyExePath = path.join(__dirname, 'aware-cam-proxy-win.exe'); if (!fs.existsSync(proxyExePath)) { return { success: false, message: 'Camera proxy executable not found' }; } return new Promise((resolve) => { const versionProcess = spawn(proxyExePath, ['-v'], { stdio: ['pipe', 'pipe', 'pipe'] }); let output = ''; versionProcess.stdout.on('data', (data) => { output += data.toString(); }); versionProcess.stderr.on('data', (data) => { output += data.toString(); }); versionProcess.on('close', (code) => { resolve({ success: true, version: output.trim(), exitCode: code }); }); versionProcess.on('error', (error) => { resolve({ success: false, message: error.message }); }); // Timeout after 5 seconds setTimeout(() => { versionProcess.kill(); resolve({ success: false, message: 'Version check timed out' }); }, 5000); }); } catch (error) { return { success: false, message: error.message }; } }); // Get list of active camera proxy processes ipcMain.handle('camera-proxy-list-active', async () => { try { const activeProcesses = Array.from(activeProxyProcesses.entries()).map(([pid, info]) => ({ processId: pid, deviceUuid: info.deviceUuid, startTime: info.startTime })); return { success: true, processes: activeProcesses }; } catch (error) { return { success: false, message: error.message, processes: [] }; } });