From 2e95075d3e88379cf6148360b503468e4f93e272 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sun, 26 Jul 2026 22:48:02 +0100 Subject: [PATCH] Implement local server for OTP authentication and enhance socket client functionality Add a LocalServer class to handle OTP authentication requests and server status checks. Update index.js to integrate local server functionality, including checks for server status and OTP authentication flow. Enhance SocketClient with connection waiting and improved OTP authentication handling. Update config.json with a new authCode for the host. --- config.json | 2 +- src/index.js | 94 +++++++++++++++++++- src/localserver/localserver.js | 76 ++++++++++++++++ src/socket/socketclient.js | 156 +++++++++++++++++++++++---------- 4 files changed, 278 insertions(+), 50 deletions(-) create mode 100644 src/localserver/localserver.js diff --git a/config.json b/config.json index 28875f8..f159f83 100644 --- a/config.json +++ b/config.json @@ -5,7 +5,7 @@ "apiUrl": "https://dev.tombutcher.work/api", "host": { "id": "691a1db49ce913faf0e51284", - "authCode": "9uu3DC0si__-F9FnGWTKudle5z6yZasFMlKohnShElPekRYteh-LlZaksHOXFfOO" + "authCode": "BNj13heKj9zUZIJ0IodSU-V-OSZHNJGAA-nLa4nwFrDHa6N1wah7mpw_7pqbT9Il" } }, "production": { diff --git a/src/index.js b/src/index.js index 15d443f..baebd1a 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,9 @@ +import axios from "axios"; import { loadConfig } from "./config.js"; import log4js from "log4js"; import { createElectronWindow } from "./electron/window.js"; import { setupIPC } from "./electron/ipc.js"; +import { LOCAL_SERVER_PORT, LocalServer } from "./localserver/localserver.js"; import { SocketClient } from "./socket/socketclient.js"; // Load configuration @@ -12,7 +14,89 @@ logger.level = config.logLevel; const isHeadless = process.argv.includes("--headless"); +function getArgValue(flag) { + const withEquals = process.argv.find((arg) => arg.startsWith(`${flag}=`)); + if (withEquals) { + return withEquals.slice(flag.length + 1); + } + + const index = process.argv.indexOf(flag); + if (index !== -1 && index + 1 < process.argv.length) { + return process.argv[index + 1]; + } + + return undefined; +} + +const otpCode = getArgValue("--otp") || undefined; + +async function checkRunning() { + try { + const response = await axios.get( + `http://127.0.0.1:${LOCAL_SERVER_PORT}/info`, + { timeout: 1000 }, + ); + if (response.data?.running === true) { + return response.data; + } + } catch (err) { + return false; + } +} + +async function authenticateWithOtp(otpCode) { + try { + const response = await axios.post( + `http://127.0.0.1:${LOCAL_SERVER_PORT}/otpAuth`, + { otp: otpCode }, + { timeout: 30000 }, + ); + return response.data; + } catch (err) { + if (err.response?.data) { + return err.response.data; + } + throw new Error("Failed to authenticate with OTP"); + } +} export async function init() { + logger.info("⌛ Checking if Farm Control Server is running..."); + + const runningServer = await checkRunning(); + + if (otpCode != undefined) { + logger.info("✔ Farm Control Server is running."); + } + + if (otpCode != undefined) { + logger.info("🔒 Authenticating with OTP..."); + const authenticatedServer = await authenticateWithOtp(otpCode); + if (authenticatedServer.valid == false) { + logger.error( + "✘ Failed to authenticate with OTP -", + authenticatedServer.error, + ); + } else { + logger.info("✔ Authenticated with OTP."); + logger.info("----------Host----------"); + logger.info("Name:", authenticatedServer.host?.name || "Unknown"); + logger.info("Tags: -", authenticatedServer.host?.tags?.[0] || "n/a"); + for (var i = 0; i < authenticatedServer.host?.tags?.length - 1; i++) { + const tag = authenticatedServer.host?.tags[i + 1]; + logger.info(" -", tag); + } + logger.info("State:", "Online"); + logger.info("------------------------"); + return authenticatedServer; + } + return authenticatedServer; + } + + if (runningServer != false) { + logger.warn("✘ Farm Control Server is already running."); + return runningServer; + } + if (!isHeadless) { // Create Electron window first logger.info("Creating electron window..."); @@ -31,13 +115,21 @@ export async function init() { const socketClient = new SocketClient(); // Make socket client globally accessible for IPC handlers global.socketClient = socketClient; + + const localServer = new LocalServer(socketClient); + await localServer.start(); + socketClient.connect(); process.on("SIGINT", () => { logger.info("Shutting down..."); socketClient.disconnect(); + localServer.stop(); process.exit(0); }); } -init(); +init().catch((err) => { + logger.error(err.message); + process.exit(1); +}); diff --git a/src/localserver/localserver.js b/src/localserver/localserver.js new file mode 100644 index 0000000..a6f6043 --- /dev/null +++ b/src/localserver/localserver.js @@ -0,0 +1,76 @@ +import express from "express"; +import log4js from "log4js"; +import { notPrompting } from "../utils.js"; +import { loadConfig } from "../config.js"; + +export const LOCAL_SERVER_PORT = 47291; + +const config = loadConfig(); + +const logger = log4js.getLogger("Local Server"); +logger.level = config.logLevel; + +export class LocalServer { + constructor(socketClient) { + this.socketClient = socketClient; + this.server = null; + this.app = express(); + this.app.use(express.json()); + this.setupRoutes(); + } + + setupRoutes() { + this.app.get("/info", (_req, res) => { + logger.debug("Received request to check if server is running"); + res.json({ running: true }); + }); + + this.app.post("/otpAuth", async (req, res) => { + logger.debug("Received request to authenticate with OTP"); + const { otp } = req.body; + + if (!otp) { + res.status(400).json({ error: "otp is required" }); + return; + } + + try { + logger.info("Authenticating with OTP via local server..."); + notPrompting(); + const result = await this.socketClient.authenticateWithOtp(otp, { + retryOnFailure: false, + }); + + if (!result.valid) { + res.status(401).json(result); + return; + } + + res.json(result); + } catch (err) { + logger.error("OTP authentication failed:", err); + res.status(500).json({ error: err.message }); + } + }); + } + + start() { + return new Promise((resolve, reject) => { + this.server = this.app.listen(LOCAL_SERVER_PORT, "127.0.0.1", () => { + logger.info( + `Local server listening on http://127.0.0.1:${LOCAL_SERVER_PORT}`, + ); + resolve(this.server); + }); + + this.server.on("error", reject); + }); + } + + stop() { + if (this.server) { + this.server.close(); + this.server = null; + } + } +} diff --git a/src/socket/socketclient.js b/src/socket/socketclient.js index e9ca3ba..41e1168 100644 --- a/src/socket/socketclient.js +++ b/src/socket/socketclient.js @@ -119,49 +119,109 @@ export class SocketClient { this.socket.disconnect(); } - async authenticateWithOtp(otp = undefined) { + async waitForConnection(timeoutMs = 10000) { + if (this.socket?.connected) { + return; + } + + if (!this.socket) { + throw new Error("Socket client is not connected"); + } + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + this.socket.off("connect", onConnect); + reject(new Error("Socket connection timed out")); + }, timeoutMs); + + const onConnect = () => { + clearTimeout(timeout); + resolve(); + }; + + if (this.socket.connected) { + clearTimeout(timeout); + resolve(); + return; + } + + this.socket.once("connect", onConnect); + }); + } + + async authenticateWithOtp(otp = undefined, options = {}) { + const { retryOnFailure = otp == undefined } = options; + if (otp == undefined) { otp = await askOtp(); } - await this.authenticate({ otp: otp }); - return; + + await this.waitForConnection(); + return this.authenticate({ otp }, { retryOnFailure }); } - async authenticate(authenticationData) { - logger.debug("Host authenticating..."); - this.socket.emit( - "authenticate", - authenticationData, - async (verifyResult) => { - if (verifyResult.valid == false) { - this.authenticated = false; - sendIPC("setAuthenticated", false); - logger.error("Host not authenticated:", verifyResult.error); - await this.authenticateWithOtp(); - return; - } - logger.info("Host authenticated."); - this.authenticated = true; - sendIPC("setAuthenticated", true); - sendIPC("setLoading", false); - this.loading = false; - this.host = verifyResult.host; - sendIPC("setHost", { - ...this.host, - online: true, - state: { type: "online" }, - }); - this.id = this.host._id; + async authenticate(authenticationData, options = {}) { + const { retryOnFailure = true } = options; - config.host = { id: this.id, authCode: this.host.authCode }; - saveConfig(config); - this.sendDeviceInfo(); - this.subscribeToObjectEvents(); - this.documentPrinterManager.reloadDocumentPrinters(); - this.printerManager.reloadPrinters(); - await this.fileManager.updateFiles(); + await this.waitForConnection(); + + return new Promise((resolve, reject) => { + if (!this.socket?.connected) { + reject(new Error("Socket client is not connected")); + return; } - ); + + logger.debug("Host authenticating..."); + this.socket.emit( + "authenticate", + authenticationData, + async (verifyResult) => { + if (verifyResult.valid == false) { + this.authenticated = false; + sendIPC("setAuthenticated", false); + logger.error("Host not authenticated:", verifyResult.error); + + if (retryOnFailure) { + const retryResult = await this.authenticateWithOtp(); + resolve(retryResult); + return; + } + + resolve({ valid: false, error: verifyResult.error }); + return; + } + + logger.info("Host authenticated."); + this.authenticated = true; + sendIPC("setAuthenticated", true); + sendIPC("setLoading", false); + this.loading = false; + if (this.host) { + sendIPC("setHost", { + ...this.host, + online: false, + state: { type: "offline" }, + }); + } + this.host = verifyResult.host; + sendIPC("setHost", { + ...this.host, + online: true, + state: { type: "online" }, + }); + this.id = this.host._id; + + config.host = { id: this.id, authCode: this.host.authCode }; + saveConfig(config); + this.sendDeviceInfo(); + this.subscribeToObjectEvents(); + this.documentPrinterManager.reloadDocumentPrinters(); + this.printerManager.reloadPrinters(); + await this.fileManager.updateFiles(); + resolve({ valid: true, host: this.host }); + }, + ); + }); } handleHostAction(action, callback) { @@ -194,7 +254,7 @@ export class SocketClient { this.documentPrinterManager.handleDocumentPrinterAction( id, action, - callback + callback, ); } } @@ -204,7 +264,7 @@ export class SocketClient { "Got object update for type:", data.objectType, " id:", - data._id + data._id, ); if (data.objectType == "host") { this.handleHostAction(action, callback); @@ -213,7 +273,7 @@ export class SocketClient { this.documentPrinterManager.handleDocumentPrinterUpdate( data._id, data, - callback + callback, ); } if (data.objectType == "printer") { @@ -315,7 +375,7 @@ export class SocketClient { }); resolve(result); } - } + }, ); }); } @@ -342,7 +402,7 @@ export class SocketClient { }); resolve(result); } - } + }, ); }); } @@ -374,7 +434,7 @@ export class SocketClient { }); resolve(result); } - } + }, ); }); } @@ -404,7 +464,7 @@ export class SocketClient { }); resolve(result); } - } + }, ); }); } @@ -537,7 +597,7 @@ export class SocketClient { // Listen for found services this.scanner.on("serviceFound", (data) => { logger.info( - `Found websocket service at ${data.hostname} (${data.ip})` + `Found websocket service at ${data.hostname} (${data.ip})`, ); this.socket.emit("notify_scan_network_found", data); }); @@ -545,7 +605,7 @@ export class SocketClient { // Listen for scan progress this.scanner.on("scanProgress", ({ currentIP, progress }) => { logger.info( - `Scanning ${currentIP} (${progress.toFixed(2)}% complete)` + `Scanning ${currentIP} (${progress.toFixed(2)}% complete)`, ); this.socket.emit("notify_scan_network_progress", { currentIP: currentIP, @@ -558,7 +618,7 @@ export class SocketClient { "Scanning network for websocket services on port:", data?.port || 7125, "using protocol:", - data?.protocol || "ws" + data?.protocol || "ws", ); this.scanner .scanNetwork(data?.port || 7125, data?.protocol || "ws") @@ -641,7 +701,7 @@ export class SocketClient { // Get the printer client const printerClient = this.printerManager.getPrinterClient( - data.printerId + data.printerId, ); if (!printerClient) { throw new Error(`Printer with ID ${data.printerId} not found`); @@ -649,7 +709,7 @@ export class SocketClient { // Load the filament stock const result = await printerClient.loadFilamentStock( - data.filamentStockId + data.filamentStockId, ); if (callback) {