mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 06:12:10 +03:00
## Security Fixes - Sanitize OMNIROUTE_MEMORY_MB with parseInt + range validation (64-16384) to prevent command injection via spawn() args - Validate URL protocol in shell.openExternal (http/https only) to prevent RCE in Electron renderer compromise - Bump default memory from 256MB to 512MB ## Electron package-lock.json - Added to .gitignore (5278 lines removed from tracking) ## Test Suite (64 tests, 9 suites) - electron-main.test.mjs: URL validation, IPC channels, window handler - electron-preload.test.mjs: channel whitelist, API surface, open-external - cli-memory.test.mjs: injection prevention, boundary values, .env parsing ## Documentation - Desktop App section added to all 30 READMEs (9 fully translated) - USER_GUIDE.md updated with 512MB default - .env.example reflects new defaults
335 lines
7.3 KiB
JavaScript
335 lines
7.3 KiB
JavaScript
/**
|
|
* OmniRoute Electron Desktop App - Main Process
|
|
*
|
|
* This is the entry point for the Electron desktop application.
|
|
* It manages the main window, system tray, and IPC communication.
|
|
*/
|
|
|
|
const { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage, shell } = require("electron");
|
|
const path = require("path");
|
|
const { spawn } = require("child_process");
|
|
const fs = require("fs");
|
|
|
|
// Single instance lock - prevent multiple instances
|
|
const gotTheLock = app.requestSingleInstanceLock();
|
|
if (!gotTheLock) {
|
|
app.quit();
|
|
process.exit(0);
|
|
}
|
|
|
|
app.on("second-instance", () => {
|
|
if (mainWindow) {
|
|
if (mainWindow.isMinimized()) mainWindow.restore();
|
|
mainWindow.show();
|
|
mainWindow.focus();
|
|
}
|
|
});
|
|
|
|
// Environment detection
|
|
const isDev = process.env.NODE_ENV === "development" || !app.isPackaged;
|
|
const isProduction = !isDev;
|
|
|
|
// Paths
|
|
const APP_PATH = app.getAppPath();
|
|
const RESOURCES_PATH = isProduction ? process.resourcesPath : APP_PATH;
|
|
const NEXT_SERVER_PATH = path.join(RESOURCES_PATH, "app");
|
|
|
|
// State
|
|
let mainWindow = null;
|
|
let tray = null;
|
|
let nextServer = null;
|
|
let serverPort = 20128;
|
|
|
|
// Server URL
|
|
const getServerUrl = () => `http://localhost:${serverPort}`;
|
|
|
|
/**
|
|
* Create the main application window
|
|
*/
|
|
function createWindow() {
|
|
mainWindow = new BrowserWindow({
|
|
width: 1400,
|
|
height: 900,
|
|
minWidth: 1024,
|
|
minHeight: 700,
|
|
title: "OmniRoute",
|
|
icon: path.join(RESOURCES_PATH, "assets", "icon.png"),
|
|
webPreferences: {
|
|
preload: path.join(__dirname, "preload.js"),
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
webSecurity: true,
|
|
},
|
|
show: false,
|
|
backgroundColor: "#0a0a0a",
|
|
titleBarStyle: "hiddenInset",
|
|
trafficLightPosition: { x: 16, y: 16 },
|
|
});
|
|
|
|
// Load the Next.js app
|
|
if (isDev) {
|
|
mainWindow.loadURL(getServerUrl());
|
|
mainWindow.webContents.openDevTools({ mode: "detach" });
|
|
} else {
|
|
mainWindow.loadURL(getServerUrl());
|
|
}
|
|
|
|
// Show window when ready
|
|
mainWindow.once("ready-to-show", () => {
|
|
mainWindow.show();
|
|
});
|
|
|
|
// Handle external links — validate URL protocol to prevent RCE (#150)
|
|
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
|
try {
|
|
const parsedUrl = new URL(url);
|
|
if (["http:", "https:"].includes(parsedUrl.protocol)) {
|
|
shell.openExternal(url);
|
|
} else {
|
|
console.warn("[Electron] Blocked external URL with unsafe protocol:", parsedUrl.protocol);
|
|
}
|
|
} catch {
|
|
console.error("[Electron] Invalid external URL blocked:", url);
|
|
}
|
|
return { action: "deny" };
|
|
});
|
|
|
|
// Handle window close
|
|
mainWindow.on("close", (event) => {
|
|
if (!app.isQuitting) {
|
|
event.preventDefault();
|
|
mainWindow.hide();
|
|
}
|
|
return false;
|
|
});
|
|
|
|
mainWindow.on("closed", () => {
|
|
mainWindow = null;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create system tray icon
|
|
*/
|
|
function createTray() {
|
|
const iconPath = path.join(RESOURCES_PATH, "assets", "tray-icon.png");
|
|
let icon;
|
|
|
|
try {
|
|
icon = nativeImage.createFromPath(iconPath);
|
|
if (icon.isEmpty()) {
|
|
icon = nativeImage.createEmpty();
|
|
}
|
|
} catch {
|
|
icon = nativeImage.createEmpty();
|
|
}
|
|
|
|
tray = new Tray(icon);
|
|
|
|
const contextMenu = Menu.buildFromTemplate([
|
|
{
|
|
label: "Open OmniRoute",
|
|
click: () => {
|
|
if (mainWindow) {
|
|
mainWindow.show();
|
|
mainWindow.focus();
|
|
}
|
|
},
|
|
},
|
|
{
|
|
label: "Open Dashboard",
|
|
click: () => {
|
|
shell.openExternal(getServerUrl());
|
|
},
|
|
},
|
|
{ type: "separator" },
|
|
{
|
|
label: "Server Port",
|
|
submenu: [
|
|
{ label: `Port: ${serverPort}`, enabled: false },
|
|
{ type: "separator" },
|
|
{ label: "20128", click: () => changePort(20128) },
|
|
{ label: "3000", click: () => changePort(3000) },
|
|
{ label: "8080", click: () => changePort(8080) },
|
|
],
|
|
},
|
|
{ type: "separator" },
|
|
{
|
|
label: "Quit",
|
|
click: () => {
|
|
app.isQuitting = true;
|
|
app.quit();
|
|
},
|
|
},
|
|
]);
|
|
|
|
tray.setToolTip("OmniRoute");
|
|
tray.setContextMenu(contextMenu);
|
|
|
|
tray.on("double-click", () => {
|
|
if (mainWindow) {
|
|
mainWindow.show();
|
|
mainWindow.focus();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Change the server port
|
|
*/
|
|
function changePort(port) {
|
|
if (port === serverPort) return;
|
|
serverPort = port;
|
|
if (mainWindow) {
|
|
mainWindow.loadURL(getServerUrl());
|
|
}
|
|
createTray();
|
|
}
|
|
|
|
/**
|
|
* Start the Next.js server (production mode)
|
|
*/
|
|
function startNextServer() {
|
|
if (isDev) {
|
|
console.log("Development mode: Connect to existing Next.js server");
|
|
return;
|
|
}
|
|
|
|
const serverScript = path.join(NEXT_SERVER_PATH, "server.js");
|
|
|
|
if (!fs.existsSync(serverScript)) {
|
|
console.error("Server script not found:", serverScript);
|
|
return;
|
|
}
|
|
|
|
console.log("Starting Next.js server...");
|
|
|
|
nextServer = spawn("node", [serverScript], {
|
|
cwd: NEXT_SERVER_PATH,
|
|
env: {
|
|
...process.env,
|
|
PORT: String(serverPort),
|
|
NODE_ENV: "production",
|
|
},
|
|
stdio: "inherit",
|
|
});
|
|
|
|
nextServer.on("error", (err) => {
|
|
console.error("Failed to start server:", err);
|
|
});
|
|
|
|
nextServer.on("exit", (code) => {
|
|
console.log("Server exited with code:", code);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Stop the Next.js server
|
|
*/
|
|
function stopNextServer() {
|
|
if (nextServer) {
|
|
nextServer.kill();
|
|
nextServer = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* IPC Handlers
|
|
*/
|
|
function setupIpcHandlers() {
|
|
// Get app info
|
|
ipcMain.handle("get-app-info", () => ({
|
|
name: app.getName(),
|
|
version: app.getVersion(),
|
|
platform: process.platform,
|
|
isDev,
|
|
port: serverPort,
|
|
}));
|
|
|
|
// Open external URL
|
|
ipcMain.handle("open-external", (event, url) => {
|
|
try {
|
|
const parsedUrl = new URL(url);
|
|
if (["http:", "https:"].includes(parsedUrl.protocol)) {
|
|
shell.openExternal(url);
|
|
}
|
|
} catch {
|
|
console.error("Invalid URL:", url);
|
|
}
|
|
});
|
|
|
|
// Get data directory
|
|
ipcMain.handle("get-data-dir", () => {
|
|
return app.getPath("userData");
|
|
});
|
|
|
|
// Restart server
|
|
ipcMain.handle("restart-server", async () => {
|
|
const serverToStop = nextServer;
|
|
stopNextServer();
|
|
if (serverToStop) {
|
|
await new Promise((resolve) => serverToStop.once("exit", resolve));
|
|
}
|
|
startNextServer();
|
|
return { success: true };
|
|
});
|
|
|
|
// Window controls
|
|
ipcMain.on("window-minimize", () => {
|
|
mainWindow?.minimize();
|
|
});
|
|
|
|
ipcMain.on("window-maximize", () => {
|
|
if (mainWindow) {
|
|
if (mainWindow.isMaximized()) {
|
|
mainWindow.unmaximize();
|
|
} else {
|
|
mainWindow.maximize();
|
|
}
|
|
}
|
|
});
|
|
|
|
ipcMain.on("window-close", () => {
|
|
mainWindow?.close();
|
|
});
|
|
}
|
|
|
|
// App lifecycle events
|
|
app.whenReady().then(() => {
|
|
startNextServer();
|
|
createWindow();
|
|
createTray();
|
|
setupIpcHandlers();
|
|
|
|
// macOS: recreate window when dock icon clicked
|
|
app.on("activate", () => {
|
|
if (BrowserWindow.getAllWindows().length === 0) {
|
|
createWindow();
|
|
} else if (mainWindow) {
|
|
mainWindow.show();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Quit when all windows are closed (except on macOS)
|
|
app.on("window-all-closed", () => {
|
|
if (process.platform !== "darwin") {
|
|
app.quit();
|
|
}
|
|
});
|
|
|
|
// Clean up before quit
|
|
app.on("before-quit", () => {
|
|
app.isQuitting = true;
|
|
stopNextServer();
|
|
});
|
|
|
|
// Handle uncaught exceptions
|
|
process.on("uncaughtException", (error) => {
|
|
console.error("Uncaught Exception:", error);
|
|
});
|
|
|
|
process.on("unhandledRejection", (reason) => {
|
|
console.error("Unhandled Rejection:", reason);
|
|
});
|