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) {