From 1fbe8d69cb4e6a172848d919274a5e3b22088aac Mon Sep 17 00:00:00 2001 From: jusax23 Date: Wed, 1 Mar 2023 23:47:21 +0100 Subject: [PATCH] acts framework --- .gitignore | 3 +- package-lock.json | 13 ++++ package.json | 3 + src/api/acts.ts | 1 + src/api/acts/login.ts | 14 ++++ src/api/post.ts | 146 +++++++++++++++++++++++++++++++++++- src/api/user.ts | 117 ++++++++++++++++++++++++++--- src/api/ws.ts | 153 +++++++++++++++++++++++++++++++++++++- src/server/permissions.ts | 20 ----- src/setup/config.ts | 2 +- src/sys/db.ts | 5 +- 11 files changed, 442 insertions(+), 35 deletions(-) create mode 100644 src/api/acts.ts create mode 100644 src/api/acts/login.ts diff --git a/.gitignore b/.gitignore index fa23dd4..1892fca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules dist config.juml -.vscode \ No newline at end of file +.vscode +test \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c5feb7f..8d91eba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 6b48e05..026e7a9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/api/acts.ts b/src/api/acts.ts new file mode 100644 index 0000000..decfc45 --- /dev/null +++ b/src/api/acts.ts @@ -0,0 +1 @@ +export * from "./acts/login.js" \ No newline at end of file diff --git a/src/api/acts/login.ts b/src/api/acts/login.ts new file mode 100644 index 0000000..7a0615f --- /dev/null +++ b/src/api/acts/login.ts @@ -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"); + } +}; \ No newline at end of file diff --git a/src/api/post.ts b/src/api/post.ts index 7bff105..49284f1 100644 --- a/src/api/post.ts +++ b/src/api/post.ts @@ -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; -} \ No newline at end of file + 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); \ No newline at end of file diff --git a/src/api/user.ts b/src/api/user.ts index 8ac86f6..a27eb74 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -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; +}; + +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 { + 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 { + 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; + } } \ No newline at end of file diff --git a/src/api/ws.ts b/src/api/ws.ts index 1180ad3..6ed49d8 100644 --- a/src/api/ws.ts +++ b/src/api/ws.ts @@ -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 }; -} \ No newline at end of file +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(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); + } +}; \ No newline at end of file diff --git a/src/server/permissions.ts b/src/server/permissions.ts index a985597..53574a0 100644 --- a/src/server/permissions.ts +++ b/src/server/permissions.ts @@ -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); -}; \ No newline at end of file diff --git a/src/setup/config.ts b/src/setup/config.ts index e88d2cb..980eef1 100644 --- a/src/setup/config.ts +++ b/src/setup/config.ts @@ -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) }; }; diff --git a/src/sys/db.ts b/src/sys/db.ts index 49d2f84..d607bfd 100644 --- a/src/sys/db.ts +++ b/src/sys/db.ts @@ -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");