const { app, BrowserWindow, ipcMain, net, protocol, shell } = require('electron'); const fs = require('node:fs/promises'); const path = require('node:path'); const APP_SCHEME = 'webavp'; const ROOT_DIR = path.resolve(__dirname, '..'); const TEMPLATE_DIR = path.join(ROOT_DIR, 'templates'); const STATIC_DIR = path.join(ROOT_DIR, 'static'); if (process.env.WEBAVP_DISABLE_GPU === '1') { app.disableHardwareAcceleration(); } protocol.registerSchemesAsPrivileged([ { scheme: APP_SCHEME, privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true, stream: true, }, }, ]); function contentTypeFor(filePath) { const ext = path.extname(filePath).toLowerCase(); switch (ext) { case '.html': return 'text/html; charset=utf-8'; case '.js': return 'text/javascript; charset=utf-8'; case '.css': return 'text/css; charset=utf-8'; case '.json': return 'application/json; charset=utf-8'; case '.svg': return 'image/svg+xml'; case '.png': return 'image/png'; case '.jpg': case '.jpeg': return 'image/jpeg'; case '.webp': return 'image/webp'; case '.mp4': return 'video/mp4'; case '.webm': return 'video/webm'; default: return 'application/octet-stream'; } } function safeJoin(baseDir, requestPath) { const decoded = decodeURIComponent(requestPath); const normalized = path.normalize(decoded).replace(/^([/\\])+/, ''); const resolved = path.resolve(baseDir, normalized); const base = path.resolve(baseDir); if (resolved !== base && !resolved.startsWith(base + path.sep)) { throw new Error('Path traversal blocked'); } return resolved; } async function serveFile(filePath) { try { const data = await fs.readFile(filePath); return new Response(data, { headers: { 'content-type': contentTypeFor(filePath) }, }); } catch (err) { if (err && err.code === 'ENOENT') { return new Response('Not found', { status: 404 }); } console.error('[WebAVP] app protocol error:', err); return new Response('Internal server error', { status: 500 }); } } function registerAppProtocol() { protocol.handle(APP_SCHEME, async (request) => { const url = new URL(request.url); if (url.hostname !== 'app') { return new Response('Unknown host', { status: 404 }); } if (url.pathname === '/' || url.pathname === '/index.html') { return serveFile(path.join(TEMPLATE_DIR, 'index.html')); } if (url.pathname.startsWith('/static/')) { try { const relativeStaticPath = url.pathname.slice('/static/'.length); return serveFile(safeJoin(STATIC_DIR, relativeStaticPath)); } catch (err) { return new Response('Forbidden', { status: 403 }); } } return new Response('Not found', { status: 404 }); }); } async function verifyCertificateOnline(_event, { serial, certificateHash } = {}) { if (!serial || !certificateHash) { return { verified: false, error: 'Missing parameters' }; } const params = new URLSearchParams({ serial: String(serial).toLowerCase(), certificateHash: String(certificateHash), }); const url = `https://aware.avasecurity.com/api/v1/public/verifyServerCertificate?${params.toString()}`; try { const response = await net.fetch(url, { method: 'GET' }); if (response.ok) { return { verified: true }; } return { verified: false, error: `HTTP ${response.status}` }; } catch (err) { return { verified: false, error: err.message || 'Certificate verification request failed' }; } } function createWindow() { const win = new BrowserWindow({ width: 1440, height: 960, minWidth: 1024, minHeight: 720, backgroundColor: '#121826', title: 'Alta Video Player', webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false, sandbox: true, }, }); win.loadURL(`${APP_SCHEME}://app/index.html`); if (process.env.WEBAVP_SMOKE_QUIT_AFTER_MS) { const quitAfterMs = Number(process.env.WEBAVP_SMOKE_QUIT_AFTER_MS); win.webContents.once('did-finish-load', async () => { try { const result = await win.webContents.executeJavaScript(`({ title: document.title, hasJsZip: typeof window.JSZip === 'function', hasNativeBridge: !!window.webavpNative?.verifyCertificateOnline })`); if (result.title !== 'Alta Video Player' || !result.hasJsZip || !result.hasNativeBridge) { console.error('[WebAVP] Electron smoke checks failed:', JSON.stringify(result)); app.exit(1); return; } console.log('[WebAVP] Electron smoke loaded webavp://app/index.html with JSZip and native bridge'); setTimeout(() => app.quit(), Number.isFinite(quitAfterMs) ? quitAfterMs : 250); } catch (err) { console.error('[WebAVP] Electron smoke failed:', err); app.exit(1); } }); win.webContents.once('did-fail-load', (_event, errorCode, errorDescription, validatedURL) => { console.error(`[WebAVP] Electron smoke failed to load ${validatedURL}: ${errorCode} ${errorDescription}`); app.exit(1); }); } win.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: 'deny' }; }); return win; } app.whenReady().then(() => { registerAppProtocol(); ipcMain.handle('certificate:verify-online', verifyCertificateOnline); createWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });