160 lines
4.8 KiB
TypeScript
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();
|
|
});
|