/* * HTTP tunnel over WebRTC - Client UI implementation * Author: Konstantinos Drakontidis * Email: gedra100sh@gmail.com */ import { app, BrowserWindow, Menu, ipcMain } from "electron"; import { ChildProcessWithoutNullStreams, spawn } from "node:child_process"; import { glob } from "glob"; import * as net from "net"; import * as path from "path"; var backend: ChildProcessWithoutNullStreams; var shutting_down = false; // Graceful shutdown function shutdown() { // Check if shutdown has already been called if (!shutting_down) { shutting_down = true; if (backend) { // Kill backend child by sending SIGINT backend.stdout.removeAllListeners(); backend.stderr.removeAllListeners(); backend.once("exit", () => { process.exit(0); }); backend.stdin.write("exit\n"); setTimeout(() => { process.exit(-1) }, 5000); } else { process.exit(0); } } } // Returns an available port number function getAvailablePort(): Promise { return new Promise((resolve, reject) => { const server = net.createServer(); server.listen(0, () => { const address = server.address(); if (typeof address === "object" && address !== null) { const port = address.port; server.close(() => resolve(port)); } else { reject(new Error("Could not get port")); } }); server.on("error", reject); }); } app.whenReady().then(async () => { const main_window = new BrowserWindow({ webPreferences: { nodeIntegration: true, contextIsolation: false }, resizable: false, width: 800, height: 600 }); // Load main window and disable menu await main_window.loadFile(path.join(__dirname, "..", "views", "main.html")); Menu.setApplicationMenu(null); var line_buffer = ""; var connected = false; // Generate a port number and search for the peer binary const http_port = await getAvailablePort(); const peer_exec = await glob(path.join(process.resourcesPath, "extra", "peer*").replaceAll("\\", "/")); if (peer_exec.length) { // Create child backend = spawn(peer_exec[0], [http_port.toString()], { cwd: path.join(process.resourcesPath, "extra") }); backend.on("spawn", () => { main_window.webContents.send("backend_spawn"); }); backend.stdout.on("data", (data: Buffer) => { main_window.webContents.send("backend_info", data.toString()); line_buffer += data.toString(); // Scan each line from stdout to check if datachannel opened var lines = line_buffer.split("\n"); line_buffer = lines.pop() || ""; for (const line of lines) { if (line.trim() === "Opened data channel.") { connected = true; main_window.webContents.send("backend_dc", http_port.toString()); main_window.setResizable(true); main_window.maximize(); } } }); backend.stderr.on("data", (data: Buffer) => { main_window.webContents.send("backend_error", data.toString()); }); backend.on("close", async () => { // Check if main window is currently showing and if not redirect if (connected) { await main_window.loadFile(path.join(__dirname, "..", "views", "main.html")); main_window.unmaximize(); main_window.setSize(800, 600, true) main_window.setResizable(false); } main_window.webContents.send("backend_quit"); }); } else { process.exit(-1); } }); // Handling basic HTTP authentication let authCallback: ((user: string, pass: string) => void) | null = null; app.on("login", (event, webContents, request, authInfo, callback) => { event.preventDefault(); authCallback = callback; const loginWin = new BrowserWindow({ parent: BrowserWindow.getFocusedWindow() ?? undefined, width: 300, height: 180, resizable: false, modal: true, frame: false, webPreferences: { nodeIntegration: true, contextIsolation: false, }, }); loginWin.loadFile(path.join(__dirname, "..", "views", "login.html")); }); app.on("window-all-closed", () => { shutdown(); }); app.on("quit", () => { shutdown(); }); ipcMain.on("login-submit", (_event, { username, password }) => { if (authCallback) { authCallback(username, password); authCallback = null; } BrowserWindow.getFocusedWindow()?.close(); }); ipcMain.on("login-cancel", () => { shutdown(); });