homebridge-smartsystem
Version:
SmartServer (Proxy Websockets to TCP sockets, Smappee MQTT, Duotecno IP Nodes, Homekit interface)
181 lines (154 loc) • 6.01 kB
text/typescript
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();
});
}
}