Files
WebAVP/electron/main.js
T
peji 407892ed9a Fix Electron cloud cert verification (use Node https, not net.fetch)
The Alta verify endpoint requests an optional TLS client certificate.
Chromium's network stack (net.fetch) reacts by aborting the handshake
with ERR_SSL_CLIENT_AUTH_CERT_NEEDED, and the select-client-certificate
event never fires to let us proceed — so cloud verification silently
failed only in the desktop build, looking like the app was offline.

Issue the request via Node's https module instead, matching the Python
app.py reference (urlopen) and curl, which ignore the optional client-
cert request and proceed normally. Same 2xx-means-verified semantics,
10s timeout, and return shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 08:39:20 -04:00

202 lines
6.4 KiB
JavaScript

const { app, BrowserWindow, ipcMain, protocol, shell } = require('electron');
const fs = require('node:fs/promises');
const path = require('node:path');
const https = require('node:https');
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 });
});
}
// Issue the GET via Node's https stack (not Electron's net.fetch). The Alta
// endpoint requests an optional TLS client certificate; Chromium's net stack
// reacts by failing the handshake with ERR_SSL_CLIENT_AUTH_CERT_NEEDED, which
// made cloud verification appear "offline" in the desktop build. Node's TLS
// (like curl and the Python app.py reference) simply proceeds without one.
function httpsGetStatus(url, timeoutMs = 10000) {
return new Promise((resolve, reject) => {
const req = https.get(url, (res) => {
res.resume(); // drain so the socket is released
resolve(res.statusCode);
});
req.on('error', reject);
req.setTimeout(timeoutMs, () => {
req.destroy(new Error('Certificate verification request timed out'));
});
});
}
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 status = await httpsGetStatus(url);
if (status >= 200 && status < 300) {
return { verified: true };
}
return { verified: false, error: `HTTP ${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();
});