Files
2025-07-25 16:25:06 +03:00

160 lines
4.8 KiB
TypeScript

/*
* 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<number> {
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();
});