acts framework

This commit is contained in:
jusax23 2023-03-01 23:47:21 +01:00
parent adf061f7dd
commit 1fbe8d69cb
Signed by: jusax23
GPG key ID: 499E2AA870C1CD41
11 changed files with 442 additions and 35 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@ node_modules
dist
config.juml
.vscode
test

13
package-lock.json generated
View file

@ -9,6 +9,7 @@
"version": "0.0.1",
"license": "AGPL-3.0-only",
"dependencies": {
"auth-header": "^1.0.0",
"commander": "^10.0.0",
"cors": "^2.8.5",
"dblang": "https://jusax.de/git/attachments/c13552b7-c9f0-4f50-bcce-96a124c1c286",
@ -19,6 +20,7 @@
"ws": "^8.12.1"
},
"devDependencies": {
"@types/auth-header": "^1.0.2",
"@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"@types/node": "^18.11.18",
@ -513,6 +515,12 @@
"node": ">= 8"
}
},
"node_modules/@types/auth-header": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/auth-header/-/auth-header-1.0.2.tgz",
"integrity": "sha512-KWpTfyz+F5GtURfp7W9c4ubFSXaPAvb1dUN5MlU3xSvlNIYhFrmrTNE7vd6SUOfSOO7FI/ePe03Y/KCPM/YOoA==",
"dev": true
},
"node_modules/@types/body-parser": {
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
@ -725,6 +733,11 @@
"node": ">= 4.0.0"
}
},
"node_modules/auth-header": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/auth-header/-/auth-header-1.0.0.tgz",
"integrity": "sha512-CPPazq09YVDUNNVWo4oSPTQmtwIzHusZhQmahCKvIsk0/xH6U3QsMAv3sM+7+Q0B1K2KJ/Q38OND317uXs4NHA=="
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",

View file

