Compare commits

..

2 Commits

Author SHA1 Message Date
82e1d078f5 edit frontend api code 2025-03-23 03:34:41 +03:00
993a0cd276 add tokens suppoort and auth to root user 2025-03-22 20:14:54 +03:00
13 changed files with 326 additions and 51 deletions

View File

@ -2,6 +2,12 @@ const crypt = require("../crypt");
// methods // methods
const isAuthed = require("./is-authed"); 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) { function parseRq (bytes) {
try { try {
@ -12,18 +18,55 @@ function parseRq (bytes) {
} }
class APIMethods { class APIMethods {
constructor (parent) { constructor (parentObj) {
this.parent = parent; this.parentObj = parentObj;
} }
info (con, req, cb) { 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) { authed (con, req, cb) {
isAuthed(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) { /*setSession (isEncrypted, address, { key, counter }, cb) {
if (!global.config.server.secureMode) if (!global.config.server.secureMode)
return cb({ error: "Encryption is off by configuration file into server" }); return cb({ error: "Encryption is off by configuration file into server" });
@ -41,7 +84,7 @@ class APIMethods {
class API { class API {
constructor () { constructor () {
this.methods = new APIMethods(this); this.methods = new APIMethods(this);
this.sessions = new WeakMap(); this.sessions = global.authed;
} }
decrypt (address, bytes) { decrypt (address, bytes) {
@ -73,14 +116,13 @@ class API {
trace_id: null, trace_id: null,
ended: true ended: true
}); });
const selmethod = this.methods[method]; if (this.methods[method] === undefined)
if (selmethod === undefined)
return cb({ return cb({
error: "unknown method: " + method, error: "unknown method: " + method,
trace_id, trace_id,
ended: true ended: true
}); });
return selmethod(connection, request, cb); return this.methods[method](connection, request, cb);
} else { } else {
return cb({ return cb({
error: "required JSON-object based request", error: "required JSON-object based request",

View File

@ -1,4 +1,3 @@
module.exports = function (con, req, cb) { 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

@ -11,12 +11,16 @@ function getTraceId() {
.join(""); .join("");
} }
const curUrl = new URL(location.href);
const [address, port] = curUrl.searchParams.get("server").split(/:/);
const isTLSmode = curUrl.searchParams.get("encrypted") === "true";
class ProtoApiMethods { class ProtoApiMethods {
constructor (api) { constructor (api) {
this.api = api; this.api = api;
} }
async _protoMethod (rqdata, threadcb) { async _protoMethod (rqdata, threadcb = null) {
const socket = this.api.socket; const socket = this.api.socket;
const trace_id = getTraceId() const trace_id = getTraceId()
@ -24,14 +28,18 @@ class ProtoApiMethods {
socket.onmessage = (ev) => { socket.onmessage = (ev) => {
ev = JSON.parse(ev.data); ev = JSON.parse(ev.data);
const data = ev.result; const data = ev.result;
return rs(data); console.debug("Method sended:", ev);
if (ev.trace_id === trace_id) if (ev.trace_id === trace_id)
if (ev.warning)
console.warn("Method warning:", ev.warning, "\ndetails:", ev);
if (!!ev.ended) if (!!ev.ended)
return rs(data); return rs(data);
if (typeof threadcb === 'function')
threadcb(data); threadcb(data);
}; };
socket.onerror = (err) => rj(err); socket.onerror = (err) => rj(err);
}); });
console.debug("Client send:", { trace_id, ...rqdata });
socket.send( socket.send(
JSON.stringify({ trace_id, ...rqdata }), JSON.stringify({ trace_id, ...rqdata }),
); );
@ -59,6 +67,20 @@ class ApiMethods extends ProtoApiMethods {
method: "authed", 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 { class ApiHTML {
@ -66,6 +88,18 @@ class ApiHTML {
this.api = api; 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) { async renderAuth (authMode, bgUrl = null) {
bgUrl = bgUrl ?? "assets/hello/1.png"; bgUrl = bgUrl ?? "assets/hello/1.png";
@ -77,6 +111,8 @@ class ApiHTML {
document.getElementById("server.area").innerHTML = ''; document.getElementById("server.area").innerHTML = '';
$(document.getElementById("server.area")).append(ServerAuth.authForm); $(document.getElementById("server.area")).append(ServerAuth.authForm);
$.find("#server-code-form")[0].hidden = authMode !== "password";
[...$.find(".auth-act-item")].forEach(authEl => { [...$.find(".auth-act-item")].forEach(authEl => {
const authElJQ = $(authEl); const authElJQ = $(authEl);
const isHidden = authEl.hidden; const isHidden = authEl.hidden;
@ -98,7 +134,19 @@ class ApiHTML {
$.find("#register")[0].hidden = false; $.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>${!isTLSmode ? "ws" : "wss"}://${address}:${port}`, token);
this.renderMainMenu(user, bgUrl);
});
}
$.find("#reg-btn")[0].onclick = () => { }
} }
} }
@ -112,6 +160,7 @@ export class ApiSocket {
this.socket = new WebSocket(`${!isTLSmode ? "ws" : "wss"}://${address}:${port}`); this.socket = new WebSocket(`${!isTLSmode ? "ws" : "wss"}://${address}:${port}`);
this.methods = new ApiMethods(this); this.methods = new ApiMethods(this);
this.user = null;
this.html = new ApiHTML(this); this.html = new ApiHTML(this);
} }

View File

@ -10,10 +10,10 @@ export class ServerAuth {
!!this.isTLSenabled ? "wss" : "ws" !!this.isTLSenabled ? "wss" : "ws"
}:${address}:${port}`) ?? "null"); }:${address}:${port}`) ?? "null");
if (!serverdata) return null; if (!serverdata) return null;
} }
} }
// Authorization form
ServerAuth.authForm = document.createElement("div"); ServerAuth.authForm = document.createElement("div");
["auth-window"].forEach(c => ["auth-window"].forEach(c =>
ServerAuth.authForm.classList.add(c)); ServerAuth.authForm.classList.add(c));
@ -89,3 +89,5 @@ ServerAuth.authForm.innerHTML = `
</div> </div>
</center> </center>
`; `;
// Main menu form

View File

@ -1,4 +1,11 @@
import { ApiSocket } from "/js/connect/api.js"; 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 // load styles
document.head.innerHTML += '\\n <link href="/bootstrap/bootstrap.min.css" rel="stylesheet">'; document.head.innerHTML += '\\n <link href="/bootstrap/bootstrap.min.css" rel="stylesheet">';
@ -23,7 +30,7 @@ document.body.innerHTML = `<nav id="navbar-main" class="navbar navbar-connect-da
<div class="vr" style="margin-right:5px"></div> <div class="vr" style="margin-right:5px"></div>
<div class="d-flex" style="margin-right:10px" id="navbar-user-menu"> <div class="d-flex" style="margin-right:10px" id="navbar-user-menu">
<button id="toggle-theme" class="btn btn-outline-light" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"><i class="fas fa-sun"></i></button> <button id="toggle-theme" class="btn btn-outline-light" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false"><i class="fas fa-sun"></i></button>
</div> </div>
</nav> </nav>
<div id="server.area"> <div id="server.area">
@ -49,7 +56,21 @@ socket.run()
console.log("socket sends:", data); console.log("socket sends:", data);
document.title = data.name; document.title = data.name;
document.getElementById("server-name").innerText = data.name; document.getElementById("server-name").innerText = data.name;
const token = localStorage.getItem(`my-token>${!isTLSmode ? "ws" : "wss"}://${address}:${port}`);
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); await socket.html.renderAuth(data.authMode, data.extSource?.bgmain ?? null);
else
await socket.html.renderMainMenu(user, data.extSource?.bgmain ?? null);
}) })
.catch(err => { .catch(err => {
console.error(err); console.error(err);

View File

@ -16,7 +16,7 @@ class PacketParseResult {
} }
module.exports = async (bytes) => { module.exports = async (bytes) => {
console.log("((('", bytes.toString("utf8"), "')))"); //console.log("((('", bytes.toString("utf8"), "')))");
const packet = 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; 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.useFrontend) socket.httpReverse.end();
if (global.config.server.useWebSocketAPI) socket.wsReverse.end(); if (global.config.server.useWebSocketAPI) socket.wsReverse.end();
socket.mainconnect = "tcp"; socket.mainconnect = "tcp";
return await this.api.exec(peerAddress, data, () => {}); return await this.api.exec(socket, data, () => {});
} }
} else { } else {
switch (socket.mainconnect) { switch (socket.mainconnect) {
@ -151,7 +151,7 @@ class Server extends EventEmitter {
} }
} }
case "tcp": case "tcp":
return await this.api.exec(peerAddress, data, () => {}); return await this.api.exec(socket, data, () => {});
default: default:
return; return;
} }
@ -191,10 +191,10 @@ const defaultConfigData = {
apiType: "kobold", apiType: "kobold",
}, },
rootuser: { rootuser: {
"login": "admin", login: "admin",
"password": "admin", password: "admin",
"activated": true activated: true,
} },
}; };
const server = new Server(); const server = new Server();

View File

@ -13,7 +13,7 @@ module.exports = (address, port, api) => {
console.log("peerAddress is", peerAddress); console.log("peerAddress is", peerAddress);
ws.on('message', async (message) => { ws.on('message', async (message) => {
logger.wslog("api received:", 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); logger.wslog("api sended:", rs);
let result = JSON.stringify(rs); let result = JSON.stringify(rs);
if (encryptor) if (encryptor)