UNPKG

@showbridge/lib

Version:

Main library for showbridge protocol router

268 lines (267 loc) 10.5 kB
import cors from 'cors'; import express from 'express'; import http from 'http'; import { WebSocketServer } from 'ws'; import { get, has, set } from 'lodash-es'; import superagent from 'superagent'; import Config from '../config.js'; import { HTTPMessage, WebSocketMessage } from '../messages/index.js'; import { disabled, logger } from '../utils/index.js'; import Protocol from './protocol.js'; class HTTPProtocol extends Protocol { constructor(protocolObj, router) { super(protocolObj, router); this.app = express(); this.httpServer = http.createServer(this.app); this.setupWebSocket(); // Express Server this.app.use(cors()); this.app.use(express.json()); this.app.use(express.urlencoded({ extended: true })); this.app.use(express.raw()); this.app.use(express.text()); // eslint-disable-next-line consistent-return this.app.use((err, req, res, next) => { if (err instanceof SyntaxError && res.status === 400 && 'body' in err) { return res.status(400).send({ msg: err.message, errorType: 'invalid_body', }); } next(); }); this.app.get('/config', (req, res) => { res.send(this.router.config); }); this.app.get('/config/schema', (req, res) => { res.send(this.router.config.schema); }); this.app.post('/config', (req, res) => { try { const configToUpdate = new Config(req.body, this.router.config.schema); // TODO(jwetzell): handle errors on the reload and send them back this.emit('configUploaded', configToUpdate); this.router.config = configToUpdate; res.status(200).send({ msg: 'config reloaded check console for any errors' }); } catch (error) { logger.error(error); res.status(400).send({ msg: 'invalid config', errorType: 'config_validation', errors: error, }); } }); this.app.get('/vars*', (req, res) => { if (req.path === '/vars') { res.send(this.router.vars); } else { const varsKey = req.path.replace('/vars/', '').replaceAll('/', '.'); if (has(this.router.vars, varsKey)) { res.send(get(this.router.vars, varsKey)); } else { res.status(404).send({ msg: `no value found at vars.${varsKey}`, errorType: 'vars_key', }); } } }); this.app.post('/vars*', (req, res) => { if (req.path === '/vars') { this.router.vars = req.body; this.router.emit('varsUpdated', this.router.vars); res.status(200).send({ msg: 'vars updated' }); } else { const varsKey = req.path.replace('/vars/', '').replaceAll('/', '.'); if (varsKey !== '') { set(this.router.vars, varsKey, req.body); this.router.emit('varsUpdated', this.router.vars); res.status(200).send({ msg: `vars.${varsKey} updated` }); } } }); // TODO(jwetzell): error handling on these endpoints // TODO(jwetzell): ability for user to control the HTTP responses this.app.get('/*', (req, res) => { const parsedHTTP = new HTTPMessage(req, res); this.emit('messageIn', parsedHTTP); try { res.status(200).send({ msg: 'ok' }); } catch (error) { logger.trace('http: default message supressed likely from http-response action'); } }); this.app.post('/*', (req, res) => { const parsedHTTP = new HTTPMessage(req, res); this.emit('messageIn', parsedHTTP); try { res.status(200).send({ msg: 'ok' }); } catch (error) { logger.trace('http: default message supressed likely from http-response action'); } }); this.httpServer.on('clientError', (error) => { logger.error(error); }); } setupWebSocket() { this.webUISockets = []; this.openWebSockets = []; this.wsServer = new WebSocketServer({ noServer: true, }); this.wsServer.on('connection', (ws, req) => { if (ws.protocol === 'webui') { const webUISocket = ws; // NOTE(jwetzell): this setups the websocket protocol for the webui // storing the socket for later reference and not setting the usual message listener this.webUISockets.push(webUISocket); webUISocket.onclose = () => { const socketIndex = this.webUISockets.indexOf(webUISocket); if (socketIndex > -1) { this.webUISockets.splice(socketIndex, 1); } }; webUISocket.on('message', (msgBuffer) => { const wsMsg = new WebSocketMessage(msgBuffer, { protocol: 'tcp', address: req.socket?.remoteAddress, port: req.socket?.remotePort, }); if (wsMsg.payload !== undefined && typeof wsMsg.payload === 'object') { const webUIPayload = wsMsg.payload; if (webUIPayload.eventName === 'getProtocolStatus') { this.emit('getProtocolStatus', webUISocket); } else if (webUIPayload.eventName === 'runAction') { if (webUIPayload.data) { // TODO(jwetzell) error handling here? this.emit('runAction', webUIPayload.data.action, webUIPayload.data.msg, webUIPayload.data.vars); } } } }); } else { const webSocketConnection = ws; this.openWebSockets.push(webSocketConnection); webSocketConnection.onclose = () => { const socketIndex = this.openWebSockets.indexOf(ws); if (socketIndex > -1) { this.openWebSockets.splice(socketIndex, 1); } }; webSocketConnection.on('message', (msgBuffer) => { // NOTE(jwetzell): extract some key request properties const wsMsg = new WebSocketMessage(msgBuffer, { protocol: 'tcp', address: req.socket?.remoteAddress, port: req.socket?.remotePort, }); this.emit('messageIn', wsMsg); }); } }); this.httpServer.on('upgrade', (req, socket, head) => { this.wsServer.handleUpgrade(req, socket, head, (ws) => { this.wsServer.emit('connection', ws, req); }); }); } servePath(filePath) { this.app.use('/', express.static(filePath)); // NOTE(jwetzell): move the static paths up in priority this.app._router.stack.sort((a, b) => { if (a.name === 'serveStatic') { return -1; } if (b.name === 'serveStatic') { return 1; } return 0; }); } reload(params) { if (this.server !== undefined) { this.server.close(); this.server.listen(params.port, params.address ? params.address : '0.0.0.0', () => { logger.debug(`http: web interface listening on port ${this.httpServer.address().address}:${this.httpServer.address().port}`); this.emit('started'); }); return; } if (this.webUISockets.length > 0) { this.webUISockets.forEach((webUISocket) => { webUISocket.close(); }); } try { this.server = this.httpServer.listen(params.port, params.address ? params.address : '0.0.0.0', () => { logger.debug(`http: web interface listening on port ${this.httpServer.address().address}:${this.httpServer.address().port}`); this.emit('started'); }); this.server.on('close', () => { this.emit('stopped'); }); } catch (error) { logger.error(`http: problem launching server - ${error}`); } } sendToWebUISockets(eventName, data) { this.webUISockets.forEach((socket) => { socket.send(JSON.stringify({ eventName, data, })); }); } send(url, method, body, contentType) { const request = superagent(method, url); if (contentType !== undefined) { request.type(contentType); } if (body !== '') { request.send(body); } request.end((error) => { if (error) { logger.error(`http: problem sending http - ${error}`); } }); } stop() { // NOTE(jwetzell): close all web sockets this.webUISockets.forEach((socket) => { socket.close(); }); this.openWebSockets.forEach((socket) => { socket.close(); }); // NOTE(jwetzell): close http server if (this.server) { if (this.server.listening) { this.server.close(); } this.emit('stopped'); } else { this.emit('stopped'); } } get status() { return { enabled: !disabled.protocols.has('http'), listening: this.server ? this.server.listening : false, address: this.server ? this.server.address() : {}, }; } } export default HTTPProtocol;