242 lines
8.1 KiB
JavaScript
242 lines
8.1 KiB
JavaScript
'use strict';
|
|
|
|
const { app, BrowserWindow, dialog, ipcMain, safeStorage, shell } = require('electron');
|
|
const http = require('node:http');
|
|
const path = require('node:path');
|
|
const { pathToFileURL } = require('node:url');
|
|
const { createSessionStore } = require('./src/session-store');
|
|
const { createAltaClient } = require('./src/alta-client');
|
|
const { createProxyManager } = require('./src/proxy-launch');
|
|
const { checkForUpdate } = require('./src/update-policy');
|
|
const {
|
|
AppRuntime,
|
|
PAIRING_ENVELOPE_FILENAME,
|
|
PairingController,
|
|
createBridgeHandler,
|
|
} = require('./src/electron-runtime');
|
|
|
|
const BRIDGE_HOST = '127.0.0.1';
|
|
const BRIDGE_PORT = 18247;
|
|
|
|
let mainWindow = null;
|
|
let bridgeServer = null;
|
|
let runtime = null;
|
|
let pairingController = null;
|
|
let firstRunPairingSecret = null;
|
|
let quitCleanupInProgress = false;
|
|
let allowConfirmedQuit = false;
|
|
|
|
function getAppDirectory() {
|
|
return app.isPackaged
|
|
? path.dirname(process.env.PORTABLE_EXECUTABLE_FILE || app.getPath('exe'))
|
|
: __dirname;
|
|
}
|
|
|
|
function sendConnectionState() {
|
|
if (mainWindow && !mainWindow.isDestroyed() && runtime) {
|
|
mainWindow.webContents.send('connection-state-changed', runtime.getConnectionState());
|
|
}
|
|
}
|
|
|
|
function createWindow() {
|
|
mainWindow = new BrowserWindow({
|
|
width: 1400,
|
|
height: 900,
|
|
icon: path.join(__dirname, 'assets', 'icon.png'),
|
|
title: 'Alta Video Camera Proxy',
|
|
webPreferences: {
|
|
nodeIntegration: false,
|
|
contextIsolation: true,
|
|
sandbox: true,
|
|
preload: path.join(__dirname, 'preload.js'),
|
|
},
|
|
});
|
|
|
|
mainWindow.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
|
|
mainWindow.webContents.on('will-navigate', (event) => event.preventDefault());
|
|
mainWindow.loadFile(path.join(__dirname, 'index.html'));
|
|
if (process.argv.includes('--dev')) mainWindow.webContents.openDevTools();
|
|
}
|
|
|
|
function isTrustedSender(event) {
|
|
if (!mainWindow || mainWindow.isDestroyed() || !event) return false;
|
|
const webContents = mainWindow.webContents;
|
|
const frame = event.senderFrame;
|
|
const expectedUrl = pathToFileURL(path.join(__dirname, 'index.html')).href;
|
|
return event.sender === webContents &&
|
|
frame === webContents.mainFrame &&
|
|
frame.url === expectedUrl;
|
|
}
|
|
|
|
function registerIpc(channel, handler) {
|
|
ipcMain.handle(channel, async (event, ...args) => {
|
|
if (!isTrustedSender(event)) throw new Error('Forbidden IPC sender');
|
|
return handler(...args);
|
|
});
|
|
}
|
|
|
|
function registerIpcHandlers() {
|
|
registerIpc('get-devices', () => runtime.getDevices());
|
|
registerIpc('get-device-sites', () => runtime.getDeviceSites());
|
|
registerIpc('get-device-groups', () => runtime.getDeviceGroups());
|
|
registerIpc('get-device-hierarchy', () => runtime.getDeviceHierarchy());
|
|
registerIpc('get-auth-info', () => runtime.getAuthInfo());
|
|
registerIpc('launch-proxy', (deviceId) => runtime.launchProxy(deviceId));
|
|
registerIpc('stop-proxy', async (key) => {
|
|
const result = await runtime.stopProxy(key);
|
|
sendConnectionState();
|
|
return result;
|
|
});
|
|
registerIpc('disconnect', async () => {
|
|
const state = await runtime.disconnect();
|
|
sendConnectionState();
|
|
return state;
|
|
});
|
|
registerIpc('get-connection-state', () => runtime.getConnectionState());
|
|
registerIpc('check-for-updates', () => runtime.checkForUpdates());
|
|
registerIpc('open-fixed-releases-page', () => runtime.openFixedReleasesPage());
|
|
registerIpc('rotate-pairing', () => {
|
|
firstRunPairingSecret = null;
|
|
return pairingController.rotate();
|
|
});
|
|
registerIpc('revoke-pairing', () => {
|
|
firstRunPairingSecret = null;
|
|
return pairingController.revoke();
|
|
});
|
|
registerIpc('get-pairing-status', () => {
|
|
const result = pairingController.getStatus();
|
|
if (firstRunPairingSecret) {
|
|
result.secret = firstRunPairingSecret;
|
|
firstRunPairingSecret = null;
|
|
}
|
|
return result;
|
|
});
|
|
}
|
|
|
|
function startBridgeServer() {
|
|
if (!pairingController.bridgeAuth) return;
|
|
const handler = createBridgeHandler({
|
|
bridgeAuth: pairingController.bridgeAuth,
|
|
sessionStore: runtime.sessionStore,
|
|
onConnectionStateChanged: () => {
|
|
runtime.onSessionChanged();
|
|
sendConnectionState();
|
|
},
|
|
});
|
|
bridgeServer = http.createServer((request, response) => {
|
|
handler(request, response).catch(() => {
|
|
if (!response.writableEnded) {
|
|
response.writeHead(500, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
response.end(JSON.stringify({ success: false, message: 'Bridge request failed' }));
|
|
}
|
|
});
|
|
});
|
|
bridgeServer.on('clientError', (_error, socket) => socket.destroy());
|
|
bridgeServer.on('error', (error) => {
|
|
console.error(`Bridge server unavailable (${error.code || 'UNKNOWN'}).`);
|
|
});
|
|
bridgeServer.listen(BRIDGE_PORT, BRIDGE_HOST);
|
|
}
|
|
|
|
app.whenReady().then(() => {
|
|
const sessionStore = createSessionStore();
|
|
const altaClient = createAltaClient({ sessionStore });
|
|
const proxyManager = createProxyManager({ appDirectory: getAppDirectory() });
|
|
const secureStorageAvailable = safeStorage && safeStorage.isEncryptionAvailable();
|
|
pairingController = new PairingController({
|
|
envelopePath: path.join(app.getPath('userData'), PAIRING_ENVELOPE_FILENAME),
|
|
protect: secureStorageAvailable ? (value) => safeStorage.encryptString(value) : undefined,
|
|
unprotect: secureStorageAvailable ? (value) => safeStorage.decryptString(value) : undefined,
|
|
});
|
|
const pairingState = pairingController.initialize();
|
|
firstRunPairingSecret = pairingState.secret || null;
|
|
runtime = new AppRuntime({
|
|
sessionStore,
|
|
altaClient,
|
|
proxyManager,
|
|
checkForUpdate,
|
|
currentVersion: app.getVersion(),
|
|
openExternal: (url) => shell.openExternal(url),
|
|
});
|
|
|
|
registerIpcHandlers();
|
|
createWindow();
|
|
startBridgeServer();
|
|
});
|
|
|
|
function closeBridgeServer() {
|
|
if (!bridgeServer) return Promise.resolve();
|
|
const server = bridgeServer;
|
|
return new Promise((resolve, reject) => {
|
|
const finish = (error) => {
|
|
if (!error || error.code === 'ERR_SERVER_NOT_RUNNING') {
|
|
if (bridgeServer === server) bridgeServer = null;
|
|
resolve();
|
|
return;
|
|
}
|
|
reject(error);
|
|
};
|
|
try { server.close(finish); } catch (error) { finish(error); }
|
|
});
|
|
}
|
|
|
|
async function notifyQuitBlocked(state) {
|
|
sendConnectionState();
|
|
const activeCount = state && Array.isArray(state.activeProxies) ? state.activeProxies.length : 0;
|
|
const activeProxiesRemain = activeCount > 0;
|
|
const processDetail = activeProxiesRemain
|
|
? `${activeCount} app-owned proxy process${activeCount === 1 ? '' : 'es'} remain active.`
|
|
: 'No app-owned proxy process remains active.';
|
|
const sessionDetail = state && state.connected
|
|
? ' The Alta session is still connected; resolve the process error and try quitting again.'
|
|
: ' Resolve the shutdown error and try quitting again.';
|
|
await dialog.showMessageBox({
|
|
type: 'error',
|
|
title: activeProxiesRemain ? 'Alta Camera Proxy is still running' : 'Alta Camera Proxy could not close',
|
|
message: activeProxiesRemain
|
|
? 'The app could not confirm that every proxy process exited.'
|
|
: 'The app could not complete shutdown safely.',
|
|
detail: `${processDetail}${sessionDetail}`,
|
|
buttons: ['OK'],
|
|
noLink: true,
|
|
});
|
|
}
|
|
|
|
app.on('before-quit', (event) => {
|
|
if (allowConfirmedQuit) return;
|
|
event.preventDefault();
|
|
if (quitCleanupInProgress) return;
|
|
quitCleanupInProgress = true;
|
|
|
|
(async () => {
|
|
try {
|
|
const state = runtime
|
|
? await runtime.disconnect()
|
|
: { success: true, connected: false, activeProxies: [] };
|
|
if (!state.success || state.activeProxies.length > 0) {
|
|
await notifyQuitBlocked(state);
|
|
return;
|
|
}
|
|
await closeBridgeServer();
|
|
if (runtime) runtime.sessionStore.dispose();
|
|
allowConfirmedQuit = true;
|
|
app.quit();
|
|
} catch {
|
|
const state = runtime
|
|
? runtime.getConnectionState()
|
|
: { connected: false, activeProxies: [] };
|
|
await notifyQuitBlocked(state);
|
|
} finally {
|
|
quitCleanupInProgress = false;
|
|
}
|
|
})();
|
|
});
|
|
|
|
app.on('window-all-closed', () => {
|
|
if (process.platform !== 'darwin') app.quit();
|
|
});
|
|
|
|
app.on('activate', () => {
|
|
if (BrowserWindow.getAllWindows().length === 0 && runtime) createWindow();
|
|
});
|