UNPKG

homebridge-smartsystem

Version:

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

471 lines (468 loc) 18.8 kB
"use strict"; // Generic Webapp implementation // Johan Coppieters, Feb-Apr 2019. // var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebApp = exports.Context = exports.kVersion = exports.single = void 0; const ejs = require("ejs"); const http = require("http"); const fs = require("fs"); const url = require("url"); const qs = require("querystring"); const base_1 = require("./base"); const types = require("../duotecno/types"); const logger = require("../duotecno/logger"); const logger_1 = require("../duotecno/logger"); const child_process_1 = require("child_process"); ; const kLogoutHeader = { "Set-Cookie": "ticket=x" }; ////////////////////// // Helper functions // ////////////////////// // move most of them to types.ts function single(val) { return (val instanceof Array) ? val[0] : val; } exports.single = single; exports.kVersion = require("../package.json").version; console.log("stolen version: " + exports.kVersion); /////////////////// // Context Class // /////////////////// class Context { constructor(body, req, res) { var _a; this.message = ""; // display if not empty this.time = types.time; this.hex = types.hex; this.watt = types.watt; this.date = types.date; this.datetime = types.datetime; this.makeInt = types.makeInt; const bodyParams = qs.parse(body); const urlParts = url.parse(req.url, true); this.params = Object.assign(Object.assign({}, bodyParams), urlParts.query); // url = /[request]/[action]/[id] let path = this.path = urlParts.pathname; if (path[0] === "/") path = path.substring(1); const parts = path.split("/"); // get action and id from the params or if not specified from the url when available this.action = single(this.params.action) || ((parts.length > 1) ? parts[1] : "") || single(this.params.daction); this.id = single(this.params.id) || ((parts.length > 2) ? parts[2] : ""); // some easy to use stuff this.method = req.method; this.request = parts[0]; this.req = req; this.res = res; this.parseCookies(); // can be used for sending different answers this.jsonrequest = !!(((_a = req.headers['content-type']) === null || _a === void 0 ? void 0 : _a.toLowerCase().indexOf('application/json')) > -1); // helpers for ejs this.today = new Date(); this.year = this.today.getFullYear(); this.version = exports.kVersion; this.nums = [0, 0, 0]; // check short urls with numbers a parts const p1 = parseInt(this.request); if (!isNaN(p1)) this.nums[0] = p1; const p2 = parseInt(this.action); if (!isNaN(p2)) this.nums[1] = p2; const p3 = parseInt(this.id); if (!isNaN(p3)) this.nums[2] = p3; } parseCookies() { var _a; this.cookies = {}; const cookieHeader = (_a = this.req.headers) === null || _a === void 0 ? void 0 : _a.cookie; if (!cookieHeader) return; cookieHeader.split(`;`).forEach((cookie) => { let [name, ...rest] = cookie.split(`=`); name = name === null || name === void 0 ? void 0 : name.trim(); if (!name) return; const value = rest.join(`=`).trim(); if (!value) return; this.cookies[name] = decodeURIComponent(value); }); } getCookie(name) { return this.cookies[name]; } addr(a) { const parts = a.split(";"); return { logicalNodeAddress: parseInt(parts[0], 16), logicalAddress: parseInt((parts.length > 1) ? parts[1] : "0", 16) }; } parseAddress(suggestion) { if (suggestion) { const parts = suggestion.split(":"); if (parts.length > 1) { this["masterAddress"] = parts[0]; this["masterPort"] = parseInt(parts[1]); return true; } } return false; } getMaster(tryName, newAction) { const suggestion = this[tryName] || this.params[tryName]; // try "in 1 variable" in the "action" part of the URL if (this.parseAddress(suggestion)) { // replace action if one was suggested if (newAction) this.action = newAction; } else { const master = (this.params["masterAddress"]) ? this.getParam({ name: "masterAddress", type: "string" }) : this.getParam({ name: "master", type: "string" }); // try "in 1 variable" in the "masterAddress" or "master" if (!this.parseAddress(master)) { // old fashion way -> in 2 variables this["masterAddress"] = master; this["masterPort"] = (this.params["masterPort"]) ? this.getParam({ name: "masterPort", type: "integer", default: 5001 }) : this.getParam({ name: "port", type: "integer", default: 5001 }); } } } getUnit() { const unitStr = this.getParam({ name: "unit", type: "string" }); if (unitStr.indexOf(";") > 0) { // unit=0x23;0x12 return this.addr(unitStr); } else { // node=0x23, unit=0x12 or node=35, unit=17 return { logicalNodeAddress: this.getParam({ name: "node", type: "integer" }), logicalAddress: this.getParam({ name: "unit", type: "integer" }) }; } } getParam(spec) { // infer a type if non is given if (typeof spec.type === "undefined") { if (typeof spec.default === "number") spec.type = "integer"; else if (typeof spec.default === "string") spec.type = "string"; else if (spec.default instanceof Array) { if (spec.default.length > 0) spec.type = (typeof spec.default[0] === "number") ? "integers" : "strings"; else spec.type = "string"; } else spec.type = "string"; } let val = this.params[spec.name]; if (spec.type === "integer") { let num = this.makeInt(single(val)); return (isNaN(num) ? spec.default : num); } else if ((typeof val === "undefined") || (val === null)) { return spec.default; } else if (spec.type === "string") { return single(val); } else if (spec.type === "integers") { if (val instanceof Array) return val.map(s => parseInt(s)); else { let num = this.makeInt(single(val)); return (isNaN(num) ? spec.default : [num]); } } else if (spec.type === "strings") { if (val instanceof Array) return val; else { return [val]; } } else { // no type nor default specified return val; } } } exports.Context = Context; /* add later: https -> let webServer: http.Server | https.Server; try { // try https const cert = fs.readFileSync('ssh.cert'); const key = fs.readFileSync('ssh.key'); webServer = https.createServer({cert, key}, http_request); log("server", " - Running in encrypted HTTPS (wss://) mode using: ssh.cert, ssh.key"); } catch(e) { try { // fallback to http webServer = http.createServer(http_request); log("server", " - Running in unencrypted HTTP (ws://) mode"); } catch(e) { // give up err("server", " - Can't start up: " + e.message); } */ class WebApp extends base_1.Base { constructor(type) { super(type); this.token = ""; // "$#@!" + new Date().getTime() + "!@#$"; // should never match this.user = ""; this.password = ""; this.port = 80; (0, logger_1.log)("server", "Creating http server"); this.files = {}; this.server = http.createServer((req, res) => { this.doServe(req, res).then(result => { res.writeHead(result.status, Object.assign({ 'Content-Type': result.type || 'text/html' }, result.header)); res.write(result.data); res.end(); }).catch(reason => { res.writeHead(501, { 'Content-Type': 'text/html' }); res.write(reason.toString()); res.end(); }); }); } serve(onConnected) { (0, logger_1.log)("server", "WebApp - Start serving http on port " + this.port); this.server.listen(this.port, onConnected); } addFile(name, filename, type, enc) { this.addContent(name, fs.readFileSync(filename, { encoding: enc || 'utf-8' }), type || "text/html", filename); } addContent(name, content, type, filename) { this.files[name] = { content, type, filename }; } getFile(name) { return this.files[name]; } image(filename, type) { type = type || "image/jpeg"; if (type.indexOf("/") < 0) type = "image/" + type; const data = fs.readFileSync(filename); return { status: 200, type: type, data }; } file(name) { const file = this.getFile(name); return { status: 200, data: file.content, type: file.type }; } html(html) { return { status: 200, data: html, type: "text/html" }; } ejs(name, context, objects, header = {}) { // types.debug("webapp", context); !! circular ref in socket/http // try to get cached file let file = this.getFile(name); // for non cached files if (!file) { try { const filename = __dirname + "/views/" + name; file = { content: fs.readFileSync(filename, { encoding: 'utf-8' }), type: "text/html", filename }; } catch (e) { return this.error(context, e.toString()); } } if (file) { // copy objects into context for (const key in objects) context[key] = objects[key]; try { const html = ejs.render(file.content, context, { root: __dirname, filename: file.filename }); return { status: 200, type: file.type, data: html, header }; } catch (e) { return this.error(context, e.toString()); } } else { return this.notFound(); } } json(objects) { return { status: 200, data: JSON.stringify(objects), type: "application/json" }; } needsLogin(context) { return (context.request != "login"); } doServe(req, res) { return __awaiter(this, void 0, void 0, function* () { const context = yield this.parseRequest(req, res); let prevRequest = ""; try { if (this.needsLogin(context)) { logger.debug("webapp", "checking ticket: " + context.cookies["DTicket"] + " = " + this.token + " for " + context.request + "/" + context.action + " => " + (context.cookies["DTicket"] == this.token)); // if a password is set, check if the tickets is valid, if not: redirect to login page if (!this.token || (context.cookies["DTicket"] != this.token)) { context.request = "login"; context.action = ""; } } const httpResult = yield this.doRequest(context); // see if we can go to the original request... if ((context.request === "login") && (context.path != "/login")) { httpResult.header = Object.assign(Object.assign({}, httpResult.header), { "Set-Cookie": "DPath=" + context.path }); } return httpResult; } catch (e) { return this.error(context, e); } }); } doRequest(context) { return __awaiter(this, void 0, void 0, function* () { if ((context.request === "favicon.ico") || (context.request === "404")) { return this.notFound(); } else if (context.request === "restart") { return this.doRestart(context.jsonrequest); } else if (context.request === "reboot") { this.doRestart(context.jsonrequest); return this.doReboot(context, context.jsonrequest); } else if (context.request === "login") { return this.doLogin(context); } else { return { status: 200, type: "text/html", data: "hello there ! " + context.method + ":" + context.request + "/" + (context.action || "") }; } }); } notFound(filename = "") { return { status: 404, type: "text/html", data: "<html><head><title>File " + filename + " not found</title></head><body>These are not the droids your are looking for</body></html>" }; } redirect(url, header = {}) { return { status: 303, type: "text/html", header: Object.assign({ "Location": url }, header), data: "<html><head><meta http-equiv=\"Refresh\" content=\"0; URL=" + url + "\"></head></html>" }; } error(context, msg = "", json = false) { return json ? { status: 202, type: "json/application", data: '{"error": "' + msg + '"}' } : { status: 202, type: "text/html", data: "<html><head><title>Error in " + context.request + "/" + context.action + "</title></head>" + "<body><h1>Error</h1>During " + context.method + " of " + context.request + "/" + context.action + " :</br>" + msg + "</body></html>" }; } pwOK(context, user, pw) { return __awaiter(this, void 0, void 0, function* () { // to be overriden return true; }); } doLogin(context) { return __awaiter(this, void 0, void 0, function* () { if (context.action === "logout") { return this.doLogout(context); } else if (context.action === "login") { return this.doTryLogin(context); } else { return this.ejs("login", context, { user: this.user }); } }); } doTryLogin(context) { return __awaiter(this, void 0, void 0, function* () { const pw = context.getParam({ name: "password", type: "string" }); const user = context.getParam({ name: "user", type: "string" }); if (yield this.pwOK(context, user, pw)) { const path = context.getCookie("DPath") || "/"; return this.redirect(path, { "Set-Cookie": "DTicket=" + this.token }); } else { return this.ejs("login", context, { message: "authentication failed", user }, { "Set-Cookie": "DTicket=x" }); } }); } doLogout(context) { return __awaiter(this, void 0, void 0, function* () { this.token = ""; return this.redirect("/", { "Set-Cookie": "DTicket=x" }); }); } doRestart(json) { // stop the process, pm2 will restart it setTimeout(() => { process.exit(-1); }, 500); if (json) return { status: 200, type: "application/json", data: "Restarting App" }; else return { status: 200, type: "text/html", data: '<html><meta http-equiv="refresh" content="10; url=/">Restarting app... please wait</html>' }; } doReboot(context, json) { return __awaiter(this, void 0, void 0, function* () { // make sure the nodejs process is sudo-er // 1) create nodesudo << pi // ALL=/sbin/shutdown // pi ALL=NOPASSWD: /sbin/shutdown // 2) add file to sudo // sudo /etc/sudoers.d/mysudoersfile return new Promise((resolve, reject) => { (0, child_process_1.exec)("sudo shutdown -r now", (error, stdout, stderr) => { if (error) { // context.message = stderr; // console.log("error shutting down: ", error, stdout, stderr); resolve({ status: 200, type: "text/html", header: kLogoutHeader, data: '<html><meta http-equiv="refresh" content="10; url=/settings"><body><h1>Failed to restart OS</h1><p>' + stderr.replace("\n", "<br>") + '</p></body></html>' }); } else { if (json) resolve({ status: 200, type: "application/json", data: "Rebooting OS" }); else resolve({ status: 200, type: "text/html", data: '<html><meta http-equiv="refresh" content="20; url=/">Restarting OS... please wait</html>' }); } }); }); }); } parseRequest(req, res) { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve, reject) => { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('error', error => { reject(error); }); req.on('end', () => { resolve(new Context(body, req, res)); }); }); }); } } exports.WebApp = WebApp; //# sourceMappingURL=webapp.js.map