import { createHash, timingSafeEqual } from "node:crypto"; // The dashboard's password (DASHBOARD_PASSWORD), asked by the browser's own sign-in window // (HTTP Basic). Without it set, the dashboard is off. Ten wrong passwords from one address lock // it out for ten minutes. const PASSWORD = process.env.DASHBOARD_PASSWORD || ""; const MAX_FAILURES = 10; const LOCKOUT_MS = 10 * 60 * 1000; export const dashboardEnabled = Boolean(PASSWORD); /** Compares in constant time: the digests have equal length whatever was typed. */ const same = (a, b) => timingSafeEqual(createHash("sha256").update(a).digest(), createHash("sha256").update(b).digest()); export function dashboardAuth() { const failures = new Map(); return (req, res, next) => { // Status pages are nobody's search result and nobody's cache. res.set({ "Cache-Control": "no-store", "X-Robots-Tag": "noindex" }); if (!PASSWORD) return res.status(404).type("text/plain; charset=utf-8").send("Дашборд выключен: задайте DASHBOARD_PASSWORD."); const now = Date.now(); for (const [ip, f] of failures) if (now - f.start >= LOCKOUT_MS) failures.delete(ip); const tries = failures.get(req.ip); if (tries && tries.count >= MAX_FAILURES) { res.set("Retry-After", String(Math.ceil((tries.start + LOCKOUT_MS - now) / 1000))); return res.status(429).type("text/plain; charset=utf-8").send("Слишком много неверных паролей. Попробуйте через 10 минут."); } const [scheme, value = ""] = (req.get("Authorization") || "").split(" "); if (scheme === "Basic") { const decoded = Buffer.from(value, "base64").toString("utf8"); // Any user name; only the password counts. if (same(decoded.slice(decoded.indexOf(":") + 1), PASSWORD)) { failures.delete(req.ip); return next(); } const f = tries || { count: 0, start: now }; f.count++; failures.set(req.ip, f); } res.set("WWW-Authenticate", 'Basic realm="PSU schedule status", charset="UTF-8"'); res.status(401).type("text/plain; charset=utf-8").send("Нужен пароль дашборда."); }; }