diff --git a/server.js b/server.js index 0269416..96c1c73 100644 --- a/server.js +++ b/server.js @@ -22,9 +22,14 @@ const fs = require("fs"); const path = require("path"); const { WebSocketServer, WebSocket } = require("ws"); +require("dotenv").config(); + const WEB_PORT = Number(process.env.AFK_PARENT_PORT || 3008); const PARENT_SECRET = process.env.PARENT_SECRET || ""; const WEB_UI_PATH = path.join(__dirname, "web", "index.html"); +const UI_AUTH_USER = process.env.UI_AUTH_USER || "admin"; +const UI_AUTH_PASS = process.env.UI_AUTH_PASS || "afk"; +const UI_AUTH_REALM = process.env.UI_AUTH_REALM || "AFK Bot UI"; // ─── state ─────────────────────────────────────────────────────────────────── @@ -63,6 +68,40 @@ function listCachedBots() { return [...cachedBots.values()]; } +function decodeBasicAuth(authHeader) { + if (!authHeader || !authHeader.startsWith("Basic ")) return null; + const base64 = authHeader.slice("Basic ".length).trim(); + if (!base64) return null; + + try { + const decoded = Buffer.from(base64, "base64").toString("utf8"); + const sep = decoded.indexOf(":"); + if (sep === -1) return null; + return { + user: decoded.slice(0, sep), + pass: decoded.slice(sep + 1), + }; + } catch { + return null; + } +} + +function isUiAuthorized(req) { + const auth = decodeBasicAuth(req.headers.authorization || ""); + return Boolean( + auth && auth.user === UI_AUTH_USER && auth.pass === UI_AUTH_PASS, + ); +} + +function requestUiAuth(res) { + res.writeHead(401, { + "Content-Type": "text/plain; charset=utf-8", + "WWW-Authenticate": `Basic realm="${UI_AUTH_REALM}"`, + "Cache-Control": "no-store", + }); + res.end("Authentication required"); +} + // ─── forward server console logs to browsers ───────────────────────────────── { const _log = console.log.bind(console); @@ -107,6 +146,11 @@ const httpServer = http.createServer((req, res) => { } if (req.url === "/" || req.url === "/index.html") { + if (!isUiAuthorized(req)) { + requestUiAuth(res); + return; + } + try { const html = fs.readFileSync(WEB_UI_PATH, "utf8"); res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); @@ -167,6 +211,14 @@ httpServer.on("upgrade", (req, socket, head) => { parentWssI.emit("connection", ws, req), ); } else if (pathname === "/ws") { + if (!isUiAuthorized(req)) { + socket.write( + `HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Basic realm="${UI_AUTH_REALM}"\r\nConnection: close\r\n\r\n`, + ); + socket.destroy(); + return; + } + browserWss.handleUpgrade(req, socket, head, (ws) => browserWss.emit("connection", ws), );