From 993a0cd2763382dd57df667d2b875e352eab8b33 Mon Sep 17 00:00:00 2001 From: fullgream Date: Sat, 22 Mar 2025 20:14:54 +0300 Subject: [PATCH] add tokens suppoort and auth to root user --- server/api/index.js | 56 +++++++++++++++++--- server/api/is-authed.js | 5 +- server/api/log-in.js | 57 +++++++++++++++++++++ server/api/setup-token.js | 24 +++++++++ server/api/utils/gen-token.js | 19 +++++++ server/api/utils/user.js | 62 +++++++++++++++++++++++ server/config.json | 50 +++++++++--------- server/frontend/public/js/connect/api.js | 53 +++++++++++++++++-- server/frontend/public/js/connect/auth.js | 4 +- server/frontend/public/js/connect/main.js | 23 ++++++++- server/packet-parser.js | 2 +- server/server.js | 14 ++--- server/ws-handler.js | 2 +- 13 files changed, 321 insertions(+), 50 deletions(-) create mode 100644 server/api/log-in.js create mode 100644 server/api/setup-token.js create mode 100644 server/api/utils/gen-token.js create mode 100644 server/api/utils/user.js diff --git a/server/api/index.js b/server/api/index.js index 714d9d0..c09fcef 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -2,6 +2,12 @@ const crypt = require("../crypt"); // methods const isAuthed = require("./is-authed"); +const logIn = require("./log-in"); +const setupToken = require("./setup-token"); + +// authed sessions +global.authed = new WeakMap(); +global.authed.tokens = new Object(); function parseRq (bytes) { try { @@ -12,18 +18,55 @@ function parseRq (bytes) { } class APIMethods { - constructor (parent) { - this.parent = parent; + constructor (parentObj) { + this.parentObj = parentObj; } info (con, req, cb) { - cb({ result: require("./server-info"), trace_id: req.trace_id, ended: true }); + const serverInfo = require("./server-info"); + cb({ result: serverInfo, trace_id: req.trace_id, ended: true }); } authed (con, req, cb) { isAuthed(con, req, cb); } + async token (con, req, cb) { + const authedMethod = this.authed; + const isAuthed = await (new Promise((rs, rj) => { + authedMethod(con, req, ({ result, ended }) => { + if (ended) + return rs(result); + }); + })); + if (!isAuthed) { + return setupToken(con, req, cb); + } + cb({ + error: "already logged-in", + trace_id: req.trace_id, + ended: true + }); + } + + async login (con, req, cb) { + const authedMethod = this.authed; + const isAuthed = await (new Promise((rs, rj) => { + authedMethod(con, req, ({ result, ended }) => { + if (ended) + return rs(result); + }); + })); + if (!isAuthed) { + return logIn(con, req, cb); + } + cb({ + error: "already logged-in", + trace_id: req.trace_id, + ended: true + }); + } + /*setSession (isEncrypted, address, { key, counter }, cb) { if (!global.config.server.secureMode) return cb({ error: "Encryption is off by configuration file into server" }); @@ -41,7 +84,7 @@ class APIMethods { class API { constructor () { this.methods = new APIMethods(this); - this.sessions = new WeakMap(); + this.sessions = global.authed; } decrypt (address, bytes) { @@ -73,14 +116,13 @@ class API { trace_id: null, ended: true }); - const selmethod = this.methods[method]; - if (selmethod === undefined) + if (this.methods[method] === undefined) return cb({ error: "unknown method: " + method, trace_id, ended: true }); - return selmethod(connection, request, cb); + return this.methods[method](connection, request, cb); } else { return cb({ error: "required JSON-object based request", diff --git a/server/api/is-authed.js b/server/api/is-authed.js index 4d56dd1..4e73158 100644 --- a/server/api/is-authed.js +++ b/server/api/is-authed.js @@ -1,4 +1,3 @@ module.exports = function (con, req, cb) { - return cb({ error: "At develop", trace_id: req.trace_id, ended: true }); - //; -}; \ No newline at end of file + return cb({ result: global.authed.has(con), trace_id: req.trace_id, ended: true }); +}; diff --git a/server/api/log-in.js b/server/api/log-in.js new file mode 100644 index 0000000..2dd620e --- /dev/null +++ b/server/api/log-in.js @@ -0,0 +1,57 @@ +const { User } = require("./utils/user"); +const genToken = require("./utils/gen-token"); + +function verifyParams(req) { + return ( + typeof req.username === "string" && + typeof req.password === "string" && + (req.gtf === undefined || typeof req.gtf === "boolean") + ); +} + +module.exports = async function (con, req, cb) { + if (!verifyParams(req)) + return cb({ + error: + "Invalid params. Params username and password must be a string and exists, gtf must be a boolean", + trace_id: req.trace_id, + ended: true, + }); + const invalidData = () => + cb({ + error: "Invalid login or password", + trace_id: req.trace_id, + ended: true, + }); + const ok = (userdata, token = null) => + cb({ result: { ...userdata, token }, trace_id: req.trace_id, ended: true }); + + // At develop! + cb({ warning: "Method at develop", trace_id: req.trace_id, ended: false }); + if ( + !!global.config.rootuser?.activated && + req.username === global.config.rootuser.login + ) { + if (req.password === global.config.rootuser.password) { + const user = await User.getUserByName(req.username); + if (user === null) { + console.error("Root user object is null"); + return cb({ + error: "Unknown error", + trace_id: req.trace_id, + ended: true, + }); + } + global.authed.set(con, user); + if (!req.gtf) return ok(user.getUserForAPI(true)); + return ok(user.getUserForAPI(true), genToken.registerToken(user)); + } + return invalidData(); + } + return cb({ + error: "Method at develop", + trace_id: req.trace_id, + ended: true, + }); + // (!) Добавить чтение из БД +}; diff --git a/server/api/setup-token.js b/server/api/setup-token.js new file mode 100644 index 0000000..0b346f2 --- /dev/null +++ b/server/api/setup-token.js @@ -0,0 +1,24 @@ +const { User } = require("./utils/user"); + +function verifyParams(req) { + return typeof req.token === "string"; +} + +module.exports = async function (con, req, cb) { + if (!verifyParams(req)) + return cb({ + error: "Invalid params. Required param token must be a string", + trace_id: req.trace_id, + ended: true, + }); + const invalidData = () => + cb({ error: "Invalid token", trace_id: req.trace_id, ended: true }); + const ok = (userdata) => + cb({ result: userdata, trace_id: req.trace_id, ended: true }); + + // At develop! + const user = User.getByToken(req.token); + if (user === null) return invalidData(); + global.authed.set(con, user); + ok(user.getUserForAPI(true)); +}; diff --git a/server/api/utils/gen-token.js b/server/api/utils/gen-token.js new file mode 100644 index 0000000..cc337a8 --- /dev/null +++ b/server/api/utils/gen-token.js @@ -0,0 +1,19 @@ +function randint(min, max) { + return Math.ceil(Math.random() * (max - min) + min); +} + +module.exports = function () { + return Buffer.from([...new Array(128)].map(() => randint(0, 255))).toString( + "base64", + ); +}; +module.exports.registerToken = function (user) { + let token; + do { + token = module.exports(); + } while (global.authed.tokens[token] !== undefined); + global.authed.tokens[token] = user; + // (!) Потом добавить регистрацию в БД! + + return token; +}; diff --git a/server/api/utils/user.js b/server/api/utils/user.js new file mode 100644 index 0000000..c9cc693 --- /dev/null +++ b/server/api/utils/user.js @@ -0,0 +1,62 @@ +/* + * Acces Levels: + * * -1 - banned + * * 0 - user + * * 1 - moderator + * * 2 - editor + * * 3 - admin + * * 4 - superadmin + * * 5 - root + */ + +class User { + constructor(name, accessLevel) { + if ( + typeof accessLevel !== "number" || + { + "-1": true, + 0: true, + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + }["" + accessLevel] !== true + ) { + throw new Error( + `Invalid access level: ${accessLevel} (must be ranged from -1 to 5)`, + ); + } + this.name = name; + this.accessLevel = accessLevel; + } + + static async getUserByName(name) { + if (User.cache[name]) return User.cache[name]; + if ( + !!global.config.rootuser?.activated && + name === global.config.rootuser.login + ) { + const user = new User(name, 5); + User.cache[name] = user; + return user; + } + // Потом добавить чтение из БД + return null; + } + + static getByToken(token) { + const user = global.authed.tokens[token]; + return user ?? null; + } + + getUserForAPI(getFullInfo = false) { + return { + username: this.name, + accessLevel: this.accessLevel, + }; + } +} +User.cache = {}; + +module.exports = { User }; diff --git a/server/config.json b/server/config.json index 8ed91ac..e6a71bd 100644 --- a/server/config.json +++ b/server/config.json @@ -1,13 +1,13 @@ { - "server": { - "address": "0.0.0.0", - "port": 8841, - "useWebSocketAPI": true, - "useFrontend": true, - "staticInfo": { - "wsPort": 57891, - "httpPort": 49901 - }, + "server": { + "address": "0.0.0.0", + "port": 8841, + "useWebSocketAPI": true, + "useFrontend": true, + "staticInfo": { + "wsPort": 57891, + "httpPort": 49901 + }, "auth-mode": 1, "info": { "name": "FullGreaM Test Neuro-RP Server", @@ -15,20 +15,20 @@ "pubaddress": "airp.fullgream.tech", "pubusetls": true } - }, - "database": { - "dialect": "sqlite", - "path": "./data" - }, - "ai": { - "apiType": "kobold" - }, - "source": { - "bgmain": "/home/fullgream/Загрузки/21380429c504a974479d31c96a9c3c3f.jpg" - }, - "rootuser": { - "login": "admin", - "password": "admin", - "activated": true - } + }, + "database": { + "dialect": "sqlite", + "path": "./data" + }, + "ai": { + "apiType": "kobold" + }, + "source": { + "bgmain": "/home/fullgream/Загрузки/21380429c504a974479d31c96a9c3c3f.jpg" + }, + "rootuser": { + "login": "admin", + "password": "admin", + "activated": true + } } diff --git a/server/frontend/public/js/connect/api.js b/server/frontend/public/js/connect/api.js index 5dd0e8a..58bc457 100644 --- a/server/frontend/public/js/connect/api.js +++ b/server/frontend/public/js/connect/api.js @@ -16,7 +16,7 @@ class ProtoApiMethods { this.api = api; } - async _protoMethod (rqdata, threadcb) { + async _protoMethod (rqdata, threadcb = null) { const socket = this.api.socket; const trace_id = getTraceId() @@ -24,14 +24,18 @@ class ProtoApiMethods { socket.onmessage = (ev) => { ev = JSON.parse(ev.data); const data = ev.result; - return rs(data); + console.debug("Method sended:", ev); if (ev.trace_id === trace_id) + if (ev.warning) + console.warn("Method warning:", ev.warning, "\ndetails:", ev); if (!!ev.ended) return rs(data); - threadcb(data); + if (typeof threadcb === 'function') + threadcb(data); }; socket.onerror = (err) => rj(err); }); + console.debug("Client send:", { trace_id, ...rqdata }); socket.send( JSON.stringify({ trace_id, ...rqdata }), ); @@ -59,6 +63,20 @@ class ApiMethods extends ProtoApiMethods { method: "authed", }); } + + async token (token) { + return await super._protoMethod({ + method: "token", token + }); + } + + async login (username, password, gtf = false) { + return await super._protoMethod({ + method: "login", + username, password, + gtf + }); + } } class ApiHTML { @@ -66,6 +84,18 @@ class ApiHTML { this.api = api; } + async renderMainMenu (user, bgUrl = null) { + bgUrl = bgUrl ?? "assets/hello/1.png"; + + document.body.style.backgroundImage = `url(${ + JSON.stringify(bgUrl) + })`; + document.body.style.backgroundSize = `${screen.width}px ${screen.height}px`; + + document.getElementById("server.area").innerHTML = ''; + //$(document.getElementById("server.area")).append(ServerAuth.authForm); + } + async renderAuth (authMode, bgUrl = null) { bgUrl = bgUrl ?? "assets/hello/1.png"; @@ -77,6 +107,8 @@ class ApiHTML { document.getElementById("server.area").innerHTML = ''; $(document.getElementById("server.area")).append(ServerAuth.authForm); + $.find("#server-code-form")[0].hidden = authMode !== "password"; + [...$.find(".auth-act-item")].forEach(authEl => { const authElJQ = $(authEl); const isHidden = authEl.hidden; @@ -98,7 +130,19 @@ class ApiHTML { $.find("#register")[0].hidden = false; } - $.find("#server-code-form")[0].hidden = authMode !== "password"; + $.find("#auth-btn")[0].onclick = () => { + this.api.methods.login( + $.find("#username")[0].value, + $.find("#password")[0].value, + true + ) + .then(user => { + const { token } = user; + localStorage.setItem("my-token", token); + this.renderMainMenu(user, bgUrl); + }); + } + $.find("#reg-btn")[0].onclick = () => { } } } @@ -112,6 +156,7 @@ export class ApiSocket { this.socket = new WebSocket(`${!isTLSmode ? "ws" : "wss"}://${address}:${port}`); this.methods = new ApiMethods(this); + this.user = null; this.html = new ApiHTML(this); } diff --git a/server/frontend/public/js/connect/auth.js b/server/frontend/public/js/connect/auth.js index 13d94bd..fe59f18 100644 --- a/server/frontend/public/js/connect/auth.js +++ b/server/frontend/public/js/connect/auth.js @@ -10,10 +10,10 @@ export class ServerAuth { !!this.isTLSenabled ? "wss" : "ws" }:${address}:${port}`) ?? "null"); if (!serverdata) return null; - } } +// Authorization form ServerAuth.authForm = document.createElement("div"); ["auth-window"].forEach(c => ServerAuth.authForm.classList.add(c)); @@ -89,3 +89,5 @@ ServerAuth.authForm.innerHTML = ` `; + +// Main menu form diff --git a/server/frontend/public/js/connect/main.js b/server/frontend/public/js/connect/main.js index 9ba6fa4..b7db2c7 100644 --- a/server/frontend/public/js/connect/main.js +++ b/server/frontend/public/js/connect/main.js @@ -1,4 +1,11 @@ import { ApiSocket } from "/js/connect/api.js"; +// cookie parser function +function getCookies () { + return Object.fromEntries(document.cookie.split(/\s{0,};\s{0,}/gmiu).map(i => i.split(/\s{0,}=\s{0,}/gmiu))); +} +if (getCookies().cw !== "1") { + window.location.href = "/connect"; +} // load styles document.head.innerHTML += '\\n '; @@ -49,7 +56,21 @@ socket.run() console.log("socket sends:", data); document.title = data.name; document.getElementById("server-name").innerText = data.name; - await socket.html.renderAuth(data.authMode, data.extSource?.bgmain ?? null); + const token = localStorage.getItem("my-token"); + let user; + if (token) { + try { + user = await socket.methods.token(token); + } + catch (_) {} + } + + const isAuthed = await socket.methods.authed(); + + if (!user || !isAuthed) + await socket.html.renderAuth(data.authMode, data.extSource?.bgmain ?? null); + else + await socket.html.renderMainMenu(user, data.extSource?.bgmain ?? null); }) .catch(err => { console.error(err); diff --git a/server/packet-parser.js b/server/packet-parser.js index 9b0944b..b04df25 100644 --- a/server/packet-parser.js +++ b/server/packet-parser.js @@ -16,7 +16,7 @@ class PacketParseResult { } module.exports = async (bytes) => { - console.log("((('", bytes.toString("utf8"), "')))"); + //console.log("((('", bytes.toString("utf8"), "')))"); const packet = bytes.toString("utf8"); const isHTTP = packet.match(/(GET|POST|PUT|DELETE|OPTIONS|HEAD|PATCH|TRACE|CONNECT)\s{1,}\/[a-zA-Z0-9_\-\.\/?=&%:]{0,}\s{1,}HTTP(S|)\/(1\.0|1\.1|2\.0)(\n|\r)/gmu)?.length > 0 ?? false; diff --git a/server/server.js b/server/server.js index 3d1ac09..3145c79 100644 --- a/server/server.js +++ b/server/server.js @@ -117,7 +117,7 @@ class Server extends EventEmitter { if (global.config.server.useFrontend) socket.httpReverse.end(); if (global.config.server.useWebSocketAPI) socket.wsReverse.end(); socket.mainconnect = "tcp"; - return await this.api.exec(peerAddress, data, () => {}); + return await this.api.exec(socket, data, () => {}); } } else { switch (socket.mainconnect) { @@ -151,7 +151,7 @@ class Server extends EventEmitter { } } case "tcp": - return await this.api.exec(peerAddress, data, () => {}); + return await this.api.exec(socket, data, () => {}); default: return; } @@ -190,11 +190,11 @@ const defaultConfigData = { ai: { apiType: "kobold", }, - rootuser: { - "login": "admin", - "password": "admin", - "activated": true - } + rootuser: { + login: "admin", + password: "admin", + activated: true, + }, }; const server = new Server(); diff --git a/server/ws-handler.js b/server/ws-handler.js index 6d5fca5..949aded 100644 --- a/server/ws-handler.js +++ b/server/ws-handler.js @@ -13,7 +13,7 @@ module.exports = (address, port, api) => { console.log("peerAddress is", peerAddress); ws.on('message', async (message) => { logger.wslog("api received:", message); - await api.exec(peerAddress, message, (rs, encryptor) => { + await api.exec(req.socket, message, (rs, encryptor) => { logger.wslog("api sended:", rs); let result = JSON.stringify(rs); if (encryptor)