add tokens suppoort and auth to root user

This commit is contained in:
fullgream 2025-03-22 20:14:54 +03:00
parent 1ef209d9a6
commit 993a0cd276
13 changed files with 321 additions and 50 deletions

View File

@ -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",

View File

@ -1,4 +1,3 @@
module.exports = function (con, req, cb) {
return cb({ error: "At develop", trace_id: req.trace_id, ended: true });
//;
return cb({ result: global.authed.has(con), trace_id: req.trace_id, ended: true });
};

57
server/api/log-in.js Normal file
View File

@ -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,
});
// (!) Добавить чтение из БД
};

24
server/api/setup-token.js Normal file
View File

@ -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));
};

View File

@ -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;
};

62
server/api/utils/user.js Normal file
View File

@ -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 };

View File

@ -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);
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);
}

View File

@ -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 = `
</div>
</center>
`;
// Main menu form

View File

@ -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 <link href="/bootstrap/bootstrap.min.css" rel="stylesheet">';
@ -49,7 +56,21 @@ socket.run()
console.log("socket sends:", data);
document.title = data.name;
document.getElementById("server-name").innerText = data.name;
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);

View File

@ -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;

View File

@ -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;
}
@ -191,10 +191,10 @@ const defaultConfigData = {
apiType: "kobold",
},
rootuser: {
"login": "admin",
"password": "admin",
"activated": true
}
login: "admin",
password: "admin",
activated: true,
},
};
const server = new Server();

View File

@ -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)