From 1e588718a855ae2871a8841f6c6e621f49795454 Mon Sep 17 00:00:00 2001 From: "jakob.stendahl" Date: Sat, 17 Dec 2022 21:31:41 +0100 Subject: Start moving to esm, work on updater --- src/Logger/index.cjs | 79 ++++++ src/Logger/index.js | 79 ------ src/NeoRuntimeManager/IPC.cjs | 199 +++++++++++++++ src/NeoRuntimeManager/IPC.js | 199 --------------- src/NeoRuntimeManager/RuntimeProcess.cjs | 106 ++++++++ src/NeoRuntimeManager/RuntimeProcess.js | 106 -------- src/NeoRuntimeManager/index.cjs | 339 +++++++++++++++++++++++++ src/NeoRuntimeManager/index.js | 339 ------------------------- src/SSLCert/index.cjs | 146 +++++++++++ src/SSLCert/index.js | 144 ----------- src/SelfUpdater/index.js | 183 +++++++------- src/SocketIO/index.cjs | 409 +++++++++++++++++++++++++++++++ src/SocketIO/index.js | 409 ------------------------------- src/UserData/index.cjs | 332 +++++++++++++++++++++++++ src/UserData/index.js | 332 ------------------------- 15 files changed, 1700 insertions(+), 1701 deletions(-) create mode 100644 src/Logger/index.cjs delete mode 100644 src/Logger/index.js create mode 100644 src/NeoRuntimeManager/IPC.cjs delete mode 100644 src/NeoRuntimeManager/IPC.js create mode 100644 src/NeoRuntimeManager/RuntimeProcess.cjs delete mode 100644 src/NeoRuntimeManager/RuntimeProcess.js create mode 100644 src/NeoRuntimeManager/index.cjs delete mode 100644 src/NeoRuntimeManager/index.js create mode 100644 src/SSLCert/index.cjs delete mode 100644 src/SSLCert/index.js create mode 100644 src/SocketIO/index.cjs delete mode 100644 src/SocketIO/index.js create mode 100644 src/UserData/index.cjs delete mode 100644 src/UserData/index.js (limited to 'src') diff --git a/src/Logger/index.cjs b/src/Logger/index.cjs new file mode 100644 index 0000000..2ee216a --- /dev/null +++ b/src/Logger/index.cjs @@ -0,0 +1,79 @@ +let fse = require("fs-extra"); + +const level = { + EMERG: "EMERGENCY", + ALERT: "ALERT", + CRIT: "CRITICAL", + ERROR: "ERROR", + WARNING: "WARNING", + NOTICE: "NOTICE", + INFO: "INFO", + DEBUG: "DEBUG", + + ACCESS: "" +}; + + +Object.defineProperty(String.prototype, "lPad", { + value: function lPad(len, chr="0") { + str = this; + var i = -1; + if (!chr && chr !== 0) chr = ' '; + len = len - this.length; + while (++i < len) { + str = chr + str; + } + return str; + }, + writeable: true, + configurable: true +}); + + +function getTimeStamp() { + let CDate = new Date(); + let day = CDate.getDate().toString().lPad(2); + let month = (CDate.getMonth() + 1).toString().lPad(2); // +1 because js starts to count at 0 + let year = CDate.getFullYear(); + let hour = CDate.getHours().toString().lPad(2); + let min = CDate.getMinutes().toString().lPad(2); + let sec = CDate.getSeconds().toString().lPad(2); + let ms = Math.round(CDate.getMilliseconds() / 10).toString().lPad(2); // divide by 10 to make the last digit decimal, then round. + + return `${day}.${month}.${year}-${hour}:${min}:${sec}.${ms}`; +} + + +function log(object, logLevel=level.DEBUG, file="/lux-neo.log") { + fse.ensureFileSync(__logdir + file); + + let formattedLogString = `[${getTimeStamp()}] ${logLevel} ${object}`; + console.log(formattedLogString); // @TODO: This should probably be removed, used for dev currently + + + fse.appendFile( + __logdir + "/lux-neo.log", + formattedLogString + '\n' + ).catch(err => { + console.log("EMERGENCY Could not write to log-file 'lux-neo.log'..."); + console.log("DEBUG FileWriteError: " + err) + }); + + if (__event != undefined) { + __event.emit("logger", logLevel, object); + } +} + +module.exports = { + level, + log, + emerg: (object) => { log(object, level.EMERG); }, + alert: (object) => { log(object, level.ALERT); }, + crit: (object) => { log(object, level.CRIT); }, + error: (object) => { log(object, level.ERROR); }, + warning: (object) => { log(object, level.WARNING); }, + notice: (object) => { log(object, level.NOTICE); }, + info: (object) => { log(object, level.INFO); }, + debug: (object) => { log(object, level.DEBUG); }, + access: (object) => { log(object, level.ACCESS, file="/access.log"); } +}; diff --git a/src/Logger/index.js b/src/Logger/index.js deleted file mode 100644 index 2ee216a..0000000 --- a/src/Logger/index.js +++ /dev/null @@ -1,79 +0,0 @@ -let fse = require("fs-extra"); - -const level = { - EMERG: "EMERGENCY", - ALERT: "ALERT", - CRIT: "CRITICAL", - ERROR: "ERROR", - WARNING: "WARNING", - NOTICE: "NOTICE", - INFO: "INFO", - DEBUG: "DEBUG", - - ACCESS: "" -}; - - -Object.defineProperty(String.prototype, "lPad", { - value: function lPad(len, chr="0") { - str = this; - var i = -1; - if (!chr && chr !== 0) chr = ' '; - len = len - this.length; - while (++i < len) { - str = chr + str; - } - return str; - }, - writeable: true, - configurable: true -}); - - -function getTimeStamp() { - let CDate = new Date(); - let day = CDate.getDate().toString().lPad(2); - let month = (CDate.getMonth() + 1).toString().lPad(2); // +1 because js starts to count at 0 - let year = CDate.getFullYear(); - let hour = CDate.getHours().toString().lPad(2); - let min = CDate.getMinutes().toString().lPad(2); - let sec = CDate.getSeconds().toString().lPad(2); - let ms = Math.round(CDate.getMilliseconds() / 10).toString().lPad(2); // divide by 10 to make the last digit decimal, then round. - - return `${day}.${month}.${year}-${hour}:${min}:${sec}.${ms}`; -} - - -function log(object, logLevel=level.DEBUG, file="/lux-neo.log") { - fse.ensureFileSync(__logdir + file); - - let formattedLogString = `[${getTimeStamp()}] ${logLevel} ${object}`; - console.log(formattedLogString); // @TODO: This should probably be removed, used for dev currently - - - fse.appendFile( - __logdir + "/lux-neo.log", - formattedLogString + '\n' - ).catch(err => { - console.log("EMERGENCY Could not write to log-file 'lux-neo.log'..."); - console.log("DEBUG FileWriteError: " + err) - }); - - if (__event != undefined) { - __event.emit("logger", logLevel, object); - } -} - -module.exports = { - level, - log, - emerg: (object) => { log(object, level.EMERG); }, - alert: (object) => { log(object, level.ALERT); }, - crit: (object) => { log(object, level.CRIT); }, - error: (object) => { log(object, level.ERROR); }, - warning: (object) => { log(object, level.WARNING); }, - notice: (object) => { log(object, level.NOTICE); }, - info: (object) => { log(object, level.INFO); }, - debug: (object) => { log(object, level.DEBUG); }, - access: (object) => { log(object, level.ACCESS, file="/access.log"); } -}; diff --git a/src/NeoRuntimeManager/IPC.cjs b/src/NeoRuntimeManager/IPC.cjs new file mode 100644 index 0000000..0684418 --- /dev/null +++ b/src/NeoRuntimeManager/IPC.cjs @@ -0,0 +1,199 @@ +/** + * This module is used to communicate with a python NeoRuntime instance. + * + * @author jakobst1n. + * @since 3.10.2021 + */ + +const net = require("net"); +let logger = require("../Logger/index.cjs"); + +/** @type {int} How long wait between each reconnection attempt */ +const RECONNECT_INTERVAL = 1000; +/** @type {Object} ENUM-ish for command that can be sent to neoruntime */ +const COMMAND = Object.freeze({SET_GLOB : 0, + SET_VAR : 1, + SET_SEND_STRIP_BUF: 2}); +/** @type {Object} ENUM-ish for globvars */ +const GLOBVAR = Object.freeze({POWER_ON : 0, + BRIGHTNESS: 1}); +/** @type {Object} ENUM-ish for what type of data neoruntime sends */ +const DATATYPE = Object.freeze({STATES : 1, + STRIP_BUF: 2, + MATRIX: 3}); + +/** + * class that will keep a active connection to a socket if possible, and + * automatically reconnect. It will emit events when data is received, + * and it will send commands to the process. */ +class IPC { + + constructor(_socketFile, _eventEmitter) { + this.socketFile = _socketFile; + this.eventEmitter = _eventEmitter; + + this.client; + this.connected = false; + this.reconnectInterval = false; + + this.globvars = {}; + this.variables = {}; + + this.reconnect(); + } + + /** + * If we are not already attempting to reconnect, this will start a + * interval that tries to reconnect. */ + reconnect() { + if (this.reconnectInterval === false) { + this.reconnectInterval = setInterval(this.tryOpenSocketConnection.bind(this), RECONNECT_INTERVAL); + } + } + + /** + * This will attempt to connect to the socket, and then setup all listeners + * if it succedes. */ + tryOpenSocketConnection() { + // logger.info("Attempting to start IPC"); + + this.client = net.createConnection(this.socketFile) + .on('connect', () => { + clearInterval(this.reconnectInterval); + this.reconnectInterval = false; + logger.info("IPC Connected."); + }) + .on("ready", () => { + this.connected = true; + }) + .on('data', (data) => { + let json_data; + switch (data[0]) { + case DATATYPE.STATES: + try { + json_data = JSON.parse(data.toString("ascii", 1)); + } catch (e) { + logger.warning("Could not parse json data from neoruntime"); + return; + } + + if (json_data.hasOwnProperty("globvars")) { + forEachDiff(json_data["globvars"], this.globvars, (key, newVal) => { + this.eventEmitter.emit("change", key, newVal); + }); + this.globvars = json_data["globvars"]; + } + if (json_data.hasOwnProperty("variables")) { + forEachDiff(json_data["variables"], this.variables, (key, newVal) => { + this.eventEmitter.emit("change", `variable/${key}`, newVal); + }); + this.variables = json_data["variables"]; + } + break; + + case DATATYPE.MATRIX: + try { + json_data = JSON.parse(data.toString("ascii", 1)); + } catch (e) { + logger.warning("Could not parse json data from neoruntime"); + console.log(e); + } + this.eventEmitter.emit("matrix", json_data); + break; + + case DATATYPE.STRIP_BUF: + this.eventEmitter.emit("strip_buffer", Array.from(data.values()).slice(1)); + break; + + default: + logger.info(data); + } + + }) + .on("timeout", () => { + logger.info("IPC Timeout"); + }) + .on("close", (hadError) => { + logger.info("IPC Close, hadError: ", hadError); + this.connected = false; + this.reconnect(); + }) + .on("end", () => { + logger.info("IPC End"); + this.connected = false; + }) + .on('error', (data) => { + logger.info('IPC Server not active.'); + this.connected = false; + this.reconnect(); + }) + ; + } + + /** + * Will send a command to the socket if we have a active connection, + * if not it will just drop the command. there is no queue implemented + * for such events. */ + sendCommand(commandType, name, value) { + if (this.connected) { + let buf; + + switch (commandType) { + case (COMMAND.SET_GLOB): + buf = Buffer.allocUnsafe(3); + buf[1] = name; + buf[2] = value; + break; + + case (COMMAND.SET_VAR): + if (name.length > 32) { return {success: false, reason: "name too long", detail: "max size of name is 32 bytes"}; } + if (value.length > 93) { return {success: false, reason: "value too long", detail: "max size of value is 93 bytes"}; } + buf = Buffer.allocUnsafe(3 + name.length + value.length); + buf[1] = name.length; + buf[2] = value.length; + buf.write(name, 3, name.length, "ascii"); + buf.write(value, 3+name.length, value.length, "ascii"); + break; + + case (COMMAND.SET_SEND_STRIP_BUF): + buf = Buffer.allocUnsafe(2); + buf[1] = (name) ? 1 : 0; + break; + + default: + logger.warning(`IPC UNKNOWN COMMANDTYPE ${commandType}`) + return {success: false, reason: "ipc command unknown", detail: commandType}; + } + + buf[0] = commandType; + this.client.write(buf); + return {success: true} + } + return {success: false, reason: "socket not connected", detail: "This usually means the python script is not running"}; + } + +} + +const isObject = v => v && typeof v === 'object'; + +/** + * Will call callback on all the differences between the dicts + */ +function forEachDiff(dict1, dict2, callback) { + for (const key of new Set([...Object.keys(dict1), ...Object.keys(dict2)])) { + if (isObject(dict1[key]) && isObject(dict2[key])) { + if (dict1[key].value !== dict2[key].value) { + callback(key, dict1[key]); + } + } else if (dict1[key] !== dict2[key]) { + if (isObject(dict2[key]) && (dict1[key] == null)) { + dict2[key].value = null; + callback(key, dict2[key]) + } else { + callback(key, dict1[key]); + } + } + } +} + +module.exports = {IPC, COMMAND, GLOBVAR}; diff --git a/src/NeoRuntimeManager/IPC.js b/src/NeoRuntimeManager/IPC.js deleted file mode 100644 index 56c8b5d..0000000 --- a/src/NeoRuntimeManager/IPC.js +++ /dev/null @@ -1,199 +0,0 @@ -/** - * This module is used to communicate with a python NeoRuntime instance. - * - * @author jakobst1n. - * @since 3.10.2021 - */ - -const net = require("net"); -let logger = require(__appdir + "/src/Logger"); - -/** @type {int} How long wait between each reconnection attempt */ -const RECONNECT_INTERVAL = 1000; -/** @type {Object} ENUM-ish for command that can be sent to neoruntime */ -const COMMAND = Object.freeze({SET_GLOB : 0, - SET_VAR : 1, - SET_SEND_STRIP_BUF: 2}); -/** @type {Object} ENUM-ish for globvars */ -const GLOBVAR = Object.freeze({POWER_ON : 0, - BRIGHTNESS: 1}); -/** @type {Object} ENUM-ish for what type of data neoruntime sends */ -const DATATYPE = Object.freeze({STATES : 1, - STRIP_BUF: 2, - MATRIX: 3}); - -/** - * class that will keep a active connection to a socket if possible, and - * automatically reconnect. It will emit events when data is received, - * and it will send commands to the process. */ -class IPC { - - constructor(_socketFile, _eventEmitter) { - this.socketFile = _socketFile; - this.eventEmitter = _eventEmitter; - - this.client; - this.connected = false; - this.reconnectInterval = false; - - this.globvars = {}; - this.variables = {}; - - this.reconnect(); - } - - /** - * If we are not already attempting to reconnect, this will start a - * interval that tries to reconnect. */ - reconnect() { - if (this.reconnectInterval === false) { - this.reconnectInterval = setInterval(this.tryOpenSocketConnection.bind(this), RECONNECT_INTERVAL); - } - } - - /** - * This will attempt to connect to the socket, and then setup all listeners - * if it succedes. */ - tryOpenSocketConnection() { - // logger.info("Attempting to start IPC"); - - this.client = net.createConnection(this.socketFile) - .on('connect', () => { - clearInterval(this.reconnectInterval); - this.reconnectInterval = false; - logger.info("IPC Connected."); - }) - .on("ready", () => { - this.connected = true; - }) - .on('data', (data) => { - let json_data; - switch (data[0]) { - case DATATYPE.STATES: - try { - json_data = JSON.parse(data.toString("ascii", 1)); - } catch (e) { - logger.warning("Could not parse json data from neoruntime"); - return; - } - - if (json_data.hasOwnProperty("globvars")) { - forEachDiff(json_data["globvars"], this.globvars, (key, newVal) => { - this.eventEmitter.emit("change", key, newVal); - }); - this.globvars = json_data["globvars"]; - } - if (json_data.hasOwnProperty("variables")) { - forEachDiff(json_data["variables"], this.variables, (key, newVal) => { - this.eventEmitter.emit("change", `variable/${key}`, newVal); - }); - this.variables = json_data["variables"]; - } - break; - - case DATATYPE.MATRIX: - try { - json_data = JSON.parse(data.toString("ascii", 1)); - } catch (e) { - logger.warning("Could not parse json data from neoruntime"); - console.log(e); - } - this.eventEmitter.emit("matrix", json_data); - break; - - case DATATYPE.STRIP_BUF: - this.eventEmitter.emit("strip_buffer", Array.from(data.values()).slice(1)); - break; - - default: - logger.info(data); - } - - }) - .on("timeout", () => { - logger.info("IPC Timeout"); - }) - .on("close", (hadError) => { - logger.info("IPC Close, hadError: ", hadError); - this.connected = false; - this.reconnect(); - }) - .on("end", () => { - logger.info("IPC End"); - this.connected = false; - }) - .on('error', (data) => { - logger.info('IPC Server not active.'); - this.connected = false; - this.reconnect(); - }) - ; - } - - /** - * Will send a command to the socket if we have a active connection, - * if not it will just drop the command. there is no queue implemented - * for such events. */ - sendCommand(commandType, name, value) { - if (this.connected) { - let buf; - - switch (commandType) { - case (COMMAND.SET_GLOB): - buf = Buffer.allocUnsafe(3); - buf[1] = name; - buf[2] = value; - break; - - case (COMMAND.SET_VAR): - if (name.length > 32) { return {success: false, reason: "name too long", detail: "max size of name is 32 bytes"}; } - if (value.length > 93) { return {success: false, reason: "value too long", detail: "max size of value is 93 bytes"}; } - buf = Buffer.allocUnsafe(3 + name.length + value.length); - buf[1] = name.length; - buf[2] = value.length; - buf.write(name, 3, name.length, "ascii"); - buf.write(value, 3+name.length, value.length, "ascii"); - break; - - case (COMMAND.SET_SEND_STRIP_BUF): - buf = Buffer.allocUnsafe(2); - buf[1] = (name) ? 1 : 0; - break; - - default: - logger.warning(`IPC UNKNOWN COMMANDTYPE ${commandType}`) - return {success: false, reason: "ipc command unknown", detail: commandType}; - } - - buf[0] = commandType; - this.client.write(buf); - return {success: true} - } - return {success: false, reason: "socket not connected", detail: "This usually means the python script is not running"}; - } - -} - -const isObject = v => v && typeof v === 'object'; - -/** - * Will call callback on all the differences between the dicts - */ -function forEachDiff(dict1, dict2, callback) { - for (const key of new Set([...Object.keys(dict1), ...Object.keys(dict2)])) { - if (isObject(dict1[key]) && isObject(dict2[key])) { - if (dict1[key].value !== dict2[key].value) { - callback(key, dict1[key]); - } - } else if (dict1[key] !== dict2[key]) { - if (isObject(dict2[key]) && (dict1[key] == null)) { - dict2[key].value = null; - callback(key, dict2[key]) - } else { - callback(key, dict1[key]); - } - } - } -} - -module.exports = {IPC, COMMAND, GLOBVAR}; diff --git a/src/NeoRuntimeManager/RuntimeProcess.cjs b/src/NeoRuntimeManager/RuntimeProcess.cjs new file mode 100644 index 0000000..be78fa9 --- /dev/null +++ b/src/NeoRuntimeManager/RuntimeProcess.cjs @@ -0,0 +1,106 @@ +let fs = require("fs-extra"); +let spawn = require("child_process"); + +class RuntimeProcess { + + constructor(_modePath, _eventEmitter) { + this.modePath = _modePath; + this.logfile = `${this.modePath}/mode.log`; + this.errfile = `${this.modePath}/mode.error`; + + this.stdout = ""; + this.stderr = ""; + + this.fl = false; + this.proc = null; + + this.isRunning = false; + this.exitCode = null; + + this.eventEmitter = _eventEmitter; + } + + start() { + if (this.isRunning) { + console.log("PROCESS ALREADY RUNNING"); + return {success: false, reason: "already running"}; + } + this.isRunning = true; + this.proc = spawn.spawn( + // `${__appdir}/NeoRuntime/Runtime/venv/bin/python`, + "python3", + [ + "-u", // This makes us able to get real-time output + `${__appdir}/NeoRuntime/Runtime/neo_runtime.py`, + `--strip-config="${__configdir}/strip.ini"`, + `--mode-path="${this.modePath}"`, + `--mode-entry=script` + ] + ); + + this.proc.on('error', (err) => { + console.log(err); + }); + + fs.ensureFileSync(this.logfile); + fs.ensureFileSync(this.errfile); + this.eventEmitter.emit("proc:start"); + + this.proc.stdout.on('data', (_stdout) => { + let stdout_str = _stdout.toString(); + fs.appendFile(this.logfile, `[${timestamp()}]: ` + stdout_str); + this.eventEmitter.emit("proc:stdout", stdout_str); + }); + + this.proc.stdout.on('end', () => { + fs.appendFile(this.logfile, "\n"); + }); + + this.proc.stderr.on('data', (_stderr) => { + let stderr_str = _stderr.toString(); + fs.appendFile(this.errfile, `[${timestamp()}]: ` + stderr_str); + this.eventEmitter.emit("proc:stderr", stderr_str); + }); + + this.proc.stderr.on('end', () => { + fs.appendFile(this.logfile, "\n"); + }); + + this.proc.on('close', (code) => { + if (code) { + fs.appendFile(this.logfile, `[${timestamp()}]: ` + "Script exited with code " + code.toString()); + } + this.eventEmitter.emit("proc:exit", 0); + this.isRunning = false; + this.exitCode = code; + }); + + return {success: true}; + } + + stop(restart=false) { + try { + if (restart) { + this.proc.once("close", () => { + setTimeout(() => this.start(), 500); + }); + } + this.proc.kill("SIGINT"); + return {success: true} + } catch (err) { + console.log(err); + return {success:false, reason:err} + } + } +} + +/** + * Creates and returns a timestamp that can be used in logfiles. + * + * @return {string} timestamp + */ +function timestamp() { + return (new Date()).toISOString(); +} + +module.exports = RuntimeProcess; diff --git a/src/NeoRuntimeManager/RuntimeProcess.js b/src/NeoRuntimeManager/RuntimeProcess.js deleted file mode 100644 index be78fa9..0000000 --- a/src/NeoRuntimeManager/RuntimeProcess.js +++ /dev/null @@ -1,106 +0,0 @@ -let fs = require("fs-extra"); -let spawn = require("child_process"); - -class RuntimeProcess { - - constructor(_modePath, _eventEmitter) { - this.modePath = _modePath; - this.logfile = `${this.modePath}/mode.log`; - this.errfile = `${this.modePath}/mode.error`; - - this.stdout = ""; - this.stderr = ""; - - this.fl = false; - this.proc = null; - - this.isRunning = false; - this.exitCode = null; - - this.eventEmitter = _eventEmitter; - } - - start() { - if (this.isRunning) { - console.log("PROCESS ALREADY RUNNING"); - return {success: false, reason: "already running"}; - } - this.isRunning = true; - this.proc = spawn.spawn( - // `${__appdir}/NeoRuntime/Runtime/venv/bin/python`, - "python3", - [ - "-u", // This makes us able to get real-time output - `${__appdir}/NeoRuntime/Runtime/neo_runtime.py`, - `--strip-config="${__configdir}/strip.ini"`, - `--mode-path="${this.modePath}"`, - `--mode-entry=script` - ] - ); - - this.proc.on('error', (err) => { - console.log(err); - }); - - fs.ensureFileSync(this.logfile); - fs.ensureFileSync(this.errfile); - this.eventEmitter.emit("proc:start"); - - this.proc.stdout.on('data', (_stdout) => { - let stdout_str = _stdout.toString(); - fs.appendFile(this.logfile, `[${timestamp()}]: ` + stdout_str); - this.eventEmitter.emit("proc:stdout", stdout_str); - }); - - this.proc.stdout.on('end', () => { - fs.appendFile(this.logfile, "\n"); - }); - - this.proc.stderr.on('data', (_stderr) => { - let stderr_str = _stderr.toString(); - fs.appendFile(this.errfile, `[${timestamp()}]: ` + stderr_str); - this.eventEmitter.emit("proc:stderr", stderr_str); - }); - - this.proc.stderr.on('end', () => { - fs.appendFile(this.logfile, "\n"); - }); - - this.proc.on('close', (code) => { - if (code) { - fs.appendFile(this.logfile, `[${timestamp()}]: ` + "Script exited with code " + code.toString()); - } - this.eventEmitter.emit("proc:exit", 0); - this.isRunning = false; - this.exitCode = code; - }); - - return {success: true}; - } - - stop(restart=false) { - try { - if (restart) { - this.proc.once("close", () => { - setTimeout(() => this.start(), 500); - }); - } - this.proc.kill("SIGINT"); - return {success: true} - } catch (err) { - console.log(err); - return {success:false, reason:err} - } - } -} - -/** - * Creates and returns a timestamp that can be used in logfiles. - * - * @return {string} timestamp - */ -function timestamp() { - return (new Date()).toISOString(); -} - -module.exports = RuntimeProcess; diff --git a/src/NeoRuntimeManager/index.cjs b/src/NeoRuntimeManager/index.cjs new file mode 100644 index 0000000..81bd7e8 --- /dev/null +++ b/src/NeoRuntimeManager/index.cjs @@ -0,0 +1,339 @@ +/** + * This module is used to execute and communicate with a python NeoRuntime instance. + * + * @author jakobst1n. + * @since 19.12.2019 + */ + +const fs = require("fs"); +const fsPromises = fs.promises; +const RuntimeProcess = require("./RuntimeProcess.cjs"); +const IPC = require("./IPC.cjs"); +const logger = require("../Logger/index.cjs"); +const EventEmitter = require('events'); + +/** @type {object} this should be a pointer to a object referencing all neoModules (see app.js) */ +let neoModules; + +/** @type {string} Currently active mode */ +let modeId = null; +/** @type {int} Last exit code of a mode */ +let modeExitCode = 0; +/** @type {RuntimeProcess} This is the current RuntimeProcess instance */ +let runtimeProcess = null; +/** @type {IPC} The IPC instance, used to communicate with the script */ +let ipc = null; +/** @type {EventEmitter} This is used to emit events when things change */ +const eventEmitter = new EventEmitter(); +/** @type {boolean} If this is true, we will not do things the usual way */ +let modeDebuggerActive = false; +/** @type {string} Should be the modeId the debugger is attached to */ +let modeDebuggerId = null; +/** @type {object} Handler for proc:start when debugger is active */ +let modeDebuggerProcStartHandler = null; +/** @type {object} The last received matrix setup */ +let matrix = null; +/** @type {object} intervall for sending current state */ +let debugModeStateEmitIntervall = null; + +eventEmitter.on("proc:exit", (code) => modeExitCode = code); +eventEmitter.on("matrix", (_matrix) => matrix = _matrix); + +/** + * Check if a path id actually a mode (if it is a folder with a script.py file) + * + * @param {string} path - Path to check. + * + * @return {boolean} wether the path points to a valid mode. + */ +function isMode(path) { + if (!fs.existsSync(path)) { return false; } + let folderStat = fs.statSync(path); + if (!folderStat.isDirectory()) { return false; } + if (!fs.existsSync(path + "/script.py")) { return false; } + return true; +} + +/** + * Get all ids of modes that can be set. + * + * @returns {array} All modeids + */ +function listModes() { + let modeDirs = [ + ["builtin/", fs.readdirSync(__appdir + "/NeoRuntime/builtin")], + ["remote/", fs.readdirSync(__datadir + "/remoteCode")], + ["user/", fs.readdirSync(__datadir + "/userCode")] + ] + let validModes = []; + for (modeDir of modeDirs) { + for (modeName of modeDir[1]) { + let modeId = `${modeDir[0]}${modeName}`; + if (isMode(getModePath(modeId))) { + validModes.push(modeId); + } + } + } + return validModes; +} + +/** + * Change mode, stop the old one and start the new one. + * + * @param {string} _modeId - Id of the mode to change to. + * + * @return {object} A standardform return object. + */ +function setMode(_modeId) { + if (modeDebuggerActive && (_modeId != modeDebuggerId)) { + return {success: false, reason: "debugger active", detail: "Cannot change mode when debugger is active."} + } + if (!isMode(getModePath(_modeId))) { + console.log(`Invalid mode "${_modeId}".`); + return {success: false, reason: "unknown modeId"}; + } + logger.info(`Changing mode to "${_modeId}".`); + + stopMode(); + + modeId = _modeId; + neoModules.userData.config.activeMode = modeId; + eventEmitter.emit("change", "mode", modeId); + + runtimeProcess = new RuntimeProcess(getModePath(_modeId), eventEmitter); + startMode(); + + return {success: true} +}; + +/** + * Get current mode + * + * @return {string} current modeId + */ +function currentMode() { + return modeId; +} + +/** + * Will attempt to stop current mode + * + * @return {object} A standardform return object. + */ +function stopMode(restart=false) { + if (modeRunning()) { + runtimeProcess.stop(restart); + } + return {success: true} +}; + +/** + * Will attempt to start current mode + * + * @return {object} A standardform return object. + */ +function startMode() { + if (runtimeProcess === null) { + return {success: false, reason: "no runtimeprocess", detail: "Runtimeprocess not set, did you mean to call setMode?"}; + } + return runtimeProcess.start(); +}; + +/** + * Will attempt to restart current mode + * + * @return {object} A standardform return object. + */ +function restartMode() { + return stopMode(true); +}; + +/** + * Checks if mode is running currently + * + * @return {boolean} if mode is running + */ +function modeRunning() { + if (runtimeProcess === null) { return false; } + return runtimeProcess.isRunning; +}; + +/** + * Get the full system path to a mode + * + * @param {string} modeId + * + * @return {string} Full path of mode + */ +function getModePath(modeId) { + let path = modeId.split("/"); + let location = path.splice(0, 1).toString(); + if (location === "user") { path = __datadir + "/userCode/" + path.join("/"); } + if (location === "remote") { path = __datadir + "/remoteCode/" + path.join("/"); } + if (location === "builtin") { path = __appdir + "/NeoRuntime/builtin/" + path.join("/"); } + return path; +} + +/** + * Function that returns all globvars (brightness, power_on) as the values they + * had last time we heard from the python script. + * + * @return {object} + */ +function getGlobvars() { + if (!modeRunning()) { return {}; } + return ipc.globvars; +} + +/** + * Sets value of a globvar power_on/brightness. + * + * @param {string} name - Name of the variable power_on/brightness + * @param {any} value - The value the variable should be set to + * + * @return {object} Standardform return object + */ +function setGlobvar(name, value) { + if (!modeRunning()) { return; } + + switch(name) { + case "power_on": + return ipc.sendCommand(IPC.COMMAND.SET_GLOB, IPC.GLOBVAR.POWER_ON, (value) ? 1 : 0); + case "brightness": + return ipc.sendCommand(IPC.COMMAND.SET_GLOB, IPC.GLOBVAR.BRIGHTNESS, value); + default: + return {success: false, reason: "unknown globvar", detail: name}; + } +} + +/** + * Get all variables declared in mode + * + * @return {object} + */ +function getVariables() { + if (!modeRunning()) { return {}; } + return ipc.variables; +} + +/** + * Sets value of a variable + * + * @param {string} name - Name of the variable + * @param {any} value - The value the variable should be set to + * + * @return {object} Standardform return object + */ +function setVariable(name, value) { + if (!modeRunning()) { return; } + return ipc.sendCommand(IPC.COMMAND.SET_VAR, name, value); +} + +/** + * A function intented to be used in an interval to emit + * the current debug-state. + * + */ +function debugModeEmitState() { + eventEmitter.emit("debugger:state", { + mode: modeDebuggerId, + running: runtimeProcess.isRunning, + debugMode: modeDebuggerActive, + matrix: matrix + }); +} + +/** + * Start debugger for a mode + * + * @param {string} modeId - The mode to debug + * + * @return {object} Standardform return object + */ +function startDebugger(debuggerModeId) { + if (debuggerModeId.substr(0, 5) !== "user/") { return {success: false, reason: "not user mode"}; } + if (!isMode(getModePath(debuggerModeId))) { return {success: false, reason: "unknown modeId"}; } + if (modeDebuggerActive) { return {success: false, reason: "debugger already active"}; } + logger.info(`Starting debugger for ${debuggerModeId}`); + + if (modeDebuggerProcStartHandler == null) { + modeDebuggerProcStartHandler = eventEmitter.on("proc:start", () => { + setTimeout(() => { + ipc.sendCommand(IPC.COMMAND.SET_SEND_STRIP_BUF, true); + }, 2000); + }); + } else { + console.log(modeDebuggerProcStartHandler); + } + + if (debugModeStateEmitIntervall == null) { + debugModeStateEmitIntervall = setInterval(debugModeEmitState, 1000); + } + + modeDebuggerActive = true; + modeDebuggerId = debuggerModeId; + setTimeout(() => { + setMode(debuggerModeId); + }, 300); + return {success: true, code: fs.readFileSync(getModePath(debuggerModeId) + "/script.py").toString()} +} + +/** + * Save mode + */ +function saveModeCode(_modeId, code) { + if (!modeDebuggerActive) { return {success: false, reason: "debugger not active"}; }; + if (_modeId != modeDebuggerId) { return {success: false, reason: "modeid not the same as debuggermodeid"}; }; + fs.writeFileSync(getModePath(`${modeDebuggerId}/script.py`), code); + return {success: true}; +} + +/** + * Stop the active debugger + * + * @return {object} Standardform return object + */ +function stopDebugger() { + if (!modeDebuggerActive) { return {success: true, detail: "No debugger active"} } + logger.info(`Stopping debugger`); + modeDebuggerActive = false; + eventEmitter.removeAllListeners("proc:start", modeDebuggerProcStartHandler); + modeDebuggerProcStartHandler = null; + + clearInterval(debugModeStateEmitIntervall); + debugModeStateEmitIntervall = null; + + ipc.sendCommand(IPC.COMMAND.SET_SEND_STRIP_BUF, false); + return {success: true} +} + +module.exports = (_neoModules) => { + neoModules = _neoModules; + ipc = new IPC.IPC(neoModules.userData.config.neoRuntimeIPC.socketFile, eventEmitter); + return { + event: eventEmitter, + modes: listModes, + mode: { + current: currentMode, + set: (modeId) => setMode(modeId), + status: { + modeRunning: modeRunning(), + modeExitCode: modeExitCode + }, + globvars: { + get: getGlobvars, + set: setGlobvar + }, + variables: { + get: getVariables, + set: setVariable + } + }, + getModePath, + isMode, + modeRunning, + startDebugger, stopDebugger, saveModeCode, + startMode, stopMode, restartMode, + matrix + } +}; diff --git a/src/NeoRuntimeManager/index.js b/src/NeoRuntimeManager/index.js deleted file mode 100644 index 5989f61..0000000 --- a/src/NeoRuntimeManager/index.js +++ /dev/null @@ -1,339 +0,0 @@ -/** - * This module is used to execute and communicate with a python NeoRuntime instance. - * - * @author jakobst1n. - * @since 19.12.2019 - */ - -const fs = require("fs"); -const fsPromises = fs.promises; -const RuntimeProcess = require("./RuntimeProcess"); -const IPC = require("./IPC"); -const logger = require(__appdir + "/src/Logger"); -const EventEmitter = require('events'); - -/** @type {object} this should be a pointer to a object referencing all neoModules (see app.js) */ -let neoModules; - -/** @type {string} Currently active mode */ -let modeId = null; -/** @type {int} Last exit code of a mode */ -let modeExitCode = 0; -/** @type {RuntimeProcess} This is the current RuntimeProcess instance */ -let runtimeProcess = null; -/** @type {IPC} The IPC instance, used to communicate with the script */ -let ipc = null; -/** @type {EventEmitter} This is used to emit events when things change */ -const eventEmitter = new EventEmitter(); -/** @type {boolean} If this is true, we will not do things the usual way */ -let modeDebuggerActive = false; -/** @type {string} Should be the modeId the debugger is attached to */ -let modeDebuggerId = null; -/** @type {object} Handler for proc:start when debugger is active */ -let modeDebuggerProcStartHandler = null; -/** @type {object} The last received matrix setup */ -let matrix = null; -/** @type {object} intervall for sending current state */ -let debugModeStateEmitIntervall = null; - -eventEmitter.on("proc:exit", (code) => modeExitCode = code); -eventEmitter.on("matrix", (_matrix) => matrix = _matrix); - -/** - * Check if a path id actually a mode (if it is a folder with a script.py file) - * - * @param {string} path - Path to check. - * - * @return {boolean} wether the path points to a valid mode. - */ -function isMode(path) { - if (!fs.existsSync(path)) { return false; } - let folderStat = fs.statSync(path); - if (!folderStat.isDirectory()) { return false; } - if (!fs.existsSync(path + "/script.py")) { return false; } - return true; -} - -/** - * Get all ids of modes that can be set. - * - * @returns {array} All modeids - */ -function listModes() { - let modeDirs = [ - ["builtin/", fs.readdirSync(__appdir + "/NeoRuntime/builtin")], - ["remote/", fs.readdirSync(__datadir + "/remoteCode")], - ["user/", fs.readdirSync(__datadir + "/userCode")] - ] - let validModes = []; - for (modeDir of modeDirs) { - for (modeName of modeDir[1]) { - let modeId = `${modeDir[0]}${modeName}`; - if (isMode(getModePath(modeId))) { - validModes.push(modeId); - } - } - } - return validModes; -} - -/** - * Change mode, stop the old one and start the new one. - * - * @param {string} _modeId - Id of the mode to change to. - * - * @return {object} A standardform return object. - */ -function setMode(_modeId) { - if (modeDebuggerActive && (_modeId != modeDebuggerId)) { - return {success: false, reason: "debugger active", detail: "Cannot change mode when debugger is active."} - } - if (!isMode(getModePath(_modeId))) { - console.log(`Invalid mode "${_modeId}".`); - return {success: false, reason: "unknown modeId"}; - } - logger.info(`Changing mode to "${_modeId}".`); - - stopMode(); - - modeId = _modeId; - neoModules.userData.config.activeMode = modeId; - eventEmitter.emit("change", "mode", modeId); - - runtimeProcess = new RuntimeProcess(getModePath(_modeId), eventEmitter); - startMode(); - - return {success: true} -}; - -/** - * Get current mode - * - * @return {string} current modeId - */ -function currentMode() { - return modeId; -} - -/** - * Will attempt to stop current mode - * - * @return {object} A standardform return object. - */ -function stopMode(restart=false) { - if (modeRunning()) { - runtimeProcess.stop(restart); - } - return {success: true} -}; - -/** - * Will attempt to start current mode - * - * @return {object} A standardform return object. - */ -function startMode() { - if (runtimeProcess === null) { - return {success: false, reason: "no runtimeprocess", detail: "Runtimeprocess not set, did you mean to call setMode?"}; - } - return runtimeProcess.start(); -}; - -/** - * Will attempt to restart current mode - * - * @return {object} A standardform return object. - */ -function restartMode() { - return stopMode(true); -}; - -/** - * Checks if mode is running currently - * - * @return {boolean} if mode is running - */ -function modeRunning() { - if (runtimeProcess === null) { return false; } - return runtimeProcess.isRunning; -}; - -/** - * Get the full system path to a mode - * - * @param {string} modeId - * - * @return {string} Full path of mode - */ -function getModePath(modeId) { - let path = modeId.split("/"); - let location = path.splice(0, 1).toString(); - if (location === "user") { path = __datadir + "/userCode/" + path.join("/"); } - if (location === "remote") { path = __datadir + "/remoteCode/" + path.join("/"); } - if (location === "builtin") { path = __appdir + "/NeoRuntime/builtin/" + path.join("/"); } - return path; -} - -/** - * Function that returns all globvars (brightness, power_on) as the values they - * had last time we heard from the python script. - * - * @return {object} - */ -function getGlobvars() { - if (!modeRunning()) { return {}; } - return ipc.globvars; -} - -/** - * Sets value of a globvar power_on/brightness. - * - * @param {string} name - Name of the variable power_on/brightness - * @param {any} value - The value the variable should be set to - * - * @return {object} Standardform return object - */ -function setGlobvar(name, value) { - if (!modeRunning()) { return; } - - switch(name) { - case "power_on": - return ipc.sendCommand(IPC.COMMAND.SET_GLOB, IPC.GLOBVAR.POWER_ON, (value) ? 1 : 0); - case "brightness": - return ipc.sendCommand(IPC.COMMAND.SET_GLOB, IPC.GLOBVAR.BRIGHTNESS, value); - default: - return {success: false, reason: "unknown globvar", detail: name}; - } -} - -/** - * Get all variables declared in mode - * - * @return {object} - */ -function getVariables() { - if (!modeRunning()) { return {}; } - return ipc.variables; -} - -/** - * Sets value of a variable - * - * @param {string} name - Name of the variable - * @param {any} value - The value the variable should be set to - * - * @return {object} Standardform return object - */ -function setVariable(name, value) { - if (!modeRunning()) { return; } - return ipc.sendCommand(IPC.COMMAND.SET_VAR, name, value); -} - -/** - * A function intented to be used in an interval to emit - * the current debug-state. - * - */ -function debugModeEmitState() { - eventEmitter.emit("debugger:state", { - mode: modeDebuggerId, - running: runtimeProcess.isRunning, - debugMode: modeDebuggerActive, - matrix: matrix - }); -} - -/** - * Start debugger for a mode - * - * @param {string} modeId - The mode to debug - * - * @return {object} Standardform return object - */ -function startDebugger(debuggerModeId) { - if (debuggerModeId.substr(0, 5) !== "user/") { return {success: false, reason: "not user mode"}; } - if (!isMode(getModePath(debuggerModeId))) { return {success: false, reason: "unknown modeId"}; } - if (modeDebuggerActive) { return {success: false, reason: "debugger already active"}; } - logger.info(`Starting debugger for ${debuggerModeId}`); - - if (modeDebuggerProcStartHandler == null) { - modeDebuggerProcStartHandler = eventEmitter.on("proc:start", () => { - setTimeout(() => { - ipc.sendCommand(IPC.COMMAND.SET_SEND_STRIP_BUF, true); - }, 2000); - }); - } else { - console.log(modeDebuggerProcStartHandler); - } - - if (debugModeStateEmitIntervall == null) { - debugModeStateEmitIntervall = setInterval(debugModeEmitState, 1000); - } - - modeDebuggerActive = true; - modeDebuggerId = debuggerModeId; - setTimeout(() => { - setMode(debuggerModeId); - }, 300); - return {success: true, code: fs.readFileSync(getModePath(debuggerModeId) + "/script.py").toString()} -} - -/** - * Save mode - */ -function saveModeCode(_modeId, code) { - if (!modeDebuggerActive) { return {success: false, reason: "debugger not active"}; }; - if (_modeId != modeDebuggerId) { return {success: false, reason: "modeid not the same as debuggermodeid"}; }; - fs.writeFileSync(getModePath(`${modeDebuggerId}/script.py`), code); - return {success: true}; -} - -/** - * Stop the active debugger - * - * @return {object} Standardform return object - */ -function stopDebugger() { - if (!modeDebuggerActive) { return {success: true, detail: "No debugger active"} } - logger.info(`Stopping debugger`); - modeDebuggerActive = false; - eventEmitter.removeAllListeners("proc:start", modeDebuggerProcStartHandler); - modeDebuggerProcStartHandler = null; - - clearInterval(debugModeStateEmitIntervall); - debugModeStateEmitIntervall = null; - - ipc.sendCommand(IPC.COMMAND.SET_SEND_STRIP_BUF, false); - return {success: true} -} - -module.exports = (_neoModules) => { - neoModules = _neoModules; - ipc = new IPC.IPC(neoModules.userData.config.neoRuntimeIPC.socketFile, eventEmitter); - return { - event: eventEmitter, - modes: listModes, - mode: { - current: currentMode, - set: (modeId) => setMode(modeId), - status: { - modeRunning: modeRunning(), - modeExitCode: modeExitCode - }, - globvars: { - get: getGlobvars, - set: setGlobvar - }, - variables: { - get: getVariables, - set: setVariable - } - }, - getModePath, - isMode, - modeRunning, - startDebugger, stopDebugger, saveModeCode, - startMode, stopMode, restartMode, - matrix - } -}; diff --git a/src/SSLCert/index.cjs b/src/SSLCert/index.cjs new file mode 100644 index 0000000..05d9a32 --- /dev/null +++ b/src/SSLCert/index.cjs @@ -0,0 +1,146 @@ +/** + * Module that exports an instance of CertMon + * see class definition to see what it does. + * + * Requires global var '__datadir' to be set. + * + * @author jakobst1n. + * @since 14.16.2019 + */ +let logger = require("../Logger/index.cjs"); +const fs = require("fs"); +const { execSync } = require("child_process"); + +var neoModules; + + /** + * This checks if the server has a valid certificate, if not, + * it will generate one. + */ + class CertMon { + + constructor(configPath, certPath, httpsConfig) { + this.certPath = __configdir + "/certs/"; + + let valid = this.checkValidity(); + if (!valid) { + logger.notice("No valid certificate found, creating one now."); + this.generateCert(); + } + + let interval = setInterval(() => { + let certIsValid = this.checkValidity(); + if (!valid) { + logger.crit("Certificate no longer valid, server should reboot to make a new one."); + } + }, 1440000); // Run once every day + } + + checkValidity() { + let sslConfig = this.getConfig(); + if (!sslConfig["certMade"]) { + logger.debug("'certMade' in config is false, assuming no valid certificate"); + return false; + } + let expire = ((sslConfig["certExpire"] - Date.now()) / 86400000).toFixed(2); + if (expire > 0) { + logger.debug(`Certificate should be valid for ${expire} more days.`); + } else { + expire = Math.abs(expire); + logger.debug(`Certificate expired ${expire} days ago`); + return false; + } + return true; + } + + getConfig() { + return neoModules.userData.config.SSLCert; + } + + updateConfig(parameters) { + neoModules.userData.config.set(parameters); + } + + generateCert() { + let certPath = this.certPath; + let config = this.getConfig(); + + + // Create Root Certificate Autority + let res = openssl( + `genrsa ` + + `-out "${certPath}/root-CA.key.pem" ` + + `4096` + ); + + // Self sign the Root Certificate Autority + res = openssl( + `req ` + + `-x509 ` + + `-new ` + + `-nodes ` + + `-key "${certPath}/root-CA.key.pem" ` + + `-days 1024 ` + + `-out "${certPath}/root-CA.crt.pem" ` + + `-sha256 ` + + `-subj "/C=NO/ST=Oslo/L=Oslo/O=Luxcena Neo Self-Signing Authority/CN=${config.CN}"` + ); + + // Create a Device Certificate for each domain, + // such as example.com, *.example.com, awesome.example.com + // NOTE: You MUST match CN to the domain name or ip address you want to use + res = openssl( + `genrsa ` + + `-out "${certPath}/privkey.pem" ` + + `4096` + ); + + // Create a request from your Device, which your Root CA will sign + res = openssl( + `req ` + + `-new ` + + `-key "${certPath}/privkey.pem" ` + + `-out "${certPath}/csr.pem" ` + + `-subj "/C=NO/ST=Oslo/L=Oslo/O=Luxcena Neo Self-Signing Autohity/CN=${config.CN}"` + ); + + // Sign the request from Device with your Root CA + // -CAserial certs/ca/my-root-ca.srl + res = openssl( + `x509 ` + + `-req ` + + `-in "${certPath}/csr.pem" ` + + `-CA "${certPath}/root-CA.crt.pem" ` + + `-CAkey "${certPath}/root-CA.key.pem" ` + + `-CAcreateserial ` + + `-out "${certPath}/cert.pem" ` + + `-sha256 ` + + `-days 500` + ); + + let creationDate = Date.now(); + config.certMade = true; + config.certDate = creationDate; + config.certExpire = creationDate + (500*86400000); + config.certCN = config.CN; + + logger.info("Self-signed certificate created."); + + } + + } + +function openssl(command) { + try { + let stdout = execSync("openssl " + command); + return true + } catch (e) { + return false + } + } + +module.exports = (_neoModules) => { + neoModules = _neoModules; + return new CertMon(); +}; + diff --git a/src/SSLCert/index.js b/src/SSLCert/index.js deleted file mode 100644 index d235c9b..0000000 --- a/src/SSLCert/index.js +++ /dev/null @@ -1,144 +0,0 @@ -/** - * Module that exports an instance of CertMon - * see class definition to see what it does. - * - * Requires global var '__datadir' to be set. - * - * @author jakobst1n. - * @since 14.16.2019 - */ - let logger = require(__appdir + "/src/Logger"); - const fs = require("fs"); - const { execSync } = require("child_process"); - -var neoModules; - - /** - * This checks if the server has a valid certificate, if not, - * it will generate one. - */ - class CertMon { - - constructor(configPath, certPath, httpsConfig) { - this.certPath = __configdir + "/certs/"; - - let valid = this.checkValidity(); - if (!valid) { - logger.notice("No valid certificate found, creating one now."); - this.generateCert(); - } - - let interval = setInterval(() => { - let certIsValid = this.checkValidity(); - if (!valid) { - logger.crit("Certificate no longer valid, server should reboot to make a new one."); - } - }, 1440000); // Run once every day - } - - checkValidity() { - let sslConfig = this.getConfig(); - if (!sslConfig["certMade"]) { - logger.debug("'certMade' in config is false, assuming no valid certificate"); - return false; - } - let expire = ((sslConfig["certExpire"] - Date.now()) / 86400000).toFixed(2); - if (expire > 0) { - logger.debug(`Certificate should be valid for ${expire} more days.`); - } else { - expire = Math.abs(expire); - logger.debug(`Certificate expired ${expire} days ago`); - return false; - } - return true; - } - - getConfig() { - return neoModules.userData.config.SSLCert; - } - - updateConfig(parameters) { - neoModules.userData.config.set(parameters); - } - - generateCert() { - let certPath = this.certPath; - let config = this.getConfig(); - - - // Create Root Certificate Autority - let res = openssl( - `genrsa ` + - `-out "${certPath}/root-CA.key.pem" ` + - `2048` - ); - - // Self sign the Root Certificate Autority - res = openssl( - `req ` + - `-x509 ` + - `-new ` + - `-nodes ` + - `-key "${certPath}/root-CA.key.pem" ` + - `-days 1024 ` + - `-out "${certPath}/root-CA.crt.pem" ` + - `-subj "/C=NO/ST=Oslo/L=Oslo/O=Luxcena Neo Self-Signing Authority/CN=${config.CN}"` - ); - - // Create a Device Certificate for each domain, - // such as example.com, *.example.com, awesome.example.com - // NOTE: You MUST match CN to the domain name or ip address you want to use - res = openssl( - `genrsa ` + - `-out "${certPath}/privkey.pem" ` + - `2048` - ); - - // Create a request from your Device, which your Root CA will sign - res = openssl( - `req ` + - `-new ` + - `-key "${certPath}/privkey.pem" ` + - `-out "${certPath}/csr.pem" ` + - `-subj "/C=NO/ST=Oslo/L=Oslo/O=Luxcena Neo Self-Signing Autohity/CN=${config.CN}"` - ); - - // Sign the request from Device with your Root CA - // -CAserial certs/ca/my-root-ca.srl - res = openssl( - `x509 ` + - `-req ` + - `-in "${certPath}/csr.pem" ` + - `-CA "${certPath}/root-CA.crt.pem" ` + - `-CAkey "${certPath}/root-CA.key.pem" ` + - `-CAcreateserial ` + - `-out "${certPath}/cert.pem" ` + - `-days 500` - ); - - let creationDate = Date.now(); - config.certMade = true; - config.certDate = creationDate; - config.certExpire = creationDate + (500*86400000); - config.certCN = config.CN; - - logger.info("Self-signed certificate created."); - - } - - } - -function openssl(command) { - try { - let stdout = execSync("openssl " + command); - return true - } catch (e) { - return false - } - } - -module.exports = (_neoModules) => { - neoModules = _neoModules; - return new CertMon(); -}; - diff --git a/src/SelfUpdater/index.js b/src/SelfUpdater/index.js index cc7ce13..f54af51 100644 --- a/src/SelfUpdater/index.js +++ b/src/SelfUpdater/index.js @@ -1,13 +1,31 @@ -let fs = require("fs-extra"); -const fsPromises = fs.promises; -let url = require("url"); -let request = require('request'); -const spawn = require('child_process').spawn; -const EventEmitter = require('events') -let logger = require(__appdir + "/src/Logger"); +import { existsSync, readFileSync } from 'fs'; +import { ensureDirSync } from 'fs-extra'; +import { copyFile, rm } from 'fs/promises'; +import url from 'node:url'; +import { spawn } from 'child_process'; +import { EventEmitter } from 'events'; +import logger from '../Logger/index.cjs' +import fetch from 'node-fetch'; let neoModules; +/** + * Get the latest release from GitHub + */ +async function getLatestRelease() { + let res = await fetch("https://api.github.com/repos/jakobst1n/luxcena-neo/releases/86402456"); + + if (res.status !== 200) { + console.log(res.status); + this.remoteVersionNumber = "Unknown"; + this.newVersion = false; + throw Error(`Could not get latest release (${res.status})...`); + } + + return await res.json() +} + + /** * This just tests if the current appdir is the "default" location */ @@ -20,17 +38,17 @@ function isInstalledInDefaultLocation() { * it will add a number at the end if something already exists, */ function createUniqueDir(path, prefix) { - fs.ensureDirSync(path); + ensureDirSync(path); let fn = `${path}/${prefix}`; let i = 0; let cFn = fn; while (true) { - if (fs.existsSync(cFn)) { + if (existsSync(cFn)) { i++; cFn = `${fn}.${i}`; continue; } - fs.ensureDirSync(cFn); + ensureDirSync(cFn); return cFn; } } @@ -107,56 +125,56 @@ class Updater { this.backupdir = null; this.backupcomplete = false; - if (!isInstalledInDefaultLocation()) { - return {success: false, reason: "not installed in default location", detail: __appdir}; - } this.updating = true; this.event.emit("start"); neoModules.neoRuntimeManager.stopMode(); try { + // Get info about the latest release + this.latestRelease = await getLatestRelease(); + // Download update - this.setStep("Downloading update (1/8)"); + this.setStep("Downloading update (1/7)"); this.setCommand("Create updatedir"); this.updatedir = createUniqueDir("/tmp", "luxcena-neo.update"); + this.setCommand("Download package"); await this.downloadUpdate(this.updatedir); - + // Create backup - this.setStep("Creating backup (2/8)"); + this.setStep("Creating backup (2/7)"); this.setCommand("Create backupdir"); this.backupdir = createUniqueDir("/var/luxcena-neo/backups", "backup"); this.setCommand(`Copy ${__appdir} into ${this.backupdir}`); - await fs.copy(__appdir, this.backupdir); + await copyFile(__appdir, this.backupdir); this.backupcomplete = true; - // Install update - this.setStep("Installing update (3/8)"); - this.setCommand(`Copy ${this.updatedir} into /opt/luxcena-neo`); - await fs.copy(this.updatedir, __appdir); - // Install dependencies - this.setStep("Installing dependencies (4/8)"); + this.setStep("Installing dependencies (3/7)"); await this.installDependencies(); - - // Create python virtualenv - this.setStep("Making virtualenv (5/8)"); - await this.makeVirtualenv(); - - // Build source code - this.setStep("Building source (6/8)"); - await this.build(); + return + + // Install package + this.setStep("Installing package (4/7)"); + await this.installPackage(this.updatedir); + + // Install update + this.setStep("Installing update (5/7)"); + this.setCommand(`Copy ${this.updatedir} into ${__appdir}`); + await copyFile(this.updatedir, __appdir); // Cleanup - this.setStep("Cleaning up (7/8)"); + this.setStep("Cleaning up (6/7)"); await this.cleanup(); // Restart self, systemd service restart policy will start us up again. - this.setStep("Stopping luxcena neo service in the hope that systemd will restart it. (8/8)"); + this.setStep("Stopping luxcena neo service in the hope that systemd will restart it. (7/7)"); this.setCommand("EXIT"); this.updating = false; this.event.emit("end"); - process.exit(0); + setTimeout(() => { + process.exit(0); + }, 1000); } catch (e) { logger.crit(`Updater failed miserably...`); @@ -174,7 +192,7 @@ class Updater { if (this.backupcomplete && (this.backupdir != null)) { this.setStep("Restoring backup"); this.setCommand(`Copy ${this.backupdir} into /opt/luxcena-neo`); - await fs.copy(this.backupdir, __appdir); + await copyFile(this.backupdir, __appdir); } this.setStep("Cleaning up"); await this.cleanup(); @@ -201,9 +219,7 @@ class Updater { * into the temporary folder */ async downloadUpdate(tmpdir) { - let url = (await this.run(`git`, ["-C", __appdir, "remote", "get-url", "origin"])).out.replace("\n",""); - let branch = (await this.run(`git`, ["-C", __appdir, "rev-parse", "--abbrev-ref", "HEAD"])).out.replace("\n",""); - await this.run(`git`, ["clone", "-b", branch, url, tmpdir]); + await this.run(`curl`, ["-s", "-L", "-o", `${tmpdir}/${this.latestRelease["assets"][0]["name"]}`, this.latestRelease["assets"][0]["browser_download_url"]]); } async installDependencies() { @@ -218,45 +234,24 @@ class Updater { await this.run("rm", ["node-v14.10.0-linux-armv6l.tar.gz"]); } else { await this.run("sh", ["-c", "wget -qO- https://deb.nodesource.com/setup_14.x | bash -"]); - await this.run("apt", ["-qy", "install", "nodejs", "python3-pip"]); - await this.run("pip3", ["install", "virtualenv"]); - await this.run("sh", ["-c", `export NODE_ENV=development; npm --prefix \"${__appdir}\" install \"${__appdir}\"`]); } - } - - async makeVirtualenv() { - this.setCommand("Deleting old virtualenv"); - if (fs.existsSync(`${__appdir}/NeoRuntime/Runtime/venv`)) { - await fs.remove(`${__appdir}/NeoRuntime/Runtime/venv`); - } - - await this.run("virtualenv", ["-p", "/usr/bin/python3", `${__appdir}/NeoRuntime/Runtime/venv`]); - await this.run("sh", ["-c", `. ${__appdir}/NeoRuntime/Runtime/venv/bin/activate && pip install rpi_ws281x`]); - } - async build() { - await this.run("sh", ["-c", `npm --prefix \"${__appdir}\" run build:frontend`]); - await this.run("sh", ["-c", `npm --prefix \"${__appdir}\" run build:fontawesome`]); - await this.run("sh", ["-c", `npm --prefix \"${__appdir}\" run build:dialog-polyfill`]); + await this.run("apt", ["-qy", "install", "nodejs", "python3-pip"]); + await this.run("pip3", ["install", "virtualenv"]); } - async installSystemdService() { - this.setCommand("Deleting old systemd service"); - await fs.remove("/etc/systemd/system/luxcena-neo.service"); - this.setCommand("Installing new systemd service"); - await fs.copy("/opt/luxcena-neo/bin/luxcena-neo.service", "/etc/systemd/system/luxcena-neo.service"); - await this.run("systemctl", ["daemon-reload"]); - await this.run("systemctl", ["enable", "luxcena-neo"]); + async installPackage(tmpdir) { + await this.run("sh", ["-c", `export NODE_ENV=production; npm --prefix "${tmpdir}/luxcena-neo/" install "${tmpdir}/${this.latestRelease["assets"][0]["name"]}"`]); } async cleanup() { if (this.updatedir != null) { this.setCommand(`Removing temporary update files ${this.updatedir}`); - await fs.remove(this.updatedir); + await rm(this.updatedir); } if (this.backupdir != null) { this.setCommand(`Removing ${this.backupdir}, thinking everything went fine :)`); - await fs.remove(this.backupdir); + await rm(this.backupdir); } } @@ -265,10 +260,8 @@ class Updater { class SelfUpdater { constructor() { - this.branch; this.repoUrl; this.localPackageJson; - this.remotePackageJSON this.localVersionNumber; this.remoteVersionNumber; this.newVersion = false; @@ -281,37 +274,41 @@ class SelfUpdater { this.updater = new Updater(); } - async checkVersion() { - this.localPackageJson = JSON.parse(fs.readFileSync(__appdir + "/package.json")); + async getCurrentVersionNumber() { + this.localPackageJson = JSON.parse(readFileSync(__appdir + "/package.json")); this.localVersionNumber = this.localPackageJson["version"]; - this.branch = (await promiseSpawn(`git`, ["-C", __appdir, "rev-parse", "--abbrev-ref", "HEAD"])).out.replace("\n",""); - request.get( - "https://raw.githubusercontent.com/JakobST1n/Luxcena-Neo/" + this.branch + "/package.json", - (error, response, body) => { - if (!error && (response.statusCode === 200)) { - this.remotePackageJSON = JSON.parse(body); - this.remoteVersionNumber = this.remotePackageJSON["version"]; - if (this.localVersionNumber != this.remoteVersionNumber) { - logger.notice("A new version is available on \"" + this.branch + "\" (v" + this.remoteVersionNumber + ")"); - this.newVersion = true; - - } else { - logger.info(`Running newest version (${this.localVersionNumber})`); - this.newVersion = false; - } - } else { - logger.notice("Could not find latest version! Please check you internet connection."); - this.remotePackageJSON = null; - this.remoteVersionNumber = "Unknown"; - this.newVersion = false; - } - } - ); + return this.localVersionNumber; + } + + async getLatestVersionNumber() { + this.remoteVersionNumber = (await getLatestRelease())["tag_name"]; + return this.remoteVersionNumber; + } + + async checkVersion() { + let current_version; + let latest_version; + try { + current_version = await this.getCurrentVersionNumber(); + latest_version = await this.getLatestVersionNumber(); + } catch (err) { + logger.notice("Could not find latest version! Please check you internet connection."); + return; + } + + if (current_version != latest_version) { + logger.notice(`A new version is available on (v${latest_version})`); + this.newVersion = true; + + } else { + logger.info(`Running newest version (${current_version})`); + this.newVersion = false; + } } }   -module.exports = (_neoModules) => { +export default function(_neoModules) { neoModules = _neoModules; return new SelfUpdater(); }; diff --git a/src/SocketIO/index.cjs b/src/SocketIO/index.cjs new file mode 100644 index 0000000..6905a92 --- /dev/null +++ b/src/SocketIO/index.cjs @@ -0,0 +1,409 @@ +/** + * This module contains code for handling socketIO clients. + * + * There are two classes, one is a SocketIO controller module. + * The other one is a authorizedclient. + * + * @author jakobst1n. + * @since 19.12.2019 + */ + +let logger = require("../Logger/index.cjs"); +var exec = require('child_process').exec; +var CryptoJS = require("crypto-js"); +let fs = require("fs"); +const { performance } = require("perf_hooks"); + +let neoModules; + +const sanitizePath = (path) => path.match(/(user|remote|builtin\/[a-zA-Z0-9-_\/]{1,200})(\.[a-zA-Z0-9]{1,10})?/)[0]; + +/** + * Create the open socketio namespace and setup all listeners. + * + * @param {io} socketio + */ +function createOpenSocketNamespace(io) { + const openNamespace = io.of("/open") + + openNamespace.on("connection", (socket) => { + logger.access(`SOCKET:open Client (${socket.id}@${socket.handshake.headers.host}) connected.`); + + socket.on("name:get", () => { + socket.emit("name", neoModules.userData.config.instanceName); + }); + socket.on("mode:set", (modeId) => { + neoModules.neoRuntimeManager.mode.set(modeId); + }); + socket.on("mode:get", () => { + socket.emit("mode", neoModules.neoRuntimeManager.mode.current()); + }); + socket.on("modelist:get", () => { + socket.emit("modelist", neoModules.neoRuntimeManager.modes()) + }); + socket.on("brightness:set", (brightness) => { + neoModules.neoRuntimeManager.mode.globvars.set("brightness", brightness); + }); + socket.on("brightness:get", () => { + socket.emit("brightness", neoModules.neoRuntimeManager.mode.globvars.get().brightness); + }); + socket.on("power:set", (power) => { + neoModules.neoRuntimeManager.mode.globvars.set("power_on", power); + }); + socket.on("power:get", () => { + socket.emit("power", neoModules.neoRuntimeManager.mode.globvars.get().power_on); + }); + socket.on("var:set", (name, value) => { + neoModules.neoRuntimeManager.mode.variables.set(name, value.toString()); + }); + socket.on("vars:get", () => { + socket.emit("vars", neoModules.neoRuntimeManager.mode.variables.get()); + }); + socket.on("modeinfo:get", () => { + socket.emit("modeinfo", { + mode: neoModules.neoRuntimeManager.mode.current(), + brightness: neoModules.neoRuntimeManager.mode.globvars.get().brightness, + power: neoModules.neoRuntimeManager.mode.globvars.get().power_on, + vars: neoModules.neoRuntimeManager.mode.variables.get() + }); + }); + socket.on("authenticate:user", (username, password, callback) => { + let user = neoModules.userData.user.get(username); + if (user == null) { + callback({success: false, reason: "Invalid username/password"}) + logger.access(`SOCKET:open Client (${socket.id}@${socket.handshake.headers.host}) tried to log in as '${username}', wrong username and/or password.`); + return; + } + + let providedHash = hashPassword(password, user.salt); + if (providedHash.hash == user.password) { + let token = createToken(socket); + while (session_tokens.hasOwnProperty(token)) { + token = createToken(socket); + } + + session_tokens[token] = { + expire: (~~Date.now())+(2678400), + host: socket.handshake.headers.host, + user: {username: user.username} + }; + + callback({success: true, user: {username: username}, token: token}) + logger.access(`SOCKET:open Client (${socket.id}@${socket.handshake.headers.host}) authenticated as user '${username}'`); + return; + } + + callback({success: false, reason: "Invalid username/password"}) + logger.access(`SOCKET:open Client (${socket.id}@${socket.handshake.headers.host}) tried to log in as '${username}', wrong username and/or password.`); + }); + + socket.on("disconnect", () => { + logger.access(`SOCKET:open Client (${socket.id}@${socket.handshake.headers.host}) disconnected.`); + }); + + if (neoModules.selfUpdater.updater.updating) { + socket.emit("updater", "start"); + } + }); + + neoModules.neoRuntimeManager.event.on("change", (name, value) => { + if (name == "modelist") { + openNamespace.emit("modelist", neoModules.neoRuntimeManager.modes()); + } else if (["mode", "power_on", "brightness"].includes(name)) { + if (name == "power_on") { name = "power"; } + openNamespace.emit(name, value); + } else { + openNamespace.emit("var", name, value); + } + }); + neoModules.selfUpdater.updater.event.on("start", () => { + openNamespace.emit("updater", "start"); + }); + neoModules.selfUpdater.updater.event.on("end", () => { + openNamespace.emit("updater", "end"); + }); +} + +/** + * @type {object} This is the collection of valid session tokens. + */ +let session_tokens = {}; + +/** + * Middleware that will stop the request if the client does not have a valid + * session token. + * + * @param {object} socket - The socket instance of the connected client + * @param {function} next - The callback to continue the middleware chain + */ +function authorize_middleware(socket, next) { + const token = socket.handshake.auth.token; + + if (session_tokens.hasOwnProperty(token) && + // session_tokens[token].host === socket.handshake.headers.host && + session_tokens[token].expire > (~~(Date.now()))) { + socket.data.user = session_tokens[token].user; + next(); + } else { + const err = new Error("not authorized"); + err.data = { content: "invalid session token" }; + next(err); + } +} + +/** + * Create the open socketio namespace and setup all listeners. + * A valid session token is required to connect to this namespace. + * + * @param {io} socetio + */ +function createAuthorizedNamespace(io) { + const authorizedNamespace = io.of("/authed"); + authorizedNamespace.use(authorize_middleware); + authorizedNamespace.on("connection", (socket) => { + logger.access(`SOCKET:authed Client (${socket.id}@${socket.handshake.headers.host}) connected.`); + let debuggerOpen = false; + + socket.emit("user", socket.data.user); + + /* InstanceName */ + socket.on("name:set", (name, fn) => { + neoModules.userData.config.instanceName = name; + fn({success: true}); + io.emit("name", neoModules.userData.config.instanceName); + }); + + /* UserData */ + socket.on("mode:create", (name, template, fn) => { + fn(neoModules.userData.mode.create(name, template)); + }); + socket.on("mode:delete", (modeid, fn) => { + fn(neoModules.userData.mode.delete(modeid)); + }); + + /* LED Config */ + socket.on("led_config:get", () => { + socket.emit("led_config", neoModules.userData.strip.get()); + }); + socket.on("led_config:set", (config) => { + neoModules.userData.strip.set(config); + }); + + /* SelfUpdater */ + socket.on("version:current_number", () => { + socket.emit("version:current_number", neoModules.selfUpdater.localVersionNumber); + }); + socket.on("version:branch", (fn) => { + socket.emit("version:branch", neoModules.selfUpdater.branch); + }); + socket.on("version:newest_number", (fn) => { + socket.emit("version:newest_number", neoModules.selfUpdater.remoteVersionNumber); + }); + socket.on("version:check_for_update", (fn) => { + neoModules.selfUpdater.checkVersion().then(() => { + socket.emit("version:newest_number", neoModules.selfUpdater.remoteVersionNumber); + fn({success: true}); + }); + }); + socket.on("system:update_version", () => { + neoModules.selfUpdater.updater.forceUpdate(); + }); + + /* SSLCert */ + socket.on("sslcert:info", (fn) => { + socket.emit("sslcert:info", {...neoModules.SSLCert.getConfig(), "isValid": neoModules.SSLCert.checkValidity()}); + }); + socket.on("sslcert:generate_new", (fn) => { + neoModules.SSLCert.generateCert(); + fn({success: true}); + }); + + /* System actions */ + socket.on("restart:system", () => { + exec('shutdown -r now', function(error, stdout, stderr){ callback(stdout); }); + }); + socket.on("restart:service", () => { + let p = exec('systemctl restart luxcena-neo'); + p.unref(); + }); + + /* Users */ + socket.on("users:get", () => { + socket.emit("users", neoModules.userData.users()) + }); + socket.on("user:delete", (username, fn) => { + if (username == socket.data.user.username) { fn({success: false, reason: "cannot delete logged in account"}); return; } + fn(neoModules.userData.user.delete(username)); + socket.emit("users", neoModules.userData.users()) + }); + socket.on("user:changeusername", (oldusername, newusername, fn) => { + if (oldusername == socket.data.user.username) { fn({success: false, reason: "cannot change username of logged in account"}); return; } + let user = neoModules.userData.user.get(oldUserName); + if (user == null) { fn({success: false, reason: "unknown username", detail: oldusername}); return; } + user.username = newusername; + let res = neoModules.userData.user.save(user); + if (!res.success) { fn(res); return; } + res = neoModules.userData.user.delete(oldusername) + if (!res.success) { fn(res); return; } + fn({success: true}); + socket.emit("users", neoModules.userData.users()) + }); + socket.on("user:newpassword", (username, newPassword, fn) => { + let user = neoModules.userData.user.get(username); + if (user == null) { fn({success: false, reason: "unknown username", detail: username}); return; } + let newHash = hashPassword(newPassword); + fn(neoModules.userData.user.save(username, newHash.salt, newHash.hash)); + socket.emit("users", neoModules.userData.users()) + }); + socket.on("user:create", (username, newPassword, fn) => { + let user = neoModules.userData.user.get(username); + if (user != null) { fn({success: false, reason: "user already exists", detail: username}); return; } + if (username.length < 1) { fn({success: false, reason: "no username provided"}); return; } + let newHash = hashPassword(newPassword); + fn(neoModules.userData.user.save(username, newHash.salt, newHash.hash)); + socket.emit("users", neoModules.userData.users()) + }); + + /* Editor/debugger */ + let onProcStart = () => socket.emit("editor:proc:start"); + let onProcStop = (code) => socket.emit("editor:proc:exit", code); + let onProcStdout = (stdout) => socket.volatile.emit("editor:proc:stdout", stdout); + let onProcStderr = (stderr) => socket.volatile.emit("editor:proc:stderr", stderr); + let onDebuggerState = (state) => socket.volatile.emit("editor:debugger:state", state); + let closeDebugger = () => { + debuggerOpen = false; + neoModules.neoRuntimeManager.event.removeListener("proc:start", onProcStart); + neoModules.neoRuntimeManager.event.removeListener("proc:stop", onProcStop); + neoModules.neoRuntimeManager.event.removeListener("proc:stdout", onProcStdout); + neoModules.neoRuntimeManager.event.removeListener("proc:stderr", onProcStderr); + neoModules.neoRuntimeManager.event.removeListener("debugger:state", onDebuggerState); + return neoModules.neoRuntimeManager.stopDebugger(); + }; + socket.on("editor:open", (modeId, fn) => { + neoModules.neoRuntimeManager.event.on("proc:start", onProcStart); + neoModules.neoRuntimeManager.event.on("proc:exit", onProcStop); + neoModules.neoRuntimeManager.event.on("proc:stdout", onProcStdout); + neoModules.neoRuntimeManager.event.on("proc:stderr", onProcStderr); + neoModules.neoRuntimeManager.event.on("debugger:state", onDebuggerState); + let res = neoModules.neoRuntimeManager.startDebugger(modeId); + if (!res.success) { fn(res); return; } + logger.info(`Starting debugger for ${modeId}.`) + debuggerOpen = true; + fn({success: true}) + socket.emit("editor:code", modeId, res.code); + + if (neoModules.neoRuntimeManager.modeRunning()) { + socket.emit("editor:proc:start"); + } + }); + socket.on("editor:save", (modeId, code, fn) => { + if (!debuggerOpen) { fn({success: false, reason: "debugger not open"}); return; }; + fn(neoModules.neoRuntimeManager.saveModeCode(modeId, code)); + }); + socket.on("editor:startmode", (fn) => { + if (neoModules.neoRuntimeManager.modeRunning()) { + fn({success: true}); + socket.emit("editor:proc:start"); + } else { + fn(neoModules.neoRuntimeManager.startMode()); + } + }); + socket.on("editor:stopmode", (fn) => { + fn(neoModules.neoRuntimeManager.stopMode()); + }); + socket.on("editor:restartmode", (fn) => { + fn(neoModules.neoRuntimeManager.restartMode()); + }); + socket.on("editor:close", (fn) => { + fn(closeDebugger()); + logger.info("Stopped debugger"); + }); + + /* Matrix and strip buffer */ + socket.on("matrix:get", () => { + socket.emit("matrix", neoModules.neoRuntimeManager.matrix); + }); + + socket.on("disconnect", () => { + logger.access(`SOCKET:authed Client (${socket.id}@${socket.handshake.headers.host}) disconnected.`); + if (debuggerOpen) { + closeDebugger(); + logger.info("Stopped debugger because client disconnected") + } + }); + }); + + neoModules.neoRuntimeManager.event.on("matrix", (matrix) => { + authorizedNamespace.emit("matrix", matrix); + }); + let lastStripBufferEmit = performance.now(); + neoModules.neoRuntimeManager.event.on("strip_buffer", (strip_buffer) => { + if ((performance.now() - lastStripBufferEmit) > 50) { + authorizedNamespace.volatile.emit("strip_buffer", strip_buffer); + lastStripBufferEmit = performance.now(); + } // We just drop packets + }); + neoModules.selfUpdater.updater.event.on("step", (step) => { + authorizedNamespace.emit("updater:step", step); + }); + neoModules.selfUpdater.updater.event.on("command", (command) => { + authorizedNamespace.emit("updater:command", command); + }); + neoModules.selfUpdater.updater.event.on("error", (updateLog) => { + authorizedNamespace.emit("updater:error", updateLog); + }); +} + +/** + * Protect + */ +function limitEmits(fn) { + let lastEmit = performance.now(); + + return { + } +} + +/** + * Creates an access-token from the clients host-name and the current EPOCH. + * + * @param {client} + * + * @return {string} - The access-token. + */ + function createToken(client) { + let time = Date.now().toString(); + let host = client.handshake.headers.host; + return (CryptoJS.SHA256(`${host}${time}`).toString()); +} + +/** + * Create a new salt and hash from a password. + * + * @param {string} password - The password to hash. + * @param {string} salt - If set, this salt will be used, else a new salt is generated. + * + * @return {object} A object containing a password and a salt property. + */ +function hashPassword(password, salt = null) { + if (salt == null) { + salt = CryptoJS.lib.WordArray.random(128 / 2); + } else { + salt = CryptoJS.enc.Hex.parse(salt); + } + let hash = CryptoJS.PBKDF2(password, salt, { + keySize: 512 / 32, + iterations: 1000, + hasher: CryptoJS.algo.SHA512 + }); + return {hash: hash.toString(), salt: salt.toString()} +} + +module.exports = (_neoModules, io) => { + neoModules = _neoModules; + return { + openNamespace: createOpenSocketNamespace(io), + authorizedNamespace: createAuthorizedNamespace(io) + } +}; + diff --git a/src/SocketIO/index.js b/src/SocketIO/index.js deleted file mode 100644 index 675efc5..0000000 --- a/src/SocketIO/index.js +++ /dev/null @@ -1,409 +0,0 @@ -/** - * This module contains code for handling socketIO clients. - * - * There are two classes, one is a SocketIO controller module. - * The other one is a authorizedclient. - * - * @author jakobst1n. - * @since 19.12.2019 - */ - -let logger = require(__appdir + "/src/Logger"); -var exec = require('child_process').exec; -var CryptoJS = require("crypto-js"); -let fs = require("fs"); -const { performance } = require("perf_hooks"); - -let neoModules; - -const sanitizePath = (path) => path.match(/(user|remote|builtin\/[a-zA-Z0-9-_\/]{1,200})(\.[a-zA-Z0-9]{1,10})?/)[0]; - -/** - * Create the open socketio namespace and setup all listeners. - * - * @param {io} socketio - */ -function createOpenSocketNamespace(io) { - const openNamespace = io.of("/open") - - openNamespace.on("connection", (socket) => { - logger.access(`SOCKET:open Client (${socket.id}@${socket.handshake.headers.host}) connected.`); - - socket.on("name:get", () => { - socket.emit("name", neoModules.userData.config.instanceName); - }); - socket.on("mode:set", (modeId) => { - neoModules.neoRuntimeManager.mode.set(modeId); - }); - socket.on("mode:get", () => { - socket.emit("mode", neoModules.neoRuntimeManager.mode.current()); - }); - socket.on("modelist:get", () => { - socket.emit("modelist", neoModules.neoRuntimeManager.modes()) - }); - socket.on("brightness:set", (brightness) => { - neoModules.neoRuntimeManager.mode.globvars.set("brightness", brightness); - }); - socket.on("brightness:get", () => { - socket.emit("brightness", neoModules.neoRuntimeManager.mode.globvars.get().brightness); - }); - socket.on("power:set", (power) => { - neoModules.neoRuntimeManager.mode.globvars.set("power_on", power); - }); - socket.on("power:get", () => { - socket.emit("power", neoModules.neoRuntimeManager.mode.globvars.get().power_on); - }); - socket.on("var:set", (name, value) => { - neoModules.neoRuntimeManager.mode.variables.set(name, value.toString()); - }); - socket.on("vars:get", () => { - socket.emit("vars", neoModules.neoRuntimeManager.mode.variables.get()); - }); - socket.on("modeinfo:get", () => { - socket.emit("modeinfo", { - mode: neoModules.neoRuntimeManager.mode.current(), - brightness: neoModules.neoRuntimeManager.mode.globvars.get().brightness, - power: neoModules.neoRuntimeManager.mode.globvars.get().power_on, - vars: neoModules.neoRuntimeManager.mode.variables.get() - }); - }); - socket.on("authenticate:user", (username, password, callback) => { - let user = neoModules.userData.user.get(username); - if (user == null) { - callback({success: false, reason: "Invalid username/password"}) - logger.access(`SOCKET:open Client (${socket.id}@${socket.handshake.headers.host}) tried to log in as '${username}', wrong username and/or password.`); - return; - } - - let providedHash = hashPassword(password, user.salt); - if (providedHash.hash == user.password) { - let token = createToken(socket); - while (session_tokens.hasOwnProperty(token)) { - token = createToken(socket); - } - - session_tokens[token] = { - expire: (~~Date.now())+(2678400), - host: socket.handshake.headers.host, - user: {username: user.username} - }; - - callback({success: true, user: {username: username}, token: token}) - logger.access(`SOCKET:open Client (${socket.id}@${socket.handshake.headers.host}) authenticated as user '${username}'`); - return; - } - - callback({success: false, reason: "Invalid username/password"}) - logger.access(`SOCKET:open Client (${socket.id}@${socket.handshake.headers.host}) tried to log in as '${username}', wrong username and/or password.`); - }); - - socket.on("disconnect", () => { - logger.access(`SOCKET:open Client (${socket.id}@${socket.handshake.headers.host}) disconnected.`); - }); - - if (neoModules.selfUpdater.updater.updating) { - socket.emit("updater", "start"); - } - }); - - neoModules.neoRuntimeManager.event.on("change", (name, value) => { - if (name == "modelist") { - openNamespace.emit("modelist", neoModules.neoRuntimeManager.modes()); - } else if (["mode", "power_on", "brightness"].includes(name)) { - if (name == "power_on") { name = "power"; } - openNamespace.emit(name, value); - } else { - openNamespace.emit("var", name, value); - } - }); - neoModules.selfUpdater.updater.event.on("start", () => { - openNamespace.emit("updater", "start"); - }); - neoModules.selfUpdater.updater.event.on("end", () => { - openNamespace.emit("updater", "end"); - }); -} - -/** - * @type {object} This is the collection of valid session tokens. - */ -let session_tokens = {}; - -/** - * Middleware that will stop the request if the client does not have a valid - * session token. - * - * @param {object} socket - The socket instance of the connected client - * @param {function} next - The callback to continue the middleware chain - */ -function authorize_middleware(socket, next) { - const token = socket.handshake.auth.token; - - if (session_tokens.hasOwnProperty(token) && - // session_tokens[token].host === socket.handshake.headers.host && - session_tokens[token].expire > (~~(Date.now()))) { - socket.data.user = session_tokens[token].user; - next(); - } else { - const err = new Error("not authorized"); - err.data = { content: "invalid session token" }; - next(err); - } -} - -/** - * Create the open socketio namespace and setup all listeners. - * A valid session token is required to connect to this namespace. - * - * @param {io} socetio - */ -function createAuthorizedNamespace(io) { - const authorizedNamespace = io.of("/authed"); - authorizedNamespace.use(authorize_middleware); - authorizedNamespace.on("connection", (socket) => { - logger.access(`SOCKET:authed Client (${socket.id}@${socket.handshake.headers.host}) connected.`); - let debuggerOpen = false; - - socket.emit("user", socket.data.user); - - /* InstanceName */ - socket.on("name:set", (name, fn) => { - neoModules.userData.config.instanceName = name; - fn({success: true}); - io.emit("name", neoModules.userData.config.instanceName); - }); - - /* UserData */ - socket.on("mode:create", (name, template, fn) => { - fn(neoModules.userData.mode.create(name, template)); - }); - socket.on("mode:delete", (modeid, fn) => { - fn(neoModules.userData.mode.delete(modeid)); - }); - - /* LED Config */ - socket.on("led_config:get", () => { - socket.emit("led_config", neoModules.userData.strip.get()); - }); - socket.on("led_config:set", (config) => { - neoModules.userData.strip.set(config); - }); - - /* SelfUpdater */ - socket.on("version:current_number", () => { - socket.emit("version:current_number", neoModules.selfUpdater.localVersionNumber); - }); - socket.on("version:branch", (fn) => { - socket.emit("version:branch", neoModules.selfUpdater.branch); - }); - socket.on("version:newest_number", (fn) => { - socket.emit("version:newest_number", neoModules.selfUpdater.remoteVersionNumber); - }); - socket.on("version:check_for_update", (fn) => { - neoModules.selfUpdater.checkVersion().then(() => { - socket.emit("version:newest_number", neoModules.selfUpdater.remoteVersionNumber); - fn({success: true}); - }); - }); - socket.on("system:update_version", () => { - neoModules.selfUpdater.updater.forceUpdate(); - }); - - /* SSLCert */ - socket.on("sslcert:info", (fn) => { - socket.emit("sslcert:info", {...neoModules.SSLCert.getConfig(), "isValid": neoModules.SSLCert.checkValidity()}); - }); - socket.on("sslcert:generate_new", (fn) => { - neoModules.SSLCert.generateCert(); - fn({success: true}); - }); - - /* System actions */ - socket.on("restart:system", () => { - exec('shutdown -r now', function(error, stdout, stderr){ callback(stdout); }); - }); - socket.on("restart:service", () => { - let p = exec('systemctl restart luxcena-neo'); - p.unref(); - }); - - /* Users */ - socket.on("users:get", () => { - socket.emit("users", neoModules.userData.users()) - }); - socket.on("user:delete", (username, fn) => { - if (username == socket.data.user.username) { fn({success: false, reason: "cannot delete logged in account"}); return; } - fn(neoModules.userData.user.delete(username)); - socket.emit("users", neoModules.userData.users()) - }); - socket.on("user:changeusername", (oldusername, newusername, fn) => { - if (oldusername == socket.data.user.username) { fn({success: false, reason: "cannot change username of logged in account"}); return; } - let user = neoModules.userData.user.get(oldUserName); - if (user == null) { fn({success: false, reason: "unknown username", detail: oldusername}); return; } - user.username = newusername; - let res = neoModules.userData.user.save(user); - if (!res.success) { fn(res); return; } - res = neoModules.userData.user.delete(oldusername) - if (!res.success) { fn(res); return; } - fn({success: true}); - socket.emit("users", neoModules.userData.users()) - }); - socket.on("user:newpassword", (username, newPassword, fn) => { - let user = neoModules.userData.user.get(username); - if (user == null) { fn({success: false, reason: "unknown username", detail: username}); return; } - let newHash = hashPassword(newPassword); - fn(neoModules.userData.user.save(username, newHash.salt, newHash.hash)); - socket.emit("users", neoModules.userData.users()) - }); - socket.on("user:create", (username, newPassword, fn) => { - let user = neoModules.userData.user.get(username); - if (user != null) { fn({success: false, reason: "user already exists", detail: username}); return; } - if (username.length < 1) { fn({success: false, reason: "no username provided"}); return; } - let newHash = hashPassword(newPassword); - fn(neoModules.userData.user.save(username, newHash.salt, newHash.hash)); - socket.emit("users", neoModules.userData.users()) - }); - - /* Editor/debugger */ - let onProcStart = () => socket.emit("editor:proc:start"); - let onProcStop = (code) => socket.emit("editor:proc:exit", code); - let onProcStdout = (stdout) => socket.volatile.emit("editor:proc:stdout", stdout); - let onProcStderr = (stderr) => socket.volatile.emit("editor:proc:stderr", stderr); - let onDebuggerState = (state) => socket.volatile.emit("editor:debugger:state", state); - let closeDebugger = () => { - debuggerOpen = false; - neoModules.neoRuntimeManager.event.removeListener("proc:start", onProcStart); - neoModules.neoRuntimeManager.event.removeListener("proc:stop", onProcStop); - neoModules.neoRuntimeManager.event.removeListener("proc:stdout", onProcStdout); - neoModules.neoRuntimeManager.event.removeListener("proc:stderr", onProcStderr); - neoModules.neoRuntimeManager.event.removeListener("debugger:state", onDebuggerState); - return neoModules.neoRuntimeManager.stopDebugger(); - }; - socket.on("editor:open", (modeId, fn) => { - neoModules.neoRuntimeManager.event.on("proc:start", onProcStart); - neoModules.neoRuntimeManager.event.on("proc:exit", onProcStop); - neoModules.neoRuntimeManager.event.on("proc:stdout", onProcStdout); - neoModules.neoRuntimeManager.event.on("proc:stderr", onProcStderr); - neoModules.neoRuntimeManager.event.on("debugger:state", onDebuggerState); - let res = neoModules.neoRuntimeManager.startDebugger(modeId); - if (!res.success) { fn(res); return; } - logger.info(`Starting debugger for ${modeId}.`) - debuggerOpen = true; - fn({success: true}) - socket.emit("editor:code", modeId, res.code); - - if (neoModules.neoRuntimeManager.modeRunning()) { - socket.emit("editor:proc:start"); - } - }); - socket.on("editor:save", (modeId, code, fn) => { - if (!debuggerOpen) { fn({success: false, reason: "debugger not open"}); return; }; - fn(neoModules.neoRuntimeManager.saveModeCode(modeId, code)); - }); - socket.on("editor:startmode", (fn) => { - if (neoModules.neoRuntimeManager.modeRunning()) { - fn({success: true}); - socket.emit("editor:proc:start"); - } else { - fn(neoModules.neoRuntimeManager.startMode()); - } - }); - socket.on("editor:stopmode", (fn) => { - fn(neoModules.neoRuntimeManager.stopMode()); - }); - socket.on("editor:restartmode", (fn) => { - fn(neoModules.neoRuntimeManager.restartMode()); - }); - socket.on("editor:close", (fn) => { - fn(closeDebugger()); - logger.info("Stopped debugger"); - }); - - /* Matrix and strip buffer */ - socket.on("matrix:get", () => { - socket.emit("matrix", neoModules.neoRuntimeManager.matrix); - }); - - socket.on("disconnect", () => { - logger.access(`SOCKET:authed Client (${socket.id}@${socket.handshake.headers.host}) disconnected.`); - if (debuggerOpen) { - closeDebugger(); - logger.info("Stopped debugger because client disconnected") - } - }); - }); - - neoModules.neoRuntimeManager.event.on("matrix", (matrix) => { - authorizedNamespace.emit("matrix", matrix); - }); - let lastStripBufferEmit = performance.now(); - neoModules.neoRuntimeManager.event.on("strip_buffer", (strip_buffer) => { - if ((performance.now() - lastStripBufferEmit) > 50) { - authorizedNamespace.volatile.emit("strip_buffer", strip_buffer); - lastStripBufferEmit = performance.now(); - } // We just drop packets - }); - neoModules.selfUpdater.updater.event.on("step", (step) => { - authorizedNamespace.emit("updater:step", step); - }); - neoModules.selfUpdater.updater.event.on("command", (command) => { - authorizedNamespace.emit("updater:command", command); - }); - neoModules.selfUpdater.updater.event.on("error", (updateLog) => { - authorizedNamespace.emit("updater:error", updateLog); - }); -} - -/** - * Protect - */ -function limitEmits(fn) { - let lastEmit = performance.now(); - - return { - } -} - -/** - * Creates an access-token from the clients host-name and the current EPOCH. - * - * @param {client} - * - * @return {string} - The access-token. - */ - function createToken(client) { - let time = Date.now().toString(); - let host = client.handshake.headers.host; - return (CryptoJS.SHA256(`${host}${time}`).toString()); -} - -/** - * Create a new salt and hash from a password. - * - * @param {string} password - The password to hash. - * @param {string} salt - If set, this salt will be used, else a new salt is generated. - * - * @return {object} A object containing a password and a salt property. - */ -function hashPassword(password, salt = null) { - if (salt == null) { - salt = CryptoJS.lib.WordArray.random(128 / 2); - } else { - salt = CryptoJS.enc.Hex.parse(salt); - } - let hash = CryptoJS.PBKDF2(password, salt, { - keySize: 512 / 32, - iterations: 1000, - hasher: CryptoJS.algo.SHA512 - }); - return {hash: hash.toString(), salt: salt.toString()} -} - -module.exports = (_neoModules, io) => { - neoModules = _neoModules; - return { - openNamespace: createOpenSocketNamespace(io), - authorizedNamespace: createAuthorizedNamespace(io) - } -}; - diff --git a/src/UserData/index.cjs b/src/UserData/index.cjs new file mode 100644 index 0000000..0c861b1 --- /dev/null +++ b/src/UserData/index.cjs @@ -0,0 +1,332 @@ +/** + * This module is the entry of UserData. This will ensure the user-dirs and all config-files. + * Also, it will + * + * @author jakobst1n. + * @since 19.12.2019 + */ + +let logger = require("../Logger/index.cjs"); +let fse = require("fs-extra"); +let ini = require('ini'); + +let neoModules; + +/** + * This method will ensure that all required fields are in config.ini + */ +function ensureMainConfig() { + var config = ini.decode(fse.readFileSync(__configdir + "/config.ini", 'utf-8')) + + if (config.instanceName == null) { config.instanceName = "neoStrip"; } + if (config.activeMode == null) { config.activeMode = "builtin/static"; } + + if (config.HTTP == null) { config.HTTP = {}; } + if (config.HTTP.port == null) { config.HTTP.port = 443; } + + if (config.SelfUpdater == null) { config.SelfUpdater = {}; } + if (config.SelfUpdater.checkVersionInterval == null) { config.SelfUpdater.checkVersionInterval = 1; } + if (config.SelfUpdater.automaticUpdate == null) { config.SelfUpdater.automaticUpdate = false; } + + if (config.SSLCert == null) { config.SSLCert = {}; } + if (config.SSLCert.CN == null) { config.SSLCert.CN = "localhost"; } + if (config.SSLCert.certMade == null) { config.SSLCert.certMade = false; } + if (config.SSLCert.certDate == null) { config.SSLCert.certDate = 0; } + if (config.SSLCert.certExpire == null) { config.SSLCert.certExpire = 0; } + if (config.SSLCert.certCN == null) { config.SSLCert.certCN = ""; } + + if (config.DiscoveryServer == null) { config.DiscoveryServer = {}; } + if (config.DiscoveryServer.address == null) { config.DiscoveryServer.address = "https://erj46s.deta.dev"; } + if (config.DiscoveryServer.broadcastSelf == null) { config.DiscoveryServer.broadcastSelf = false; } + + if (config.neoRuntimeIPC == null) { config.neoRuntimeIPC = {}; } + if (config.neoRuntimeIPC.socketFile == null) { config.neoRuntimeIPC.socketFile = "/tmp/neo_runtime.sock"; } + + fse.writeFileSync(__configdir + "/config.ini", ini.encode(config)) +} + +/** + * This method will ensure that all required fields are in config.ini + */ +function ensureStripConfig() { + var config = ini.decode(fse.readFileSync(__configdir + "/strip.ini", 'utf-8')) + + if (config.DEFAULT == null) { config.DEFAULT = {}; } + if (config.DEFAULT.led_pin == null) { config.DEFAULT.led_pin = 18; } + if (config.DEFAULT.led_freq_hz == null) { config.DEFAULT.led_freq_hz = 80000; } + if (config.DEFAULT.led_dma == null) { config.DEFAULT.led_dma = 10; } + if (config.DEFAULT.led_invert == null) { config.DEFAULT.led_invert = false; } + if (config.DEFAULT.led_channel == null) { config.DEFAULT.led_channel = 0 } + if (config.DEFAULT.segments == null) { config.DEFAULT.segments = "50 50"; } + if (config.DEFAULT.matrix == null) { config.DEFAULT.matrix = "[[[0,false]],[[1,false]]]"; } + + fse.writeFileSync(__configdir + "/strip.ini", ini.encode(config)) +} + +/** + * This method will make sure all files and folders needed for the app exists, + * it will also make sure all files contain all needed data. + */ +function init() { + // Generate all user-folders + logger.info("Ensuring all folder in UserDir exists..."); + + fse.ensureDirSync(__datadir + "/"); + fse.ensureDirSync(__configdir); + fse.ensureDirSync(__configdir + "/certs"); + fse.ensureDirSync(__datadir + "/userCode/"); + fse.ensureDirSync(__datadir + "/remoteCode/"); + + // Generate config-files + if (!fse.existsSync(__configdir + "/config.ini")) { + fse.closeSync(fse.openSync(__configdir + "/config.ini", 'w')); + } + ensureMainConfig(); + + if (!fse.existsSync(__configdir + "/strip.ini")) { + fse.closeSync(fse.openSync(__configdir + "/strip.ini", 'w')); + } + ensureStripConfig(); + + if (!fse.existsSync(__configdir + "/users.ini")) { + fse.writeFileSync(__configdir + "/users.ini", ini.encode({ + "neo": { + "password": "5adbc90fb4716fff62d9cf634837e22f29b011803ba29cee51f921b920fa941651737bd15d00dc72e4cbeee5e64e06ec99cc50ea917285a029797a98740cce0f", + "salt": "59b6de1040f3ae3c63de984ca5d61ef46f41dc6ecead3a9d5dab69f0bb3636aa49017e179b74dbcdb407f62bc139a7d55aa78fe2bbdd5327609ea124b2fa03b1" + } + })) + } +}; + +/** + * Recursive function which adds setters and getters to all properties + * in a nested object. This will make us able to save config values + * directly without doing anything other that `prop = value`. + * + * @param {object} config - The full config object. + * @param {string} configFile - The path of the configfile. + * + * @return {object} The config object with setters for values. + */ + function withSetters(config, configFile) { + let outConfig = {}; + function iter(inNode, outNode) { + for (const key of Object.keys(inNode)) { + if (typeof(inNode[key]) === "object") { + outNode[key] = {}; + iter(inNode[key], outNode[key]); + } else { + outNode[`_${key}`] = inNode[key]; + Object.defineProperty(outNode, key, { + get: function() { return this[`_${key}`]; }, + set: function(value) { + this[`_${key}`] = value; + saveConfig(configFile, outConfig); + }, + enumerable: true + }); + } + } + } + iter(config, outConfig); + return outConfig +} + +/** + * Returns a object with only the actual values and not setters, this is the + * inverse of withSetters. + * + * @param {object} config - The full config object. + * + * @return {object} The config object without setters. + */ +function withoutSetters(config) { + let out = {}; + function iter(inNode, outNode) { + for (const key of Object.keys(inNode).filter(k => (k.substr(0, 1) != "_"))) { + if (typeof(inNode[key]) === "object") { + outNode[key] = {}; + iter(inNode[key], outNode[key], out); + } else { + outNode[key] = inNode[`_${key}`]; + } + } + } + iter(config, out); + return out; +} + +/** + * Save config object, this will run stripSetters on the object it saves. + * + * @param {string} file - filename to save the config object to. + * @param {object} object - the config object to save. + */ +function saveConfig(file, object, removeSetters=true) { + if (removeSetters) { + object = withoutSetters(object); + } + fse.writeFileSync(file, ini.encode(object)); +} + +/** + * Reads a ini file and add setters to all properties + * + * @param {string} file - filename of file to read. + * + * @return {object} The config in the file. + */ +function getFullConfig(file, addSetters=true) { + let fullConfig = ini.decode(fse.readFileSync(file, "utf-8")); + if (addSetters) { + fullConfig = withSetters(fullConfig, file); + } + return fullConfig; +} + +/** + * Save a user the user config file, this will append if a new user, and + * overwrite if it is a existsing user. + * + * @param {string} username - The username, case-insensitive. + * @param {string} salt - Salt used for password-checking. + * @param {string} password - Hashed password. + * + * @return {object} Standardform return object + */ + function saveUser(username, salt, password) { + let config = ini.decode(fse.readFileSync(__configdir + "/users.ini", 'utf-8')) + config[username] = {} + config[username].salt = salt + config[username].password = password + fse.writeFileSync(__configdir + "/users.ini", ini.encode(config)) + return {success: true} +} + +/** + * Get a user, this will return null if no user is found. + * + * @return {object} with username, salt and hash properties. + */ +function getUser(username) { + let config = ini.decode(fse.readFileSync(__configdir + "/users.ini", 'utf-8')) + if (Object.prototype.hasOwnProperty.call(config, username)) { + return {...config[username], username: username} + } + return null; +} + +/** + * Get all users + * + * @return {array} usernames + */ +function getUsers() { + let config = ini.decode(fse.readFileSync(__configdir + "/users.ini", "utf-8")); + let users = []; + for (const username of Object.keys(config)) { + users.push(username); + } + return users; +} + +/** + * Delete a user + * + * @return {object} Standardform success object. + */ +function deleteUser(username) { + let config = ini.decode(fse.readFileSync(__configdir + "/users.ini", 'utf-8')) + if (config.length <= 1) { return {success: false, reason: "cannot delete only user"}; } + if (!Object.prototype.hasOwnProperty.call(config, username)) { return {success: false, reason: "user not found", detail: username}; } + delete config[username]; + fse.writeFileSync(__configdir + "/users.ini", ini.encode(config)); + return {success: true} +} + +/** + * Create a new mode in the user directory. + * + * @param {string} name - The name of the file to use, a trailing number will + * be added if there are any conflicts. + * @param {string} template - Id of the template, builtin/static, template/base etc... + * + * @return {object} a standard convention result object. + */ +function createNewUserMode(name, template) { + source_script = null; + if ((template === "template/base") || (template === "") || (template == null)) { + source_script = __appdir + "/NeoRuntime/special/template_base/"; + } else { + source_script = neoModules.neoRuntimeManager.getModePath(template); + } + if (!neoModules.neoRuntimeManager.isMode(source_script)) { + return {success: false, reason: "Source script not found"}; + } + + let newModeName = neoModules.neoRuntimeManager.getModePath(`user/${name}`); + let counter = 0; + while (neoModules.neoRuntimeManager.isMode(newModeName)) { + counter += 1; + newModeName = neoModules.neoRuntimeManager.getModePath(`user/${name}_${counter}`); + } + + fse.ensureDirSync(newModeName); + fse.copySync(`${source_script}/script.py`, `${newModeName}/script.py`) + neoModules.neoRuntimeManager.event.emit("change", "modelist"); + return {success: true}; +} + +/** + * Delete a user created mode + * + * @param {string} modeid - modeid to delete + * + * @return {object} a standard convention result object. + */ +function deleteUserMode(modeid) { + if (modeid.substr(0, 5) !== "user/") { + return {success: false, reason: "Not user mode"} + } + let modePath = neoModules.neoRuntimeManager.getModePath(modeid); + if (!neoModules.neoRuntimeManager.isMode(modePath)) { + return {success: false, reason: "Mode does not found"} + } + if (modeid === neoModules.neoRuntimeManager.mode.current()) { + return {success: false, reason: "Cannot delete currently active mode"} + } + fse.removeSync(modePath); + neoModules.neoRuntimeManager.event.emit("change", "modelist"); + return {success: true} +} + +module.exports = (_neoModules) => { + neoModules = _neoModules; + init(); + return { + users: getUsers, + user: { + save: saveUser, + get: getUser, + delete: deleteUser + }, + strip: { + get: () => { + let c = getFullConfig(`${__configdir}/strip.ini`, addSetters=false); + c.DEFAULT.matrix = JSON.parse(c.DEFAULT.matrix); + c.DEFAULT.segments = c.DEFAULT.segments.split(" "); + return c.DEFAULT; + }, + set: (c) => { + c.segments = c.segments.join(" "); + c.matrix = JSON.stringify(c.matrix); + return saveConfig(`${__configdir}/strip.ini`, {DEFAULT: c}, removeSetters=false); + }, + }, + config: getFullConfig(`${__configdir}/config.ini`), + mode: { + create: createNewUserMode, + delete: deleteUserMode + } + } +}; diff --git a/src/UserData/index.js b/src/UserData/index.js deleted file mode 100644 index e442a79..0000000 --- a/src/UserData/index.js +++ /dev/null @@ -1,332 +0,0 @@ -/** - * This module is the entry of UserData. This will ensure the user-dirs and all config-files. - * Also, it will - * - * @author jakobst1n. - * @since 19.12.2019 - */ - -let logger = require(__appdir + "/src/Logger"); -let fse = require("fs-extra"); -let ini = require('ini'); - -let neoModules; - -/** - * This method will ensure that all required fields are in config.ini - */ -function ensureMainConfig() { - var config = ini.decode(fse.readFileSync(__configdir + "/config.ini", 'utf-8')) - - if (config.instanceName == null) { config.instanceName = "neoStrip"; } - if (config.activeMode == null) { config.activeMode = "builtin/static"; } - - if (config.HTTP == null) { config.HTTP = {}; } - if (config.HTTP.port == null) { config.HTTP.port = 443; } - - if (config.SelfUpdater == null) { config.SelfUpdater = {}; } - if (config.SelfUpdater.checkVersionInterval == null) { config.SelfUpdater.checkVersionInterval = 1; } - if (config.SelfUpdater.automaticUpdate == null) { config.SelfUpdater.automaticUpdate = false; } - - if (config.SSLCert == null) { config.SSLCert = {}; } - if (config.SSLCert.CN == null) { config.SSLCert.CN = "localhost"; } - if (config.SSLCert.certMade == null) { config.SSLCert.certMade = false; } - if (config.SSLCert.certDate == null) { config.SSLCert.certDate = 0; } - if (config.SSLCert.certExpire == null) { config.SSLCert.certExpire = 0; } - if (config.SSLCert.certCN == null) { config.SSLCert.certCN = ""; } - - if (config.DiscoveryServer == null) { config.DiscoveryServer = {}; } - if (config.DiscoveryServer.address == null) { config.DiscoveryServer.address = "https://erj46s.deta.dev"; } - if (config.DiscoveryServer.broadcastSelf == null) { config.DiscoveryServer.broadcastSelf = false; } - - if (config.neoRuntimeIPC == null) { config.neoRuntimeIPC = {}; } - if (config.neoRuntimeIPC.socketFile == null) { config.neoRuntimeIPC.socketFile = "/tmp/neo_runtime.sock"; } - - fse.writeFileSync(__configdir + "/config.ini", ini.encode(config)) -} - -/** - * This method will ensure that all required fields are in config.ini - */ -function ensureStripConfig() { - var config = ini.decode(fse.readFileSync(__configdir + "/strip.ini", 'utf-8')) - - if (config.DEFAULT == null) { config.DEFAULT = {}; } - if (config.DEFAULT.led_pin == null) { config.DEFAULT.led_pin = 18; } - if (config.DEFAULT.led_freq_hz == null) { config.DEFAULT.led_freq_hz = 80000; } - if (config.DEFAULT.led_dma == null) { config.DEFAULT.led_dma = 10; } - if (config.DEFAULT.led_invert == null) { config.DEFAULT.led_invert = false; } - if (config.DEFAULT.led_channel == null) { config.DEFAULT.led_channel = 0 } - if (config.DEFAULT.segments == null) { config.DEFAULT.segments = "50 50"; } - if (config.DEFAULT.matrix == null) { config.DEFAULT.matrix = "[[[0,false]],[[1,false]]]"; } - - fse.writeFileSync(__configdir + "/strip.ini", ini.encode(config)) -} - -/** - * This method will make sure all files and folders needed for the app exists, - * it will also make sure all files contain all needed data. - */ -function init() { - // Generate all user-folders - logger.info("Ensuring all folder in UserDir exists..."); - - fse.ensureDirSync(__datadir + "/"); - fse.ensureDirSync(__configdir); - fse.ensureDirSync(__configdir + "/certs"); - fse.ensureDirSync(__datadir + "/userCode/"); - fse.ensureDirSync(__datadir + "/remoteCode/"); - - // Generate config-files - if (!fse.existsSync(__configdir + "/config.ini")) { - fse.closeSync(fse.openSync(__configdir + "/config.ini", 'w')); - } - ensureMainConfig(); - - if (!fse.existsSync(__configdir + "/strip.ini")) { - fse.closeSync(fse.openSync(__configdir + "/strip.ini", 'w')); - } - ensureStripConfig(); - - if (!fse.existsSync(__configdir + "/users.ini")) { - fse.writeFileSync(__configdir + "/users.ini", ini.encode({ - "neo": { - "password": "5adbc90fb4716fff62d9cf634837e22f29b011803ba29cee51f921b920fa941651737bd15d00dc72e4cbeee5e64e06ec99cc50ea917285a029797a98740cce0f", - "salt": "59b6de1040f3ae3c63de984ca5d61ef46f41dc6ecead3a9d5dab69f0bb3636aa49017e179b74dbcdb407f62bc139a7d55aa78fe2bbdd5327609ea124b2fa03b1" - } - })) - } -}; - -/** - * Recursive function which adds setters and getters to all properties - * in a nested object. This will make us able to save config values - * directly without doing anything other that `prop = value`. - * - * @param {object} config - The full config object. - * @param {string} configFile - The path of the configfile. - * - * @return {object} The config object with setters for values. - */ - function withSetters(config, configFile) { - let outConfig = {}; - function iter(inNode, outNode) { - for (const key of Object.keys(inNode)) { - if (typeof(inNode[key]) === "object") { - outNode[key] = {}; - iter(inNode[key], outNode[key]); - } else { - outNode[`_${key}`] = inNode[key]; - Object.defineProperty(outNode, key, { - get: function() { return this[`_${key}`]; }, - set: function(value) { - this[`_${key}`] = value; - saveConfig(configFile, outConfig); - }, - enumerable: true - }); - } - } - } - iter(config, outConfig); - return outConfig -} - -/** - * Returns a object with only the actual values and not setters, this is the - * inverse of withSetters. - * - * @param {object} config - The full config object. - * - * @return {object} The config object without setters. - */ -function withoutSetters(config) { - let out = {}; - function iter(inNode, outNode) { - for (const key of Object.keys(inNode).filter(k => (k.substr(0, 1) != "_"))) { - if (typeof(inNode[key]) === "object") { - outNode[key] = {}; - iter(inNode[key], outNode[key], out); - } else { - outNode[key] = inNode[`_${key}`]; - } - } - } - iter(config, out); - return out; -} - -/** - * Save config object, this will run stripSetters on the object it saves. - * - * @param {string} file - filename to save the config object to. - * @param {object} object - the config object to save. - */ -function saveConfig(file, object, removeSetters=true) { - if (removeSetters) { - object = withoutSetters(object); - } - fse.writeFileSync(file, ini.encode(object)); -} - -/** - * Reads a ini file and add setters to all properties - * - * @param {string} file - filename of file to read. - * - * @return {object} The config in the file. - */ -function getFullConfig(file, addSetters=true) { - let fullConfig = ini.decode(fse.readFileSync(file, "utf-8")); - if (addSetters) { - fullConfig = withSetters(fullConfig, file); - } - return fullConfig; -} - -/** - * Save a user the user config file, this will append if a new user, and - * overwrite if it is a existsing user. - * - * @param {string} username - The username, case-insensitive. - * @param {string} salt - Salt used for password-checking. - * @param {string} password - Hashed password. - * - * @return {object} Standardform return object - */ - function saveUser(username, salt, password) { - let config = ini.decode(fse.readFileSync(__configdir + "/users.ini", 'utf-8')) - config[username] = {} - config[username].salt = salt - config[username].password = password - fse.writeFileSync(__configdir + "/users.ini", ini.encode(config)) - return {success: true} -} - -/** - * Get a user, this will return null if no user is found. - * - * @return {object} with username, salt and hash properties. - */ -function getUser(username) { - let config = ini.decode(fse.readFileSync(__configdir + "/users.ini", 'utf-8')) - if (Object.prototype.hasOwnProperty.call(config, username)) { - return {...config[username], username: username} - } - return null; -} - -/** - * Get all users - * - * @return {array} usernames - */ -function getUsers() { - let config = ini.decode(fse.readFileSync(__configdir + "/users.ini", "utf-8")); - let users = []; - for (const username of Object.keys(config)) { - users.push(username); - } - return users; -} - -/** - * Delete a user - * - * @return {object} Standardform success object. - */ -function deleteUser(username) { - let config = ini.decode(fse.readFileSync(__configdir + "/users.ini", 'utf-8')) - if (config.length <= 1) { return {success: false, reason: "cannot delete only user"}; } - if (!Object.prototype.hasOwnProperty.call(config, username)) { return {success: false, reason: "user not found", detail: username}; } - delete config[username]; - fse.writeFileSync(__configdir + "/users.ini", ini.encode(config)); - return {success: true} -} - -/** - * Create a new mode in the user directory. - * - * @param {string} name - The name of the file to use, a trailing number will - * be added if there are any conflicts. - * @param {string} template - Id of the template, builtin/static, template/base etc... - * - * @return {object} a standard convention result object. - */ -function createNewUserMode(name, template) { - source_script = null; - if ((template === "template/base") || (template === "") || (template == null)) { - source_script = __appdir + "/NeoRuntime/special/template_base/"; - } else { - source_script = neoModules.neoRuntimeManager.getModePath(template); - } - if (!neoModules.neoRuntimeManager.isMode(source_script)) { - return {success: false, reason: "Source script not found"}; - } - - let newModeName = neoModules.neoRuntimeManager.getModePath(`user/${name}`); - let counter = 0; - while (neoModules.neoRuntimeManager.isMode(newModeName)) { - counter += 1; - newModeName = neoModules.neoRuntimeManager.getModePath(`user/${name}_${counter}`); - } - - fse.ensureDirSync(newModeName); - fse.copySync(`${source_script}/script.py`, `${newModeName}/script.py`) - neoModules.neoRuntimeManager.event.emit("change", "modelist"); - return {success: true}; -} - -/** - * Delete a user created mode - * - * @param {string} modeid - modeid to delete - * - * @return {object} a standard convention result object. - */ -function deleteUserMode(modeid) { - if (modeid.substr(0, 5) !== "user/") { - return {success: false, reason: "Not user mode"} - } - let modePath = neoModules.neoRuntimeManager.getModePath(modeid); - if (!neoModules.neoRuntimeManager.isMode(modePath)) { - return {success: false, reason: "Mode does not found"} - } - if (modeid === neoModules.neoRuntimeManager.mode.current()) { - return {success: false, reason: "Cannot delete currently active mode"} - } - fse.removeSync(modePath); - neoModules.neoRuntimeManager.event.emit("change", "modelist"); - return {success: true} -} - -module.exports = (_neoModules) => { - neoModules = _neoModules; - init(); - return { - users: getUsers, - user: { - save: saveUser, - get: getUser, - delete: deleteUser - }, - strip: { - get: () => { - let c = getFullConfig(`${__configdir}/strip.ini`, addSetters=false); - c.DEFAULT.matrix = JSON.parse(c.DEFAULT.matrix); - c.DEFAULT.segments = c.DEFAULT.segments.split(" "); - return c.DEFAULT; - }, - set: (c) => { - c.segments = c.segments.join(" "); - c.matrix = JSON.stringify(c.matrix); - return saveConfig(`${__configdir}/strip.ini`, {DEFAULT: c}, removeSetters=false); - }, - }, - config: getFullConfig(`${__configdir}/config.ini`), - mode: { - create: createNewUserMode, - delete: deleteUserMode - } - } -}; -- cgit v1.2.3