Browse Source

authentication part

isundil 1 month ago
parent
commit
8eb8512db7

+ 2 - 1
config.sample.json

@@ -3,5 +3,6 @@
     "slave": false,
     "slaves": [ "http://some.host:1234" ],
     "masterPubKey": "generated by npm run generate-key",
-    "masterPrivateKey": "generated by npm run generate-key"
+    "masterPrivateKey": "generated by npm run generate-key",
+    "apiKeys": []
 }

+ 0 - 21
front/templates/login.ts

@@ -1,21 +0,0 @@
-import menu from "./menu";
-import {Page} from "./page";
-
-class LoginPage extends Page {
-    public constructor() {
-        super("login");
-    }
-
-    protected load(): Promise<void> {
-        return Promise.resolve();
-    }
-
-    public override show(): Promise<void> {
-        super.showWithoutHistory();
-        menu.hide();
-        return this.load();
-    }
-}
-
-export default new LoginPage();
-

+ 4 - 4
front/templates/templateManager.ts

@@ -1,5 +1,4 @@
 import {DAL} from "../DAL/login";
-import login from "./login";
 import {Page} from "./page";
 import systemInfo from "./systemInfo";
 import systemMonitor from "./systemMonitor";
@@ -11,7 +10,6 @@ class TemplateManager {
     protected loading: boolean =false;
 
     public constructor() {
-        this.pages.push(login);
         this.pages.push(systemInfo);
         this.pages.push(systemMonitor);
         this.pages.push(systemServices);
@@ -21,8 +19,10 @@ class TemplateManager {
 
     public async showDefaultPage(): Promise<void> {
         this.setLoading(true);
-        if (!await DAL.isLoggedUser())
-            return login.show();
+        if (!await DAL.isLoggedUser()) {
+            document.location = "login";
+            return;
+        }
         const hash = String(window.location.hash).slice(1);
         let page = this.pages.find(x => x.getPageName() === hash);
         if (!page)

+ 3 - 1
package.json

@@ -24,16 +24,18 @@
     "@open-wc/webpack-import-meta-loader": "^0.4.7",
     "@types/bootstrap": "^4.1.0",
     "@types/bootstrap-select": "^1.13.7",
+    "@types/cookie-parser": "^1.4.9",
     "@types/datatables.net": "^1.10.28",
     "@types/express": "^5.0.3",
     "@types/express-handlebars": "^5.3.1",
+    "@types/jquery": "^3.5.33",
     "@types/node": "^24.5.2",
     "@types/react": "^19.1.1",
     "@types/react-dom": "^19.1.9",
     "@types/swagger-ui-express": "^4.1.8",
-    "@types/jquery": "^3.5.33",
     "bootstrap": "^4.1.0",
     "bootstrap-select": "^1.13.18",
+    "cookie-parser": "^1.4.7",
     "datatables.net": "^2.3.4",
     "datatables.net-bs4": "^2.3.4",
     "datatables.net-columncontrol-bs4": "^1.1.0",

+ 2 - 0
src/config.ts

@@ -8,6 +8,7 @@ export interface Configuration {
     masterPubKey: string;
     masterPrivateKey: string;
     hostname: string;
+    apiKeys: string[];
 }
 
 class ConfigurationManagerLoader {
@@ -30,6 +31,7 @@ class ConfigurationManagerLoader {
             masterPubKey: config.masterPubKey || "",
             masterPrivateKey: config.masterPrivateKey || "",
 
+            apiKeys: config.apiKeys || [],
             hostname: os.hostname()
         };
     }

+ 5 - 1
src/index.ts

@@ -1,12 +1,13 @@
 import express, { json, NextFunction, Request, Response, urlencoded } from "express";
 import { RegisterRoutes } from "../build/routes";
-import { HtmlController } from "./routes/htmlControllers";
+import { HtmlController, SecurityRequirement } from "./routes/htmlControllers";
 import swaggerUi from "swagger-ui-express";
 import { create } from 'express-handlebars';
 import path from "path";
 import ConfigurationManager from "./config";
 import slavery from "./slavery";
 import {UnauthorizedMasterApiKey} from "./models/unauthorizedApi";
+import cookieParser from "cookie-parser";
 
 (async () => {
     if (process.argv.indexOf("--generate-key") > 0) {
@@ -27,6 +28,9 @@ import {UnauthorizedMasterApiKey} from "./models/unauthorizedApi";
     // Swagger UI
     app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(require(path.join(__dirname, "../build/swagger.json"))));
 
+    app.use(cookieParser());
+    app.use((req, _, next) => { SecurityRequirement.pingSession(req); next(); });
+
     // API routes
     app.use(
         urlencoded({

+ 9 - 2
src/routes/api_sysInfoController.ts

@@ -2,6 +2,7 @@ import {
     Controller,
     Get,
     Header,
+    Request,
     Route
 } from "tsoa";
 import {CpuInfo, DriveInfo, LiveSystemInfoDescription, NetworkInfo, SystemInfoDescription} from "../models/systemInfo";
@@ -11,6 +12,8 @@ import si from 'systeminformation';
 import ConfigurationManager from "../config";
 import slavery from "../slavery";
 import {UnauthorizedMasterApiKey} from "../models/unauthorizedApi";
+import { SecurityRequirement } from "./htmlControllers";
+import express from "express";
 
 @Route("/api/sysinfo")
 export class SystemdInfoController extends Controller {
@@ -63,9 +66,11 @@ export class SystemdInfoController extends Controller {
     }
 
     @Get("/")
-    public async getInfo(@Header() apiKeySignature: string|undefined): Promise<SystemInfoDescription> {
+    public async getInfo(@Header() apiKeySignature: string|undefined, @Request() req: express.Request): Promise<SystemInfoDescription> {
         if (ConfigurationManager.masterPubKey && !slavery.checkMasterKey(apiKeySignature))
             throw new UnauthorizedMasterApiKey();
+        else if (!ConfigurationManager.slave)
+            SecurityRequirement.requireLoggedUser(req);
         const system = await si.system();
         let result: SystemInfoDescription = { systemInfo: {}};
         result.systemInfo[ConfigurationManager.hostname] = {
@@ -109,9 +114,11 @@ export class SystemdInfoController extends Controller {
     }
 
     @Get("/live")
-    public async getLiveInfo(@Header() apiKeySignature: string|undefined): Promise<LiveSystemInfoDescription> {
+    public async getLiveInfo(@Header() apiKeySignature: string|undefined, @Request() req: express.Request): Promise<LiveSystemInfoDescription> {
         if (ConfigurationManager.masterPubKey && !slavery.checkMasterKey(apiKeySignature))
             throw new UnauthorizedMasterApiKey();
+        else if (!ConfigurationManager.slave)
+            SecurityRequirement.requireLoggedUser(req);
         const memInfo = await si.mem();
         const cpuSpeed = await this.getCpuUsage();
         let result: LiveSystemInfoDescription = { liveSystemInfo: {}};

+ 6 - 1
src/routes/api_systemdController.ts

@@ -2,6 +2,7 @@ import {
     Controller,
     Get,
     Header,
+    Request,
     Route
 } from "tsoa";
 import systemdService from "../services/systemdService";
@@ -9,14 +10,18 @@ import ConfigurationManager from "../config";
 import {ServiceDescription} from "../models/service";
 import slavery from "../slavery";
 import {UnauthorizedMasterApiKey} from "../models/unauthorizedApi";
+import { SecurityRequirement } from "./htmlControllers";
+import express from "express";
 
 @Route("/api/services")
 export class SystemdServiceController extends Controller {
 
     @Get("/list")
-    public async getAllServices(@Header() apiKeySignature: string|undefined): Promise<ServiceDescription> {
+    public async getAllServices(@Request() req: express.Request, @Header() apiKeySignature: string|undefined): Promise<ServiceDescription> {
         if (ConfigurationManager.masterPubKey && !slavery.checkMasterKey(apiKeySignature))
             throw new UnauthorizedMasterApiKey();
+        else if (!ConfigurationManager.slave)
+            SecurityRequirement.requireLoggedUser(req);
         let result: ServiceDescription = { services: {}};
         result.services[ConfigurationManager.hostname] = (await systemdService.getAllServices());
         await slavery.getSlaveData(result.services, "services", "/api/services/list");

+ 147 - 3
src/routes/htmlControllers.ts

@@ -1,14 +1,158 @@
-import Express from "express"
+import Express, {Request, Response} from "express"
+import gUserService from "../services/userService";
+import ConfigurationManager from "../config";
+
+export class UnauthorizedUser extends Error {
+    private isLoggedIn: boolean;
+
+    constructor(isLoggedIn: boolean) {
+        super("Unauthorized user");
+        this.isLoggedIn = isLoggedIn;
+    }
+
+    public isLoggedUser(): boolean {
+        return this.isLoggedIn;
+    }
+}
+
+export interface SessionData {
+    logged: boolean;
+}
+
+export interface Session {
+    data: SessionData;
+    validUntil: number;
+}
+
+class SessionManager {
+    sessions: Map<string, Session> = new Map();
+    private sessionTimeout: number = 60 * 60* 1000;
+
+    constructor() {
+        setInterval(() => this.clean.bind(this), 60 * 1000);
+    }
+    public get(sessionId: string|undefined): SessionData|null {
+        if (!sessionId)
+            return null;
+        const session = this.sessions.get(sessionId);
+        if (!session || session.validUntil < Date.now())
+            return null;
+        return this.sessions.get(sessionId)?.data || null;
+    }
+    public pingSession(sessionId: string|undefined) {
+        if (!sessionId)
+            return;
+        const session = this.sessions.get(sessionId);
+        if (!session || session.validUntil < Date.now())
+            return;
+        this.sessions.get(sessionId)!.validUntil = Date.now() + this.sessionTimeout;
+    }
+    public create(): string {
+        const uuid = crypto.randomUUID();
+        this.sessions.set(uuid, <Session> {
+            data: {
+                logged: true
+            },
+            validUntil: Date.now() + this.sessionTimeout
+        });
+        return uuid;
+    }
+    public remove(sessionId?: string) {
+        if (!sessionId)
+            return;
+        this.sessions.delete(sessionId);
+    }
+    public clean() {
+        const now = Date.now();
+        for (var i of this.sessions) {
+            if (i[1].validUntil < now)
+                this.sessions.delete(i[0]);
+        }
+    }
+}
+
+export type { SessionManager }
+export const gSessionManager = new SessionManager();
+const COOKIE_SESSION = "sessionId";
+
+export class SecurityRequirement {
+    private static throwIfRequired(req: Request, value: boolean, nothrow: boolean|undefined): boolean {
+        if (!value && nothrow !== true)
+            throw new UnauthorizedUser(!!this.getSessionObj(req));
+        return value;
+    }
+    public static getSessionObj(req: Request): SessionData|null {
+        return gSessionManager.get(req.cookies?.[COOKIE_SESSION])
+    }
+    public static requireLoggedUser(req: Request, nothrow?: boolean): boolean {
+        if (req.query.apiKey && ConfigurationManager.apiKeys.includes(req.query.apiKey.toString()))
+            return true;
+        const sessionObj = this.getSessionObj(req);
+        return this.throwIfRequired(req, sessionObj !== null && sessionObj.logged, nothrow);
+    }
+    public static setLoggedUser(req: Request, res: Response) {
+        if (!req.cookies[COOKIE_SESSION]) {
+            res.cookie(COOKIE_SESSION, gSessionManager.create());
+            return;
+        }
+        let sessionObj = this.getSessionObj(req.cookies[COOKIE_SESSION]);
+        if (sessionObj)
+            sessionObj.logged = true;
+        else
+            res.cookie(COOKIE_SESSION, gSessionManager.create());
+    }
+    public static logout(req: Request, res: Response) {
+        gSessionManager.remove(req.cookies?.[COOKIE_SESSION]);
+        res.cookie(COOKIE_SESSION, null);
+    }
+    public static tryLogin(req: Request, res: Response, username: string, password: string): boolean {
+        if (!gUserService.tryLogin(username, password))
+            return false;
+        this.setLoggedUser(req, res);
+        return true;
+    }
+    public static pingSession(req: Request) {
+        gSessionManager.pingSession(req.cookies?.[COOKIE_SESSION]);
+    }
+}
 
 export class HtmlController {
     private static GenerateContext(override: any): object {
         override = override || {};
         override.title = override.title || "Title";
-        override.javascript = override.javascript !== undefined ? override.javascript : "js/front.js"
+        override.javascript = override.javascript !== undefined ? override.javascript : "js/front.min.js"
         return override;
     }
 
+    private static renderIndex(req: Request, res: Response) {
+        if (!SecurityRequirement.requireLoggedUser(req, true))
+            return res.redirect("/login");
+        res.render("index", HtmlController.GenerateContext(null));
+    }
+
+    private static renderLogin(req: Request, res: Response) {
+        if (SecurityRequirement.requireLoggedUser(req, true))
+            return res.redirect("/");
+        res.render("login", HtmlController.GenerateContext({javascript: null}));
+    }
+
+    private static renderLogout(req: Request, res: Response) {
+        SecurityRequirement.logout(req, res);
+        return res.redirect("/");
+    }
+
+    private static postLogin(req: Request, res: Response) {
+        if (SecurityRequirement.tryLogin(req, res, req.body.username, req.body.password)) {
+            res.redirect("/");
+            return;
+        }
+        res.render("login", HtmlController.GenerateContext({javascript: null, failed: true}));
+    }
+
     public static RegisterHtmlPages(app: Express.Express): void {
-        app.get("/", (_, res) => { res.render("index", HtmlController.GenerateContext(null)); });
+        app.get("/", (req, res) => this.renderIndex(req, res));
+        app.get("/login", (req, res) => this.renderLogin(req, res));
+        app.post("/login", (req, res) => this.postLogin(req, res));
+        app.get("/logout", (req, res) => this.renderLogout(req, res));
     }
 }

+ 10 - 0
src/services/userService.ts

@@ -0,0 +1,10 @@
+
+class UserService {
+    public tryLogin(_username: string, _password: string) {
+        return false;
+    }
+}
+
+const gUserService = new UserService();
+export type { UserService };
+export default gUserService;

+ 11 - 1
templates/index.handlebars

@@ -1,4 +1,14 @@
-{{> login}}
+<nav id="navbar">
+    <ul>
+        <li>
+            <select id="navbar-hostfilter" class="selectpicker" multiple data-live-search="true">
+            </select>
+        </li>
+        <li><a href="#systemInfo">System Info</a></li>
+        <li><a href="#systemMonitor">System Monitoring</a></li>
+        <li><a href="#services">Services</a></li>
+    </ul>
+</nav>
 {{> systemInfo}}
 {{> systemMonitor}}
 {{> systemServices}}

+ 0 - 11
templates/layouts/main.handlebars

@@ -11,17 +11,6 @@
     <title>{{title}}</title>
 </head>
 <body>
-    <nav id="navbar">
-        <ul>
-            <li>
-                <select id="navbar-hostfilter" class="selectpicker" multiple data-live-search="true">
-                </select>
-            </li>
-            <li><a href="#systemInfo">System Info</a></li>
-            <li><a href="#systemMonitor">System Monitoring</a></li>
-            <li><a href="#services">Services</a></li>
-        </ul>
-    </nav>
     {{{body}}}
     {{#if javascript}}<script src="{{javascript}}"></script>{{/if}}
 </body>

+ 7 - 0
templates/login.handlebars

@@ -0,0 +1,7 @@
+<section id="page-login">
+    <form id="login-form" method="POST" action="#">
+        <input type="text" id="login-username" name="username" />
+        <input type="password" id="login-password" name="password" />
+        <input type="submit" />
+    </form>
+</section>

+ 0 - 7
templates/partials/login.handlebars

@@ -1,7 +0,0 @@
-<section id="page-login" class="hidden">
-    <form id="login-form">
-        <input type="text" id="login-username" />
-        <input type="password" id="login-password" />
-        <input type="submit" />
-    </form>
-</section>