Add Electron desktop shell and clean project docs
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
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();
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('webavpNative', {
|
||||
isElectron: true,
|
||||
verifyCertificateOnline({ serial, certificateHash }) {
|
||||
return ipcRenderer.invoke('certificate:verify-online', { serial, certificateHash });
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user