acts framework
This commit is contained in:
parent
adf061f7dd
commit
1fbe8d69cb
11 changed files with 442 additions and 35 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
|||
node_modules
|
||||
dist
|
||||
config.juml
|
||||
.vscode
|
||||
.vscode
|
||||
test
|
13
package-lock.json
generated
13
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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
1
src/api/acts.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./acts/login.js"
|
14
src/api/acts/login.ts
Normal file
14
src/api/acts/login.ts
Normal 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");
|
||||
}
|
||||
};
|
146
src/api/post.ts
146
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;
|
||||
|
||||
}
|
||||
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);
|
117
src/api/user.ts
117
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<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;
|
||||
}
|
||||
}
|
153
src/api/ws.ts
153
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 };
|
||||
|
||||
}
|
||||
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);
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
};
|
|
@ -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)
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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");
|
||||
|
|
Loading…
Reference in a new issue