@ -6,6 +6,7 @@
"type": "module",
"scripts": {
"main": "tsc && node . -c config.juml",
"debug": "tsc && node . -c config.juml -d",
"setup": "tsc && node . -c config.juml -s"
},
"repository": {
@ -18,6 +19,7 @@
"author": "jusax23, comcloudway",
"license": "AGPL-3.0-only",
"devDependencies": {
"@types/auth-header": "^1.0.2",
"@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"@types/node": "^18.11.18",
@ -29,6 +31,7 @@
"typescript": "^4.9.4"
},
"dependencies": {
"auth-header": "^1.0.0",
"commander": "^10.0.0",
"cors": "^2.8.5",
"dblang": "https://jusax.de/git/attachments/c13552b7-c9f0-4f50-bcce-96a124c1c286",

1
src/api/acts.ts Normal file
View file

@ -0,0 +1 @@
export * from "./acts/login.js"

14
src/api/acts/login.ts Normal file
View file

@ -0,0 +1,14 @@
import { Act, Client, STATE } from "../user.js";
export const dummy:Act = {
state: STATE.no,
right: 0,
data: {
sign: "string"
},
func: async (client: Client, data, aws) => {
aws("ok","dummy");
}
};

View file

@ -1,5 +1,149 @@
import express from "express";
import { Act, checktype, Client, STATE } from "./user.js";
import { debug, error } from "../sys/log.js";
import * as authorization from 'auth-header';
import * as importActs from "./acts.js"
import { and, eq, select } from "dblang";
import { accounts } from "../sys/db.js";
import { db } from "../sys/db.js"
import { sha256 } from "../sys/crypto.js";
import { get64, uts } from "../sys/tools.js";
import { addShutdownTask } from "nman";
let acts = importActs as { [key: string]: Act };
let tempTokens: { [key: string]: postClient } = {};
export const addPostMethods = (server: express.Express) => {
for (const act in acts) {
let methode = acts[act];
server.post("/api/" + act, async (req, res) => {
debug("POST", "reveived:", req.body);
const aws = (state: string, data: any) => {
res.status(state == "error" ? 400 : 200);
if (typeof data == "string") res.send(data);
else res.json(data);
};
let client: postClient | null = null;
try {
let auth = authorization.parse(req.headers["authorization"] ?? "");
if (auth.token != null && typeof auth.token == "string") {
if (tempTokens[auth.token] != null) {
client = tempTokens[auth.token];
} else {
aws("error", "token");
return;
}
} else if (auth?.params?.name != null && auth?.params?.accountKey != null && typeof auth?.params?.name == "string" && typeof auth?.params?.accountKey == "string") {
client = new postClient(req.socket.remoteAddress ?? "");
client.name = auth?.params?.name;
client.server = "localhost";
let accountKey = auth?.params?.accountKey;
let query = await select([accounts.accID, accounts.accountKey, accounts.accountKeySalt], accounts)
.where(and(
eq(accounts.name, client.name),
eq(accounts.deleted, 0)
))
.query(db);
if (query.length == 0 || query[0].accountKey != sha256((query[0].accountKeySalt ?? '') + accountKey)) {
client.suspect();
aws("error", "auth");
return;
}
client.accID = query[0][accounts.accID];
client.state = STATE.client;
}
} catch (error) {
}
if (client == null) client = new postClient(req.socket.remoteAddress ?? "");
let send = false;
await client.runAct(methode, req.body, (state: string, data: any) => {
aws(state, data);
send = true;
});
if (!send) aws("error", "server");
});
}
}
class postClient extends Client {
lastReq = uts();
constructor(ip: string) {
super(ip);
}
async runAct(act: Act, json: any, aws: (state: string, data: any) => void) {
this.lastReq = uts();
try {
let { state, data, right, func } = act;
if (!(state & this.state)) {
aws("error", "wrongstate");
debug("POST", "send:", "error", "wrongstate");
this.suspect();
return;
}
if (json.data === null) {
aws("error", "data");
debug("POST", "send:", "error", "data");
return;
}
if (data) {
for (let d in data) {
if (!checktype(json.data[d], data[d])) {
aws("error", "data");
debug("POST", "Data check error. Key: ", d, "; Type:", data[d], "; Value:", json.data[d]);
debug("POST", "send:", "error", "data");
return;
}
}
}
if (right && !(await this.checkRight(right))) {
aws("error", "right");
debug("POST", "send:", "error", "right");
this.suspect();
return;
}
var send = false;
try {
await func(this, json.data, (state, data = "") => {
debug("POST", "send:", state, data);
aws(state, data);
send = true;
});
} catch (e) {
error("POST", "act error:", e);
}
if (!send) {
debug("POST", "send:", "error", "server");
aws("error", "server");
}
} catch (error) {
debug("POST", "error:", error);
aws("error", "server");
}
}
}
export const addTempToken = (client: Client) => {
if (!(client instanceof postClient)) return false;
let token = get64(128);
if (tempTokens[token] != null) token = get64(128);
if (tempTokens[token] != null) token = get64(128);
if (tempTokens[token] != null) return false;
tempTokens[token] = client;
return token;
};
addShutdownTask(() => {
let keys = Object.keys(tempTokens);
for (let i = 0; i < keys.length; i++) {
const c = tempTokens[keys[i]];
if (c.lastReq + 60 * 60 * 3 < uts()) {
delete tempTokens[keys[i]];
}
}
}, 1000);

View file

@ -1,16 +1,115 @@
import { and, eq, naturalJoin, select, update } from "dblang";
import { accounts, db, roomMembers, rooms } from "../sys/db.js";
import { addBruteforcePotantial } from "../sys/bruteforce.js";
export const STATE = {
no: 0b0001,
remoteP: 0b0010,
remote: 0b0100,
client: 0b1000,
client: 0b1000
};
export abstract class Client{
name: String;
server: String;
ip: String;
rights: number;
state: number;
accID = -1;
export const MODE = {
ws: 0b01,
post: 0b10,
both: 0b11,
};
export type Act = {
state: number,
right: number,
data: {
[key: string]: any
},
func: (client: Client, data: any, aws: (code: string, data: any) => void) => Promise<void>;
};
export class Client {
name: string = "";
server: string = "";
ip: string = "";
state: number = STATE.no;
accID: number = -1;
constructor(ip: string) {
this.ip = ip;
}
suspect() {
addBruteforcePotantial(this.ip);
}
async isInRoom(name: string): Promise<number> {
if (this.state != STATE.client) return -1;
let query = await select([rooms.roomID], naturalJoin(rooms, roomMembers))
.where(and(
eq(rooms.name, name),
eq(roomMembers.name, this.name),
eq(roomMembers.server, this.server)
))
.query(db);
if (query.length == 0) return -1;
return query[0][rooms.roomID];
}
async isRoomAdmin(name: string, roomRightRequires: number): Promise<number> {
if (this.state != STATE.client) return -1;
let query = await select([rooms.roomID, rooms.owner, rooms.rights, roomMembers.admin], naturalJoin(rooms, roomMembers))
.where(and(
eq(rooms.name, name),
eq(roomMembers.admin, true),
eq(roomMembers.name, this.name),
eq(roomMembers.server, this.server)
))
.query(db);
if (query.length == 0) return -1;
if (
query[0][roomMembers.admin] == 0
&& query[0][rooms.owner] != this.accID
&& !(query[0][rooms.rights] & roomRightRequires)
) return -1;
return query[0][rooms.roomID];
}
async checkRight(right: number) {
try {
if (right == 0) return true;
let data = await select([accounts.rights], accounts)
.where(eq(accounts.accID, this.accID))
.query(db);
return ((data[0][accounts.rights]) & right) == right;
} catch (e) {
}
return false;
};
async setRights(rights: number) {
await update(accounts)
.set(accounts.rights, rights)
.where(eq(accounts.accID, this.accID))
.query(db);
};
}
export function checktype(data: any, type: string) {
const re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if (typeof data == type) {
return true;
} else if (type == "int" && typeof data == "number" && Number.isInteger(data)) {
return true;
} else if (type == "email" && typeof data == "string" && re.test(data)) {
return true;
} else if (type == "name" && typeof data == "string" && /^[a-zA-Z0-9\-_.]+$/.test(data)) {
return true;
} else if (type.startsWith("name-") && typeof data == "string" && /^[a-zA-Z0-9\-_.]+$/.test(data)) {
return parseInt(type.split("-")[1]) >= data.length;
} else if (type.startsWith("string-") && typeof data == "string") {
return parseInt(type.split("-")[1]) >= data.length;
} else if (type == "any") {
return true;
} else {
return false;
}
}

View file

@ -1,6 +1,155 @@
import ws from "ws";
import http from "http";
import { bruteforcecheck } from "../sys/bruteforce.js";
import { Act, checktype, Client } from "./user.js";
import { debug, error } from "../sys/log.js";
import * as importActs from "./acts.js"
export const wsOnConnection = (socket:ws.WebSocket, req: http.IncomingMessage) => {
let acts = importActs as { [key: string]: Act };
export const wsOnConnection = (socket: ws.WebSocket, req: http.IncomingMessage) => {
let ip = req.socket.remoteAddress;
if (bruteforcecheck(ip ?? "")) return void socket.close();
new wsClient(socket, req);
}
let clients: wsClient[] = [];
class wsClient extends Client {
socket: ws.WebSocket;
open = true;
activeRequests = 0;
constructor(socket: ws.WebSocket, req: http.IncomingMessage) {
super(req.socket.remoteAddress ?? "");
this.socket = socket;
socket.on("message", async (msg: any) => {
try {
this.activeRequests++;
let msgStr = msg.toString();
debug("WebSocket", "reveived:", msgStr);
let json = JSON.parse(msgStr) as { act: string, id: number, data: any };
if (closed) {
socket.send(JSON.stringify({
id: json.id,
state: "error",
data: "closed"
}));
debug("WebSocket", "send:", "error", "closed");
return;
}
if (typeof json.act != "string") {
return;
}
if (acts[json.act] == null) {
socket.send(JSON.stringify({
id: json.id,
state: "error",
data: "notfound"
}));
debug("WebSocket", "send:", "error", "notfound");
return;
}
let { state, data, right, func } = acts[json.act];
if (!(state & this.state)) {
socket.send(JSON.stringify({
id: json.id,
state: "error",
data: "wrongstate"
}));
debug("WebSocket", "send:", "error", "wrongstate");
this.suspect();
return;
}
if (json.data === null) {
socket.send(JSON.stringify({
id: json.id,
state: "error",
data: "data"
}));
debug("POST", "send:", "error", "data");
return;
}
if (data) {
for (let d in data) {
if (!checktype(json.data[d], data[d])) {
socket.send(JSON.stringify({
id: json.id,
state: "error",
data: "data"
}));
debug("WebSocket", "Data check error. Key: ", d, "; Type:", data[d], "; Value:", json.data[d]);
debug("WebSocket", "send:", "error", "data");
return;
}
}
}
if (right && !(await this.checkRight(right))) {
socket.send(JSON.stringify({
id: json.id,
state: "error",
data: "right"
}));
debug("WebSocket", "send:", "error", "right");
this.suspect();
return;
}
var send = false;
try {
await func(this, json.data, (state, data = "") => {
debug("WebSocket", "send:", state, data);
socket.send(JSON.stringify({ id: json.id, state, data }));
send = true;
});
} catch (e) {
error("WebSocket", "act error:", e);
}
if (!send) {
debug("WebSocket", "send:", "error", "server");
socket.send(JSON.stringify({
id: json.id,
state: "error",
data: "server"
}));
}
} catch (error) {
debug("WebSocket", "error:", error);
socket.send("error");
} finally {
this.activeRequests--;
}
});
socket.on('close', () => {
var i = clients.indexOf(this);
delete clients[i];
if (i >= 0) {
clients.splice(i, 1);
}
});
}
close(ms: number) {
return new Promise<void>(res => {
if (this.activeRequests == 0) {
this.socket.close();
return void res();
}
setTimeout(() => {
this.socket.close();
res();
}, Math.max(100, ms));
});
}
}
export const closeWebSocket = async () => {
for (let i = 0; i < clients.length; i++) {
clients[i].open = false;
}
var now = performance.now();
for (let i = 0; i < clients.length; i++) {
await clients[i].close(now - performance.now() + 25000);
}
};

View file

@ -17,23 +17,3 @@ export const PERMISSIONS = {
EDIT_USERS: 0b1000000000000000,
ALL: 0b1111111111111111,
};
export const checkRight = async (accID: number, right: number) => {
try {
if (right == 0) return true;
let data = await select([accounts.rights], accounts)
.where(eq(accounts.accID, accID))
.query(db);
return ((data[0][accounts.rights]) & right) == right;
} catch (e) {
}
return false;
};
export const setRights = async (accID: number, rights: number) => {
await update(accounts)
.set(accounts.rights, rights)
.where(eq(accounts.accID, accID))
.query(db);
};

View file

@ -28,7 +28,7 @@ const loading = () => {
process.stdout.write("\r\x1b[1m" + P[(x++) % P.length]+" ");
}, 150);
return () => {
process.stdout.write("\r");
process.stdout.write("\r\x1b[0m");
clearInterval(intID)
};
};

View file

@ -109,7 +109,10 @@ roomMembers.addAttributes({
onDelete: onAction.cascade,
onUpdate: onAction.cascade
}
}
},
name: { type: TEXT },
server: { type: TEXT },
admin: { type: BOOL, default: false }
});
export const roomOTAs = db.newTable("roomOTAs");