UNPKG

websocketiot

Version:

npm module for creating node.js-esp websocket IOT project

290 lines (247 loc) 12.5 kB
import { __dirname, objify, getUniqueID, stringify, jsonify, extension, fileName, isType, } from './common.js'; // !TODO: /* ! ! MAKE CODE WORK ON ES AND COMMONJS ! ! */ import http from 'http' import express from 'express'; import nedb from 'nedb'; import WebSocket, { WebSocketServer } from 'ws'; import * as fs from 'fs' import Nedb from 'nedb'; type msgTransmiter = (msg: WsiotMsg) => void; type MsgHandler = (msg: WsiotMsg, connection?: Connection) => void; interface BasicWsiotMsg extends Object { message: string; } interface WsiotMsg extends BasicWsiotMsg { [key: string]: any; } // authenticate interface AuthMsg extends BasicWsiotMsg { message: 'authRequest'; type: ConnectionType; deviceKind?: string; id?: string; } interface MessageLiterals { basic: WsiotMsg; auth: AuthMsg; } type ConnectionType = 'client' | 'device' | 'uknown'; //* connection object class Connection { // statics prop public static scream: msgTransmiter = (msg: WsiotMsg) => { Object.keys(this.connections).forEach((connection) => { this.connections[connection].send(msg) }) }; public static readonly connections: { [key: string]: Connection } = {};// store the connection public static readonly addConnection = (connection: Connection) => Connection.connections[connection.id] = connection;// syntax sugar to add connection public static readonly send = (ws: WebSocket, msg: WsiotMsg) => ws.send(stringify(msg)); public static readonly sendError = (ws: WebSocket, error: string) => ws.send(stringify({ message: "error", error })); public static readonly logRawFunc = (device: string, ...params: any[]) => console.dirxml(device + ':', ...params); public static readonly log = (...params: any[]) => this.logRawFunc('uknown', ...params); public static readonly attachMsgHandler = (ws: WebSocket, run: MsgHandler, request?: string, authenticated: () => boolean = () => false, connection?: Connection) => { // TODO: make the "no request found msg" ws.on('message', (msg) => { const objMsg: WsiotMsg | Error = objify(msg.toString()); if (objMsg instanceof Error) { this.sendError(ws, "request must be in json format!"); return; } if (authenticated() != (objMsg.message != Connection.msgLiterals.auth.message)) { Connection.sendError(ws, authenticated() ? `this device already connected as ${connection?.device}. an relogin attemp is forbids` : "please authenticate this connection before preceeding"); connection?.log("reloggin attemp"); return; } if (request && (objMsg.message != request)) return; //* in case you forgot again, the `request` parameter is the "id" for the request handler. so if its not the correct request it will return like in below if (!this.checkFormat(objMsg, this.msgLiterals.basic)) return; run(objMsg, connection); }) } public static readonly checkFormat = <T extends WsiotMsg>(req: WsiotMsg, type: T, ws?: WebSocket): req is T => { var formated = isType(req, type); if (!formated && ws) this.sendError(ws, 'bad format, format is ' + stringify(type)); return formated; } // instence prop public ws: WebSocket; public readonly id: string; public readonly connectionType: ConnectionType = 'uknown'; public readonly connection: Connection = this; public readonly isDecoy: boolean; public get device(): string { return this.connectionType + ":" + this.id }; private saveData: boolean; public data: object | undefined; readonly run: MsgHandler; log(...params: any[]) { Connection.logRawFunc(this.device, ...params) }; send(msg: WsiotMsg) { if (!this.isDecoy) this.ws.send(stringify(msg)) }; sendError(error: string) { if (!this.isDecoy) Connection.sendError(this.ws, error) }; attachMsgHandler(run: MsgHandler, request?: string) { Connection.attachMsgHandler(this.ws, run, request, () => true, this.connection) }; checkFormat<T extends WsiotMsg>(req: WsiotMsg, type: T, sendError = true): req is T { return Connection.checkFormat(req, type, sendError ? this.ws : undefined) } constructor(ws: WebSocket, run: MsgHandler, isDecoy = false, id: string = getUniqueID(), saveData = false, connectionType?: ConnectionType, data: object | undefined = undefined) { this.ws = ws; this.run = run; this.isDecoy = isDecoy; this.id = id; this.saveData = saveData; this.data = data; if (connectionType) this.connectionType = connectionType; Connection.addConnection(this); this.attachMsgHandler(this.run); // TODO: function to save in the db // TODO: ws terminator // TODO: function to delete or add this db } // message instance literal static readonly msgLiterals: MessageLiterals = { basic: { message: '' }, auth: { type: 'uknown', message: 'authRequest' }, } } // subClass client, for client class Client extends Connection { // overrides public readonly connection: Connection = this; public readonly connectionType: ConnectionType = 'client'; public static scream: msgTransmiter = (msg) => { Object.keys(this.clients).forEach((connection) => { this.clients[connection].send(msg) }) }; // static prop public static readonly clients: { [key: string]: Client } = {};// store the connection public static readonly addClient = (client: Client) => Client.clients[client.id] = client;// syntax sugar to add connection public static clientHandler: MsgHandler = async (req, connection) => { connection?.log("client code is undefined") }; constructor(ws: WebSocket, id?: string, run?: MsgHandler) { // client handler super(ws, Client.clientHandler, false, id); Client.addClient(this); } } // subClass device, for robots abstract class Device extends Connection { // overrides public readonly connection: Connection = this; public readonly connectionType: ConnectionType = 'device'; public get device(): string { return this.deviceKind + ":" + this.id }; public static scream: msgTransmiter = (msg, deviceKind?: string) => { Object.keys(this.devices).filter((val) => this.devices[val].deviceKind === deviceKind).forEach((device) => this.devices[device].send(msg)) }; // static prop // list of devices type public static readonly DeviceKinds: { [key: string]: new (ws: WebSocket, id?: string | undefined) => Device } = {}; public static readonly addDeviceKinds = (deviceClass: new (ws: WebSocket, id?: string | undefined) => Device) => Device.DeviceKinds[deviceClass.name] = deviceClass; public static readonly devices: { [key: string]: Device } = {};// store the connection public static readonly addDevice = (device: Device) => Device.devices[device.id] = device;// syntax sugar to add connection // abstacts public abstract readonly deviceKind: string; //override constructor(ws: WebSocket, run: MsgHandler, id?: string, saveData = false) { super(ws, run, false, id, saveData); Device.addDevice(this); } // TODO: ws setter for reconnecting // TODO: ws updater(terminator) } //* server option interface ServerOption { port: number, app: express.Application, server: http.Server, wss: WebSocketServer, dbPath: string, db: nedb, saveDB: boolean, usePublic: boolean, publicPath: string, useRouter: boolean, routersPath: string, initOnStart: boolean, } class IOTServer { readonly port: number = 8080; readonly app: express.Application = express(); readonly server: http.Server = http.createServer(this.app); readonly wss: WebSocketServer = new WebSocketServer({ server: this.server });; readonly dbPath: string = `./src/db/iot.db`; readonly db: Nedb = new nedb(); readonly saveDB: boolean = false; readonly usePublic: boolean = false; readonly publicPath: string = `./src/public`; readonly useRouter: boolean = false; readonly routersPath: string = `dist/router`; //in folder readonly initOnStart: boolean = true; readonly init = () => this.server.listen(this.port, () => { console.dir(`server is up on port ${this.port}!`); }) //start ws server; public runExtraAuth = () => { }; extraAuth(run: () => void) { this.runExtraAuth = run } constructor(option: object | ServerOption = {}) { for (const [key, value] of Object.entries(option)) if (value !== undefined) this[key as keyof typeof this] = value; const { port, app, server, wss, dbPath, db, saveDB, usePublic, publicPath, useRouter, routersPath, initOnStart } = this; if (this.saveDB)// if the saveDb is true, then save it { this.db = new nedb({ filename: dbPath, autoload: true }); db.persistence.setAutocompactionInterval(5000); } db.loadDatabase(); if (usePublic) //if its use public folder, then use it app.use(express.static(publicPath)); // listen to public folder as all express should do if (useRouter)//use all the router in the routerPath fs.readdir(routersPath, (err, files) => { if (err) { console.warn(err); return; } files.forEach((file) => { if (extension(file) !== 'js') return; import(`${"file://" + routersPath.replaceAll("\\", "/").replaceAll(" ", "%20")}/${file}`).then(module => app.use(`/${fileName(file)}`, module.router)); } ) }) if (initOnStart) this.init(); // use auth protocol on websocket wss.on('connection', (ws) => this.authProtocol(ws, this)); } // authenticate who is connecting private authProtocol(ws: WebSocket, server: IOTServer) { Connection.log("websocketiot connection been made") // is the connection already authenticated or not var connection: Connection; var authenticated = false; // send the identification request just in case const identifyMsg: WsiotMsg = { message: "auth is requested" } Connection.send(ws, identifyMsg); Connection.attachMsgHandler(ws, (req) => { if (connection) return; Connection.log(req); if (!Connection.checkFormat(req, Connection.msgLiterals.auth, ws)) return; // authenticate the connection // if its an client switch (req.type) { case 'client': connection = new Client(ws, req.id); break; case 'device': if (!req.deviceKind) { // *badRequest Connection.sendError(ws, 'device kind is not specified') return; } // checking if the device type is legit let typeFound = false Object.keys(Device.DeviceKinds).forEach((key) => { if (key === req.deviceKind) typeFound = true }) if (!typeFound) return; connection = new Device.DeviceKinds[req.deviceKind](ws, req.id); break; default: Connection.sendError(ws, 'device type is not valid, device type avilable is "client" or "device" only'); return; }; connection.log("connected as ", connection.connectionType); server.runExtraAuth(); connection.send({ message: `connected as ${connection.device}`, id: connection.id }); authenticated = true; }, Connection.msgLiterals.auth.message, () => authenticated) } } export { IOTServer, Connection, Client, Device, msgTransmiter, MsgHandler, ConnectionType, BasicWsiotMsg, WsiotMsg, AuthMsg, MessageLiterals, ServerOption }; export default IOTServer; // const Server = (new IOTServer({ usePublic: true }));