homebridge-smartsystem
Version:
SmartServer (Proxy Websockets to TCP sockets, Smappee MQTT, Duotecno IP Nodes, Homekit interface)
471 lines (468 loc) • 18.8 kB
JavaScript
"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