From a7f7edcd0bb3138b7898fa2b95156cfdf3911296 Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 19 Sep 2019 16:24:35 +0200 Subject: [PATCH] Fixing several bugs and adding very basic rule support --- src/connection.ts | 46 ++++++++++++++----- src/database/database.ts | 19 +++++++- src/database/query.ts | 95 ++++++++++++++++++++++++++++++---------- src/database/rules.ts | 8 ++-- src/rules.ts | 3 -- src/web/v1/admin.ts | 13 ++++-- views/forms.hbs | 32 +++++++------- views/tables.hbs | 20 ++++----- 8 files changed, 163 insertions(+), 73 deletions(-) delete mode 100644 src/rules.ts diff --git a/src/connection.ts b/src/connection.ts index f5367cd..a29f22c 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -2,11 +2,14 @@ import * as io from "socket.io"; import { Server } from "http"; import { DatabaseManager } from "./database/database"; import Logging from "@hibas123/logging"; +import Query from "./database/query"; +import Session from "./database/session"; type QueryTypes = "get" | "set" | "push" | "subscribe" | "unsubscribe"; export class ConnectionManager { static server: io.Server; + static bind(server: Server) { this.server = io(server); this.server.on("connection", this.onConnection.bind(this)); @@ -14,9 +17,13 @@ export class ConnectionManager { private static onConnection(socket: io.Socket) { const reqMap = new Map(); + const stored = new Map(); + const session = new Session(); + const answer = (id: string, data: any, err: boolean = false) => { let time = process.hrtime(reqMap.get(id)); - Logging.debug(`Sending answer for ${id} with data`, data, err ? "as error" : "", "Took", time[1] / 1000, "us"); + reqMap.delete(id); + // Logging.debug(`Sending answer for ${id} with data`, data, err ? "as error" : "", "Took", time[1] / 1000, "us"); socket.emit("message", id, err, data); } @@ -25,33 +32,48 @@ export class ConnectionManager { }) socket.on("query", async (id: string, type: QueryTypes, database: string, path: string[], data: any) => { - Logging.debug(`Request with id ${id} from type ${type} for database ${database} and path ${path} with data ${data}`) + Logging.debug(`Request with id '${id}' from type '${type}' for database '${database}' and path '${path}' with data`, data) reqMap.set(id, process.hrtime()); try { const db = DatabaseManager.getDatabase(database); + const perms = db.rules.hasPermission(path, session); + const noperm = new Error("No permisison!"); if (!db) answer(id, "Database not found!", true); else { - const query = db.getQuery(path); + const query = stored.get(id) || db.getQuery(path); switch (type) { case "get": + if (!perms.read) + throw noperm; answer(id, await query.get()); - break; + return; case "set": + if (!perms.write) + throw noperm; answer(id, await query.set(data)); - break; + return; case "push": + if (!perms.write) + throw noperm; answer(id, await query.push(data)); - break; + return; case "subscribe": - answer(id, await query.subscribe()); - break; + if (!perms.read) + throw noperm; + query.subscribe(data, (data) => { + answer(id, data); + }); + stored.set(id, query); + return; case "unsubscribe": - answer(id, await query.unsubscribe()); - break; + query.unsubscribe(); + stored.delete(id); + return; } + answer(id, "Invalid request!", true); } } catch (err) { Logging.error(err); @@ -61,7 +83,9 @@ export class ConnectionManager { }) socket.on("disconnect", () => { - + reqMap.clear(); + stored.clear(); + socket.removeAllListeners(); }) } } \ No newline at end of file diff --git a/src/database/database.ts b/src/database/database.ts index f8c4d72..726458d 100644 --- a/src/database/database.ts +++ b/src/database/database.ts @@ -3,6 +3,8 @@ import Settings from "../settings"; import getLevelDB from "../storage"; import PathLock from "./lock"; import Query from "./query"; +import { Observable } from "@hibas123/utils"; +import Logging from "@hibas123/logging"; export class DatabaseManager { static databases = new Map(); @@ -16,10 +18,11 @@ export class DatabaseManager { }) } - static addDatabase(name: string) { + static async addDatabase(name: string) { if (this.databases.has(name)) throw new Error("Database already exists!"); + await Settings.addDatabase(name); let database = new Database(name); this.databases.set(name, database); return database; @@ -39,11 +42,23 @@ export class DatabaseManager { } } +export enum ChangeTypes { + SET, + PUSH +} + +export type Change = { + type: ChangeTypes; + path: string[] +} + export class Database { public level = getLevelDB(this.name); - private rules: Rules; + public rules: Rules; public locks = new PathLock() + public changeObservable = new Observable(); + toJSON() { return { name: this.name, diff --git a/src/database/query.ts b/src/database/query.ts index ef41290..3615acb 100644 --- a/src/database/query.ts +++ b/src/database/query.ts @@ -1,8 +1,7 @@ -import { Database } from "./database"; +import { Database, Change, ChangeTypes } from "./database"; import Encoder, { DataTypes } from "@hibas123/binary-encoder"; import { resNull } from "../storage"; -import { Bytes } from "leveldown"; import { LevelUpChain } from "levelup"; import shortid = require("shortid"); import Logging from "@hibas123/nodelogging"; @@ -14,20 +13,14 @@ enum FieldTypes { interface IField { type: FieldTypes; - // fields?: string[]; value?: any } -const FieldEncoder = new Encoder({ +export const FieldEncoder = new Encoder({ type: { index: 1, type: DataTypes.UINT8 }, - // fields: { - // index: 2, - // type: DataTypes.STRING, - // array: true - // }, value: { index: 3, type: DataTypes.AUTO, @@ -43,6 +36,8 @@ export default class Query { if (path.find(segment => segment.indexOf("/") >= 0)) { throw new Error("Path cannot contain '/'!"); } + + this.onChange = this.onChange.bind(this); } private pathToKey(path?: string[]) { @@ -90,15 +85,13 @@ export default class Query { if (obj.type === FieldTypes.VALUE) { return obj.value; } else { - let res = {}; - - let fields = await this.getFields(this.path); - let a = fields.map(field => field.split("/").filter(e => e !== "")).sort((a, b) => a.length - b.length).map(async path => { - let field = await this.getField(path); - Logging.debug("Path:", path, "Field:", field); - let shortened = path.slice(this.path.length); + let sorted = fields.map(field => field.split("/").filter(e => e !== "")).sort((a, b) => a.length - b.length) + let res = {}; + for (let path of sorted) { + let field = await this.getField(path); + let shortened = path.slice(this.path.length); let t = res; for (let section of shortened.slice(0, -1)) { t = t[section]; @@ -109,9 +102,7 @@ export default class Query { } else { t[path[path.length - 1]] = field.value; } - }) - - await Promise.all(a); + } return res; } @@ -127,10 +118,17 @@ export default class Query { let id = shortid.generate(); let q = new Query(this.database, [...this.path, id]); await q.set(value); + this.database.changeObservable.send({ + path: [...this.path, id], + type: ChangeTypes.PUSH + }) return id; } async set(value: any) { + if (value === null || value === undefined) + return this.delete(value); + const lock = await this.database.locks.lock(this.path); let batch = this.database.level.batch(); try { @@ -152,7 +150,6 @@ export default class Query { } const saveValue = (path: string[], value: any) => { - Logging.debug("Save Value:", path, value); if (typeof value === "object") { //TODO: Handle case array! // Field type array? @@ -173,6 +170,10 @@ export default class Query { saveValue(this.path, value); await batch.write(); + this.database.changeObservable.send({ + path: this.path, + type: ChangeTypes.SET + }) } catch (err) { if (batch.length > 0) batch.clear(); @@ -182,7 +183,7 @@ export default class Query { } } - async delete(batch?: LevelUpChain) { + private async delete(batch?: LevelUpChain) { let lock = batch ? undefined : await this.database.locks.lock(this.path); const commit = batch ? false : true; if (!batch) @@ -206,6 +207,54 @@ export default class Query { } } - async subscribe() { } - async unsubscribe() { } + + subscription: { + type: ChangeTypes; + send: (data: any) => void; + }; + subscribe(type: "set" | "push", send: (data: any) => void) { + this.subscription = { + send, + type: type === "set" ? ChangeTypes.SET : ChangeTypes.PUSH + }; + this.database.changeObservable.subscribe(this.onChange); + Logging.debug("Subscribe"); + } + + async onChange(change: Change) { + Logging.debug("Change:", change); + if (!this.subscription) + return this.database.changeObservable.unsubscribe(this.onChange); + + const { type, send } = this.subscription; + + if (type === change.type) { + Logging.debug("Path", this.path, change.path); + if (this.path.length === change.path.length - (type === ChangeTypes.PUSH ? 1 : 0)) { + let valid = true; + for (let i = 0; i < this.path.length; i++) { + if (this.path[i] !== change.path[i]) { + valid = false; + break; + } + } + if (valid) { + Logging.debug("Send Change:", change); + if (type === ChangeTypes.PUSH) { + send({ + id: change.path[change.path.length - 1], + data: await new Query(this.database, change.path).get() + }) + } else { + send(await this.get()) + } + } + } + } + } + + unsubscribe() { + this.subscription = undefined; + this.database.changeObservable.unsubscribe(this.onChange); + } } \ No newline at end of file diff --git a/src/database/rules.ts b/src/database/rules.ts index 091871d..c875530 100644 --- a/src/database/rules.ts +++ b/src/database/rules.ts @@ -57,7 +57,7 @@ export class Rules { this.rules = analyze(parsed); } - hasPermission(path: string[], session: Session) { + hasPermission(path: string[], session: Session): { read: boolean, write: boolean } { let read = this.rules[".read"] || false; let write = this.rules[".write"] || false; @@ -65,7 +65,7 @@ export class Rules { for (let segment of path) { rules = rules[segment]; - if (rules[segment]) { + if (rules) { if (rules[".read"]) { read = rules[".read"] } @@ -79,8 +79,8 @@ export class Rules { } return { - read, - write + read: read as boolean, + write: write as boolean } } diff --git a/src/rules.ts b/src/rules.ts deleted file mode 100644 index cec454e..0000000 --- a/src/rules.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function checkRules(rules: any) { - -} \ No newline at end of file diff --git a/src/web/v1/admin.ts b/src/web/v1/admin.ts index 92e9cfe..c2db86e 100644 --- a/src/web/v1/admin.ts +++ b/src/web/v1/admin.ts @@ -3,13 +3,18 @@ import Settings from "../../settings"; import getForm from "../helper/form"; import Logging from "@hibas123/nodelogging"; import getTable from "../helper/table"; -import { BadRequestError } from "../helper/errors"; +import { BadRequestError, NoPermissionError } from "../helper/errors"; import { DatabaseManager } from "../../database/database"; +import { FieldEncoder } from "../../database/query"; +import getTemplate from "../helper/hb"; +import config from "../../config"; const AdminRoute = new Router(); -AdminRoute.use((ctx, next) => { - //TODO: Check permission +AdminRoute.use(async (ctx, next) => { + const { key } = ctx.query; + if (key !== config.general.admin) + throw new NoPermissionError("No permission!"); return next(); }) @@ -50,7 +55,7 @@ AdminRoute.get("/data", async ctx => { }); let res = [["key", "value"]]; stream.on("data", ({ key, value }) => { - res.push([key, value]); + res.push([key, JSON.stringify(FieldEncoder.decode(value))]); }) stream.on("error", no); diff --git a/views/forms.hbs b/views/forms.hbs index 9c1e8ca..b9c56ef 100644 --- a/views/forms.hbs +++ b/views/forms.hbs @@ -7,7 +7,7 @@ {{title}} - +