UNPKG

homebridge-smartsystem

Version:

SmartServer (Proxy Websockets to TCP sockets, Smappee MQTT, Duotecno IP Nodes, Homekit interface)

181 lines (154 loc) 6.01 kB
import * as fs from 'fs'; import * as mime from 'mime-types'; import * as net from 'net'; import { Server as WSServer } from 'ws'; import { System } from '../duotecno/system'; import { log, logSettings, LogLevel, err } from "../duotecno/logger"; import { Support } from './support'; import { Context, HttpResponse, WebApp } from "./webapp"; //////////////// // Web server // //////////////// export class SocApp extends WebApp { support: Support; system: System; // hashmap with all connected clients clients: { [url:string]: { connected: boolean, lastseen: Date} } = {}; constructor(system: System, type: string, port?: number) { super(type); if (this.config.debug) logSettings["socapp"] = LogLevel.debug; this.port = port || this.config.port || this.port || 80; this.system = system; this.support = new Support(system); } needsLogin(context: Context): boolean { return false; } async doRequest(context: Context): Promise<HttpResponse> { if (context.request === "") { return this.serveFile(context, "/index.html"); } else if (context.request === "log") { return this.renderLog(); } else if (context.request === "restart") { return this.doRestart(context.jsonrequest); } else if (context.request === "reboot") { return this.doReboot(context, context.jsonrequest); } else if (context.req.url === "/apple-touch-icon-precomposed.png") { return this.serveFile(context, "/assets/imgs/apple-touch-icon-precomposed.png"); } else { try { return this.serveFile(context, context.path); } catch(error) { err("socapp", "Error serving URL: " + context.req.url); return this.serveFile(context, "/index.html"); // return this.notFound(context.req.url); } } } serve() { log("socapp", "SocApp - Start http server on port " + this.port); super.serve(() => { const wsServer = new WSServer({server: this.server}); wsServer.on('connection', this.handleClient.bind(this)); }); } serveFile(context: Context, fn: string): HttpResponse { const data = fs.readFileSync("www"+fn); const type = mime.lookup(fn) || "application/text"; log("socapp", "Serve - " + (context.req?.socket?.remoteAddress || "no-remote") + ": " + fn + " - " + type); return { status: 200, data, type }; } renderLog(): HttpResponse { const clientKeys = Object.keys(this.clients); const seen = clientKeys.length; const connected = clientKeys.reduce((acc, k) => acc + (this.clients[k].connected ? 1 : 0), 0); const cList = clientKeys.filter(k => this.clients[k].connected).map(k => `<li>${k}: ${this.clients[k].lastseen}</li>`).join(""); const sList = clientKeys.filter(k => !this.clients[k].connected).map(k => `<li>${k}: ${this.clients[k].lastseen}</li>`).join(""); return { status: 200, type: "text/html", data: ` <html><header><title>Akiworks SmartServer status</title></header> <body> <h1>Connections: ${seen}</h1> <h2>Open: ${connected}</h2> <ul>${cList}</ul> <h2>closed: ${seen-connected}</h2> <ul>${sList}</ul> </body> <footer><hr /><small>© Duotechno & Johan Coppieters</small></footer></html> `}; } // Handle new incomming WebSocket client handleClient(client, req) { let target: net.Socket; const clientAddr = client._socket.remoteAddress; const logger = (msg) => log('socapp', clientAddr + ': '+ msg); const info = (this.config.debug) ? (msg) => logger('socapp - ' + clientAddr + ': '+ msg) : (msg) => null; ///////////////////////////////////// // url parsing to find ip and port // ///////////////////////////////////// const url = (req ? req.url : client.upgradeReq.url).substr(1); const parts = url.split(':'); if (parts.length != 2) { logger("invalid target address: " + url); return; } const ipAddress = parts[0]; const portNr = parts[1]; logger("websocket proxy to ip: " + ipAddress + ", port: " + portNr); ///////////////////////////////// // Target connection handling // ///////////////////////////////// target = net.createConnection(portNr, ipAddress, () => { target.setNoDelay(); // disable Nagle logger("connected to target -> " + url); this.clients[url] = {connected: true, lastseen: new Date()}; }); target.on('data', (data) => { const msg = data.toString(); info("received from target: " + msg.substr(0, msg.length-1)); try { client.send(msg); this.clients[url].lastseen = new Date(); } catch(e) { logger("client sending error: " + e.message + ", cleaning up target"); target.end(); } }); target.on('end', () => { logger('target disconnected'); client.close(); this.clients[url].connected = false; }); target.on('error', (err) => { logger('target error' + err); target.end(); client.close(); if (this.clients[url]) this.clients[url].connected = false; }); /////////////////////////////////// // websocket connection handling // /////////////////////////////////// client.on('message', (msg) => { // log('got message from websocket: ' + msg.substr(0, msg.length-1)); // Let's hope the socket is buffering even before the connection is fully open // if (! connected) log('Not yet connected!!') else const result = this.support.handle(msg); if (result.done) { if (result.answer) client.send(result.answer); } else { info('received from client: ' + msg.substr(0, msg.length-1)) target.write(msg); } }); client.on('close', (code, reason) => { logger('websocket disconnected: ' + code + ' [' + reason + ']'); target.end(); }); client.on('error', (err) => { logger('websocket error: ' + err); target.end(); }); } }